283 lines
10 KiB
Go
283 lines
10 KiB
Go
package platform
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type modelIdentity struct {
|
|
SourceCanvasID string `json:"sourceCanvasId"`
|
|
Structure string `json:"structure"`
|
|
SourceID string `json:"sourceId"`
|
|
ElementID string `json:"elementId"`
|
|
}
|
|
type modelMigration struct {
|
|
Model canvasModel
|
|
Identities []modelIdentity
|
|
NewCanvases []string
|
|
}
|
|
type modelSpace struct{ X, Y, Scale float64 }
|
|
|
|
// No source document is changed. IDs are allocated once inside the eventual
|
|
// migration transaction and the complete mapping is stored with its receipt.
|
|
func migrateCanvasModel(root string, snapshots map[string]canvasSnapshot, relations []subgraphRelation, allocate func() string) (modelMigration, error) {
|
|
result := modelMigration{Model: emptyModel(root), Identities: []modelIdentity{}, NewCanvases: []string{}}
|
|
documents, nodes := map[string]map[string]any{}, map[string]map[string]any{}
|
|
identities, children, synthetic := map[string]string{}, map[string]string{}, map[string]bool{}
|
|
used := map[string]bool{}
|
|
for board := range snapshots {
|
|
used[board] = true
|
|
}
|
|
nextID := func() (string, error) {
|
|
id := allocate()
|
|
if !uuidSlugPattern.MatchString(id) || used[id] {
|
|
return "", errCanvasModel
|
|
}
|
|
used[id] = true
|
|
return id, nil
|
|
}
|
|
identityKey := func(board, structure, id string) string { return board + "/" + structure + "/" + id }
|
|
for _, board := range sortedKeys(snapshots) {
|
|
if !uuidSlugPattern.MatchString(board) || snapshots[board].Version < 1 {
|
|
return result, errCanvasModel
|
|
}
|
|
normalized, valid := normalizeCanvas(snapshots[board].Document)
|
|
if !valid {
|
|
return result, errCanvasModel
|
|
}
|
|
var document map[string]any
|
|
if json.Unmarshal(normalized, &document) != nil {
|
|
return result, errCanvasModel
|
|
}
|
|
documents[board] = document
|
|
result.Model.Canvases[board] = modelCanvas{ID: board}
|
|
result.Model.Versions[board] = snapshots[board].Version + 1
|
|
for _, collection := range []string{"nodes", "edges"} {
|
|
structure := "node"
|
|
if collection == "edges" {
|
|
structure = "connection"
|
|
}
|
|
for _, value := range document[collection].([]any) {
|
|
record := value.(map[string]any)
|
|
old := record["id"].(string)
|
|
id, err := nextID()
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
identities[identityKey(board, structure, old)] = id
|
|
result.Identities = append(result.Identities, modelIdentity{SourceCanvasID: board, Structure: structure, SourceID: old, ElementID: id})
|
|
if structure == "node" {
|
|
nodes[canvasNodeKey(board, old)] = record
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, relation := range relations {
|
|
if relation.Archived {
|
|
continue
|
|
}
|
|
key := canvasNodeKey(relation.Parent, relation.Frame)
|
|
frame := nodes[key]
|
|
if frame == nil || documents[relation.Child] == nil || children[key] != "" || relation.Child == root {
|
|
return result, errCanvasModel
|
|
}
|
|
data := frame["data"].(map[string]any)
|
|
sub, _ := data["subgraph"].(map[string]any)
|
|
if data["kind"] != "frame" || sub["boardId"] != relation.Child || result.Model.Canvases[relation.Child].Parent != nil {
|
|
return result, errCanvasModel
|
|
}
|
|
children[key] = relation.Child
|
|
result.Model.Canvases[relation.Child] = modelCanvas{ID: relation.Child, Parent: &modelParent{CanvasID: relation.Parent, EntryElementID: identities[identityKey(relation.Parent, "node", relation.Frame)]}}
|
|
}
|
|
// Ordinary legacy frameId groups also become actual canvases, even if empty.
|
|
for _, key := range sortedKeys(nodes) {
|
|
node := nodes[key]
|
|
data := node["data"].(map[string]any)
|
|
if data["kind"] != "frame" {
|
|
continue
|
|
}
|
|
if _, linked := data["subgraph"]; linked && children[key] == "" {
|
|
return result, errCanvasModel
|
|
}
|
|
if children[key] != "" {
|
|
continue
|
|
}
|
|
id, err := nextID()
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
board := strings.SplitN(key, "/", 2)[0]
|
|
children[key], synthetic[key] = id, true
|
|
result.Model.Canvases[id] = modelCanvas{ID: id, Parent: &modelParent{CanvasID: board, EntryElementID: identities[identityKey(board, "node", node["id"].(string))]}}
|
|
result.Model.Versions[id] = 1
|
|
result.NewCanvases = append(result.NewCanvases, id)
|
|
}
|
|
for _, board := range sortedKeys(documents) {
|
|
for index, value := range documents[board]["nodes"].([]any) {
|
|
node := value.(map[string]any)
|
|
original := node["data"].(map[string]any)
|
|
data := map[string]any{}
|
|
for key, value := range original {
|
|
data[key] = value
|
|
}
|
|
kind := data["kind"].(string)
|
|
owner := board
|
|
geometry := legacyModelGeometry(node, kind)
|
|
if frameID, exists := data["frameId"].(string); exists {
|
|
frame := nodes[canvasNodeKey(board, frameID)]
|
|
child := children[canvasNodeKey(board, frameID)]
|
|
if frame == nil || child == "" {
|
|
return result, errCanvasModel
|
|
}
|
|
position := frame["position"].(map[string]any)
|
|
frameData := frame["data"].(map[string]any)
|
|
scale := modelContentScale(frameData)
|
|
geometry["x"] = (geometry["x"].(float64) - position["x"].(float64) - 24) / scale
|
|
geometry["y"] = (geometry["y"].(float64) - position["y"].(float64) - 36) / scale
|
|
geometry["width"], geometry["height"] = geometry["width"].(float64)/scale, geometry["height"].(float64)/scale
|
|
owner = child
|
|
}
|
|
for _, key := range []string{"kind", "rotation", "frameId", "subgraph", "ports"} {
|
|
delete(data, key)
|
|
}
|
|
id := identities[identityKey(board, "node", node["id"].(string))]
|
|
result.Model.Elements[id] = map[string]any{"id": id, "incarnation": float64(1), "ownerCanvasId": owner, "componentId": kind, "dataVersion": float64(1), "structure": "node", "geometry": geometry, "data": data, "orderKey": fmt.Sprintf("%s/%012d", board, index)}
|
|
}
|
|
}
|
|
spaces, err := modelSpaces(result.Model)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
resolve := func(board, node, handle, direction string) (map[string]any, error) {
|
|
seen := map[string]bool{}
|
|
for depth := 0; depth <= 8; depth++ {
|
|
key := canvasNodeKey(board, node)
|
|
legacy := nodes[key]
|
|
if legacy == nil || seen[key+"/"+handle] {
|
|
return nil, errCanvasModel
|
|
}
|
|
seen[key+"/"+handle] = true
|
|
data := legacy["data"].(map[string]any)
|
|
if data["kind"] != "frame" || !strings.HasPrefix(handle, "port:") {
|
|
endpoint := map[string]any{"elementId": identities[identityKey(board, "node", node)]}
|
|
if handle != "" {
|
|
endpoint["portId"] = handle
|
|
}
|
|
return endpoint, nil
|
|
}
|
|
ports, _ := data["ports"].([]any)
|
|
found := false
|
|
for _, value := range ports {
|
|
port := value.(map[string]any)
|
|
if port["id"] == strings.TrimPrefix(handle, "port:") && port["direction"] == direction {
|
|
node, handle = modelString(port["nodeId"]), modelString(port["handle"])
|
|
if !synthetic[key] {
|
|
board = children[key]
|
|
}
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, errCanvasModel
|
|
}
|
|
}
|
|
return nil, errCanvasModel
|
|
}
|
|
for _, board := range sortedKeys(documents) {
|
|
for index, value := range documents[board]["edges"].([]any) {
|
|
edge := value.(map[string]any)
|
|
source, err := resolve(board, modelString(edge["source"]), modelString(edge["sourceHandle"]), "output")
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
target, err := resolve(board, modelString(edge["target"]), modelString(edge["targetHandle"]), "input")
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
owner, err := modelCommonCanvas(result.Model, modelString(result.Model.Elements[source["elementId"].(string)]["ownerCanvasId"]), modelString(result.Model.Elements[target["elementId"].(string)]["ownerCanvasId"]))
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
from, to := spaces[board], spaces[owner]
|
|
projectEdgePath(edge, from.Scale/to.Scale, (from.X-to.X)/to.Scale, (from.Y-to.Y)/to.Scale)
|
|
data, _ := edge["data"].(map[string]any)
|
|
if data == nil {
|
|
data = map[string]any{}
|
|
}
|
|
path := data["path"]
|
|
delete(data, "path")
|
|
id := identities[identityKey(board, "connection", edge["id"].(string))]
|
|
result.Model.Elements[id] = map[string]any{"id": id, "incarnation": float64(1), "ownerCanvasId": owner, "componentId": "connection", "dataVersion": float64(1), "structure": "connection", "source": source, "target": target, "path": path, "data": data, "orderKey": fmt.Sprintf("%s/%012d", board, index)}
|
|
}
|
|
}
|
|
if err := validateCanvasModel(result.Model); err != nil {
|
|
return result, err
|
|
}
|
|
return result, nil
|
|
}
|
|
func modelContentScale(data map[string]any) float64 {
|
|
presentation, _ := data["presentation"].(map[string]any)
|
|
if scale, ok := presentation["contentScale"].(float64); ok && scale > 0 {
|
|
return scale
|
|
}
|
|
return 1
|
|
}
|
|
func legacyModelGeometry(node map[string]any, kind string) map[string]any {
|
|
width, height := float64(260), float64(200)
|
|
switch kind {
|
|
case "plain-text":
|
|
width, height = 240, 72
|
|
case "plain-image":
|
|
width, height = 320, 240
|
|
case "icon":
|
|
width, height = 80, 80
|
|
case "frame":
|
|
width, height = 640, 400
|
|
}
|
|
if value, exists := node["width"].(float64); exists {
|
|
width = value
|
|
}
|
|
if value, exists := node["height"].(float64); exists {
|
|
height = value
|
|
}
|
|
position := node["position"].(map[string]any)
|
|
data := node["data"].(map[string]any)
|
|
rotation, _ := data["rotation"].(float64)
|
|
return map[string]any{"x": position["x"], "y": position["y"], "width": width, "height": height, "rotation": rotation}
|
|
}
|
|
func modelSpaces(model canvasModel) (map[string]modelSpace, error) {
|
|
result := map[string]modelSpace{model.Root: {Scale: 1}}
|
|
remaining := len(model.Canvases) - 1
|
|
for depth := 0; remaining > 0 && depth < 9; depth++ {
|
|
progress := false
|
|
for _, id := range sortedKeys(model.Canvases) {
|
|
canvas := model.Canvases[id]
|
|
if _, exists := result[id]; exists || canvas.Parent == nil {
|
|
continue
|
|
}
|
|
parent, exists := result[canvas.Parent.CanvasID]
|
|
if !exists {
|
|
continue
|
|
}
|
|
entry := model.Elements[canvas.Parent.EntryElementID]
|
|
if entry == nil {
|
|
return nil, errCanvasModel
|
|
}
|
|
geometry := entry["geometry"].(map[string]any)
|
|
data := entry["data"].(map[string]any)
|
|
result[id] = modelSpace{X: parent.X + (geometry["x"].(float64)+24)*parent.Scale, Y: parent.Y + (geometry["y"].(float64)+36)*parent.Scale, Scale: parent.Scale * modelContentScale(data)}
|
|
remaining--
|
|
progress = true
|
|
}
|
|
if !progress {
|
|
return nil, errCanvasModel
|
|
}
|
|
}
|
|
if remaining != 0 {
|
|
return nil, errCanvasModel
|
|
}
|
|
return result, nil
|
|
}
|