254 lines
7.7 KiB
Go
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
|
|
}
|