Files
codex-bot d6ee8942f1
Foundry Go checks / check (push) Successful in 2m40s
Foundry Go checks / check (pull_request) Successful in 2m36s
Implement Canvas V2 subgraphs, assets and asynchronous AI execution
2026-11-06 18:36:00 +08:00

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