196 lines
6.4 KiB
Go
196 lines
6.4 KiB
Go
package platform
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// A plan contains only explicit legacy memberships. Coordinates are local to
|
|
// the containing canvas; the viewport is never part of this transformation.
|
|
type legacyFramePlan struct {
|
|
FrameID string `json:"frame_id"`
|
|
NodeIDs []string `json:"node_ids"`
|
|
InternalEdges []string `json:"internal_edges"`
|
|
BoundaryEdges []string `json:"boundary_edges"`
|
|
}
|
|
type legacyMigrationPlan struct {
|
|
PlanID string `json:"plan_id"`
|
|
ParentVersion int64 `json:"parent_version"`
|
|
SourceDigest string `json:"source_digest"`
|
|
ChildCount int `json:"child_count"`
|
|
Frames []legacyFramePlan `json:"frames"`
|
|
Issues []string `json:"issues"`
|
|
}
|
|
|
|
func legacyPlan(project, workspace string, parent canvasSnapshot) legacyMigrationPlan {
|
|
var doc map[string]any
|
|
_ = json.Unmarshal(parent.Document, &doc)
|
|
canonical, _ := json.Marshal(doc)
|
|
digest := sha256.Sum256(canonical)
|
|
p := legacyMigrationPlan{ParentVersion: parent.Version, SourceDigest: hex.EncodeToString(digest[:]), Frames: []legacyFramePlan{}, Issues: []string{}}
|
|
nodes, _ := doc["nodes"].([]any)
|
|
edges, _ := doc["edges"].([]any)
|
|
membership := map[string]string{}
|
|
for _, v := range nodes {
|
|
n := v.(map[string]any)
|
|
d := n["data"].(map[string]any)
|
|
if f, ok := d["frameId"].(string); ok {
|
|
membership[n["id"].(string)] = f
|
|
}
|
|
}
|
|
for _, v := range nodes {
|
|
n := v.(map[string]any)
|
|
d := n["data"].(map[string]any)
|
|
if d["kind"] != "frame" || d["subgraph"] != nil {
|
|
continue
|
|
}
|
|
id := n["id"].(string)
|
|
f := legacyFramePlan{FrameID: id, NodeIDs: []string{}, InternalEdges: []string{}, BoundaryEdges: []string{}}
|
|
for node, frame := range membership {
|
|
if frame == id {
|
|
f.NodeIDs = append(f.NodeIDs, node)
|
|
}
|
|
}
|
|
sort.Strings(f.NodeIDs)
|
|
for _, v := range edges {
|
|
e := v.(map[string]any)
|
|
a, b := membership[e["source"].(string)] == id, membership[e["target"].(string)] == id
|
|
if a && b {
|
|
f.InternalEdges = append(f.InternalEdges, e["id"].(string))
|
|
} else if a || b {
|
|
f.BoundaryEdges = append(f.BoundaryEdges, e["id"].(string))
|
|
}
|
|
}
|
|
p.Frames = append(p.Frames, f)
|
|
}
|
|
sort.Slice(p.Frames, func(i, j int) bool { return p.Frames[i].FrameID < p.Frames[j].FrameID })
|
|
p.ChildCount = len(p.Frames)
|
|
identity := fmt.Sprintf("%s/%s/%s/%d/%s", project, workspace, parent.BoardID, parent.Version, p.SourceDigest)
|
|
id := sha256.Sum256([]byte(identity))
|
|
p.PlanID = hex.EncodeToString(id[:])
|
|
return p
|
|
}
|
|
|
|
// migrateLegacyDocument mutates only a freshly decoded snapshot. Boundary edges
|
|
// stay in the parent and are redirected through stable, deduplicated ports.
|
|
func migrateLegacyDocument(raw json.RawMessage, children map[string]string) (json.RawMessage, map[string]json.RawMessage, error) {
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
nodes := doc["nodes"].([]any)
|
|
edges := doc["edges"].([]any)
|
|
frames := map[string]map[string]any{}
|
|
owner := map[string]string{}
|
|
childDocs := map[string]map[string]any{}
|
|
for _, v := range nodes {
|
|
n := v.(map[string]any)
|
|
id := n["id"].(string)
|
|
if _, ok := children[id]; ok {
|
|
frames[id] = n
|
|
childDocs[id] = map[string]any{"schema_version": 2, "nodes": []any{}, "edges": []any{}, "viewport": map[string]any{"x": 0, "y": 0, "zoom": 1}}
|
|
}
|
|
}
|
|
parentNodes := []any{}
|
|
for _, v := range nodes {
|
|
n := v.(map[string]any)
|
|
d := n["data"].(map[string]any)
|
|
frame, _ := d["frameId"].(string)
|
|
if f := frames[frame]; f != nil {
|
|
p := f["position"].(map[string]any)
|
|
pos := n["position"].(map[string]any)
|
|
pos["x"] = pos["x"].(float64) - p["x"].(float64) - 24
|
|
pos["y"] = pos["y"].(float64) - p["y"].(float64) - 36
|
|
delete(d, "frameId")
|
|
owner[n["id"].(string)] = frame
|
|
childDocs[frame]["nodes"] = append(childDocs[frame]["nodes"].([]any), n)
|
|
} else {
|
|
parentNodes = append(parentNodes, n)
|
|
}
|
|
}
|
|
for id, f := range frames {
|
|
d := f["data"].(map[string]any)
|
|
d["subgraph"] = map[string]any{"boardId": children[id]}
|
|
d["ports"] = []any{}
|
|
d["presentation"] = map[string]any{"collapsed": false, "contentScale": 1, "expandedSize": map[string]any{"width": f["width"], "height": f["height"]}, "collapsedSize": map[string]any{"width": 240, "height": 140}}
|
|
}
|
|
parentEdges := []any{}
|
|
for _, v := range edges {
|
|
e := v.(map[string]any)
|
|
a, b := owner[e["source"].(string)], owner[e["target"].(string)]
|
|
if a != "" && a == b {
|
|
// Only knot positions change coordinate spaces. Control vectors do not.
|
|
p := frames[a]["position"].(map[string]any)
|
|
translateEdgeKnots(e, -p["x"].(float64)-24, -p["y"].(float64)-36)
|
|
childDocs[a]["edges"] = append(childDocs[a]["edges"].([]any), e)
|
|
continue
|
|
}
|
|
for _, end := range []string{"source", "target"} {
|
|
node := e[end].(string)
|
|
frame := owner[node]
|
|
if frame == "" {
|
|
continue
|
|
}
|
|
handle, _ := e[end+"Handle"].(string)
|
|
if handle == "" {
|
|
if end == "source" {
|
|
handle = "right"
|
|
} else {
|
|
handle = "left"
|
|
}
|
|
}
|
|
direction := "input"
|
|
if end == "source" {
|
|
direction = "output"
|
|
}
|
|
sum := sha256.Sum256([]byte(direction + "/" + node + "/" + handle))
|
|
portID := "p-" + hex.EncodeToString(sum[:16])
|
|
data := frames[frame]["data"].(map[string]any)
|
|
ports := data["ports"].([]any)
|
|
found := false
|
|
for _, v := range ports {
|
|
if v.(map[string]any)["id"] == portID {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
data["ports"] = append(ports, map[string]any{"id": portID, "direction": direction, "nodeId": node, "handle": handle})
|
|
}
|
|
e[end] = frame
|
|
e[end+"Handle"] = "port:" + portID
|
|
}
|
|
parentEdges = append(parentEdges, e)
|
|
}
|
|
doc["nodes"] = parentNodes
|
|
doc["edges"] = parentEdges
|
|
doc["schema_version"] = 2
|
|
encoded, _ := json.Marshal(doc)
|
|
parent, ok := normalizeCanvas(encoded)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("invalid migrated parent")
|
|
}
|
|
result := map[string]json.RawMessage{}
|
|
for frame, d := range childDocs {
|
|
encoded, _ := json.Marshal(d)
|
|
normalized, ok := normalizeCanvas(encoded)
|
|
if !ok {
|
|
return nil, nil, fmt.Errorf("invalid migrated child")
|
|
}
|
|
result[frame] = normalized
|
|
}
|
|
return parent, result, nil
|
|
}
|
|
func translateEdgeKnots(e map[string]any, x, y float64) {
|
|
data, _ := e["data"].(map[string]any)
|
|
path, _ := data["path"].(map[string]any)
|
|
knots, _ := path["knots"].([]any)
|
|
for _, v := range knots {
|
|
p := v.(map[string]any)["position"].(map[string]any)
|
|
p["x"] = p["x"].(float64) + x
|
|
p["y"] = p["y"].(float64) + y
|
|
}
|
|
}
|