153 lines
4.3 KiB
Go
153 lines
4.3 KiB
Go
package platform
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
)
|
|
|
|
var errGraphOwnership = errors.New("ownership_conflict")
|
|
var errGraphCapacity = errors.New("graph_limit_exceeded")
|
|
|
|
// dissolveDocuments operates on copies. Its caller commits both snapshots and
|
|
// ownership changes atomically after checking every participating version.
|
|
// An empty child document is returned so the archived board does not remain a
|
|
// second live owner of the nodes restored to the parent.
|
|
func dissolveDocuments(parentRaw, childRaw json.RawMessage, frameID string) (json.RawMessage, json.RawMessage, error) {
|
|
var parent, child map[string]any
|
|
if err := json.Unmarshal(parentRaw, &parent); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := json.Unmarshal(childRaw, &child); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
nodes := parent["nodes"].([]any)
|
|
childNodes := child["nodes"].([]any)
|
|
var frame map[string]any
|
|
remaining := []any{}
|
|
ids := map[string]bool{}
|
|
for _, v := range nodes {
|
|
n := v.(map[string]any)
|
|
id := n["id"].(string)
|
|
if id == frameID {
|
|
frame = n
|
|
} else {
|
|
remaining = append(remaining, n)
|
|
ids[id] = true
|
|
}
|
|
}
|
|
if frame == nil {
|
|
return nil, nil, errGraphOwnership
|
|
}
|
|
if len(remaining)+len(childNodes) > 500 {
|
|
return nil, nil, errGraphCapacity
|
|
}
|
|
data := frame["data"].(map[string]any)
|
|
origin := frame["position"].(map[string]any)
|
|
x, y := origin["x"].(float64)+24, origin["y"].(float64)+36
|
|
scale := float64(1)
|
|
if p, ok := data["presentation"].(map[string]any); ok {
|
|
if value, ok := p["contentScale"].(float64); ok {
|
|
scale = value
|
|
}
|
|
}
|
|
childKinds := map[string]string{}
|
|
for _, v := range childNodes {
|
|
n := v.(map[string]any)
|
|
id := n["id"].(string)
|
|
if ids[id] {
|
|
return nil, nil, errGraphOwnership
|
|
}
|
|
ids[id] = true
|
|
childKinds[id] = n["data"].(map[string]any)["kind"].(string)
|
|
pos := n["position"].(map[string]any)
|
|
pos["x"] = x + scale*pos["x"].(float64)
|
|
pos["y"] = y + scale*pos["y"].(float64)
|
|
// contentScale is presentation, not a destructive resize of the child.
|
|
// Preserve each node's intrinsic dimensions, text and nested content scale.
|
|
remaining = append(remaining, n)
|
|
}
|
|
ports := map[string]map[string]any{}
|
|
if values, ok := data["ports"].([]any); ok {
|
|
for _, v := range values {
|
|
p := v.(map[string]any)
|
|
ports[p["id"].(string)] = p
|
|
}
|
|
}
|
|
edges := parent["edges"].([]any)
|
|
edgeIDs := map[string]bool{}
|
|
for _, v := range edges {
|
|
e := v.(map[string]any)
|
|
edgeIDs[e["id"].(string)] = true
|
|
for _, end := range []string{"source", "target"} {
|
|
if e[end] != frameID {
|
|
continue
|
|
}
|
|
h, _ := e[end+"Handle"].(string)
|
|
if len(h) < 5 || h[:5] != "port:" {
|
|
return nil, nil, errGraphOwnership
|
|
}
|
|
p := ports[h[5:]]
|
|
if p == nil {
|
|
return nil, nil, errGraphOwnership
|
|
}
|
|
node, _ := p["nodeId"].(string)
|
|
if childKinds[node] == "" {
|
|
return nil, nil, errGraphOwnership
|
|
}
|
|
e[end] = node
|
|
e[end+"Handle"] = p["handle"]
|
|
}
|
|
}
|
|
for _, v := range child["edges"].([]any) {
|
|
e := v.(map[string]any)
|
|
id := e["id"].(string)
|
|
if edgeIDs[id] {
|
|
return nil, nil, errGraphOwnership
|
|
}
|
|
edgeIDs[id] = true
|
|
// Project a child path into parent coordinates. Endpoint controls are
|
|
// relative vectors; only knot positions receive the translation.
|
|
projectEdgePath(e, scale, x, y)
|
|
edges = append(edges, e)
|
|
}
|
|
if len(edges) > 2000 {
|
|
return nil, nil, errGraphCapacity
|
|
}
|
|
parent["schema_version"] = 2
|
|
parent["nodes"] = remaining
|
|
parent["edges"] = edges
|
|
encoded, _ := json.Marshal(parent)
|
|
normalized, ok := normalizeCanvas(encoded)
|
|
if !ok {
|
|
return nil, nil, errGraphCapacity
|
|
}
|
|
child["schema_version"] = 2
|
|
child["nodes"] = []any{}
|
|
child["edges"] = []any{}
|
|
empty, _ := json.Marshal(child)
|
|
return normalized, empty, nil
|
|
}
|
|
|
|
func projectEdgePath(e map[string]any, scale, x, y float64) {
|
|
data, _ := e["data"].(map[string]any)
|
|
path, _ := data["path"].(map[string]any)
|
|
for _, key := range []string{"startControl", "endControl"} {
|
|
if p, ok := path[key].(map[string]any); ok {
|
|
p["x"] = scale * p["x"].(float64)
|
|
p["y"] = scale * p["y"].(float64)
|
|
}
|
|
}
|
|
knots, _ := path["knots"].([]any)
|
|
for _, v := range knots {
|
|
knot := v.(map[string]any)
|
|
p := knot["position"].(map[string]any)
|
|
p["x"] = x + scale*p["x"].(float64)
|
|
p["y"] = y + scale*p["y"].(float64)
|
|
for _, key := range []string{"in", "out"} {
|
|
p := knot[key].(map[string]any)
|
|
p["x"] = scale * p["x"].(float64)
|
|
p["y"] = scale * p["y"].(float64)
|
|
}
|
|
}
|
|
}
|