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

254 lines
7.7 KiB
Go

package platform
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sort"
)
type canvasCoordinateSpace struct{ X, Y, Scale float64 }
type memberLogicalEndpoint struct{ Key, Handle string }
type memberLogicalEdge struct {
OriginalBoard string
Data map[string]any
Source, Target memberLogicalEndpoint
}
// All documents are copied before transforming them. Each node/edge retains its
// ID; collisions in a destination reject the entire operation instead of renaming.
func moveMembersDocuments(parent, source, target string, nodeIDs []string, raw map[string]json.RawMessage) (map[string]json.RawMessage, error) {
if source == target || source == "" || target == "" || len(nodeIDs) == 0 {
return nil, errGraphOwnership
}
docs := map[string]map[string]any{}
boards := []string{}
for board, encoded := range raw {
var doc map[string]any
if err := json.Unmarshal(encoded, &doc); err != nil {
return nil, err
}
docs[board] = doc
boards = append(boards, board)
}
sort.Strings(boards)
if docs[parent] == nil || docs[source] == nil || docs[target] == nil {
return nil, errGraphOwnership
}
spaces := map[string]canvasCoordinateSpace{parent: {Scale: 1}}
frames := map[string]map[string]any{}
childForFrame := map[string]string{}
for _, v := range docs[parent]["nodes"].([]any) {
n := v.(map[string]any)
data := n["data"].(map[string]any)
sub, _ := data["subgraph"].(map[string]any)
child, _ := sub["boardId"].(string)
if child == "" || docs[child] == nil {
continue
}
p := n["position"].(map[string]any)
scale := float64(1)
if presentation, ok := data["presentation"].(map[string]any); ok {
if value, ok := presentation["contentScale"].(float64); ok {
scale = value
}
}
spaces[child] = canvasCoordinateSpace{X: p["x"].(float64) + 24, Y: p["y"].(float64) + 36, Scale: scale}
frames[child] = n
childForFrame[n["id"].(string)] = child
}
if _, ok := spaces[source]; !ok {
return nil, errGraphOwnership
}
if _, ok := spaces[target]; !ok {
return nil, errGraphOwnership
}
nodes := map[string]map[string]any{}
owner := map[string]string{}
selected := map[string]bool{}
for _, id := range nodeIDs {
if selected[id] {
return nil, errGraphOwnership
}
selected[id] = true
}
for _, board := range boards {
for _, v := range docs[board]["nodes"].([]any) {
n := v.(map[string]any)
key := canvasNodeKey(board, n["id"].(string))
nodes[key] = n
owner[key] = board
}
}
for id := range selected {
key := canvasNodeKey(source, id)
n := nodes[key]
if n == nil {
return nil, errGraphOwnership
}
if nodes[canvasNodeKey(target, id)] != nil {
return nil, errGraphOwnership
}
if targetFrame := frames[target]; targetFrame != nil && source == parent && id == targetFrame["id"] {
return nil, errGraphOwnership
}
owner[key] = target
p := n["position"].(map[string]any)
from, to := spaces[source], spaces[target]
p["x"] = (from.X + from.Scale*p["x"].(float64) - to.X) / to.Scale
p["y"] = (from.Y + from.Scale*p["y"].(float64) - to.Y) / to.Scale
delete(n["data"].(map[string]any), "frameId")
}
resolve := func(board, node, handle string) (memberLogicalEndpoint, error) {
if board == parent {
if child := childForFrame[node]; child != "" {
data := frames[child]["data"].(map[string]any)
ports, _ := data["ports"].([]any)
for _, v := range ports {
p := v.(map[string]any)
if "port:"+p["id"].(string) == handle {
inside := p["nodeId"].(string)
if nodes[canvasNodeKey(child, inside)] == nil {
return memberLogicalEndpoint{}, errGraphOwnership
}
h, _ := p["handle"].(string)
return memberLogicalEndpoint{Key: canvasNodeKey(child, inside), Handle: h}, nil
}
}
return memberLogicalEndpoint{}, errGraphOwnership
}
}
key := canvasNodeKey(board, node)
if nodes[key] == nil {
return memberLogicalEndpoint{}, errGraphOwnership
}
return memberLogicalEndpoint{Key: key, Handle: handle}, nil
}
logical := []memberLogicalEdge{}
for _, board := range boards {
for _, v := range docs[board]["edges"].([]any) {
e := v.(map[string]any)
sh, _ := e["sourceHandle"].(string)
th, _ := e["targetHandle"].(string)
a, err := resolve(board, e["source"].(string), sh)
if err != nil {
return nil, err
}
b, err := resolve(board, e["target"].(string), th)
if err != nil {
return nil, err
}
logical = append(logical, memberLogicalEdge{OriginalBoard: board, Data: e, Source: a, Target: b})
}
}
// Remove ports whose target changed ownership. Rebuild only the boundaries
// still needed by logical edges; stable endpoint hashes deduplicate retries.
for child, frame := range frames {
data := frame["data"].(map[string]any)
ports, _ := data["ports"].([]any)
kept := []any{}
for _, v := range ports {
p := v.(map[string]any)
if owner[canvasNodeKey(child, p["nodeId"].(string))] == child {
kept = append(kept, p)
}
}
data["ports"] = kept
}
outputNodes := map[string][]any{}
outputEdges := map[string][]any{}
edgeIDs := map[string]map[string]bool{}
for _, board := range boards {
outputNodes[board] = []any{}
outputEdges[board] = []any{}
edgeIDs[board] = map[string]bool{}
}
for _, board := range boards {
for _, v := range docs[board]["nodes"].([]any) {
n := v.(map[string]any)
destination := owner[canvasNodeKey(board, n["id"].(string))]
outputNodes[destination] = append(outputNodes[destination], n)
}
}
endpoint := func(value memberLogicalEndpoint, edgeBoard, direction string) (string, string, error) {
board := owner[value.Key]
n := nodes[value.Key]
node := n["id"].(string)
if board == edgeBoard {
return node, value.Handle, nil
}
frame := frames[board]
if edgeBoard != parent || frame == nil {
return "", "", errGraphOwnership
}
handle := value.Handle
if handle == "" {
handle = "left"
if direction == "output" {
handle = "right"
}
}
sum := sha256.Sum256([]byte(direction + "/" + node + "/" + handle))
id := "p-" + hex.EncodeToString(sum[:16])
data := frame["data"].(map[string]any)
ports := data["ports"].([]any)
for _, v := range ports {
p := v.(map[string]any)
if p["nodeId"] == node && p["handle"] == handle && p["direction"] == direction {
return frame["id"].(string), "port:" + p["id"].(string), nil
}
}
data["ports"] = append(ports, map[string]any{"id": id, "nodeId": node, "handle": handle, "direction": direction})
return frame["id"].(string), "port:" + id, nil
}
for _, edge := range logical {
destination := parent
if owner[edge.Source.Key] == owner[edge.Target.Key] {
destination = owner[edge.Source.Key]
}
e := edge.Data
id := e["id"].(string)
if edgeIDs[destination][id] {
return nil, errGraphOwnership
}
edgeIDs[destination][id] = true
a, sh, err := endpoint(edge.Source, destination, "output")
if err != nil {
return nil, err
}
b, th, err := endpoint(edge.Target, destination, "input")
if err != nil {
return nil, err
}
e["source"] = a
e["target"] = b
if sh != "" {
e["sourceHandle"] = sh
} else {
delete(e, "sourceHandle")
}
if th != "" {
e["targetHandle"] = th
} else {
delete(e, "targetHandle")
}
from, to := spaces[edge.OriginalBoard], spaces[destination]
projectEdgePath(e, from.Scale/to.Scale, (from.X-to.X)/to.Scale, (from.Y-to.Y)/to.Scale)
outputEdges[destination] = append(outputEdges[destination], e)
}
result := map[string]json.RawMessage{}
for _, board := range boards {
doc := docs[board]
doc["schema_version"] = 2
doc["nodes"] = outputNodes[board]
doc["edges"] = outputEdges[board]
encoded, _ := json.Marshal(doc)
normalized, ok := normalizeCanvas(encoded)
if !ok {
return nil, errGraphCapacity
}
result[board] = normalized
}
return result, nil
}