@pauloricca/spagbol 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +330 -2
  3. package/dist/controls.css +122 -0
  4. package/dist/core/controller.d.ts +89 -0
  5. package/dist/core/controller.d.ts.map +1 -0
  6. package/dist/core/controller.js +732 -0
  7. package/dist/core/controller.js.map +1 -0
  8. package/dist/core/graph.d.ts +30 -0
  9. package/dist/core/graph.d.ts.map +1 -0
  10. package/dist/core/graph.js +318 -0
  11. package/dist/core/graph.js.map +1 -0
  12. package/dist/core/grouping.d.ts +10 -0
  13. package/dist/core/grouping.d.ts.map +1 -0
  14. package/dist/core/grouping.js +130 -0
  15. package/dist/core/grouping.js.map +1 -0
  16. package/dist/core/groups.d.ts +40 -0
  17. package/dist/core/groups.d.ts.map +1 -0
  18. package/dist/core/groups.js +164 -0
  19. package/dist/core/groups.js.map +1 -0
  20. package/dist/core/index.d.ts +8 -0
  21. package/dist/core/index.d.ts.map +1 -0
  22. package/dist/core/index.js +8 -0
  23. package/dist/core/index.js.map +1 -0
  24. package/dist/core/model.d.ts +182 -0
  25. package/dist/core/model.d.ts.map +1 -0
  26. package/dist/core/model.js +149 -0
  27. package/dist/core/model.js.map +1 -0
  28. package/dist/core/observations.d.ts +10 -0
  29. package/dist/core/observations.d.ts.map +1 -0
  30. package/dist/core/observations.js +36 -0
  31. package/dist/core/observations.js.map +1 -0
  32. package/dist/core/serialization.d.ts +9 -0
  33. package/dist/core/serialization.d.ts.map +1 -0
  34. package/dist/core/serialization.js +148 -0
  35. package/dist/core/serialization.js.map +1 -0
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/link-controls.css +123 -0
  41. package/dist/react/Controls.d.ts +18 -0
  42. package/dist/react/Controls.d.ts.map +1 -0
  43. package/dist/react/Controls.js +301 -0
  44. package/dist/react/Controls.js.map +1 -0
  45. package/dist/react/SpagbolCanvas.d.ts +38 -0
  46. package/dist/react/SpagbolCanvas.d.ts.map +1 -0
  47. package/dist/react/SpagbolCanvas.js +1210 -0
  48. package/dist/react/SpagbolCanvas.js.map +1 -0
  49. package/dist/react/SpagbolCanvasControls.d.ts +36 -0
  50. package/dist/react/SpagbolCanvasControls.d.ts.map +1 -0
  51. package/dist/react/SpagbolCanvasControls.js +113 -0
  52. package/dist/react/SpagbolCanvasControls.js.map +1 -0
  53. package/dist/react/SpagbolCanvasFlow.d.ts +14 -0
  54. package/dist/react/SpagbolCanvasFlow.d.ts.map +1 -0
  55. package/dist/react/SpagbolCanvasFlow.js +6 -0
  56. package/dist/react/SpagbolCanvasFlow.js.map +1 -0
  57. package/dist/react/SpagbolCanvasLinkControls.d.ts +16 -0
  58. package/dist/react/SpagbolCanvasLinkControls.d.ts.map +1 -0
  59. package/dist/react/SpagbolCanvasLinkControls.js +7 -0
  60. package/dist/react/SpagbolCanvasLinkControls.js.map +1 -0
  61. package/dist/react/SpagbolCanvasMenu.d.ts +12 -0
  62. package/dist/react/SpagbolCanvasMenu.d.ts.map +1 -0
  63. package/dist/react/SpagbolCanvasMenu.js +65 -0
  64. package/dist/react/SpagbolCanvasMenu.js.map +1 -0
  65. package/dist/react/SpagbolControlNumber.d.ts +25 -0
  66. package/dist/react/SpagbolControlNumber.d.ts.map +1 -0
  67. package/dist/react/SpagbolControlNumber.js +209 -0
  68. package/dist/react/SpagbolControlNumber.js.map +1 -0
  69. package/dist/react/SpagbolControlNumberLink.d.ts +6 -0
  70. package/dist/react/SpagbolControlNumberLink.d.ts.map +1 -0
  71. package/dist/react/SpagbolControlNumberLink.js +159 -0
  72. package/dist/react/SpagbolControlNumberLink.js.map +1 -0
  73. package/dist/react/SpagbolControlZoom.d.ts +11 -0
  74. package/dist/react/SpagbolControlZoom.d.ts.map +1 -0
  75. package/dist/react/SpagbolControlZoom.js +24 -0
  76. package/dist/react/SpagbolControlZoom.js.map +1 -0
  77. package/dist/react/SpagbolNodeFrame.d.ts +35 -0
  78. package/dist/react/SpagbolNodeFrame.d.ts.map +1 -0
  79. package/dist/react/SpagbolNodeFrame.js +169 -0
  80. package/dist/react/SpagbolNodeFrame.js.map +1 -0
  81. package/dist/react/SpagbolNodeFrameTypePicker.d.ts +9 -0
  82. package/dist/react/SpagbolNodeFrameTypePicker.d.ts.map +1 -0
  83. package/dist/react/SpagbolNodeFrameTypePicker.js +68 -0
  84. package/dist/react/SpagbolNodeFrameTypePicker.js.map +1 -0
  85. package/dist/react/SpagbolNodeGroupPort.d.ts +15 -0
  86. package/dist/react/SpagbolNodeGroupPort.d.ts.map +1 -0
  87. package/dist/react/SpagbolNodeGroupPort.js +62 -0
  88. package/dist/react/SpagbolNodeGroupPort.js.map +1 -0
  89. package/dist/react/SpagbolNodeGroupPortAdd.d.ts +5 -0
  90. package/dist/react/SpagbolNodeGroupPortAdd.d.ts.map +1 -0
  91. package/dist/react/SpagbolNodeGroupPortAdd.js +27 -0
  92. package/dist/react/SpagbolNodeGroupPortAdd.js.map +1 -0
  93. package/dist/react/context.d.ts +84 -0
  94. package/dist/react/context.d.ts.map +1 -0
  95. package/dist/react/context.js +43 -0
  96. package/dist/react/context.js.map +1 -0
  97. package/dist/react/duplicateDrag.d.ts +19 -0
  98. package/dist/react/duplicateDrag.d.ts.map +1 -0
  99. package/dist/react/duplicateDrag.js +71 -0
  100. package/dist/react/duplicateDrag.js.map +1 -0
  101. package/dist/react/flowPresentation.d.ts +11 -0
  102. package/dist/react/flowPresentation.d.ts.map +1 -0
  103. package/dist/react/flowPresentation.js +50 -0
  104. package/dist/react/flowPresentation.js.map +1 -0
  105. package/dist/react/geometry.d.ts +13 -0
  106. package/dist/react/geometry.d.ts.map +1 -0
  107. package/dist/react/geometry.js +43 -0
  108. package/dist/react/geometry.js.map +1 -0
  109. package/dist/react/index.d.ts +11 -0
  110. package/dist/react/index.d.ts.map +1 -0
  111. package/dist/react/index.js +11 -0
  112. package/dist/react/index.js.map +1 -0
  113. package/dist/react/portState.d.ts +10 -0
  114. package/dist/react/portState.d.ts.map +1 -0
  115. package/dist/react/portState.js +35 -0
  116. package/dist/react/portState.js.map +1 -0
  117. package/dist/react/viewport.d.ts +24 -0
  118. package/dist/react/viewport.d.ts.map +1 -0
  119. package/dist/react/viewport.js +55 -0
  120. package/dist/react/viewport.js.map +1 -0
  121. package/dist/styles.css +748 -0
  122. package/package.json +58 -4
  123. package/src/core/controller.ts +844 -0
  124. package/src/core/graph.ts +451 -0
  125. package/src/core/grouping.ts +159 -0
  126. package/src/core/groups.ts +125 -0
  127. package/src/core/index.ts +7 -0
  128. package/src/core/model.ts +304 -0
  129. package/src/core/observations.ts +31 -0
  130. package/src/core/serialization.ts +184 -0
  131. package/src/index.ts +9 -0
  132. package/src/react/Controls.tsx +355 -0
  133. package/src/react/SpagbolCanvas.tsx +1859 -0
  134. package/src/react/SpagbolCanvasControls.tsx +255 -0
  135. package/src/react/SpagbolCanvasFlow.tsx +38 -0
  136. package/src/react/SpagbolCanvasLinkControls.tsx +21 -0
  137. package/src/react/SpagbolCanvasMenu.tsx +121 -0
  138. package/src/react/SpagbolControlNumber.tsx +297 -0
  139. package/src/react/SpagbolControlNumberLink.tsx +209 -0
  140. package/src/react/SpagbolControlZoom.tsx +102 -0
  141. package/src/react/SpagbolNodeFrame.tsx +329 -0
  142. package/src/react/SpagbolNodeFrameTypePicker.tsx +51 -0
  143. package/src/react/SpagbolNodeGroupPort.tsx +111 -0
  144. package/src/react/SpagbolNodeGroupPortAdd.tsx +48 -0
  145. package/src/react/context.tsx +146 -0
  146. package/src/react/controls.css +122 -0
  147. package/src/react/duplicateDrag.ts +97 -0
  148. package/src/react/flowPresentation.ts +56 -0
  149. package/src/react/geometry.ts +53 -0
  150. package/src/react/index.ts +61 -0
  151. package/src/react/link-controls.css +123 -0
  152. package/src/react/portState.ts +24 -0
  153. package/src/react/styles.css +748 -0
  154. package/src/react/viewport.ts +90 -0
@@ -0,0 +1,149 @@
1
+ export function emptyGraph() {
2
+ return { format: "spagbol", version: 1, nodes: [], links: [], areas: [] };
3
+ }
4
+ export function emptySelection() {
5
+ return { nodeIds: [], linkIds: [], areaIds: [] };
6
+ }
7
+ export function clone(value) {
8
+ return JSON.parse(JSON.stringify(value));
9
+ }
10
+ export function createId(prefix = "item") {
11
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
12
+ }
13
+ export function graphAt(document, path) {
14
+ let graph = document;
15
+ for (const id of path) {
16
+ const nested = graph.nodes.find((node) => node.id === id)?.graph;
17
+ if (!nested)
18
+ throw new Error(`Nested graph ${id} does not exist.`);
19
+ graph = nested;
20
+ }
21
+ return graph;
22
+ }
23
+ export function validPath(document, path) {
24
+ const result = [];
25
+ let graph = document;
26
+ for (const id of path) {
27
+ const nested = graph.nodes.find((node) => node.id === id)?.graph;
28
+ if (!nested)
29
+ break;
30
+ result.push(id);
31
+ graph = nested;
32
+ }
33
+ return result;
34
+ }
35
+ const immutablePortCache = new WeakMap();
36
+ export function portsFor(node, definitions) {
37
+ if (!Object.isFrozen(node))
38
+ return definitions.get(node.kind)?.ports(node) ?? node.ports ?? [];
39
+ let registries = immutablePortCache.get(node);
40
+ if (!registries) {
41
+ registries = new WeakMap();
42
+ immutablePortCache.set(node, registries);
43
+ }
44
+ let ports = registries.get(definitions);
45
+ if (!ports) {
46
+ ports = definitions.get(node.kind)?.ports(node) ?? node.ports ?? [];
47
+ registries.set(definitions, ports);
48
+ }
49
+ return ports;
50
+ }
51
+ export const spagbolGroupKinds = { group: "spagbol.group", inputs: "spagbol.group.inputs", outputs: "spagbol.group.outputs" };
52
+ const groupDefinitions = [
53
+ { kind: spagbolGroupKinds.group, label: 'Group', category: 'graph', createData: () => ({}), ports: node => node.ports ?? [], createGraph: () => ({ ...emptyGraph(), nodes: [
54
+ { id: createId('node'), kind: spagbolGroupKinds.inputs, position: { x: 0, y: 0 }, data: {}, values: {}, ports: [] },
55
+ { id: createId('node'), kind: spagbolGroupKinds.outputs, position: { x: 500, y: 0 }, data: {}, values: {}, ports: [] },
56
+ ] }) },
57
+ { kind: spagbolGroupKinds.inputs, label: 'Ins', category: 'graph', createData: () => ({}), ports: node => node.ports ?? [] },
58
+ { kind: spagbolGroupKinds.outputs, label: 'Outs', category: 'graph', createData: () => ({}), ports: node => node.ports ?? [] },
59
+ ];
60
+ export class NodeRegistry {
61
+ #definitions = new Map();
62
+ constructor(definitions = []) {
63
+ for (const definition of [...groupDefinitions, ...definitions]) {
64
+ if (this.#definitions.has(definition.kind))
65
+ throw new Error(`Duplicate node kind ${definition.kind}.`);
66
+ this.#definitions.set(definition.kind, definition);
67
+ }
68
+ }
69
+ get(kind) {
70
+ return this.#definitions.get(kind);
71
+ }
72
+ list() {
73
+ return [...this.#definitions.values()];
74
+ }
75
+ create(kind, position, id = createId("node")) {
76
+ const definition = this.get(kind);
77
+ if (!definition)
78
+ throw new Error(`Unknown node kind ${kind}.`);
79
+ const node = {
80
+ id,
81
+ kind,
82
+ position: { ...position },
83
+ data: definition.createData(),
84
+ values: {},
85
+ };
86
+ if (definition.createGraph) {
87
+ node.graph = definition.createGraph();
88
+ node.sharedGraphId = createId('shared');
89
+ node.ports = [];
90
+ }
91
+ if (definition.defaultSize)
92
+ node.size = { ...definition.defaultSize };
93
+ node.values = Object.fromEntries(definition
94
+ .ports(node)
95
+ .filter((port) => port.direction === "input")
96
+ .map((port) => [port.id, port.defaultValue ?? 0]));
97
+ return node;
98
+ }
99
+ }
100
+ export function defineNode(definition) {
101
+ return definition;
102
+ }
103
+ export const standardLinkModes = Object.freeze([
104
+ {
105
+ id: "set",
106
+ label: "set",
107
+ color: "#fff",
108
+ shortcut: "s",
109
+ description: "Average weighted set sources, replacing the local base value.",
110
+ },
111
+ {
112
+ id: "add",
113
+ label: "add",
114
+ color: "#ffe45c",
115
+ shortcut: "a",
116
+ description: "Add weighted sources after set values or the local base.",
117
+ },
118
+ {
119
+ id: "multiply",
120
+ label: "multiply",
121
+ color: "#8fdcff",
122
+ shortcut: "m",
123
+ description: "Multiply by each weighted source after addition.",
124
+ },
125
+ {
126
+ id: "bend",
127
+ label: "bend",
128
+ color: "#c084fc",
129
+ shortcut: "b",
130
+ description: "Scale by 2^(source × weight) after multiplication.",
131
+ },
132
+ ]);
133
+ export class LinkModeRegistry {
134
+ #definitions = new Map();
135
+ constructor(definitions = standardLinkModes) {
136
+ for (const definition of definitions) {
137
+ if (this.#definitions.has(definition.id))
138
+ throw new Error(`Duplicate link mode ${definition.id}.`);
139
+ this.#definitions.set(definition.id, Object.freeze({ ...definition }));
140
+ }
141
+ }
142
+ get(id) {
143
+ return this.#definitions.get(id);
144
+ }
145
+ list() {
146
+ return [...this.#definitions.values()];
147
+ }
148
+ }
149
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/core/model.ts"],"names":[],"mappings":"AAwJA,MAAM,UAAU,UAAU;IACxB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AAC5E,CAAC;AACD,MAAM,UAAU,cAAc;IAC5B,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;AACnD,CAAC;AACD,MAAM,UAAU,KAAK,CAAI,KAAQ;IAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAM,CAAC;AAChD,CAAC;AACD,MAAM,UAAU,QAAQ,CAAC,MAAM,GAAG,MAAM;IACtC,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAC3F,CAAC;AACD,MAAM,UAAU,OAAO,CACrB,QAAuB,EACvB,IAAuB;IAEvB,IAAI,KAAK,GAAG,QAAQ,CAAC;IACrB,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC;QACjE,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;QACnE,KAAK,GAAG,MAAM,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,SAAS,CACvB,QAAuB,EACvB,IAAuB;IAEvB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,GAAG,QAAQ,CAAC;IACrB,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC;QACjE,IAAI,CAAC,MAAM;YAAE,MAAM;QACnB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChB,KAAK,GAAG,MAAM,CAAC;IACjB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AACD,MAAM,kBAAkB,GAAG,IAAI,OAAO,EAA+D,CAAC;AACtG,MAAM,UAAU,QAAQ,CACtB,IAAe,EACf,WAAyB;IAEzB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAC/F,IAAI,UAAU,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,EAAE,CAAC;QAAC,UAAU,GAAG,IAAI,OAAO,EAAE,CAAC;QAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAAC,CAAC;IAC1F,IAAI,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK,EAAE,CAAC;QAAC,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAAC,UAAU,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAAC,CAAC;IACxH,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,sBAAsB,EAAE,OAAO,EAAE,uBAAuB,EAAW,CAAC;AACvI,MAAM,gBAAgB,GAA8B;IAClD,EAAE,IAAI,EAAE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,EAAE,KAAK,EAAE;gBACzK,EAAE,EAAE,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;gBACnH,EAAE,EAAE,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;aACvH,EAAE,CAAC,EAAE;IACN,EAAE,IAAI,EAAE,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE;IAC5H,EAAE,IAAI,EAAE,iBAAiB,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE;CAC/H,CAAC;AAEF,MAAM,OAAO,YAAY;IACd,YAAY,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC1D,YAAY,cAAyC,EAAE;QACrD,KAAK,MAAM,UAAU,IAAI,CAAC,GAAG,gBAAgB,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC;YAC/D,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC;gBACxC,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,CAAC,IAAI,GAAG,CAAC,CAAC;YAC7D,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IACD,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IACD,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,MAAM,CAAC,IAAY,EAAE,QAAe,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC;QACzD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,UAAU;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,IAAI,GAAG,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAc;YACtB,EAAE;YACF,IAAI;YACJ,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE;YACzB,IAAI,EAAE,UAAU,CAAC,UAAU,EAAE;YAC7B,MAAM,EAAE,EAAE;SACX,CAAC;QACF,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;YACtC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAClB,CAAC;QACD,IAAI,UAAU,CAAC,WAAW;YAAE,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;QACtE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,CAC9B,UAAU;aACP,KAAK,CAAC,IAAI,CAAC;aACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC;aAC5C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,CAAC,CACpD,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AACD,MAAM,UAAU,UAAU,CACxB,UAA6B;IAE7B,OAAO,UAAU,CAAC;AACpB,CAAC;AACD,MAAM,CAAC,MAAM,iBAAiB,GAAkC,MAAM,CAAC,MAAM,CAAC;IAC5E;QACE,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,KAAK;QACZ,KAAK,EAAE,MAAM;QACb,QAAQ,EAAE,GAAG;QACb,WAAW,EACT,+DAA+D;KAClE;IACD;QACE,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,KAAK;QACZ,KAAK,EAAE,SAAS;QAChB,QAAQ,EAAE,GAAG;QACb,WAAW,EAAE,0DAA0D;KACxE;IACD;QACE,EAAE,EAAE,UAAU;QACd,KAAK,EAAE,UAAU;QACjB,KAAK,EAAE,SAAS;QAChB,QAAQ,EAAE,GAAG;QACb,WAAW,EAAE,kDAAkD;KAChE;IACD;QACE,EAAE,EAAE,MAAM;QACV,KAAK,EAAE,MAAM;QACb,KAAK,EAAE,SAAS;QAChB,QAAQ,EAAE,GAAG;QACb,WAAW,EAAE,oDAAoD;KAClE;CACF,CAAC,CAAC;AACH,MAAM,OAAO,gBAAgB;IAClB,YAAY,GAAG,IAAI,GAAG,EAA8B,CAAC;IAC9D,YAAY,cAA6C,iBAAiB;QACxE,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;YACrC,IAAI,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;gBACtC,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,CAAC,EAAE,GAAG,CAAC,CAAC;YAC3D,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IACD,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IACD,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,CAAC;CACF","sourcesContent":["/** Saved data is JSON only. Functions, observers and UI state never enter documents. */\nexport type JsonValue =\n | null\n | boolean\n | number\n | string\n | JsonValue[]\n | { [key: string]: JsonValue };\nexport type NodeData = { [key: string]: JsonValue };\nexport interface Point {\n x: number;\n y: number;\n}\nexport interface Size {\n width: number;\n height: number;\n}\nexport interface PortDefinition {\n id: string;\n label: string;\n direction: \"input\" | \"output\";\n dataType?: string;\n connectable?: boolean;\n valueEditor?: boolean;\n defaultValue?: number;\n min?: number;\n max?: number;\n step?: number;\n integer?: boolean;\n}\nexport interface GraphNode<D extends NodeData = NodeData> {\n id: string;\n kind: string;\n label?: string;\n position: Point;\n size?: Size;\n compact?: boolean;\n compactLabel?: string;\n scale?: number;\n enabled?: boolean;\n data: D;\n values: Record<string, number>;\n /** Explicit/dynamic instance ports; a definition may derive ports instead. */\n ports?: PortDefinition[];\n graph?: GraphDocument;\n /** Equal IDs opt into shared nested structure; instance values stay local. */\n sharedGraphId?: string;\n /** Explicit nested nodes to show on this parent. No title conventions in core. */\n panelNodeIds?: string[];\n overrides?: Record<string, Record<string, number>>;\n dataOverrides?: Record<string, NodeData>;\n}\nexport interface PortEndpoint {\n nodeId: string;\n portId: string;\n}\nexport type LinkTarget =\n | ({ type: \"port\" } & PortEndpoint)\n | { type: \"link\"; linkId: string; property: string };\nexport interface GraphLink {\n id: string;\n source: PortEndpoint;\n target: LinkTarget;\n mode: string;\n weight: number;\n enabled?: boolean;\n data?: NodeData;\n}\nexport interface GraphArea {\n id: string;\n /** A domain node whose UI/layout is owned by this area. */\n ownerNodeId?: string;\n title: string;\n position: Point;\n size: Size;\n collapsed?: boolean;\n locked?: boolean;\n nodeIds?: string[];\n areaIds?: string[];\n /** The top portion remains operable while the rest is collapsed. */\n controlHeight?: number;\n data?: NodeData;\n}\nexport interface GraphDocument {\n format: \"spagbol\";\n version: 1;\n nodes: GraphNode[];\n links: GraphLink[];\n areas: GraphArea[];\n data?: NodeData;\n}\nexport interface Selection {\n port?: { nodeId: string; portId: string; direction: \"input\" | \"output\" };\n nodeIds: string[];\n linkIds: string[];\n areaIds: string[];\n}\nexport interface Diagnostic {\n severity: \"info\" | \"warning\" | \"error\";\n code: string;\n message: string;\n path: readonly string[];\n nodeId?: string;\n linkId?: string;\n portId?: string;\n}\nexport interface LinkModeDefinition {\n id: string;\n label: string;\n color: string;\n shortcut?: string;\n description: string;\n}\nexport interface NodeDefinition<D extends NodeData = NodeData> {\n kind: string;\n label: string;\n category?: string;\n description?: string;\n keywords?: string[];\n createData(): D;\n createGraph?(): GraphDocument;\n editablePorts?: \"input\" | \"output\";\n newPortLabel?: string;\n createPort?(node: GraphNode<D>, label: string): PortDefinition;\n ports(node: GraphNode<D>): readonly PortDefinition[];\n defaultSize?: Size;\n validate?(node: GraphNode<D>): readonly string[];\n /** App code can remap asset or domain references when duplicating. */\n cloneData?(data: D, ids: ReadonlyMap<string, string>): D;\n}\nexport interface GraphPolicy {\n /** All registered modes are allowed unless the consumer supplies a subset. */\n modes?: readonly string[];\n linkProperties?: readonly string[];\n allowSelfLinks?: boolean;\n allowDuplicateLinks?: boolean;\n /** Host metadata update when deleting a selected editable domain port. */\n deletePort?(node: GraphNode, port: PortDefinition): void;\n canConnect?(\n source: PortEndpoint,\n target: LinkTarget,\n graph: GraphDocument,\n ): string | undefined;\n}\nexport interface ChangeEvent {\n label: string;\n kinds: readonly ChangeKind[];\n document: GraphDocument;\n path: readonly string[];\n origin: \"edit\" | \"undo\" | \"redo\" | \"replace\";\n}\nexport type ChangeKind = \"structure\" | \"values\" | \"layout\" | \"metadata\";\nexport function emptyGraph(): GraphDocument {\n return { format: \"spagbol\", version: 1, nodes: [], links: [], areas: [] };\n}\nexport function emptySelection(): Selection {\n return { nodeIds: [], linkIds: [], areaIds: [] };\n}\nexport function clone<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\nexport function createId(prefix = \"item\"): string {\n return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\nexport function graphAt(\n document: GraphDocument,\n path: readonly string[],\n): GraphDocument {\n let graph = document;\n for (const id of path) {\n const nested = graph.nodes.find((node) => node.id === id)?.graph;\n if (!nested) throw new Error(`Nested graph ${id} does not exist.`);\n graph = nested;\n }\n return graph;\n}\nexport function validPath(\n document: GraphDocument,\n path: readonly string[],\n): string[] {\n const result: string[] = [];\n let graph = document;\n for (const id of path) {\n const nested = graph.nodes.find((node) => node.id === id)?.graph;\n if (!nested) break;\n result.push(id);\n graph = nested;\n }\n return result;\n}\nconst immutablePortCache = new WeakMap<GraphNode, WeakMap<NodeRegistry, readonly PortDefinition[]>>();\nexport function portsFor(\n node: GraphNode,\n definitions: NodeRegistry,\n): readonly PortDefinition[] {\n if (!Object.isFrozen(node)) return definitions.get(node.kind)?.ports(node) ?? node.ports ?? [];\n let registries = immutablePortCache.get(node);\n if (!registries) { registries = new WeakMap(); immutablePortCache.set(node, registries); }\n let ports = registries.get(definitions);\n if (!ports) { ports = definitions.get(node.kind)?.ports(node) ?? node.ports ?? []; registries.set(definitions, ports); }\n return ports;\n}\nexport const spagbolGroupKinds = { group: \"spagbol.group\", inputs: \"spagbol.group.inputs\", outputs: \"spagbol.group.outputs\" } as const;\nconst groupDefinitions: readonly NodeDefinition[] = [\n { kind: spagbolGroupKinds.group, label: 'Group', category: 'graph', createData: () => ({}), ports: node => node.ports ?? [], createGraph: () => ({ ...emptyGraph(), nodes: [\n { id: createId('node'), kind: spagbolGroupKinds.inputs, position: { x: 0, y: 0 }, data: {}, values: {}, ports: [] },\n { id: createId('node'), kind: spagbolGroupKinds.outputs, position: { x: 500, y: 0 }, data: {}, values: {}, ports: [] },\n ] }) },\n { kind: spagbolGroupKinds.inputs, label: 'Ins', category: 'graph', createData: () => ({}), ports: node => node.ports ?? [] },\n { kind: spagbolGroupKinds.outputs, label: 'Outs', category: 'graph', createData: () => ({}), ports: node => node.ports ?? [] },\n];\n\nexport class NodeRegistry {\n readonly #definitions = new Map<string, NodeDefinition>();\n constructor(definitions: readonly NodeDefinition[] = []) {\n for (const definition of [...groupDefinitions, ...definitions]) {\n if (this.#definitions.has(definition.kind))\n throw new Error(`Duplicate node kind ${definition.kind}.`);\n this.#definitions.set(definition.kind, definition);\n }\n }\n get(kind: string): NodeDefinition | undefined {\n return this.#definitions.get(kind);\n }\n list(): readonly NodeDefinition[] {\n return [...this.#definitions.values()];\n }\n create(kind: string, position: Point, id = createId(\"node\")): GraphNode {\n const definition = this.get(kind);\n if (!definition) throw new Error(`Unknown node kind ${kind}.`);\n const node: GraphNode = {\n id,\n kind,\n position: { ...position },\n data: definition.createData(),\n values: {},\n };\n if (definition.createGraph) {\n node.graph = definition.createGraph();\n node.sharedGraphId = createId('shared');\n node.ports = [];\n }\n if (definition.defaultSize) node.size = { ...definition.defaultSize };\n node.values = Object.fromEntries(\n definition\n .ports(node)\n .filter((port) => port.direction === \"input\")\n .map((port) => [port.id, port.defaultValue ?? 0]),\n );\n return node;\n }\n}\nexport function defineNode<D extends NodeData>(\n definition: NodeDefinition<D>,\n): NodeDefinition<D> {\n return definition;\n}\nexport const standardLinkModes: readonly LinkModeDefinition[] = Object.freeze([\n {\n id: \"set\",\n label: \"set\",\n color: \"#fff\",\n shortcut: \"s\",\n description:\n \"Average weighted set sources, replacing the local base value.\",\n },\n {\n id: \"add\",\n label: \"add\",\n color: \"#ffe45c\",\n shortcut: \"a\",\n description: \"Add weighted sources after set values or the local base.\",\n },\n {\n id: \"multiply\",\n label: \"multiply\",\n color: \"#8fdcff\",\n shortcut: \"m\",\n description: \"Multiply by each weighted source after addition.\",\n },\n {\n id: \"bend\",\n label: \"bend\",\n color: \"#c084fc\",\n shortcut: \"b\",\n description: \"Scale by 2^(source × weight) after multiplication.\",\n },\n]);\nexport class LinkModeRegistry {\n readonly #definitions = new Map<string, LinkModeDefinition>();\n constructor(definitions: readonly LinkModeDefinition[] = standardLinkModes) {\n for (const definition of definitions) {\n if (this.#definitions.has(definition.id))\n throw new Error(`Duplicate link mode ${definition.id}.`);\n this.#definitions.set(definition.id, Object.freeze({ ...definition }));\n }\n }\n get(id: string): LinkModeDefinition | undefined {\n return this.#definitions.get(id);\n }\n list(): readonly LinkModeDefinition[] {\n return [...this.#definitions.values()];\n }\n}\n"]}
@@ -0,0 +1,10 @@
1
+ /** Runtime data is instance-scoped, externally supplied and absent from history. */
2
+ export declare class ObservationStore<T = unknown> {
3
+ #private;
4
+ key(path: readonly string[], nodeId: string, instance?: string): string;
5
+ get(key: string): T | undefined;
6
+ set(key: string, value: T): void;
7
+ subscribe(key: string, listener: () => void): () => void;
8
+ clear(): void;
9
+ }
10
+ //# sourceMappingURL=observations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observations.d.ts","sourceRoot":"","sources":["../../src/core/observations.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,qBAAa,gBAAgB,CAAC,CAAC,GAAG,OAAO;;IAGvC,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAK,GAAG,MAAM;IAGnE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAG/B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAKhC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IASxD,KAAK,IAAI,IAAI;CAMd"}
@@ -0,0 +1,36 @@
1
+ /** Runtime data is instance-scoped, externally supplied and absent from history. */
2
+ export class ObservationStore {
3
+ #values = new Map();
4
+ #listeners = new Map();
5
+ key(path, nodeId, instance = "") {
6
+ return JSON.stringify([path, nodeId, instance]);
7
+ }
8
+ get(key) {
9
+ return this.#values.get(key);
10
+ }
11
+ set(key, value) {
12
+ if (Object.is(this.#values.get(key), value))
13
+ return;
14
+ this.#values.set(key, value);
15
+ for (const listener of this.#listeners.get(key) ?? [])
16
+ listener();
17
+ }
18
+ subscribe(key, listener) {
19
+ const listeners = this.#listeners.get(key) ?? new Set();
20
+ listeners.add(listener);
21
+ this.#listeners.set(key, listeners);
22
+ return () => {
23
+ listeners.delete(listener);
24
+ if (!listeners.size)
25
+ this.#listeners.delete(key);
26
+ };
27
+ }
28
+ clear() {
29
+ const keys = [...this.#values.keys()];
30
+ this.#values.clear();
31
+ for (const key of keys)
32
+ for (const listener of this.#listeners.get(key) ?? [])
33
+ listener();
34
+ }
35
+ }
36
+ //# sourceMappingURL=observations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observations.js","sourceRoot":"","sources":["../../src/core/observations.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,OAAO,gBAAgB;IAClB,OAAO,GAAG,IAAI,GAAG,EAAa,CAAC;IAC/B,UAAU,GAAG,IAAI,GAAG,EAA2B,CAAC;IACzD,GAAG,CAAC,IAAuB,EAAE,MAAc,EAAE,QAAQ,GAAG,EAAE;QACxD,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IACD,GAAG,CAAC,GAAW,EAAE,KAAQ;QACvB,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC;YAAE,OAAO;QACpD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE;YAAE,QAAQ,EAAE,CAAC;IACpE,CAAC;IACD,SAAS,CAAC,GAAW,EAAE,QAAoB;QACzC,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;QACxD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QACpC,OAAO,GAAG,EAAE;YACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC3B,IAAI,CAAC,SAAS,CAAC,IAAI;gBAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnD,CAAC,CAAC;IACJ,CAAC;IACD,KAAK;QACH,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,KAAK,MAAM,GAAG,IAAI,IAAI;YACpB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE;gBAAE,QAAQ,EAAE,CAAC;IACtE,CAAC;CACF","sourcesContent":["/** Runtime data is instance-scoped, externally supplied and absent from history. */\nexport class ObservationStore<T = unknown> {\n readonly #values = new Map<string, T>();\n readonly #listeners = new Map<string, Set<() => void>>();\n key(path: readonly string[], nodeId: string, instance = \"\"): string {\n return JSON.stringify([path, nodeId, instance]);\n }\n get(key: string): T | undefined {\n return this.#values.get(key);\n }\n set(key: string, value: T): void {\n if (Object.is(this.#values.get(key), value)) return;\n this.#values.set(key, value);\n for (const listener of this.#listeners.get(key) ?? []) listener();\n }\n subscribe(key: string, listener: () => void): () => void {\n const listeners = this.#listeners.get(key) ?? new Set();\n listeners.add(listener);\n this.#listeners.set(key, listeners);\n return () => {\n listeners.delete(listener);\n if (!listeners.size) this.#listeners.delete(key);\n };\n }\n clear(): void {\n const keys = [...this.#values.keys()];\n this.#values.clear();\n for (const key of keys)\n for (const listener of this.#listeners.get(key) ?? []) listener();\n }\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import { type GraphDocument, type JsonValue } from "./model.js";
2
+ import type { GraphFragment } from "./graph.js";
3
+ export declare function isJsonValue(value: unknown, ancestors?: Set<object>): value is JsonValue;
4
+ export declare function assertDocument(value: unknown): asserts value is GraphDocument;
5
+ /** Preserve array order. Never sort links as a side effect of serialization. */
6
+ export declare function writeDocument(value: GraphDocument): string;
7
+ export declare function readDocument(input: string | unknown): GraphDocument;
8
+ export declare function readFragment(input: string | unknown): GraphFragment;
9
+ //# sourceMappingURL=serialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialization.d.ts","sourceRoot":"","sources":["../../src/core/serialization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAmChD,wBAAgB,WAAW,CACzB,KAAK,EAAE,OAAO,EACd,SAAS,cAAoB,GAC5B,KAAK,IAAI,SAAS,CAgBpB;AAoGD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAK7E;AACD,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAG1D;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,aAAa,CAInE;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,aAAa,CAYnE"}
@@ -0,0 +1,148 @@
1
+ import { clone } from "./model.js";
2
+ function record(value) {
3
+ return !!value && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function finite(value) {
6
+ return typeof value === "number" && Number.isFinite(value);
7
+ }
8
+ function string(value) {
9
+ return typeof value === "string" && value.length > 0;
10
+ }
11
+ function point(value) {
12
+ return record(value) && finite(value.x) && finite(value.y);
13
+ }
14
+ function size(value) {
15
+ return (record(value) &&
16
+ finite(value.width) &&
17
+ value.width > 0 &&
18
+ finite(value.height) &&
19
+ value.height > 0);
20
+ }
21
+ function endpoint(value) {
22
+ return record(value) && string(value.nodeId) && string(value.portId);
23
+ }
24
+ function optional(value, predicate) {
25
+ return value === undefined || predicate(value);
26
+ }
27
+ function numberRecord(value) {
28
+ return record(value) && Object.values(value).every(finite);
29
+ }
30
+ export function isJsonValue(value, ancestors = new Set()) {
31
+ if (value === null ||
32
+ typeof value === "string" ||
33
+ typeof value === "boolean" ||
34
+ finite(value))
35
+ return true;
36
+ if (!Array.isArray(value) && !record(value))
37
+ return false;
38
+ if (ancestors.has(value))
39
+ return false;
40
+ const next = new Set([...ancestors, value]);
41
+ return (Object.getPrototypeOf(value) ===
42
+ (Array.isArray(value) ? Array.prototype : Object.prototype) &&
43
+ Object.values(value).every((child) => isJsonValue(child, next)));
44
+ }
45
+ function boolean(value) {
46
+ return typeof value === "boolean";
47
+ }
48
+ function strings(value) {
49
+ return Array.isArray(value) && value.every(string);
50
+ }
51
+ function node(value, depth) {
52
+ return (record(value) &&
53
+ string(value.id) &&
54
+ string(value.kind) &&
55
+ point(value.position) &&
56
+ record(value.data) &&
57
+ isJsonValue(value.data) &&
58
+ numberRecord(value.values) &&
59
+ optional(value.label, (text) => typeof text === "string") &&
60
+ optional(value.size, size) &&
61
+ optional(value.enabled, boolean) &&
62
+ optional(value.compact, boolean) &&
63
+ optional(value.compactLabel, text => typeof text === "string") &&
64
+ optional(value.scale, scale => finite(scale) && scale > 0) &&
65
+ optional(value.sharedGraphId, string) &&
66
+ optional(value.panelNodeIds, strings) &&
67
+ optional(value.dataOverrides, values => record(values) && Object.values(values).every(item => record(item) && isJsonValue(item))) &&
68
+ optional(value.overrides, (overrides) => record(overrides) && Object.values(overrides).every(numberRecord)) &&
69
+ optional(value.ports, (ports) => Array.isArray(ports) &&
70
+ ports.every((port) => record(port) &&
71
+ string(port.id) &&
72
+ string(port.label) &&
73
+ (port.direction === "input" || port.direction === "output") &&
74
+ optional(port.dataType, string) &&
75
+ optional(port.connectable, boolean) &&
76
+ optional(port.valueEditor, boolean) &&
77
+ optional(port.integer, boolean) &&
78
+ ["defaultValue", "min", "max", "step"].every((key) => optional(port[key], finite)) &&
79
+ !(finite(port.min) && finite(port.max) && port.min > port.max))) &&
80
+ optional(value.graph, (graph) => document(graph, depth + 1)));
81
+ }
82
+ function link(value) {
83
+ return (record(value) &&
84
+ string(value.id) &&
85
+ endpoint(value.source) &&
86
+ record(value.target) &&
87
+ ((value.target.type === "port" && endpoint(value.target)) ||
88
+ (value.target.type === "link" &&
89
+ string(value.target.linkId) &&
90
+ string(value.target.property))) &&
91
+ string(value.mode) &&
92
+ finite(value.weight) &&
93
+ optional(value.enabled, boolean) &&
94
+ optional(value.data, (data) => record(data) && isJsonValue(data)));
95
+ }
96
+ function area(value) {
97
+ return (record(value) &&
98
+ optional(value.ownerNodeId, string) &&
99
+ string(value.id) &&
100
+ typeof value.title === "string" &&
101
+ point(value.position) &&
102
+ size(value.size) &&
103
+ optional(value.locked, boolean) &&
104
+ optional(value.collapsed, boolean) &&
105
+ optional(value.nodeIds, strings) &&
106
+ optional(value.areaIds, strings) &&
107
+ optional(value.controlHeight, (height) => finite(height) && height >= 0) &&
108
+ optional(value.data, (data) => record(data) && isJsonValue(data)));
109
+ }
110
+ function document(value, depth) {
111
+ return (depth <= 32 &&
112
+ record(value) &&
113
+ value.format === "spagbol" &&
114
+ value.version === 1 &&
115
+ Array.isArray(value.nodes) &&
116
+ value.nodes.every((candidate) => node(candidate, depth)) &&
117
+ Array.isArray(value.links) &&
118
+ value.links.every(link) &&
119
+ Array.isArray(value.areas) &&
120
+ value.areas.every(area) &&
121
+ optional(value.data, (data) => record(data) && isJsonValue(data)));
122
+ }
123
+ export function assertDocument(value) {
124
+ if (!isJsonValue(value) || !document(value, 0))
125
+ throw new Error("Invalid Spagbol document. Expected format spagbol/version 1, finite JSON data and valid node/link/area shapes (maximum nesting 32).");
126
+ }
127
+ /** Preserve array order. Never sort links as a side effect of serialization. */
128
+ export function writeDocument(value) {
129
+ assertDocument(value);
130
+ return `${JSON.stringify(value, null, 2)}\n`;
131
+ }
132
+ export function readDocument(input) {
133
+ const parsed = typeof input === "string" ? JSON.parse(input) : input;
134
+ assertDocument(parsed);
135
+ return clone(parsed);
136
+ }
137
+ export function readFragment(input) {
138
+ const parsed = typeof input === "string" ? JSON.parse(input) : input;
139
+ if (!record(parsed) ||
140
+ parsed.format !== "spagbol-fragment" ||
141
+ parsed.version !== 1 ||
142
+ !Array.isArray(parsed.boundaryLinks) ||
143
+ !parsed.boundaryLinks.every(link))
144
+ throw new Error("Invalid Spagbol clipboard fragment.");
145
+ assertDocument({ ...parsed, format: "spagbol" });
146
+ return clone(parsed);
147
+ }
148
+ //# sourceMappingURL=serialization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialization.js","sourceRoot":"","sources":["../../src/core/serialization.ts"],"names":[],"mappings":"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","sourcesContent":["import { clone, type GraphDocument, type JsonValue } from \"./model.js\";\nimport type { GraphFragment } from \"./graph.js\";\n\nfunction record(value: unknown): value is Record<string, unknown> {\n return !!value && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction finite(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\nfunction string(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\nfunction point(value: unknown): boolean {\n return record(value) && finite(value.x) && finite(value.y);\n}\nfunction size(value: unknown): boolean {\n return (\n record(value) &&\n finite(value.width) &&\n value.width > 0 &&\n finite(value.height) &&\n value.height > 0\n );\n}\nfunction endpoint(value: unknown): boolean {\n return record(value) && string(value.nodeId) && string(value.portId);\n}\nfunction optional(\n value: unknown,\n predicate: (value: unknown) => boolean,\n): boolean {\n return value === undefined || predicate(value);\n}\nfunction numberRecord(value: unknown): boolean {\n return record(value) && Object.values(value).every(finite);\n}\nexport function isJsonValue(\n value: unknown,\n ancestors = new Set<object>(),\n): value is JsonValue {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\" ||\n finite(value)\n )\n return true;\n if (!Array.isArray(value) && !record(value)) return false;\n if (ancestors.has(value)) return false;\n const next = new Set([...ancestors, value]);\n return (\n Object.getPrototypeOf(value) ===\n (Array.isArray(value) ? Array.prototype : Object.prototype) &&\n Object.values(value).every((child) => isJsonValue(child, next))\n );\n}\nfunction boolean(value: unknown): boolean {\n return typeof value === \"boolean\";\n}\nfunction strings(value: unknown): boolean {\n return Array.isArray(value) && value.every(string);\n}\nfunction node(value: unknown, depth: number): boolean {\n return (\n record(value) &&\n string(value.id) &&\n string(value.kind) &&\n point(value.position) &&\n record(value.data) &&\n isJsonValue(value.data) &&\n numberRecord(value.values) &&\n optional(value.label, (text) => typeof text === \"string\") &&\n optional(value.size, size) &&\n optional(value.enabled, boolean) &&\n optional(value.compact, boolean) &&\n optional(value.compactLabel, text => typeof text === \"string\") &&\n optional(value.scale, scale => finite(scale) && scale > 0) &&\n optional(value.sharedGraphId, string) &&\n optional(value.panelNodeIds, strings) &&\n optional(value.dataOverrides, values => record(values) && Object.values(values).every(item => record(item) && isJsonValue(item))) &&\n optional(\n value.overrides,\n (overrides) =>\n record(overrides) && Object.values(overrides).every(numberRecord),\n ) &&\n optional(\n value.ports,\n (ports) =>\n Array.isArray(ports) &&\n ports.every(\n (port) =>\n record(port) &&\n string(port.id) &&\n string(port.label) &&\n (port.direction === \"input\" || port.direction === \"output\") &&\n optional(port.dataType, string) &&\n optional(port.connectable, boolean) &&\n optional(port.valueEditor, boolean) &&\n optional(port.integer, boolean) &&\n [\"defaultValue\", \"min\", \"max\", \"step\"].every((key) =>\n optional(port[key], finite),\n ) &&\n !(finite(port.min) && finite(port.max) && port.min > port.max),\n ),\n ) &&\n optional(value.graph, (graph) => document(graph, depth + 1))\n );\n}\nfunction link(value: unknown): boolean {\n return (\n record(value) &&\n string(value.id) &&\n endpoint(value.source) &&\n record(value.target) &&\n ((value.target.type === \"port\" && endpoint(value.target)) ||\n (value.target.type === \"link\" &&\n string(value.target.linkId) &&\n string(value.target.property))) &&\n string(value.mode) &&\n finite(value.weight) &&\n optional(value.enabled, boolean) &&\n optional(value.data, (data) => record(data) && isJsonValue(data))\n );\n}\nfunction area(value: unknown): boolean {\n return (\n record(value) &&\n optional(value.ownerNodeId, string) &&\n string(value.id) &&\n typeof value.title === \"string\" &&\n point(value.position) &&\n size(value.size) &&\n optional(value.locked, boolean) &&\n optional(value.collapsed, boolean) &&\n optional(value.nodeIds, strings) &&\n optional(value.areaIds, strings) &&\n optional(value.controlHeight, (height) => finite(height) && height >= 0) &&\n optional(value.data, (data) => record(data) && isJsonValue(data))\n );\n}\nfunction document(value: unknown, depth: number): boolean {\n return (\n depth <= 32 &&\n record(value) &&\n value.format === \"spagbol\" &&\n value.version === 1 &&\n Array.isArray(value.nodes) &&\n value.nodes.every((candidate) => node(candidate, depth)) &&\n Array.isArray(value.links) &&\n value.links.every(link) &&\n Array.isArray(value.areas) &&\n value.areas.every(area) &&\n optional(value.data, (data) => record(data) && isJsonValue(data))\n );\n}\nexport function assertDocument(value: unknown): asserts value is GraphDocument {\n if (!isJsonValue(value) || !document(value, 0))\n throw new Error(\n \"Invalid Spagbol document. Expected format spagbol/version 1, finite JSON data and valid node/link/area shapes (maximum nesting 32).\",\n );\n}\n/** Preserve array order. Never sort links as a side effect of serialization. */\nexport function writeDocument(value: GraphDocument): string {\n assertDocument(value);\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\nexport function readDocument(input: string | unknown): GraphDocument {\n const parsed: unknown = typeof input === \"string\" ? JSON.parse(input) : input;\n assertDocument(parsed);\n return clone(parsed);\n}\nexport function readFragment(input: string | unknown): GraphFragment {\n const parsed: unknown = typeof input === \"string\" ? JSON.parse(input) : input;\n if (\n !record(parsed) ||\n parsed.format !== \"spagbol-fragment\" ||\n parsed.version !== 1 ||\n !Array.isArray(parsed.boundaryLinks) ||\n !parsed.boundaryLinks.every(link)\n )\n throw new Error(\"Invalid Spagbol clipboard fragment.\");\n assertDocument({ ...parsed, format: \"spagbol\" });\n return clone(parsed) as unknown as GraphFragment;\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export * from "./core/index.js";
2
+ export * from "./react/index.js";
3
+ export { SpagbolControlSlider, SpagbolControlButton, type SpagbolControlSliderProps, type SpagbolControlButtonProps, type SpagbolControlButtonMode, } from "./react/Controls.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./core/index.js";
2
+ export * from "./react/index.js";
3
+ export { SpagbolControlSlider, SpagbolControlButton, } from "./react/Controls.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GAIrB,MAAM,qBAAqB,CAAC","sourcesContent":["export * from \"./core/index.js\";\nexport * from \"./react/index.js\";\nexport {\n SpagbolControlSlider,\n SpagbolControlButton,\n type SpagbolControlSliderProps,\n type SpagbolControlButtonProps,\n type SpagbolControlButtonMode,\n} from \"./react/Controls.js\";\n"]}
@@ -0,0 +1,123 @@
1
+ .spagbol-canvas .spagbol-canvas-link-controls {
2
+ position: fixed;
3
+ z-index: 0;
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 4px;
7
+ padding: 4px;
8
+ border: 1px solid var(--color-foreground, var(--spagbol-foreground, #fff));
9
+ background:
10
+ linear-gradient(var(--color-foreground, var(--spagbol-foreground, #fff)), var(--color-foreground, var(--spagbol-foreground, #fff))) left top / 100% 4px no-repeat,
11
+ linear-gradient(var(--color-foreground, var(--spagbol-foreground, #fff)), var(--color-foreground, var(--spagbol-foreground, #fff))) left bottom / 100% 4px no-repeat,
12
+ linear-gradient(var(--color-foreground, var(--spagbol-foreground, #fff)), var(--color-foreground, var(--spagbol-foreground, #fff))) left top / 4px 100% no-repeat,
13
+ linear-gradient(var(--color-foreground, var(--spagbol-foreground, #fff)), var(--color-foreground, var(--spagbol-foreground, #fff))) right top / 4px 100% no-repeat,
14
+ linear-gradient(var(--color-foreground, var(--spagbol-foreground, #fff)), var(--color-foreground, var(--spagbol-foreground, #fff))) left 26px / 100% 4px no-repeat;
15
+ color: var(--color-background, var(--spagbol-bg, #000));
16
+ font-size: 13px;
17
+ font-weight: 400;
18
+ pointer-events: all;
19
+ box-shadow: 6px 6px 0 var(--color-background-42, #0000006b);
20
+ transform-origin: center;
21
+ --edge-control-width: 96px;
22
+ --edge-weight-control-width: 74px;
23
+ }
24
+
25
+ .spagbol-canvas .spagbol-canvas-link-controls-value-row {
26
+ display: flex;
27
+ gap: 0;
28
+ }
29
+
30
+ .spagbol-canvas .spagbol-control-number-link {
31
+ width: var(--edge-weight-control-width);
32
+ height: 22px;
33
+ padding: 0 4px;
34
+ border: 0;
35
+ background: var(--color-surface, var(--spagbol-surface, #000));
36
+ color: var(--color-foreground, var(--spagbol-foreground, #fff));
37
+ cursor: ns-resize;
38
+ font-size: 13px;
39
+ font-weight: 400;
40
+ line-height: 20px;
41
+ overflow: hidden;
42
+ text-align: left;
43
+ text-overflow: clip;
44
+ white-space: nowrap;
45
+ user-select: none;
46
+ }
47
+
48
+ .spagbol-canvas .spagbol-control-number-link:focus {
49
+ outline: 1px solid var(--color-foreground, var(--spagbol-foreground, #fff));
50
+ outline-offset: 1px;
51
+ }
52
+
53
+ .spagbol-canvas .spagbol-control-number-link-editing {
54
+ display: block;
55
+ background: var(--color-surface, var(--spagbol-surface, #000));
56
+ color: var(--color-foreground, var(--spagbol-foreground, #fff));
57
+ cursor: text;
58
+ font-size: 13px;
59
+ font-weight: 400;
60
+ user-select: text;
61
+ -moz-appearance: textfield;
62
+ }
63
+
64
+ .spagbol-canvas .spagbol-control-number-link-editing::selection {
65
+ background: var(--color-foreground, var(--spagbol-foreground, #fff));
66
+ color: var(--color-background, var(--spagbol-bg, #000));
67
+ }
68
+
69
+ .spagbol-canvas .spagbol-canvas-link-controls-enabled {
70
+ display: grid;
71
+ width: 22px;
72
+ height: 22px;
73
+ flex: 0 0 22px;
74
+ place-items: center;
75
+ padding: 0;
76
+ border: 0;
77
+ border-radius: 0;
78
+ background: var(--color-surface, var(--spagbol-surface, #000));
79
+ cursor: pointer;
80
+ }
81
+
82
+ .spagbol-canvas .spagbol-canvas-link-controls-enabled::after {
83
+ display: block;
84
+ width: 9px;
85
+ height: 9px;
86
+ border: 1px solid var(--color-foreground, var(--spagbol-foreground, #fff));
87
+ border-radius: 50%;
88
+ content: '';
89
+ }
90
+
91
+ .spagbol-canvas .spagbol-canvas-link-controls-enabled-active::after {
92
+ background: var(--color-foreground, var(--spagbol-foreground, #fff));
93
+ }
94
+
95
+ .spagbol-canvas .spagbol-canvas-link-controls-enabled:hover {
96
+ background: var(--color-surface, var(--spagbol-surface, #000));
97
+ color: var(--color-foreground, var(--spagbol-foreground, #fff));
98
+ }
99
+
100
+ .spagbol-canvas .spagbol-canvas-link-controls-enabled:focus-visible {
101
+ outline: 1px solid var(--color-foreground, var(--spagbol-foreground, #fff));
102
+ outline-offset: 2px;
103
+ }
104
+
105
+ .spagbol-canvas .spagbol-canvas-link-controls-mode {
106
+ width: var(--edge-control-width);
107
+ height: 22px;
108
+ padding: 0 4px;
109
+ border: 0;
110
+ border-radius: 0;
111
+ background: var(--color-surface, var(--spagbol-surface, #000));
112
+ color: var(--color-foreground, var(--spagbol-foreground, #fff));
113
+ cursor: pointer;
114
+ font-size: 13px;
115
+ font-weight: 400;
116
+ line-height: 20px;
117
+ }
118
+
119
+ .spagbol-canvas .spagbol-canvas-link-controls-mode:focus {
120
+ outline: 1px solid var(--color-foreground, var(--spagbol-foreground, #fff));
121
+ outline-offset: 1px;
122
+ }
123
+
@@ -0,0 +1,18 @@
1
+ export interface SpagbolControlSliderProps {
2
+ value: number;
3
+ displayValue?: number;
4
+ direction: 'horizontal' | 'vertical';
5
+ onChange: (value: number) => void;
6
+ onGestureStart?(): void;
7
+ onGestureEnd?(cancel: boolean): void;
8
+ }
9
+ export declare function SpagbolControlSlider({ value, displayValue, direction, onChange, onGestureStart, onGestureEnd }: SpagbolControlSliderProps): import("react").JSX.Element;
10
+ export type SpagbolControlButtonMode = 'toggle' | 'trigger' | 'temporary';
11
+ export interface SpagbolControlButtonProps {
12
+ mode: SpagbolControlButtonMode;
13
+ pressed: number;
14
+ onPressedChange: (value: number) => void;
15
+ onClickPulse: () => void;
16
+ }
17
+ export declare function SpagbolControlButton({ mode, pressed, onPressedChange, onClickPulse }: SpagbolControlButtonProps): import("react").JSX.Element;
18
+ //# sourceMappingURL=Controls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Controls.d.ts","sourceRoot":"","sources":["../../src/react/Controls.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,YAAY,GAAG,UAAU,CAAC;IACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,cAAc,CAAC,IAAI,IAAI,CAAC;IACxB,YAAY,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACtC;AAUD,wBAAgB,oBAAoB,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,EAAE,yBAAyB,+BAiMzI;AAED,MAAM,MAAM,wBAAwB,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;AAG1E,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,EAAE,yBAAyB,+BAuF/G"}