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) } } }