@dou-ui/flow 0.0.0-stage → 0.1.0-alpha.1

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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/collaboration/index.js +360 -0
  4. package/dist/collaboration/index.js.map +1 -0
  5. package/dist/collaboration/peers.js +76 -0
  6. package/dist/collaboration/peers.js.map +1 -0
  7. package/dist/components/default-node.js +6 -0
  8. package/dist/components/default-node.js.map +1 -0
  9. package/dist/components/default-node.vue_vue_type_script_setup_true_lang.js +87 -0
  10. package/dist/components/default-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
  11. package/dist/components/default-node2.js +116 -0
  12. package/dist/components/default-node2.js.map +1 -0
  13. package/dist/components/flow-controls.js +6 -0
  14. package/dist/components/flow-controls.js.map +1 -0
  15. package/dist/components/flow-controls.vue_vue_type_script_setup_true_lang.js +148 -0
  16. package/dist/components/flow-controls.vue_vue_type_script_setup_true_lang.js.map +1 -0
  17. package/dist/components/flow-menu.js +6 -0
  18. package/dist/components/flow-menu.js.map +1 -0
  19. package/dist/components/flow-menu.vue_vue_type_script_setup_true_lang.js +52 -0
  20. package/dist/components/flow-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
  21. package/dist/components/flow-minimap.js +6 -0
  22. package/dist/components/flow-minimap.js.map +1 -0
  23. package/dist/components/flow-minimap.vue_vue_type_script_setup_true_lang.js +238 -0
  24. package/dist/components/flow-minimap.vue_vue_type_script_setup_true_lang.js.map +1 -0
  25. package/dist/components/flow-port.js +6 -0
  26. package/dist/components/flow-port.js.map +1 -0
  27. package/dist/components/flow-port.vue_vue_type_script_setup_true_lang.js +57 -0
  28. package/dist/components/flow-port.vue_vue_type_script_setup_true_lang.js.map +1 -0
  29. package/dist/components/flow-quick-add.js +6 -0
  30. package/dist/components/flow-quick-add.js.map +1 -0
  31. package/dist/components/flow-quick-add.vue_vue_type_script_setup_true_lang.js +126 -0
  32. package/dist/components/flow-quick-add.vue_vue_type_script_setup_true_lang.js.map +1 -0
  33. package/dist/components/flow-toolbar.js +6 -0
  34. package/dist/components/flow-toolbar.js.map +1 -0
  35. package/dist/components/flow-toolbar.vue_vue_type_script_setup_true_lang.js +161 -0
  36. package/dist/components/flow-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  37. package/dist/components/group-node.js +6 -0
  38. package/dist/components/group-node.js.map +1 -0
  39. package/dist/components/group-node.vue_vue_type_script_setup_true_lang.js +84 -0
  40. package/dist/components/group-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/components/group-node2.js +42 -0
  42. package/dist/components/group-node2.js.map +1 -0
  43. package/dist/components/node-view.js +6 -0
  44. package/dist/components/node-view.js.map +1 -0
  45. package/dist/components/node-view.vue_vue_type_script_setup_true_lang.js +179 -0
  46. package/dist/components/node-view.vue_vue_type_script_setup_true_lang.js.map +1 -0
  47. package/dist/context.js +13 -0
  48. package/dist/context.js.map +1 -0
  49. package/dist/core/clipboard.js +37 -0
  50. package/dist/core/clipboard.js.map +1 -0
  51. package/dist/core/connect.js +74 -0
  52. package/dist/core/connect.js.map +1 -0
  53. package/dist/core/edge-types.js +240 -0
  54. package/dist/core/edge-types.js.map +1 -0
  55. package/dist/core/extension.js +63 -0
  56. package/dist/core/extension.js.map +1 -0
  57. package/dist/core/geometry.js +232 -0
  58. package/dist/core/geometry.js.map +1 -0
  59. package/dist/core/groups.js +76 -0
  60. package/dist/core/groups.js.map +1 -0
  61. package/dist/core/history.js +79 -0
  62. package/dist/core/history.js.map +1 -0
  63. package/dist/core/model.js +21 -0
  64. package/dist/core/model.js.map +1 -0
  65. package/dist/core/node-types.js +32 -0
  66. package/dist/core/node-types.js.map +1 -0
  67. package/dist/core/order.js +42 -0
  68. package/dist/core/order.js.map +1 -0
  69. package/dist/core/transaction.js +288 -0
  70. package/dist/core/transaction.js.map +1 -0
  71. package/dist/document.js +220 -0
  72. package/dist/document.js.map +1 -0
  73. package/dist/export/color.js +38 -0
  74. package/dist/export/color.js.map +1 -0
  75. package/dist/export/context.js +223 -0
  76. package/dist/export/context.js.map +1 -0
  77. package/dist/export/draw.js +249 -0
  78. package/dist/export/draw.js.map +1 -0
  79. package/dist/flow-editor.js +6 -0
  80. package/dist/flow-editor.js.map +1 -0
  81. package/dist/flow-editor.vue_vue_type_script_setup_true_lang.js +3721 -0
  82. package/dist/flow-editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
  83. package/dist/index.js +31 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/kit.js +234 -0
  86. package/dist/kit.js.map +1 -0
  87. package/dist/layers.js +150 -0
  88. package/dist/layers.js.map +1 -0
  89. package/dist/locale.js +426 -0
  90. package/dist/locale.js.map +1 -0
  91. package/dist/style.css +1 -0
  92. package/dist/style.d.ts +1 -0
  93. package/dist/style.js +8 -0
  94. package/dist/types/collaboration/index.d.ts +42 -0
  95. package/dist/types/collaboration/peers.d.ts +27 -0
  96. package/dist/types/components/default-node.d.ts +18 -0
  97. package/dist/types/components/default-node.vue.d.ts +6 -0
  98. package/dist/types/components/flow-controls.vue.d.ts +8 -0
  99. package/dist/types/components/flow-menu.vue.d.ts +29 -0
  100. package/dist/types/components/flow-minimap.vue.d.ts +8 -0
  101. package/dist/types/components/flow-port.vue.d.ts +9 -0
  102. package/dist/types/components/flow-quick-add.vue.d.ts +31 -0
  103. package/dist/types/components/flow-toolbar.vue.d.ts +25 -0
  104. package/dist/types/components/group-node.d.ts +13 -0
  105. package/dist/types/components/group-node.vue.d.ts +6 -0
  106. package/dist/types/components/node-view.vue.d.ts +28 -0
  107. package/dist/types/context.d.ts +362 -0
  108. package/dist/types/core/clipboard.d.ts +9 -0
  109. package/dist/types/core/connect.d.ts +63 -0
  110. package/dist/types/core/edge-types.d.ts +50 -0
  111. package/dist/types/core/extension.d.ts +141 -0
  112. package/dist/types/core/geometry.d.ts +63 -0
  113. package/dist/types/core/groups.d.ts +23 -0
  114. package/dist/types/core/history.d.ts +24 -0
  115. package/dist/types/core/menu.d.ts +40 -0
  116. package/dist/types/core/model.d.ts +87 -0
  117. package/dist/types/core/node-types.d.ts +103 -0
  118. package/dist/types/core/order.d.ts +7 -0
  119. package/dist/types/core/transaction.d.ts +97 -0
  120. package/dist/types/document.d.ts +57 -0
  121. package/dist/types/export/color.d.ts +7 -0
  122. package/dist/types/export/context.d.ts +90 -0
  123. package/dist/types/export/draw.d.ts +72 -0
  124. package/dist/types/flow-editor.vue.d.ts +63 -0
  125. package/dist/types/index.d.ts +202 -0
  126. package/dist/types/kit.d.ts +22 -0
  127. package/dist/types/layers.d.ts +24 -0
  128. package/dist/types/locale.d.ts +172 -0
  129. package/dist/types/whiteboard/board-draft.vue.d.ts +3 -0
  130. package/dist/types/whiteboard/board-node.vue.d.ts +19 -0
  131. package/dist/types/whiteboard/connector.d.ts +12 -0
  132. package/dist/types/whiteboard/draw.d.ts +5 -0
  133. package/dist/types/whiteboard/image.d.ts +27 -0
  134. package/dist/types/whiteboard/index.d.ts +6 -0
  135. package/dist/types/whiteboard/ink.d.ts +30 -0
  136. package/dist/types/whiteboard/kit.d.ts +36 -0
  137. package/dist/types/whiteboard/model.d.ts +79 -0
  138. package/dist/types/whiteboard/pen.d.ts +7 -0
  139. package/dist/types/whiteboard/state.d.ts +37 -0
  140. package/dist/types/whiteboard/transform.d.ts +67 -0
  141. package/dist/types/whiteboard/whiteboard-toolbar.vue.d.ts +3 -0
  142. package/dist/whiteboard/board-draft.js +6 -0
  143. package/dist/whiteboard/board-draft.js.map +1 -0
  144. package/dist/whiteboard/board-draft.vue_vue_type_script_setup_true_lang.js +58 -0
  145. package/dist/whiteboard/board-draft.vue_vue_type_script_setup_true_lang.js.map +1 -0
  146. package/dist/whiteboard/board-node.js +6 -0
  147. package/dist/whiteboard/board-node.js.map +1 -0
  148. package/dist/whiteboard/board-node.vue_vue_type_script_setup_true_lang.js +332 -0
  149. package/dist/whiteboard/board-node.vue_vue_type_script_setup_true_lang.js.map +1 -0
  150. package/dist/whiteboard/connector.js +180 -0
  151. package/dist/whiteboard/connector.js.map +1 -0
  152. package/dist/whiteboard/draw.js +68 -0
  153. package/dist/whiteboard/draw.js.map +1 -0
  154. package/dist/whiteboard/image.js +104 -0
  155. package/dist/whiteboard/image.js.map +1 -0
  156. package/dist/whiteboard/index.js +5 -0
  157. package/dist/whiteboard/ink.js +168 -0
  158. package/dist/whiteboard/ink.js.map +1 -0
  159. package/dist/whiteboard/kit.js +406 -0
  160. package/dist/whiteboard/kit.js.map +1 -0
  161. package/dist/whiteboard/model.js +52 -0
  162. package/dist/whiteboard/model.js.map +1 -0
  163. package/dist/whiteboard/pen.js +88 -0
  164. package/dist/whiteboard/pen.js.map +1 -0
  165. package/dist/whiteboard/state.js +26 -0
  166. package/dist/whiteboard/state.js.map +1 -0
  167. package/dist/whiteboard/transform.js +256 -0
  168. package/dist/whiteboard/transform.js.map +1 -0
  169. package/dist/whiteboard/whiteboard-toolbar.js +6 -0
  170. package/dist/whiteboard/whiteboard-toolbar.js.map +1 -0
  171. package/dist/whiteboard/whiteboard-toolbar.vue_vue_type_script_setup_true_lang.js +171 -0
  172. package/dist/whiteboard/whiteboard-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
  173. package/package.json +74 -4
@@ -0,0 +1,232 @@
1
+ // src/core/geometry.ts
2
+ function boundsOf(rects) {
3
+ if (!rects.length) return null;
4
+ let left = Infinity;
5
+ let top = Infinity;
6
+ let right = -Infinity;
7
+ let bottom = -Infinity;
8
+ for (const rect of rects) {
9
+ left = Math.min(left, rect.x);
10
+ top = Math.min(top, rect.y);
11
+ right = Math.max(right, rect.x + rect.width);
12
+ bottom = Math.max(bottom, rect.y + rect.height);
13
+ }
14
+ return {
15
+ x: left,
16
+ y: top,
17
+ width: right - left,
18
+ height: bottom - top
19
+ };
20
+ }
21
+ function intersects(a, b) {
22
+ return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
23
+ }
24
+ /** The rectangle spanned by two corners, in any order. */
25
+ function rectFrom(a, b) {
26
+ return {
27
+ x: Math.min(a.x, b.x),
28
+ y: Math.min(a.y, b.y),
29
+ width: Math.abs(a.x - b.x),
30
+ height: Math.abs(a.y - b.y)
31
+ };
32
+ }
33
+ var snapTo = (value, step) => step > 0 ? Math.round(value / step) * step : value;
34
+ var linesX = (r) => [
35
+ r.x,
36
+ r.x + r.width / 2,
37
+ r.x + r.width
38
+ ];
39
+ var linesY = (r) => [
40
+ r.y,
41
+ r.y + r.height / 2,
42
+ r.y + r.height
43
+ ];
44
+ /**
45
+ * Lines the moving box up, on each axis separately, when it is within
46
+ * `threshold`: first a port with the port it is connected to, then the
47
+ * nearest edge or centre of another box. Without a match on an axis, the
48
+ * box's top left corner snaps to `grid` instead.
49
+ */
50
+ function snapBox(box, others, { threshold, grid, links = [] }) {
51
+ const best = {
52
+ x: null,
53
+ y: null
54
+ };
55
+ const consider = (axis, d, at, link) => {
56
+ const current = best[axis];
57
+ if (Math.abs(d) > threshold) return;
58
+ if (current?.link && !link) return;
59
+ if (!current || link && !current.link || Math.abs(d) < Math.abs(current.d)) best[axis] = {
60
+ d,
61
+ at,
62
+ link
63
+ };
64
+ };
65
+ for (const link of links) consider(link.axis, link.to[link.axis] - link.from[link.axis], link.to[link.axis], link);
66
+ for (const other of others) {
67
+ for (const from of linesX(box)) for (const to of linesX(other)) consider("x", to - from, to);
68
+ for (const from of linesY(box)) for (const to of linesY(other)) consider("y", to - from, to);
69
+ }
70
+ const dx = best.x ? best.x.d : snapTo(box.x, grid) - box.x;
71
+ const dy = best.y ? best.y.d : snapTo(box.y, grid) - box.y;
72
+ const moved = {
73
+ ...box,
74
+ x: box.x + dx,
75
+ y: box.y + dy
76
+ };
77
+ const guides = [];
78
+ for (const axis of ["x", "y"]) {
79
+ const match = best[axis];
80
+ if (!match) continue;
81
+ const across = axis === "x" ? "y" : "x";
82
+ if (match.link) {
83
+ const a = match.link.from[across] + (axis === "x" ? dy : dx);
84
+ const b = match.link.to[across];
85
+ guides.push({
86
+ axis,
87
+ at: match.at,
88
+ from: Math.min(a, b),
89
+ to: Math.max(a, b)
90
+ });
91
+ continue;
92
+ }
93
+ const lines = axis === "x" ? linesX : linesY;
94
+ const span = [moved, ...others.filter((r) => lines(r).some((v) => Math.abs(v - match.at) < .5))];
95
+ const start = (r) => axis === "x" ? r.y : r.x;
96
+ const length = (r) => axis === "x" ? r.height : r.width;
97
+ guides.push({
98
+ axis,
99
+ at: match.at,
100
+ from: Math.min(...span.map(start)),
101
+ to: Math.max(...span.map((r) => start(r) + length(r)))
102
+ });
103
+ }
104
+ return {
105
+ dx,
106
+ dy,
107
+ guides
108
+ };
109
+ }
110
+ /** Turns `point` by `degrees` around `centre`. */
111
+ function turn(point, centre, degrees) {
112
+ if (!degrees) return point;
113
+ const a = degrees * Math.PI / 180;
114
+ const dx = point.x - centre.x;
115
+ const dy = point.y - centre.y;
116
+ return {
117
+ x: centre.x + dx * Math.cos(a) - dy * Math.sin(a),
118
+ y: centre.y + dx * Math.sin(a) + dy * Math.cos(a)
119
+ };
120
+ }
121
+ /** The box around `rect` turned by `degrees` about its centre. */
122
+ function turnedBox(rect, degrees) {
123
+ if (!degrees) return rect;
124
+ const centre = {
125
+ x: rect.x + rect.width / 2,
126
+ y: rect.y + rect.height / 2
127
+ };
128
+ return boundsOf([
129
+ {
130
+ x: rect.x,
131
+ y: rect.y
132
+ },
133
+ {
134
+ x: rect.x + rect.width,
135
+ y: rect.y
136
+ },
137
+ {
138
+ x: rect.x,
139
+ y: rect.y + rect.height
140
+ },
141
+ {
142
+ x: rect.x + rect.width,
143
+ y: rect.y + rect.height
144
+ }
145
+ ].map((corner) => turn(corner, centre, degrees)).map((p) => ({
146
+ ...p,
147
+ width: 0,
148
+ height: 0
149
+ })));
150
+ }
151
+ /** Whether `box` and `rect` turned by `degrees` about its centre overlap. */
152
+ function intersectsTurned(box, rect, degrees) {
153
+ if (!degrees) return intersects(box, rect);
154
+ if (!intersects(box, turnedBox(rect, degrees))) return false;
155
+ const centre = {
156
+ x: rect.x + rect.width / 2,
157
+ y: rect.y + rect.height / 2
158
+ };
159
+ const corners = [
160
+ {
161
+ x: box.x,
162
+ y: box.y
163
+ },
164
+ {
165
+ x: box.x + box.width,
166
+ y: box.y
167
+ },
168
+ {
169
+ x: box.x,
170
+ y: box.y + box.height
171
+ },
172
+ {
173
+ x: box.x + box.width,
174
+ y: box.y + box.height
175
+ }
176
+ ].map((corner) => turn(corner, centre, -degrees));
177
+ const xs = corners.map((p) => p.x);
178
+ const ys = corners.map((p) => p.y);
179
+ return Math.min(...xs) < rect.x + rect.width && Math.max(...xs) > rect.x && Math.min(...ys) < rect.y + rect.height && Math.max(...ys) > rect.y;
180
+ }
181
+ /** The part of the canvas a viewport shows on a screen of `size`. */
182
+ function viewOf(view, size) {
183
+ return {
184
+ x: -view.x / view.zoom,
185
+ y: -view.y / view.zoom,
186
+ width: size.width / view.zoom,
187
+ height: size.height / view.zoom
188
+ };
189
+ }
190
+ /** Whether `inner` lies in `outer`, give or take rounding. */
191
+ function within(inner, outer) {
192
+ const e = 1e-6 * Math.max(1, outer.width, outer.height);
193
+ return inner.x >= outer.x - e && inner.y >= outer.y - e && inner.x + inner.width <= outer.x + outer.width + e && inner.y + inner.height <= outer.y + outer.height + e;
194
+ }
195
+ /**
196
+ * Keeps a pan within reach of the nodes: the view may go as far as having
197
+ * the nodes just leave it on the opposite side, a screen of room to draw in,
198
+ * and no further, so the canvas never runs off to coordinates the browser
199
+ * can't draw (it clamps lengths at about 3.3e7 px, and the layer and the
200
+ * nodes stop lining up long before). With no nodes, the origin is kept in
201
+ * view. A view already outside, e.g. set through `v-model:viewport` or left
202
+ * there when nodes were removed, isn't pulled back: it can only move closer.
203
+ * Zooming in is left as it is: it stays inside the view, so it never goes
204
+ * further out.
205
+ */
206
+ function clampPan(next, current, content, size) {
207
+ if (!size.width || !size.height) return next;
208
+ if (next.zoom > current.zoom && within(viewOf(next, size), viewOf(current, size))) return next;
209
+ const area = content ?? {
210
+ x: 0,
211
+ y: 0,
212
+ width: 0,
213
+ height: 0
214
+ };
215
+ const before = {
216
+ x: size.width / 2 - (size.width / 2 - current.x) / current.zoom * next.zoom,
217
+ y: size.height / 2 - (size.height / 2 - current.y) / current.zoom * next.zoom
218
+ };
219
+ const axis = (value, now, start, length, screen) => {
220
+ const lo = 0 - (start + length) * next.zoom;
221
+ const hi = screen - start * next.zoom;
222
+ return Math.min(Math.max(value, Math.min(lo, now)), Math.max(hi, now));
223
+ };
224
+ return {
225
+ x: axis(next.x, before.x, area.x, area.width, size.width),
226
+ y: axis(next.y, before.y, area.y, area.height, size.height),
227
+ zoom: next.zoom
228
+ };
229
+ }
230
+ export { boundsOf, clampPan, intersects, intersectsTurned, rectFrom, snapBox, snapTo, turn, turnedBox };
231
+
232
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","names":[],"sources":["../../src/core/geometry.ts"],"sourcesContent":["import type { FlowViewport, Point, Rect } from './model'\n\nexport function boundsOf(rects: readonly Rect[]): Rect | null {\n if (!rects.length) return null\n let left = Infinity\n let top = Infinity\n let right = -Infinity\n let bottom = -Infinity\n for (const rect of rects) {\n left = Math.min(left, rect.x)\n top = Math.min(top, rect.y)\n right = Math.max(right, rect.x + rect.width)\n bottom = Math.max(bottom, rect.y + rect.height)\n }\n return { x: left, y: top, width: right - left, height: bottom - top }\n}\n\nexport function intersects(a: Rect, b: Rect) {\n return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height\n}\n\n/** The rectangle spanned by two corners, in any order. */\nexport function rectFrom(a: Point, b: Point): Rect {\n return {\n x: Math.min(a.x, b.x),\n y: Math.min(a.y, b.y),\n width: Math.abs(a.x - b.x),\n height: Math.abs(a.y - b.y),\n }\n}\n\nexport const snapTo = (value: number, step: number) =>\n step > 0 ? Math.round(value / step) * step : value\n\n// Alignment guides ------------------------------------------------------------\n\nexport interface Guide {\n axis: 'x' | 'y'\n /** The canvas coordinate of the line. */\n at: number\n /** Where the line starts and ends along the other axis. */\n from: number\n to: number\n}\n\nexport interface SnapResult {\n /** How far to shift the moving box so it lines up. */\n dx: number\n dy: number\n guides: Guide[]\n}\n\nconst linesX = (r: Rect) => [r.x, r.x + r.width / 2, r.x + r.width]\nconst linesY = (r: Rect) => [r.y, r.y + r.height / 2, r.y + r.height]\n\n/**\n * A port on a moving node and the port it is connected to. Lining them up\n * makes the edge between them a straight line.\n */\nexport interface SnapLink {\n /** Where the moving port is before the snap. */\n from: Point\n to: Point\n /** `y` levels ports on the left or right; `x` ports on the top or bottom. */\n axis: 'x' | 'y'\n}\n\n/**\n * Lines the moving box up, on each axis separately, when it is within\n * `threshold`: first a port with the port it is connected to, then the\n * nearest edge or centre of another box. Without a match on an axis, the\n * box's top left corner snaps to `grid` instead.\n */\nexport function snapBox(\n box: Rect,\n others: readonly Rect[],\n { threshold, grid, links = [] }: { threshold: number; grid: number; links?: readonly SnapLink[] },\n): SnapResult {\n type Match = { d: number; at: number; link?: SnapLink }\n const best: Record<'x' | 'y', Match | null> = { x: null, y: null }\n const consider = (axis: 'x' | 'y', d: number, at: number, link?: SnapLink) => {\n const current = best[axis]\n if (Math.abs(d) > threshold) return\n // A port lined up with its partner wins over boxes lining up.\n if (current?.link && !link) return\n if (!current || (link && !current.link) || Math.abs(d) < Math.abs(current.d))\n best[axis] = { d, at, link }\n }\n for (const link of links)\n consider(link.axis, link.to[link.axis] - link.from[link.axis], link.to[link.axis], link)\n for (const other of others) {\n for (const from of linesX(box)) for (const to of linesX(other)) consider('x', to - from, to)\n for (const from of linesY(box)) for (const to of linesY(other)) consider('y', to - from, to)\n }\n const dx = best.x ? best.x.d : snapTo(box.x, grid) - box.x\n const dy = best.y ? best.y.d : snapTo(box.y, grid) - box.y\n const moved = { ...box, x: box.x + dx, y: box.y + dy }\n\n const guides: Guide[] = []\n for (const axis of ['x', 'y'] as const) {\n const match = best[axis]\n if (!match) continue\n const across = axis === 'x' ? 'y' : 'x'\n if (match.link) {\n // From the moved port to its partner.\n const a = match.link.from[across] + (axis === 'x' ? dy : dx)\n const b = match.link.to[across]\n guides.push({ axis, at: match.at, from: Math.min(a, b), to: Math.max(a, b) })\n continue\n }\n // Spanning the moved box and every box it lines up with.\n const lines = axis === 'x' ? linesX : linesY\n const span = [\n moved,\n ...others.filter((r) => lines(r).some((v) => Math.abs(v - match.at) < 0.5)),\n ]\n const start = (r: Rect) => (axis === 'x' ? r.y : r.x)\n const length = (r: Rect) => (axis === 'x' ? r.height : r.width)\n guides.push({\n axis,\n at: match.at,\n from: Math.min(...span.map(start)),\n to: Math.max(...span.map((r) => start(r) + length(r))),\n })\n }\n return { dx, dy, guides }\n}\n\n/** Turns `point` by `degrees` around `centre`. */\nexport function turn(point: Point, centre: Point, degrees: number): Point {\n if (!degrees) return point\n const a = (degrees * Math.PI) / 180\n const dx = point.x - centre.x\n const dy = point.y - centre.y\n return {\n x: centre.x + dx * Math.cos(a) - dy * Math.sin(a),\n y: centre.y + dx * Math.sin(a) + dy * Math.cos(a),\n }\n}\n\n/** The box around `rect` turned by `degrees` about its centre. */\nexport function turnedBox(rect: Rect, degrees: number): Rect {\n if (!degrees) return rect\n const centre = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }\n const corners = [\n { x: rect.x, y: rect.y },\n { x: rect.x + rect.width, y: rect.y },\n { x: rect.x, y: rect.y + rect.height },\n { x: rect.x + rect.width, y: rect.y + rect.height },\n ].map((corner) => turn(corner, centre, degrees))\n return boundsOf(corners.map((p) => ({ ...p, width: 0, height: 0 })))!\n}\n\n/** Whether `box` and `rect` turned by `degrees` about its centre overlap. */\nexport function intersectsTurned(box: Rect, rect: Rect, degrees: number): boolean {\n if (!degrees) return intersects(box, rect)\n if (!intersects(box, turnedBox(rect, degrees))) return false\n // Upright, the box's axes already part them if they can; the turned\n // rectangle's own axes are the only others that can.\n const centre = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }\n const corners = [\n { x: box.x, y: box.y },\n { x: box.x + box.width, y: box.y },\n { x: box.x, y: box.y + box.height },\n { x: box.x + box.width, y: box.y + box.height },\n ].map((corner) => turn(corner, centre, -degrees))\n const xs = corners.map((p) => p.x)\n const ys = corners.map((p) => p.y)\n return (\n Math.min(...xs) < rect.x + rect.width &&\n Math.max(...xs) > rect.x &&\n Math.min(...ys) < rect.y + rect.height &&\n Math.max(...ys) > rect.y\n )\n}\n\n/** The part of the canvas a viewport shows on a screen of `size`. */\nfunction viewOf(view: FlowViewport, size: { width: number; height: number }): Rect {\n return {\n x: -view.x / view.zoom,\n y: -view.y / view.zoom,\n width: size.width / view.zoom,\n height: size.height / view.zoom,\n }\n}\n\n/** Whether `inner` lies in `outer`, give or take rounding. */\nfunction within(inner: Rect, outer: Rect) {\n const e = 1e-6 * Math.max(1, outer.width, outer.height)\n return (\n inner.x >= outer.x - e &&\n inner.y >= outer.y - e &&\n inner.x + inner.width <= outer.x + outer.width + e &&\n inner.y + inner.height <= outer.y + outer.height + e\n )\n}\n\n/**\n * Keeps a pan within reach of the nodes: the view may go as far as having\n * the nodes just leave it on the opposite side, a screen of room to draw in,\n * and no further, so the canvas never runs off to coordinates the browser\n * can't draw (it clamps lengths at about 3.3e7 px, and the layer and the\n * nodes stop lining up long before). With no nodes, the origin is kept in\n * view. A view already outside, e.g. set through `v-model:viewport` or left\n * there when nodes were removed, isn't pulled back: it can only move closer.\n * Zooming in is left as it is: it stays inside the view, so it never goes\n * further out.\n */\nexport function clampPan(\n next: FlowViewport,\n current: FlowViewport,\n content: Rect | null,\n size: { width: number; height: number },\n): FlowViewport {\n if (!size.width || !size.height) return next\n // Zooming in shows part of what is already on screen; keep it as asked,\n // so the point under the pointer or fingers stays put even at the edge.\n if (next.zoom > current.zoom && within(viewOf(next, size), viewOf(current, size))) return next\n const area = content ?? { x: 0, y: 0, width: 0, height: 0 }\n // How far out the view is now, at the new zoom: the same centre.\n const before = {\n x: size.width / 2 - ((size.width / 2 - current.x) / current.zoom) * next.zoom,\n y: size.height / 2 - ((size.height / 2 - current.y) / current.zoom) * next.zoom,\n }\n const axis = (value: number, now: number, start: number, length: number, screen: number) => {\n // `value` is the screen offset of the canvas origin, so the view's near\n // edge on the canvas is -value / zoom: kept between start - screen / zoom\n // (the nodes touch the far edge) and start + length (they touch the near one).\n const lo = 0 - (start + length) * next.zoom\n const hi = screen - start * next.zoom\n return Math.min(Math.max(value, Math.min(lo, now)), Math.max(hi, now))\n }\n return {\n x: axis(next.x, before.x, area.x, area.width, size.width),\n y: axis(next.y, before.y, area.y, area.height, size.height),\n zoom: next.zoom,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ // src/core/groups.ts
2
+ /**
3
+ * Parent links between nodes, worked out once per document. A `parent`
4
+ * naming a missing node, or one that would close a loop, is ignored.
5
+ */
6
+ var FlowTree = class {
7
+ parents = /* @__PURE__ */ new Map();
8
+ children = /* @__PURE__ */ new Map();
9
+ constructor(nodes) {
10
+ const ids = new Set(nodes.map((n) => n.id));
11
+ for (const node of nodes) if (node.parent && node.parent !== node.id && ids.has(node.parent)) this.parents.set(node.id, node.parent);
12
+ for (const id of this.parents.keys()) {
13
+ const seen = /* @__PURE__ */ new Set([id]);
14
+ let at = this.parents.get(id);
15
+ while (at) {
16
+ if (seen.has(at)) {
17
+ this.parents.delete(id);
18
+ break;
19
+ }
20
+ seen.add(at);
21
+ at = this.parents.get(at);
22
+ }
23
+ }
24
+ for (const [child, parent] of this.parents) {
25
+ const list = this.children.get(parent);
26
+ if (list) list.push(child);
27
+ else this.children.set(parent, [child]);
28
+ }
29
+ }
30
+ parentOf(id) {
31
+ return this.parents.get(id);
32
+ }
33
+ /** The node's ancestors, nearest first. */
34
+ ancestors(id) {
35
+ const out = [];
36
+ for (let at = this.parents.get(id); at; at = this.parents.get(at)) out.push(at);
37
+ return out;
38
+ }
39
+ /** All nodes inside, at any depth. */
40
+ descendants(id) {
41
+ const out = [];
42
+ const visit = (at) => {
43
+ for (const child of this.children.get(at) ?? []) {
44
+ out.push(child);
45
+ visit(child);
46
+ }
47
+ };
48
+ visit(id);
49
+ return out;
50
+ }
51
+ depth(id) {
52
+ return this.ancestors(id).length;
53
+ }
54
+ /** `ids` without the ones inside another of them. */
55
+ topmost(ids) {
56
+ const set = new Set(ids);
57
+ return [...set].filter((id) => !this.ancestors(id).some((a) => set.has(a)));
58
+ }
59
+ };
60
+ var contains = (rect, p) => p.x >= rect.x && p.x <= rect.x + rect.width && p.y >= rect.y && p.y <= rect.y + rect.height;
61
+ /** The innermost of `containers` whose rect holds `point`. */
62
+ function containerAt(point, containers) {
63
+ let best = null;
64
+ for (const { id, rect } of containers) {
65
+ if (!contains(rect, point)) continue;
66
+ const area = rect.width * rect.height;
67
+ if (!best || area < best.area) best = {
68
+ id,
69
+ area
70
+ };
71
+ }
72
+ return best?.id;
73
+ }
74
+ export { FlowTree, containerAt };
75
+
76
+ //# sourceMappingURL=groups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups.js","names":[],"sources":["../../src/core/groups.ts"],"sourcesContent":["import type { FlowNode, Point, Rect } from './model'\n\n/**\n * Parent links between nodes, worked out once per document. A `parent`\n * naming a missing node, or one that would close a loop, is ignored.\n */\nexport class FlowTree {\n readonly parents = new Map<string, string>()\n readonly children = new Map<string, string[]>()\n\n constructor(nodes: readonly FlowNode[]) {\n const ids = new Set(nodes.map((n) => n.id))\n for (const node of nodes)\n if (node.parent && node.parent !== node.id && ids.has(node.parent))\n this.parents.set(node.id, node.parent)\n // Break loops: walk up from each node and drop the link that comes back.\n for (const id of this.parents.keys()) {\n const seen = new Set([id])\n let at = this.parents.get(id)\n while (at) {\n if (seen.has(at)) {\n this.parents.delete(id)\n break\n }\n seen.add(at)\n at = this.parents.get(at)\n }\n }\n for (const [child, parent] of this.parents) {\n const list = this.children.get(parent)\n if (list) list.push(child)\n else this.children.set(parent, [child])\n }\n }\n\n parentOf(id: string) {\n return this.parents.get(id)\n }\n\n /** The node's ancestors, nearest first. */\n ancestors(id: string) {\n const out: string[] = []\n for (let at = this.parents.get(id); at; at = this.parents.get(at)) out.push(at)\n return out\n }\n\n /** All nodes inside, at any depth. */\n descendants(id: string) {\n const out: string[] = []\n const visit = (at: string) => {\n for (const child of this.children.get(at) ?? []) {\n out.push(child)\n visit(child)\n }\n }\n visit(id)\n return out\n }\n\n depth(id: string) {\n return this.ancestors(id).length\n }\n\n /** `ids` without the ones inside another of them. */\n topmost(ids: Iterable<string>) {\n const set = new Set(ids)\n return [...set].filter((id) => !this.ancestors(id).some((a) => set.has(a)))\n }\n}\n\nconst contains = (rect: Rect, p: Point) =>\n p.x >= rect.x && p.x <= rect.x + rect.width && p.y >= rect.y && p.y <= rect.y + rect.height\n\n/** The innermost of `containers` whose rect holds `point`. */\nexport function containerAt(point: Point, containers: { id: string; rect: Rect }[]) {\n let best: { id: string; area: number } | null = null\n for (const { id, rect } of containers) {\n if (!contains(rect, point)) continue\n const area = rect.width * rect.height\n if (!best || area < best.area) best = { id, area }\n }\n return best?.id\n}\n"],"mappings":";;;;;AAMA,IAAa,WAAb,MAAsB;CACpB,0BAAmB,IAAI,IAAoB;CAC3C,2BAAoB,IAAI,IAAsB;CAE9C,YAAY,OAA4B;EACtC,MAAM,MAAM,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,EAAE,CAAC;EAC1C,KAAK,MAAM,QAAQ,OACjB,IAAI,KAAK,UAAU,KAAK,WAAW,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,GAC/D,KAAK,QAAQ,IAAI,KAAK,IAAI,KAAK,MAAM;EAEzC,KAAK,MAAM,MAAM,KAAK,QAAQ,KAAK,GAAG;GACpC,MAAM,uBAAO,IAAI,IAAI,CAAC,EAAE,CAAC;GACzB,IAAI,KAAK,KAAK,QAAQ,IAAI,EAAE;GAC5B,OAAO,IAAI;IACT,IAAI,KAAK,IAAI,EAAE,GAAG;KAChB,KAAK,QAAQ,OAAO,EAAE;KACtB;IACF;IACA,KAAK,IAAI,EAAE;IACX,KAAK,KAAK,QAAQ,IAAI,EAAE;GAC1B;EACF;EACA,KAAK,MAAM,CAAC,OAAO,WAAW,KAAK,SAAS;GAC1C,MAAM,OAAO,KAAK,SAAS,IAAI,MAAM;GACrC,IAAI,MAAM,KAAK,KAAK,KAAK;QACpB,KAAK,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC;EACxC;CACF;CAEA,SAAS,IAAY;EACnB,OAAO,KAAK,QAAQ,IAAI,EAAE;CAC5B;;CAGA,UAAU,IAAY;EACpB,MAAM,MAAgB,CAAC;EACvB,KAAK,IAAI,KAAK,KAAK,QAAQ,IAAI,EAAE,GAAG,IAAI,KAAK,KAAK,QAAQ,IAAI,EAAE,GAAG,IAAI,KAAK,EAAE;EAC9E,OAAO;CACT;;CAGA,YAAY,IAAY;EACtB,MAAM,MAAgB,CAAC;EACvB,MAAM,SAAS,OAAe;GAC5B,KAAK,MAAM,SAAS,KAAK,SAAS,IAAI,EAAE,KAAK,CAAC,GAAG;IAC/C,IAAI,KAAK,KAAK;IACd,MAAM,KAAK;GACb;EACF;EACA,MAAM,EAAE;EACR,OAAO;CACT;CAEA,MAAM,IAAY;EAChB,OAAO,KAAK,UAAU,EAAE,CAAC,CAAC;CAC5B;;CAGA,QAAQ,KAAuB;EAC7B,MAAM,MAAM,IAAI,IAAI,GAAG;EACvB,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,OAAO,CAAC,KAAK,UAAU,EAAE,CAAC,CAAC,MAAM,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC;CAC5E;AACF;AAEA,IAAM,YAAY,MAAY,MAC5B,EAAE,KAAK,KAAK,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,SAAS,EAAE,KAAK,KAAK,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK;;AAGvF,SAAgB,YAAY,OAAc,YAA0C;CAClF,IAAI,OAA4C;CAChD,KAAK,MAAM,EAAE,IAAI,UAAU,YAAY;EACrC,IAAI,CAAC,SAAS,MAAM,KAAK,GAAG;EAC5B,MAAM,OAAO,KAAK,QAAQ,KAAK;EAC/B,IAAI,CAAC,QAAQ,OAAO,KAAK,MAAM,OAAO;GAAE;GAAI;EAAK;CACnD;CACA,OAAO,MAAM;AACf"}
@@ -0,0 +1,79 @@
1
+ import { applyFlowSteps, compactFlowSteps } from "./transaction.js";
2
+ // src/core/history.ts
3
+ /** Transactions in one group merge when they come within this many ms. */
4
+ var GROUP_WINDOW = 1e3;
5
+ var FlowHistory = class {
6
+ depth;
7
+ done = [];
8
+ undone = [];
9
+ constructor(depth = 100) {
10
+ this.depth = depth;
11
+ }
12
+ get canUndo() {
13
+ return this.done.length > 0;
14
+ }
15
+ get canRedo() {
16
+ return this.undone.length > 0;
17
+ }
18
+ record(tr, selectionBefore, selectionAfter, now = Date.now()) {
19
+ if (!tr.changed || tr.meta.history === false) return;
20
+ this.undone = [];
21
+ const last = this.done[this.done.length - 1];
22
+ const group = tr.meta.group;
23
+ if (last && group && last.group === group && now - last.time < GROUP_WINDOW) {
24
+ last.steps = compactFlowSteps([...last.steps, ...tr.steps]);
25
+ last.inverse = compactFlowSteps([...tr.inverse, ...last.inverse]);
26
+ last.time = now;
27
+ last.selectionAfter = selectionAfter;
28
+ return;
29
+ }
30
+ this.done.push({
31
+ steps: tr.steps.slice(),
32
+ inverse: tr.inverse.slice(),
33
+ group,
34
+ time: now,
35
+ selectionBefore,
36
+ selectionAfter
37
+ });
38
+ if (this.done.length > this.depth) this.done.shift();
39
+ }
40
+ /** Ends the current group, so the next change starts a new undo step. */
41
+ close() {
42
+ const last = this.done[this.done.length - 1];
43
+ if (last) last.group = void 0;
44
+ }
45
+ /** Forgets the last entry if it belongs to `group`, e.g. after a cancelled drag. */
46
+ drop(group) {
47
+ if (this.done[this.done.length - 1]?.group === group) this.done.pop();
48
+ }
49
+ undo(doc) {
50
+ const entry = this.done.pop();
51
+ if (!entry) return null;
52
+ const tr = applyFlowSteps(doc, entry.inverse);
53
+ this.undone.push(entry);
54
+ return {
55
+ tr,
56
+ selection: entry.selectionBefore
57
+ };
58
+ }
59
+ redo(doc) {
60
+ const entry = this.undone.pop();
61
+ if (!entry) return null;
62
+ const tr = applyFlowSteps(doc, entry.steps);
63
+ this.done.push({
64
+ ...entry,
65
+ group: void 0
66
+ });
67
+ return {
68
+ tr,
69
+ selection: entry.selectionAfter
70
+ };
71
+ }
72
+ clear() {
73
+ this.done = [];
74
+ this.undone = [];
75
+ }
76
+ };
77
+ export { FlowHistory };
78
+
79
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","names":[],"sources":["../../src/core/history.ts"],"sourcesContent":["import type { FlowDocument, FlowSelection } from './model'\nimport {\n applyFlowSteps,\n compactFlowSteps,\n type FlowStep,\n type FlowTransaction,\n} from './transaction'\n\ninterface Entry {\n steps: FlowStep[]\n inverse: FlowStep[]\n group?: string\n time: number\n selectionBefore: FlowSelection\n selectionAfter: FlowSelection\n}\n\n/** Transactions in one group merge when they come within this many ms. */\nconst GROUP_WINDOW = 1000\n\nexport class FlowHistory {\n private done: Entry[] = []\n private undone: Entry[] = []\n\n constructor(private depth = 100) {}\n\n get canUndo() {\n return this.done.length > 0\n }\n\n get canRedo() {\n return this.undone.length > 0\n }\n\n record(\n tr: FlowTransaction,\n selectionBefore: FlowSelection,\n selectionAfter: FlowSelection,\n now = Date.now(),\n ) {\n if (!tr.changed || tr.meta.history === false) return\n this.undone = []\n const last = this.done[this.done.length - 1]\n const group = tr.meta.group\n if (last && group && last.group === group && now - last.time < GROUP_WINDOW) {\n last.steps = compactFlowSteps([...last.steps, ...tr.steps])\n last.inverse = compactFlowSteps([...tr.inverse, ...last.inverse])\n last.time = now\n last.selectionAfter = selectionAfter\n return\n }\n this.done.push({\n steps: tr.steps.slice(),\n inverse: tr.inverse.slice(),\n group,\n time: now,\n selectionBefore,\n selectionAfter,\n })\n if (this.done.length > this.depth) this.done.shift()\n }\n\n /** Ends the current group, so the next change starts a new undo step. */\n close() {\n const last = this.done[this.done.length - 1]\n if (last) last.group = undefined\n }\n\n /** Forgets the last entry if it belongs to `group`, e.g. after a cancelled drag. */\n drop(group: string) {\n const last = this.done[this.done.length - 1]\n if (last?.group === group) this.done.pop()\n }\n\n undo(doc: FlowDocument) {\n const entry = this.done.pop()\n if (!entry) return null\n const tr = applyFlowSteps(doc, entry.inverse)\n this.undone.push(entry)\n return { tr, selection: entry.selectionBefore }\n }\n\n redo(doc: FlowDocument) {\n const entry = this.undone.pop()\n if (!entry) return null\n const tr = applyFlowSteps(doc, entry.steps)\n this.done.push({ ...entry, group: undefined })\n return { tr, selection: entry.selectionAfter }\n }\n\n clear() {\n this.done = []\n this.undone = []\n }\n}\n"],"mappings":";;;AAkBA,IAAM,eAAe;AAErB,IAAa,cAAb,MAAyB;CAIH;CAHpB,OAAwB,CAAC;CACzB,SAA0B,CAAC;CAE3B,YAAY,QAAgB,KAAK;EAAb,KAAA,QAAA;CAAc;CAElC,IAAI,UAAU;EACZ,OAAO,KAAK,KAAK,SAAS;CAC5B;CAEA,IAAI,UAAU;EACZ,OAAO,KAAK,OAAO,SAAS;CAC9B;CAEA,OACE,IACA,iBACA,gBACA,MAAM,KAAK,IAAI,GACf;EACA,IAAI,CAAC,GAAG,WAAW,GAAG,KAAK,YAAY,OAAO;EAC9C,KAAK,SAAS,CAAC;EACf,MAAM,OAAO,KAAK,KAAK,KAAK,KAAK,SAAS;EAC1C,MAAM,QAAQ,GAAG,KAAK;EACtB,IAAI,QAAQ,SAAS,KAAK,UAAU,SAAS,MAAM,KAAK,OAAO,cAAc;GAC3E,KAAK,QAAQ,iBAAiB,CAAC,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,CAAC;GAC1D,KAAK,UAAU,iBAAiB,CAAC,GAAG,GAAG,SAAS,GAAG,KAAK,OAAO,CAAC;GAChE,KAAK,OAAO;GACZ,KAAK,iBAAiB;GACtB;EACF;EACA,KAAK,KAAK,KAAK;GACb,OAAO,GAAG,MAAM,MAAM;GACtB,SAAS,GAAG,QAAQ,MAAM;GAC1B;GACA,MAAM;GACN;GACA;EACF,CAAC;EACD,IAAI,KAAK,KAAK,SAAS,KAAK,OAAO,KAAK,KAAK,MAAM;CACrD;;CAGA,QAAQ;EACN,MAAM,OAAO,KAAK,KAAK,KAAK,KAAK,SAAS;EAC1C,IAAI,MAAM,KAAK,QAAQ,KAAA;CACzB;;CAGA,KAAK,OAAe;EAElB,IADa,KAAK,KAAK,KAAK,KAAK,SAAS,EACtC,EAAM,UAAU,OAAO,KAAK,KAAK,IAAI;CAC3C;CAEA,KAAK,KAAmB;EACtB,MAAM,QAAQ,KAAK,KAAK,IAAI;EAC5B,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,KAAK,eAAe,KAAK,MAAM,OAAO;EAC5C,KAAK,OAAO,KAAK,KAAK;EACtB,OAAO;GAAE;GAAI,WAAW,MAAM;EAAgB;CAChD;CAEA,KAAK,KAAmB;EACtB,MAAM,QAAQ,KAAK,OAAO,IAAI;EAC9B,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,KAAK,eAAe,KAAK,MAAM,KAAK;EAC1C,KAAK,KAAK,KAAK;GAAE,GAAG;GAAO,OAAO,KAAA;EAAU,CAAC;EAC7C,OAAO;GAAE;GAAI,WAAW,MAAM;EAAe;CAC/C;CAEA,QAAQ;EACN,KAAK,OAAO,CAAC;EACb,KAAK,SAAS,CAAC;CACjB;AACF"}
@@ -0,0 +1,21 @@
1
+ // src/core/model.ts
2
+ var emptyDocument = () => ({
3
+ version: 1,
4
+ nodes: [],
5
+ edges: []
6
+ });
7
+ var emptySelection = () => ({
8
+ nodes: [],
9
+ edges: []
10
+ });
11
+ var counter = 0;
12
+ /** A short id, unique within this page and very likely across clients. */
13
+ function createId(prefix = "n") {
14
+ counter = (counter + 1) % 1296;
15
+ const time = Date.now().toString(36).slice(-5);
16
+ const random = Math.random().toString(36).slice(2, 6);
17
+ return `${prefix}${time}${counter.toString(36).padStart(2, "0")}${random}`;
18
+ }
19
+ export { createId, emptyDocument, emptySelection };
20
+
21
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","names":[],"sources":["../../src/core/model.ts"],"sourcesContent":["/*\n * The document a flow editor edits: plain, serialisable data, bound with\n * `v-model`. Positions are canvas coordinates in px, from the top left of a\n * node. Ports are not stored; a node type declares them, and edges refer to\n * them by id.\n */\n\nexport type FlowData = Record<string, unknown>\n\nexport interface FlowNode<Data extends FlowData = FlowData> {\n id: string\n /** A registered node type; `'default'` when left out. */\n type?: string\n x: number\n y: number\n /** Only stored for nodes that can be resized; otherwise the content decides. */\n width?: number\n height?: number\n data?: Data\n /** A locked node cannot be moved or deleted. */\n locked?: boolean\n /**\n * The container node, e.g. a group, this node sits in. Its `x` and `y`\n * stay canvas coordinates; moving the container moves it along.\n */\n parent?: string\n /**\n * Stacking order among nodes that aren't containers: higher is drawn on\n * top. Nodes without it count as `0`; equal ones keep the document order.\n */\n z?: number\n}\n\nexport interface FlowEndpoint {\n node: string\n /** Leave out to attach to the node itself. */\n port?: string\n}\n\nexport interface FlowEdge<Data extends FlowData = FlowData> {\n id: string\n source: FlowEndpoint\n target: FlowEndpoint\n /** The edge type: `bezier`, `step`, `straight` or one added by an extension. The editor's `edge-type` when left out. */\n type?: string\n /** Text shown on the edge. */\n label?: string\n /** Dashes run along the edge in its direction, e.g. while data flows through it. */\n animated?: boolean\n data?: Data\n}\n\nexport interface FlowDocument {\n /** The version of the format; see `migrateDocument`. */\n version: 1\n nodes: FlowNode[]\n edges: FlowEdge[]\n}\n\nexport type PortSide = 'left' | 'right' | 'top' | 'bottom'\n\nexport interface FlowPortSpec {\n id: string\n /** Inputs receive edges, outputs start them. */\n kind: 'input' | 'output'\n /** Where the port sits; inputs default to the left, outputs to the right. */\n side?: PortSide\n label?: string\n /** What the port carries. Two ports connect when they share a type, or either is `'any'`. */\n dataType?: string | string[]\n /**\n * The most edges the port takes. Inputs take one by default, outputs any\n * number. A new edge on a full port replaces its oldest edge.\n */\n max?: number\n}\n\n/** Where the canvas is shown: `x` and `y` are the screen offset of the canvas origin. */\nexport interface FlowViewport {\n x: number\n y: number\n zoom: number\n}\n\nexport interface FlowSelection {\n nodes: string[]\n edges: string[]\n}\n\nexport interface Point {\n x: number\n y: number\n}\n\nexport interface Rect {\n x: number\n y: number\n width: number\n height: number\n}\n\nexport const emptyDocument = (): FlowDocument => ({ version: 1, nodes: [], edges: [] })\nexport const emptySelection = (): FlowSelection => ({ nodes: [], edges: [] })\n\nlet counter = 0\n\n/** A short id, unique within this page and very likely across clients. */\nexport function createId(prefix = 'n') {\n counter = (counter + 1) % 1296\n const time = Date.now().toString(36).slice(-5)\n const random = Math.random().toString(36).slice(2, 6)\n return `${prefix}${time}${counter.toString(36).padStart(2, '0')}${random}`\n}\n"],"mappings":";AAqGA,IAAa,uBAAqC;CAAE,SAAS;CAAG,OAAO,CAAC;CAAG,OAAO,CAAC;AAAE;AACrF,IAAa,wBAAuC;CAAE,OAAO,CAAC;CAAG,OAAO,CAAC;AAAE;AAE3E,IAAI,UAAU;;AAGd,SAAgB,SAAS,SAAS,KAAK;CACrC,WAAW,UAAU,KAAK;CAC1B,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CAC7C,MAAM,SAAS,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;CACpD,OAAO,GAAG,SAAS,OAAO,QAAQ,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,IAAI;AACpE"}
@@ -0,0 +1,32 @@
1
+ import { turnedBox } from "./geometry.js";
2
+ // src/core/node-types.ts
3
+ /** Declares a node type, with the data type checked against its component. */
4
+ function defineNodeType(spec) {
5
+ if (process.env.NODE_ENV !== "production" && spec.rotation && spec.frame !== false) console.warn(`[@dou-ui/flow] the node type "${spec.type}" has rotation but not frame: false: the card the editor draws around it does not turn`);
6
+ return spec;
7
+ }
8
+ var portSide = (port) => port.side ?? (port.kind === "input" ? "left" : "right");
9
+ function portsOf(spec, node) {
10
+ const ports = spec?.ports;
11
+ if (!ports) return [];
12
+ return typeof ports === "function" ? ports(node) : ports;
13
+ }
14
+ /** Whether a node can be resized, and its smallest size; `null` when it can't. */
15
+ function resizeOf(spec, node) {
16
+ if (spec?.rotation?.(node)) return null;
17
+ const value = typeof spec?.resizable === "function" ? spec.resizable(node) : spec?.resizable;
18
+ if (!value) return null;
19
+ return value === true ? {} : value;
20
+ }
21
+ function labelOf(spec, node, messages) {
22
+ if (spec?.label) return spec.label(node, messages);
23
+ const label = node.data?.label;
24
+ return typeof label === "string" ? label : node.id;
25
+ }
26
+ /** The box `node` covers on screen, drawn at `rect`. */
27
+ function nodeBounds(spec, node, rect) {
28
+ return spec.bounds?.(node, rect) ?? turnedBox(rect, spec.rotation?.(node) ?? 0);
29
+ }
30
+ export { defineNodeType, labelOf, nodeBounds, portSide, portsOf, resizeOf };
31
+
32
+ //# sourceMappingURL=node-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-types.js","names":[],"sources":["../../src/core/node-types.ts"],"sourcesContent":["import type { FlowDrawContext } from '../export/context'\nimport type { Component } from 'vue'\nimport type { FlowMessages } from '../locale'\nimport { turnedBox } from './geometry'\nimport type { FlowData, FlowNode, FlowPortSpec, PortSide, Rect } from './model'\n\n/** What a node type's component receives. */\nexport interface NodeViewProps<Data extends FlowData = FlowData> {\n node: FlowNode<Data>\n data: Data\n ports: FlowPortSpec[]\n selected: boolean\n readonly: boolean\n /** Merges `patch` into the node's data, as one undo step. */\n updateData: (patch: Partial<Data>) => void\n}\n\nexport interface NodeTypeSpec<Data extends FlowData = FlowData> {\n type: string\n /**\n * Renders the node's content. The editor draws the frame, selection and\n * focus ring around it. Place ports with `<DouFlowPort>`; ports the\n * component does not place are spread along their sides.\n */\n component?: Component\n /** The node's ports, fixed or worked out from its data. */\n ports?: FlowPortSpec[] | ((node: FlowNode<Data>) => FlowPortSpec[])\n /** Data for a new node of this type. */\n defaults?: () => Data\n /**\n * The node's accessible name; `data.label` by default. `messages` are the\n * editor's current strings, for names that follow the locale.\n */\n label?: (node: FlowNode<Data>, messages: FlowMessages) => string\n /**\n * How the type is offered by `paletteTypes` and in the list that opens\n * when an edge is dropped on the empty canvas. Types without it aren't offered.\n */\n palette?: NodePaletteSpec\n /**\n * Nodes dropped inside become its children (`parent`) and move with it;\n * with `data.collapsed` they are hidden and their edges end on it.\n * Containers are drawn under edges and other nodes.\n */\n container?: boolean\n /**\n * A handle on the selected node changes its stored `width` and `height`.\n * A function decides per node, e.g. not while it is turned.\n */\n resizable?: NodeResize | ((node: FlowNode<Data>) => NodeResize)\n /** The `width` and `height` a new node starts with. */\n size?: { width: number; height: number }\n /**\n * `false` leaves out the card the editor draws around the component (its\n * border, background and shadow), for nodes that draw their own outline,\n * e.g. a circle. Selection then shows as a thin outline around the box.\n */\n frame?: boolean\n /**\n * How far the node is drawn turned about its centre, in degrees clockwise;\n * `0` by default. The component turns what it draws, and `draw` turns its\n * export; the editor follows: the selection outline, picking with a box,\n * what is drawn in view, the toolbar and other people's outlines. For\n * nodes with `frame: false`. A turned node has no editor resize handle.\n */\n rotation?: (node: FlowNode<Data>) => number\n /**\n * The box the node covers on screen when it draws outside its own `rect`.\n * The toolbar over the selection floats above it. By default the box\n * around `rect` turned by `rotation`.\n */\n bounds?: (node: FlowNode<Data>, rect: Rect) => Rect\n /**\n * Draws the node as SVG markup for `exportSVG` and `exportPNG`, in its\n * own coordinates. Without it the node is drawn as a card with its name.\n */\n draw?: (node: FlowNode<Data>, draw: FlowDrawContext) => string\n}\n\nexport interface NodePaletteSpec {\n label: string\n icon?: Component\n /** Types with the same group are listed together under its name. */\n group?: string\n description?: string\n /** Other words the search should find the type by. */\n keywords?: string[]\n}\n\n/** Declares a node type, with the data type checked against its component. */\nexport function defineNodeType<Data extends FlowData = FlowData>(\n spec: NodeTypeSpec<Data>,\n): NodeTypeSpec<Data> {\n if (process.env.NODE_ENV !== 'production' && spec.rotation && spec.frame !== false)\n console.warn(\n `[@dou-ui/flow] the node type \"${spec.type}\" has rotation but not frame: false: the card the editor draws around it does not turn`,\n )\n return spec\n}\n\nexport const portSide = (port: FlowPortSpec): PortSide =>\n port.side ?? (port.kind === 'input' ? 'left' : 'right')\n\nexport function portsOf(spec: NodeTypeSpec | undefined, node: FlowNode): FlowPortSpec[] {\n const ports = spec?.ports\n if (!ports) return []\n return typeof ports === 'function' ? ports(node) : ports\n}\n\nexport type NodeResize = boolean | { minWidth?: number; minHeight?: number }\n\n/** Whether a node can be resized, and its smallest size; `null` when it can't. */\nexport function resizeOf(\n spec: NodeTypeSpec | undefined,\n node: FlowNode,\n): { minWidth?: number; minHeight?: number } | null {\n // the editor's handle grows a node along the page, not along a turned node's sides\n if (spec?.rotation?.(node)) return null\n const value = typeof spec?.resizable === 'function' ? spec.resizable(node) : spec?.resizable\n if (!value) return null\n return value === true ? {} : value\n}\n\nexport function labelOf(\n spec: NodeTypeSpec | undefined,\n node: FlowNode,\n messages: FlowMessages,\n): string {\n if (spec?.label) return spec.label(node, messages)\n const label = node.data?.label\n return typeof label === 'string' ? label : node.id\n}\n\n/** The box `node` covers on screen, drawn at `rect`. */\nexport function nodeBounds(spec: NodeTypeSpec, node: FlowNode, rect: Rect): Rect {\n return spec.bounds?.(node, rect) ?? turnedBox(rect, spec.rotation?.(node) ?? 0)\n}\n"],"mappings":";;;AA0FA,SAAgB,eACd,MACoB;CACpB,IAAA,QAAA,IAAA,aAA6B,gBAAgB,KAAK,YAAY,KAAK,UAAU,OAC3E,QAAQ,KACN,iCAAiC,KAAK,KAAK,uFAC7C;CACF,OAAO;AACT;AAEA,IAAa,YAAY,SACvB,KAAK,SAAS,KAAK,SAAS,UAAU,SAAS;AAEjD,SAAgB,QAAQ,MAAgC,MAAgC;CACtF,MAAM,QAAQ,MAAM;CACpB,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO,OAAO,UAAU,aAAa,MAAM,IAAI,IAAI;AACrD;;AAKA,SAAgB,SACd,MACA,MACkD;CAElD,IAAI,MAAM,WAAW,IAAI,GAAG,OAAO;CACnC,MAAM,QAAQ,OAAO,MAAM,cAAc,aAAa,KAAK,UAAU,IAAI,IAAI,MAAM;CACnF,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,UAAU,OAAO,CAAC,IAAI;AAC/B;AAEA,SAAgB,QACd,MACA,MACA,UACQ;CACR,IAAI,MAAM,OAAO,OAAO,KAAK,MAAM,MAAM,QAAQ;CACjD,MAAM,QAAQ,KAAK,MAAM;CACzB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK;AAClD;;AAGA,SAAgB,WAAW,MAAoB,MAAgB,MAAkB;CAC/E,OAAO,KAAK,SAAS,MAAM,IAAI,KAAK,UAAU,MAAM,KAAK,WAAW,IAAI,KAAK,CAAC;AAChF"}
@@ -0,0 +1,42 @@
1
+ // src/core/order.ts
2
+ var before = (a, b) => a.x - b.x || a.y - b.y;
3
+ /**
4
+ * Nodes in the order a flow reads: every node after the ones it depends on,
5
+ * and among nodes that are ready, the leftmost (then topmost) first. Nodes
6
+ * on a cycle follow the rest, left to right.
7
+ */
8
+ function flowOrder(doc) {
9
+ const byId = new Map(doc.nodes.map((node) => [node.id, node]));
10
+ const waiting = new Map(doc.nodes.map((node) => [node.id, 0]));
11
+ const next = /* @__PURE__ */ new Map();
12
+ for (const edge of doc.edges) {
13
+ const { node: from } = edge.source;
14
+ const { node: to } = edge.target;
15
+ if (from === to || !byId.has(from) || !byId.has(to)) continue;
16
+ waiting.set(to, waiting.get(to) + 1);
17
+ const list = next.get(from);
18
+ if (list) list.push(to);
19
+ else next.set(from, [to]);
20
+ }
21
+ const ready = doc.nodes.filter((node) => waiting.get(node.id) === 0);
22
+ const out = [];
23
+ while (ready.length) {
24
+ let pick = 0;
25
+ for (let i = 1; i < ready.length; i++) if (before(ready[i], ready[pick]) < 0) pick = i;
26
+ const [node] = ready.splice(pick, 1);
27
+ out.push(node);
28
+ for (const id of next.get(node.id) ?? []) {
29
+ const left = waiting.get(id) - 1;
30
+ waiting.set(id, left);
31
+ if (left === 0) ready.push(byId.get(id));
32
+ }
33
+ }
34
+ if (out.length < doc.nodes.length) {
35
+ const placed = new Set(out);
36
+ out.push(...doc.nodes.filter((node) => !placed.has(node)).sort(before));
37
+ }
38
+ return out;
39
+ }
40
+ export { flowOrder };
41
+
42
+ //# sourceMappingURL=order.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"order.js","names":[],"sources":["../../src/core/order.ts"],"sourcesContent":["import type { FlowDocument, FlowNode } from './model'\n\nconst before = (a: FlowNode, b: FlowNode) => a.x - b.x || a.y - b.y\n\n/**\n * Nodes in the order a flow reads: every node after the ones it depends on,\n * and among nodes that are ready, the leftmost (then topmost) first. Nodes\n * on a cycle follow the rest, left to right.\n */\nexport function flowOrder(doc: FlowDocument): FlowNode[] {\n const byId = new Map(doc.nodes.map((node) => [node.id, node]))\n const waiting = new Map(doc.nodes.map((node) => [node.id, 0]))\n const next = new Map<string, string[]>()\n for (const edge of doc.edges) {\n const { node: from } = edge.source\n const { node: to } = edge.target\n if (from === to || !byId.has(from) || !byId.has(to)) continue\n waiting.set(to, waiting.get(to)! + 1)\n const list = next.get(from)\n if (list) list.push(to)\n else next.set(from, [to])\n }\n const ready = doc.nodes.filter((node) => waiting.get(node.id) === 0)\n const out: FlowNode[] = []\n while (ready.length) {\n let pick = 0\n for (let i = 1; i < ready.length; i++) if (before(ready[i]!, ready[pick]!) < 0) pick = i\n const [node] = ready.splice(pick, 1)\n out.push(node!)\n for (const id of next.get(node!.id) ?? []) {\n const left = waiting.get(id)! - 1\n waiting.set(id, left)\n if (left === 0) ready.push(byId.get(id)!)\n }\n }\n if (out.length < doc.nodes.length) {\n const placed = new Set(out)\n // oxlint-disable-next-line unicorn/no-array-sort -- sorts a fresh list\n out.push(...doc.nodes.filter((node) => !placed.has(node)).sort(before))\n }\n return out\n}\n"],"mappings":";AAEA,IAAM,UAAU,GAAa,MAAgB,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;;;;;;AAOlE,SAAgB,UAAU,KAA+B;CACvD,MAAM,OAAO,IAAI,IAAI,IAAI,MAAM,KAAK,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;CAC7D,MAAM,UAAU,IAAI,IAAI,IAAI,MAAM,KAAK,SAAS,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;CAC7D,MAAM,uBAAO,IAAI,IAAsB;CACvC,KAAK,MAAM,QAAQ,IAAI,OAAO;EAC5B,MAAM,EAAE,MAAM,SAAS,KAAK;EAC5B,MAAM,EAAE,MAAM,OAAO,KAAK;EAC1B,IAAI,SAAS,MAAM,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,EAAE,GAAG;EACrD,QAAQ,IAAI,IAAI,QAAQ,IAAI,EAAE,IAAK,CAAC;EACpC,MAAM,OAAO,KAAK,IAAI,IAAI;EAC1B,IAAI,MAAM,KAAK,KAAK,EAAE;OACjB,KAAK,IAAI,MAAM,CAAC,EAAE,CAAC;CAC1B;CACA,MAAM,QAAQ,IAAI,MAAM,QAAQ,SAAS,QAAQ,IAAI,KAAK,EAAE,MAAM,CAAC;CACnE,MAAM,MAAkB,CAAC;CACzB,OAAO,MAAM,QAAQ;EACnB,IAAI,OAAO;EACX,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,IAAI,OAAO,MAAM,IAAK,MAAM,KAAM,IAAI,GAAG,OAAO;EACvF,MAAM,CAAC,QAAQ,MAAM,OAAO,MAAM,CAAC;EACnC,IAAI,KAAK,IAAK;EACd,KAAK,MAAM,MAAM,KAAK,IAAI,KAAM,EAAE,KAAK,CAAC,GAAG;GACzC,MAAM,OAAO,QAAQ,IAAI,EAAE,IAAK;GAChC,QAAQ,IAAI,IAAI,IAAI;GACpB,IAAI,SAAS,GAAG,MAAM,KAAK,KAAK,IAAI,EAAE,CAAE;EAC1C;CACF;CACA,IAAI,IAAI,SAAS,IAAI,MAAM,QAAQ;EACjC,MAAM,SAAS,IAAI,IAAI,GAAG;EAE1B,IAAI,KAAK,GAAG,IAAI,MAAM,QAAQ,SAAS,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;CACxE;CACA,OAAO;AACT"}