flexdesk 0.4.7 → 0.6.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.
- package/README.md +1036 -301
- package/css/base.css +2958 -2
- package/css/flexdesk.css +2962 -4
- package/css/overrides.css +4 -2
- package/css/tokens.css +86 -0
- package/dist/canvas.js +390 -0
- package/dist/canvas.js.map +7 -0
- package/dist/charts.js +782 -782
- package/dist/{chunk-LH5TSOZW.js → chunk-6JSOVNID.js} +42 -2
- package/dist/{chunk-LH5TSOZW.js.map → chunk-6JSOVNID.js.map} +3 -3
- package/dist/{chunk-ELXVW542.js → chunk-6NG7YVBG.js} +3 -471
- package/dist/chunk-6NG7YVBG.js.map +7 -0
- package/dist/{chunk-F5FYDCOA.js → chunk-CNXP3SIN.js} +2373 -2373
- package/dist/{chunk-F5FYDCOA.js.map → chunk-CNXP3SIN.js.map} +7 -7
- package/dist/chunk-H4LT43RS.js +475 -0
- package/dist/chunk-H4LT43RS.js.map +7 -0
- package/dist/{chunk-WMGOJ3I6.js → chunk-KH2GJTKR.js} +3702 -2628
- package/dist/chunk-KH2GJTKR.js.map +7 -0
- package/dist/chunk-SN4LSOAR.js +532 -0
- package/dist/chunk-SN4LSOAR.js.map +7 -0
- package/dist/flexdesk.css +2962 -4
- package/dist/flow.js +10030 -0
- package/dist/flow.js.map +7 -0
- package/dist/tiles.js +2195 -2195
- package/dist/tokens.css +86 -0
- package/dist/widgets.js +5824 -5796
- package/dist/widgets.js.map +1 -1
- package/dist/wm.js +8077 -8074
- package/dist/wm.js.map +7 -7
- package/package.json +89 -81
- package/src/canvas/canvas_adapter.js +523 -0
- package/src/canvas/edge_router.js +392 -0
- package/src/canvas/graph/connector_router.js +189 -0
- package/src/canvas/graph/history_service.js +170 -0
- package/src/canvas/graph/node_platform.js +116 -0
- package/src/canvas/index.js +16 -0
- package/src/flow/canvas_editor/editor.js +1081 -0
- package/src/flow/canvas_editor/index.js +16 -0
- package/src/flow/canvas_editor/layout.js +118 -0
- package/src/flow/canvas_editor/strings.js +37 -0
- package/src/flow/kit/always_before.js +325 -0
- package/src/flow/kit/catalogue.js +170 -0
- package/src/flow/kit/chip_input.js +461 -0
- package/src/flow/kit/dom.js +205 -0
- package/src/flow/kit/findings.js +129 -0
- package/src/flow/kit/history.js +173 -0
- package/src/flow/kit/index.js +38 -0
- package/src/flow/kit/keys.js +127 -0
- package/src/flow/kit/logic_graph.js +210 -0
- package/src/flow/kit/references.js +199 -0
- package/src/flow/kit/settings_panel.js +357 -0
- package/src/flow/kit/settings_schema.js +175 -0
- package/src/flow/kit/step_picker.js +209 -0
- package/src/flow/kit/strings.js +102 -0
- package/src/flow/kit/value_picker.js +238 -0
- package/src/flow/kit/widgets.js +763 -0
- package/src/flow/lanes/editor.js +1469 -0
- package/src/flow/lanes/index.js +26 -0
- package/src/flow/lanes/layout.js +525 -0
- package/src/flow/lanes/pipeline.js +327 -0
- package/src/flow/lanes/preview.js +109 -0
- package/src/flow/lanes/strings.js +98 -0
- package/src/flow/outline/editor.js +1511 -0
- package/src/flow/outline/index.js +20 -0
- package/src/flow/outline/mapping.js +286 -0
- package/src/flow/outline/menu.js +154 -0
- package/src/flow/outline/operations.js +738 -0
- package/src/flow/outline/recognise.js +755 -0
- package/src/flow/outline/strings.js +135 -0
- package/src/tiling/landing_table.js +13 -5
- package/src/tiling/wm.js +3916 -3916
- package/src/ui/components/data_table.js +4205 -3294
- package/src/ui/components/row_activation.js +70 -0
- package/src/ui/components/table_values.js +423 -0
- package/dist/chunk-ELXVW542.js.map +0 -7
- package/dist/chunk-WMGOJ3I6.js.map +0 -7
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
// src/canvas/canvas_adapter.js
|
|
2
|
+
var MIN_SCALE = 0.15;
|
|
3
|
+
var MAX_SCALE = 4;
|
|
4
|
+
var COARSE_GRID_EVERY = 5;
|
|
5
|
+
var SUB_PIXEL = 0.5;
|
|
6
|
+
var CanvasAdapter = class {
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} o
|
|
9
|
+
* @param {HTMLElement} o.container the box the canvas fills (it needs a height)
|
|
10
|
+
* @param {number} [o.gridSize] the snap grid, in canvas pixels; 0 = no snap
|
|
11
|
+
* @param {(viewport) => void} [o.onViewportChange] after every pan and zoom — and once,
|
|
12
|
+
* synchronously, from this constructor
|
|
13
|
+
* @param {(event: MouseEvent) => void} [o.onBackgroundMenu] a right-CLICK on the empty surface
|
|
14
|
+
* @param {string|null} [o.nodeSelector] what a node is, when it is not simply "an element of
|
|
15
|
+
* `nodeLayer`"; a press inside one is the embedder's, never a pan or a marquee
|
|
16
|
+
*/
|
|
17
|
+
constructor({ container, gridSize = 16, onViewportChange, onBackgroundMenu, nodeSelector = null } = {}) {
|
|
18
|
+
this.container = container;
|
|
19
|
+
this.gridSize = gridSize;
|
|
20
|
+
this.onViewportChange = onViewportChange;
|
|
21
|
+
this.onBackgroundMenu = onBackgroundMenu;
|
|
22
|
+
this.nodeSelector = nodeSelector;
|
|
23
|
+
this.scale = 1;
|
|
24
|
+
this.tx = 0;
|
|
25
|
+
this.ty = 0;
|
|
26
|
+
this.selection = /* @__PURE__ */ new Set();
|
|
27
|
+
this.root = document.createElement("div");
|
|
28
|
+
this.root.className = "twm-canvas";
|
|
29
|
+
this.root.tabIndex = 0;
|
|
30
|
+
this.surface = document.createElement("div");
|
|
31
|
+
this.surface.className = "twm-canvas__surface";
|
|
32
|
+
this.edgeLayer = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
33
|
+
this.edgeLayer.setAttribute("class", "twm-canvas__edges");
|
|
34
|
+
this.edgeLayer.setAttribute("overflow", "visible");
|
|
35
|
+
this.nodeLayer = document.createElement("div");
|
|
36
|
+
this.nodeLayer.className = "twm-canvas__nodes";
|
|
37
|
+
this.marquee = document.createElement("div");
|
|
38
|
+
this.marquee.className = "twm-canvas__marquee";
|
|
39
|
+
this.marquee.hidden = true;
|
|
40
|
+
this.surface.append(this.edgeLayer, this.nodeLayer);
|
|
41
|
+
this.root.append(this.surface, this.marquee);
|
|
42
|
+
container.appendChild(this.root);
|
|
43
|
+
this._bind();
|
|
44
|
+
this._apply();
|
|
45
|
+
}
|
|
46
|
+
// ── coordinates ─────────────────────────────────────────────────────
|
|
47
|
+
/** Client (screen) point -> canvas point. */
|
|
48
|
+
clientToCanvas(clientX, clientY) {
|
|
49
|
+
const box = this.root.getBoundingClientRect();
|
|
50
|
+
return {
|
|
51
|
+
x: (clientX - box.left - this.tx) / this.scale,
|
|
52
|
+
y: (clientY - box.top - this.ty) / this.scale
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
canvasToClient(x, y) {
|
|
56
|
+
const box = this.root.getBoundingClientRect();
|
|
57
|
+
return {
|
|
58
|
+
x: x * this.scale + this.tx + box.left,
|
|
59
|
+
y: y * this.scale + this.ty + box.top
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
snap(value) {
|
|
63
|
+
return this.gridSize ? Math.round(value / this.gridSize) * this.gridSize : value;
|
|
64
|
+
}
|
|
65
|
+
// ── viewport ────────────────────────────────────────────────────────
|
|
66
|
+
setViewport({ scale, tx, ty }) {
|
|
67
|
+
if (scale !== void 0) this.scale = clamp(scale, MIN_SCALE, MAX_SCALE);
|
|
68
|
+
if (tx !== void 0) this.tx = tx;
|
|
69
|
+
if (ty !== void 0) this.ty = ty;
|
|
70
|
+
this._apply();
|
|
71
|
+
}
|
|
72
|
+
viewport() {
|
|
73
|
+
return { scale: this.scale, tx: this.tx, ty: this.ty };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Did the right button just PAN rather than click — and clear the answer.
|
|
77
|
+
*
|
|
78
|
+
* Right-drag pans (see `_bind`), so the `contextmenu` that arrives when the
|
|
79
|
+
* button comes back up is the tail of a gesture, not a request for a menu.
|
|
80
|
+
* The kernel's own root listener has always known this. Nothing else could
|
|
81
|
+
* ask, and everything else needs to: a `contextmenu` handler on a node or an
|
|
82
|
+
* edge is on the TARGET, and target-phase listeners run before the root's,
|
|
83
|
+
* so an embedder that opens a menu opens it at the end of every pan that
|
|
84
|
+
* happens to finish over one of its elements.
|
|
85
|
+
*
|
|
86
|
+
* CONSUMING, not peeking, and that is the whole design. An embedder's
|
|
87
|
+
* element handler will often call `stopPropagation()` — deliberately, so one
|
|
88
|
+
* press never opens two menus — which means the root listener below may
|
|
89
|
+
* never run to do the clearing. A plain getter would therefore leave the
|
|
90
|
+
* flag `true` and suppress the NEXT right-click, turning one wrong menu into
|
|
91
|
+
* a menu that has stopped working. Whoever reads it first clears it; nobody
|
|
92
|
+
* sees it twice; there is no ordering left to get wrong.
|
|
93
|
+
*
|
|
94
|
+
* An embedder that consumes a `true` should still `preventDefault()` and
|
|
95
|
+
* show nothing — suppressing the browser's menu is the root listener's job
|
|
96
|
+
* and it has just been cut out of the loop.
|
|
97
|
+
*/
|
|
98
|
+
consumeRightDrag() {
|
|
99
|
+
const dragged = this._rightDragged;
|
|
100
|
+
this._rightDragged = false;
|
|
101
|
+
return dragged;
|
|
102
|
+
}
|
|
103
|
+
zoomAt(clientX, clientY, factor) {
|
|
104
|
+
const before = this.clientToCanvas(clientX, clientY);
|
|
105
|
+
this.scale = clamp(this.scale * factor, MIN_SCALE, MAX_SCALE);
|
|
106
|
+
const after = this.clientToCanvas(clientX, clientY);
|
|
107
|
+
this.tx += (after.x - before.x) * this.scale;
|
|
108
|
+
this.ty += (after.y - before.y) * this.scale;
|
|
109
|
+
this._apply();
|
|
110
|
+
}
|
|
111
|
+
fit(bounds, { padding = 60 } = {}) {
|
|
112
|
+
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return;
|
|
113
|
+
const box = this.root.getBoundingClientRect();
|
|
114
|
+
const scale = clamp(
|
|
115
|
+
Math.min(
|
|
116
|
+
(box.width - padding * 2) / bounds.width,
|
|
117
|
+
(box.height - padding * 2) / bounds.height
|
|
118
|
+
),
|
|
119
|
+
MIN_SCALE,
|
|
120
|
+
MAX_SCALE
|
|
121
|
+
);
|
|
122
|
+
this.scale = scale;
|
|
123
|
+
this.tx = (box.width - bounds.width * scale) / 2 - bounds.x * scale;
|
|
124
|
+
this.ty = (box.height - bounds.height * scale) / 2 - bounds.y * scale;
|
|
125
|
+
this._apply();
|
|
126
|
+
}
|
|
127
|
+
/** What is on screen, in CANVAS coordinates. The inverse of the transform
|
|
128
|
+
* `_apply` writes, and the only honest way for an embedder to ask "is this
|
|
129
|
+
* already visible" without duplicating the arithmetic. */
|
|
130
|
+
visibleBox() {
|
|
131
|
+
const view = this.root.getBoundingClientRect();
|
|
132
|
+
return {
|
|
133
|
+
x: -this.tx / this.scale,
|
|
134
|
+
y: -this.ty / this.scale,
|
|
135
|
+
width: view.width / this.scale,
|
|
136
|
+
height: view.height / this.scale
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Is all of `box` (canvas coordinates) inside the viewport, with `padding`
|
|
140
|
+
* screen pixels to spare on every side? */
|
|
141
|
+
contains(box, { padding = 0 } = {}) {
|
|
142
|
+
if (!box) return false;
|
|
143
|
+
const view = this.root.getBoundingClientRect();
|
|
144
|
+
return box.x * this.scale + this.tx >= padding - SUB_PIXEL && box.y * this.scale + this.ty >= padding - SUB_PIXEL && (box.x + box.width) * this.scale + this.tx <= view.width - padding + SUB_PIXEL && (box.y + box.height) * this.scale + this.ty <= view.height - padding + SUB_PIXEL;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* PAN — AND ONLY PAN — UNTIL `box` IS IN VIEW. `this.scale` is never read
|
|
148
|
+
* for a decision and never written.
|
|
149
|
+
*
|
|
150
|
+
* `fit` is the other way to put something on screen and it is a DIFFERENT
|
|
151
|
+
* gesture: it derives a scale from the box and applies it, up to the 4×
|
|
152
|
+
* ceiling. So "take me to this node" arrived as a zoom in until one node
|
|
153
|
+
* filled the pane, and "take me to this line" as whatever scale happened to
|
|
154
|
+
* frame two boxes — in either direction. The reader's complaint was *"do not
|
|
155
|
+
* zoom in, just move viewport if required"*, and it is two complaints, not
|
|
156
|
+
* one:
|
|
157
|
+
*
|
|
158
|
+
* * THE SCALE IS THE USER'S. They set it by scrolling; nothing that is
|
|
159
|
+
* merely navigation may overwrite it. Zooming OUT "to help" fit a wide
|
|
160
|
+
* pair breaks the same rule as zooming in.
|
|
161
|
+
* * ALREADY VISIBLE MEANS NOTHING HAPPENS. `fit` centres unconditionally,
|
|
162
|
+
* so asking for a node you are already looking at slid the whole drawing
|
|
163
|
+
* under the cursor. A pan that was not required is not a smaller version
|
|
164
|
+
* of the right behaviour; it is the wrong one. Hence the first branch of
|
|
165
|
+
* `_panShift`, and hence `_apply()` below is not called at all when both
|
|
166
|
+
* deltas are zero — no transform is rewritten, no `onViewportChange`
|
|
167
|
+
* fires, and so no layout save is scheduled.
|
|
168
|
+
*
|
|
169
|
+
* Otherwise the move is the MINIMUM that brings the box inside: the near
|
|
170
|
+
* edge lands on the padding and the far edge is left wherever it falls.
|
|
171
|
+
* Centring would travel further for no gain.
|
|
172
|
+
*
|
|
173
|
+
* ── WHEN THE BOX IS BIGGER THAN THE WINDOW AT THIS SCALE ──
|
|
174
|
+
*
|
|
175
|
+
* It cannot be brought in, and zooming out is exactly what the rule
|
|
176
|
+
* forbids, so `fallback` names a smaller box worth preferring — for a line,
|
|
177
|
+
* the end nearest what is already on screen. Per axis and independently,
|
|
178
|
+
* because two nodes side by side are usually too wide and perfectly fine
|
|
179
|
+
* vertically. With no fallback (or one that is itself oversized) the rule is
|
|
180
|
+
* "fill the window with as much of it as possible": if the window already
|
|
181
|
+
* lies entirely within the box, nothing can be gained by moving and nothing
|
|
182
|
+
* moves; otherwise the box is pulled across until the dead space on one
|
|
183
|
+
* side is gone.
|
|
184
|
+
*
|
|
185
|
+
* @param {{x, y, width, height}} box — canvas coordinates.
|
|
186
|
+
* @param {{padding?: number, fallback?: object}} [options]
|
|
187
|
+
* @returns {{moved: boolean, contained: boolean}} `contained` reports
|
|
188
|
+
* whether ALL of `box` ended up on screen, so a caller can say why
|
|
189
|
+
* half of what it named is still not visible rather than leaving
|
|
190
|
+
* the user to conclude the click failed.
|
|
191
|
+
*/
|
|
192
|
+
bringIntoView(box, { padding = 40, fallback = null } = {}) {
|
|
193
|
+
if (!box || box.width < 0 || box.height < 0) {
|
|
194
|
+
return { moved: false, contained: false };
|
|
195
|
+
}
|
|
196
|
+
const view = this.root.getBoundingClientRect();
|
|
197
|
+
const dx = ignoreSubPixel(
|
|
198
|
+
this._panShift(
|
|
199
|
+
box.x,
|
|
200
|
+
box.width,
|
|
201
|
+
view.width,
|
|
202
|
+
this.tx,
|
|
203
|
+
padding,
|
|
204
|
+
fallback && { start: fallback.x, size: fallback.width }
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
const dy = ignoreSubPixel(
|
|
208
|
+
this._panShift(
|
|
209
|
+
box.y,
|
|
210
|
+
box.height,
|
|
211
|
+
view.height,
|
|
212
|
+
this.ty,
|
|
213
|
+
padding,
|
|
214
|
+
fallback && { start: fallback.y, size: fallback.height }
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
const moved = dx !== 0 || dy !== 0;
|
|
218
|
+
if (moved) {
|
|
219
|
+
this.tx += dx;
|
|
220
|
+
this.ty += dy;
|
|
221
|
+
this._apply();
|
|
222
|
+
}
|
|
223
|
+
return { moved, contained: this.contains(box) };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* One axis of `bringIntoView`, in SCREEN pixels: how far the translate has
|
|
227
|
+
* to move. Positive is right/down. Returns 0 for "already in view", which is
|
|
228
|
+
* the branch the whole method exists for.
|
|
229
|
+
*/
|
|
230
|
+
_panShift(start, size, viewportSize, translate, padding, fallback) {
|
|
231
|
+
const pad = Math.min(padding, Math.max(0, (viewportSize - 1) / 2));
|
|
232
|
+
const low = pad;
|
|
233
|
+
const high = viewportSize - pad;
|
|
234
|
+
const near = start * this.scale + translate;
|
|
235
|
+
const far = (start + size) * this.scale + translate;
|
|
236
|
+
if (near >= low && far <= high) return 0;
|
|
237
|
+
if (far - near <= high - low) {
|
|
238
|
+
return near < low ? low - near : high - far;
|
|
239
|
+
}
|
|
240
|
+
if (fallback) {
|
|
241
|
+
return this._panShift(
|
|
242
|
+
fallback.start,
|
|
243
|
+
fallback.size,
|
|
244
|
+
viewportSize,
|
|
245
|
+
translate,
|
|
246
|
+
padding,
|
|
247
|
+
null
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (near <= low && far >= high) return 0;
|
|
251
|
+
return near > low ? low - near : high - far;
|
|
252
|
+
}
|
|
253
|
+
_apply() {
|
|
254
|
+
this.surface.style.transform = `translate(${this.tx}px, ${this.ty}px) scale(${this.scale})`;
|
|
255
|
+
const fine = this.gridSize * this.scale;
|
|
256
|
+
const coarse = fine * COARSE_GRID_EVERY;
|
|
257
|
+
this.root.style.backgroundSize = [
|
|
258
|
+
"160px 160px",
|
|
259
|
+
// grain — its own fixed scale
|
|
260
|
+
"auto",
|
|
261
|
+
// lighting — fills the element
|
|
262
|
+
`${coarse}px ${coarse}px`,
|
|
263
|
+
`${coarse}px ${coarse}px`,
|
|
264
|
+
`${fine}px ${fine}px`,
|
|
265
|
+
`${fine}px ${fine}px`,
|
|
266
|
+
"auto"
|
|
267
|
+
// the ground
|
|
268
|
+
].join(", ");
|
|
269
|
+
const offset = `${this.tx}px ${this.ty}px`;
|
|
270
|
+
this.root.style.backgroundPosition = ["0 0", "0 0", offset, offset, offset, offset, "0 0"].join(", ");
|
|
271
|
+
this.onViewportChange?.(this.viewport());
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Is `target` inside one of the embedder's nodes? A node is whatever the
|
|
275
|
+
* embedder put in the node layer — or, with `nodeSelector`, what that
|
|
276
|
+
* selector names. A press there belongs to the embedder: it starts no pan
|
|
277
|
+
* and no marquee, and its right-click keeps whatever menu the embedder (or,
|
|
278
|
+
* failing that, the browser) gives it.
|
|
279
|
+
*/
|
|
280
|
+
_onNode(target) {
|
|
281
|
+
if (!target || typeof target.closest !== "function") return false;
|
|
282
|
+
if (this.nodeSelector) return Boolean(target.closest(this.nodeSelector));
|
|
283
|
+
return target !== this.nodeLayer && this.nodeLayer.contains(target);
|
|
284
|
+
}
|
|
285
|
+
// ── input ───────────────────────────────────────────────────────────
|
|
286
|
+
_bind() {
|
|
287
|
+
this.root.addEventListener("wheel", (event) => {
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
if (event.ctrlKey || event.metaKey) {
|
|
290
|
+
this.zoomAt(event.clientX, event.clientY, event.deltaY < 0 ? 1.12 : 1 / 1.12);
|
|
291
|
+
} else {
|
|
292
|
+
this.tx -= event.deltaX;
|
|
293
|
+
this.ty -= event.deltaY;
|
|
294
|
+
this._apply();
|
|
295
|
+
}
|
|
296
|
+
}, { passive: false });
|
|
297
|
+
this._rightDragged = false;
|
|
298
|
+
this.root.addEventListener("contextmenu", (event) => {
|
|
299
|
+
const afterDrag = this.consumeRightDrag();
|
|
300
|
+
if (!afterDrag && this._onNode(event.target)) return;
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
event.stopPropagation();
|
|
303
|
+
if (!afterDrag) this.onBackgroundMenu?.(event);
|
|
304
|
+
});
|
|
305
|
+
this.root.addEventListener("pointerdown", (event) => {
|
|
306
|
+
this._rightDragged = false;
|
|
307
|
+
if (this._onNode(event.target)) return;
|
|
308
|
+
const panning = event.button === 1 || event.button === 2 || event.shiftKey;
|
|
309
|
+
const start = { x: event.clientX, y: event.clientY, tx: this.tx, ty: this.ty };
|
|
310
|
+
const origin = this.clientToCanvas(event.clientX, event.clientY);
|
|
311
|
+
if (!panning) {
|
|
312
|
+
this.marquee.hidden = false;
|
|
313
|
+
this.selection.clear();
|
|
314
|
+
this.onSelectionChange?.(this.selection);
|
|
315
|
+
}
|
|
316
|
+
const onMove = (move) => {
|
|
317
|
+
if (panning) {
|
|
318
|
+
if (event.button === 2 && (Math.abs(move.clientX - start.x) > 3 || Math.abs(move.clientY - start.y) > 3)) {
|
|
319
|
+
this._rightDragged = true;
|
|
320
|
+
}
|
|
321
|
+
this.tx = start.tx + (move.clientX - start.x);
|
|
322
|
+
this.ty = start.ty + (move.clientY - start.y);
|
|
323
|
+
this._apply();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const current = this.clientToCanvas(move.clientX, move.clientY);
|
|
327
|
+
const box = {
|
|
328
|
+
x: Math.min(origin.x, current.x),
|
|
329
|
+
y: Math.min(origin.y, current.y),
|
|
330
|
+
width: Math.abs(current.x - origin.x),
|
|
331
|
+
height: Math.abs(current.y - origin.y)
|
|
332
|
+
};
|
|
333
|
+
const topLeft = this.canvasToClient(box.x, box.y);
|
|
334
|
+
const root = this.root.getBoundingClientRect();
|
|
335
|
+
this.marquee.style.left = `${topLeft.x - root.left}px`;
|
|
336
|
+
this.marquee.style.top = `${topLeft.y - root.top}px`;
|
|
337
|
+
this.marquee.style.width = `${box.width * this.scale}px`;
|
|
338
|
+
this.marquee.style.height = `${box.height * this.scale}px`;
|
|
339
|
+
this.onMarquee?.(box);
|
|
340
|
+
};
|
|
341
|
+
const onUp = () => {
|
|
342
|
+
window.removeEventListener("pointermove", onMove);
|
|
343
|
+
window.removeEventListener("pointerup", onUp);
|
|
344
|
+
this.marquee.hidden = true;
|
|
345
|
+
};
|
|
346
|
+
window.addEventListener("pointermove", onMove);
|
|
347
|
+
window.addEventListener("pointerup", onUp);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
clear() {
|
|
351
|
+
this.nodeLayer.replaceChildren();
|
|
352
|
+
this.edgeLayer.replaceChildren();
|
|
353
|
+
}
|
|
354
|
+
destroy() {
|
|
355
|
+
this.root.remove();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function clamp(value, low, high) {
|
|
359
|
+
return Math.max(low, Math.min(value, high));
|
|
360
|
+
}
|
|
361
|
+
function ignoreSubPixel(shift) {
|
|
362
|
+
return Math.abs(shift) < SUB_PIXEL ? 0 : shift;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/canvas/edge_router.js
|
|
366
|
+
var OBSTACLE_ROUTING_NODE_LIMIT = 60;
|
|
367
|
+
var SVG = "http://www.w3.org/2000/svg";
|
|
368
|
+
function routeEdge(from, to, { obstacles = [], simple = false } = {}) {
|
|
369
|
+
const dx = to.x - from.x;
|
|
370
|
+
const lead = Math.max(28, Math.min(Math.abs(dx) / 2, 90));
|
|
371
|
+
const outX = from.side === "right" ? from.x + lead : from.x - lead;
|
|
372
|
+
const inX = to.side === "right" ? to.x + lead : to.x - lead;
|
|
373
|
+
if (simple || obstacles.length > OBSTACLE_ROUTING_NODE_LIMIT) {
|
|
374
|
+
return `M ${from.x} ${from.y} C ${outX} ${from.y}, ${inX} ${to.y}, ${to.x} ${to.y}`;
|
|
375
|
+
}
|
|
376
|
+
const direct = `M ${from.x} ${from.y} C ${outX} ${from.y}, ${inX} ${to.y}, ${to.x} ${to.y}`;
|
|
377
|
+
if (!crossesAny(from, to, obstacles)) return direct;
|
|
378
|
+
const midY = midpointClear(from, to, obstacles);
|
|
379
|
+
return `M ${from.x} ${from.y} C ${outX} ${from.y}, ${outX} ${midY}, ${(from.x + to.x) / 2} ${midY} S ${inX} ${to.y}, ${to.x} ${to.y}`;
|
|
380
|
+
}
|
|
381
|
+
function crossesAny(from, to, obstacles) {
|
|
382
|
+
for (const box of obstacles) {
|
|
383
|
+
if (segmentIntersectsBox(from, to, box)) return true;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
function midpointClear(from, to, obstacles) {
|
|
388
|
+
const base = (from.y + to.y) / 2;
|
|
389
|
+
for (let offset = 0; offset < 400; offset += 40) {
|
|
390
|
+
for (const candidate of [base - offset, base + offset]) {
|
|
391
|
+
const point = { x: (from.x + to.x) / 2, y: candidate };
|
|
392
|
+
if (!obstacles.some((box) => pointInBox(point, box))) return candidate;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return base;
|
|
396
|
+
}
|
|
397
|
+
function pointInBox(point, box) {
|
|
398
|
+
return point.x >= box.x && point.x <= box.x + box.width && point.y >= box.y && point.y <= box.y + box.height;
|
|
399
|
+
}
|
|
400
|
+
function segmentIntersectsBox(a, b, box) {
|
|
401
|
+
const left = Math.min(a.x, b.x), right = Math.max(a.x, b.x);
|
|
402
|
+
const top = Math.min(a.y, b.y), bottom = Math.max(a.y, b.y);
|
|
403
|
+
if (right < box.x || left > box.x + box.width) return false;
|
|
404
|
+
if (bottom < box.y || top > box.y + box.height) return false;
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
var FOOT_ENDS = {
|
|
408
|
+
// The two a diagram draws when it does not know optionality. Kept because a
|
|
409
|
+
// caller that knows only the maximum should draw only the maximum rather
|
|
410
|
+
// than guessing at a minimum it has no evidence for.
|
|
411
|
+
many: { maximum: "many", minimum: null },
|
|
412
|
+
one: { maximum: "one", minimum: null },
|
|
413
|
+
// The four a diagram can actually derive from a foreign key.
|
|
414
|
+
"zero-or-many": { maximum: "many", minimum: "zero" },
|
|
415
|
+
"one-or-many": { maximum: "many", minimum: "one" },
|
|
416
|
+
"zero-or-one": { maximum: "one", minimum: "zero" },
|
|
417
|
+
"exactly-one": { maximum: "one", minimum: "one" }
|
|
418
|
+
};
|
|
419
|
+
var FOOT_SIZE = 9;
|
|
420
|
+
var MAXIMUM_ENDS_AT = { many: 1, one: 0.45 };
|
|
421
|
+
var MINIMUM_SITS_AT = { many: 1.5, one: 0.95 };
|
|
422
|
+
var RING_RADIUS = 0.33;
|
|
423
|
+
function footAnchor(point, side, kind) {
|
|
424
|
+
const { maximum } = FOOT_ENDS[kind] || FOOT_ENDS.one;
|
|
425
|
+
const direction = side === "right" ? 1 : -1;
|
|
426
|
+
return { ...point, side, x: point.x + direction * FOOT_SIZE * MAXIMUM_ENDS_AT[maximum] };
|
|
427
|
+
}
|
|
428
|
+
function crowsFoot(point, side, kind) {
|
|
429
|
+
const group = document.createElementNS(SVG, "g");
|
|
430
|
+
group.setAttribute("class", `twm-edge__marker twm-edge__marker--${kind}`);
|
|
431
|
+
const direction = side === "right" ? 1 : -1;
|
|
432
|
+
const { maximum, minimum } = FOOT_ENDS[kind] || FOOT_ENDS.one;
|
|
433
|
+
const bar = (offset) => {
|
|
434
|
+
const line = document.createElementNS(SVG, "line");
|
|
435
|
+
line.setAttribute("x1", point.x + direction * FOOT_SIZE * offset);
|
|
436
|
+
line.setAttribute("y1", point.y - FOOT_SIZE * 0.7);
|
|
437
|
+
line.setAttribute("x2", point.x + direction * FOOT_SIZE * offset);
|
|
438
|
+
line.setAttribute("y2", point.y + FOOT_SIZE * 0.7);
|
|
439
|
+
group.appendChild(line);
|
|
440
|
+
};
|
|
441
|
+
if (maximum === "many") {
|
|
442
|
+
for (const dy of [-FOOT_SIZE * 0.8, 0, FOOT_SIZE * 0.8]) {
|
|
443
|
+
const line = document.createElementNS(SVG, "line");
|
|
444
|
+
line.setAttribute("x1", point.x + direction * FOOT_SIZE * MAXIMUM_ENDS_AT.many);
|
|
445
|
+
line.setAttribute("y1", point.y);
|
|
446
|
+
line.setAttribute("x2", point.x);
|
|
447
|
+
line.setAttribute("y2", point.y + dy);
|
|
448
|
+
group.appendChild(line);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
bar(MAXIMUM_ENDS_AT.one);
|
|
452
|
+
}
|
|
453
|
+
if (minimum === "zero") {
|
|
454
|
+
const ring = document.createElementNS(SVG, "circle");
|
|
455
|
+
ring.setAttribute("cx", point.x + direction * FOOT_SIZE * MINIMUM_SITS_AT[maximum]);
|
|
456
|
+
ring.setAttribute("cy", point.y);
|
|
457
|
+
ring.setAttribute("r", FOOT_SIZE * RING_RADIUS);
|
|
458
|
+
group.appendChild(ring);
|
|
459
|
+
} else if (minimum === "one" && maximum === "many") {
|
|
460
|
+
bar(MINIMUM_SITS_AT[maximum]);
|
|
461
|
+
}
|
|
462
|
+
return group;
|
|
463
|
+
}
|
|
464
|
+
function cardinality({ kind, optional } = {}) {
|
|
465
|
+
if (kind === "N:M") return { child: "zero-or-many", parent: "zero-or-many" };
|
|
466
|
+
return {
|
|
467
|
+
child: kind === "1:1" ? "zero-or-one" : "zero-or-many",
|
|
468
|
+
parent: optional ? "zero-or-one" : "exactly-one"
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function selfLoop(from, to, { nodeRight, gap = 46 } = {}) {
|
|
472
|
+
const right = Math.max(nodeRight ?? from.x, from.x, to.x);
|
|
473
|
+
const span = Math.abs(to.y - from.y);
|
|
474
|
+
const bulge = right + gap + Math.min(span / 2, 60);
|
|
475
|
+
return `M ${from.x} ${from.y} C ${bulge} ${from.y}, ${bulge} ${to.y}, ${to.x} ${to.y}`;
|
|
476
|
+
}
|
|
477
|
+
function returnEdge(from, to, { floor = null, lead = 36 } = {}) {
|
|
478
|
+
const bottom = Math.max(Number.isFinite(floor) ? floor : -Infinity, from.y, to.y);
|
|
479
|
+
const outX = from.x + lead;
|
|
480
|
+
const inX = to.x - lead;
|
|
481
|
+
return `M ${from.x} ${from.y} C ${outX} ${from.y}, ${outX} ${bottom}, ${from.x} ${bottom} L ${to.x} ${bottom} C ${inX} ${bottom}, ${inX} ${to.y}, ${to.x} ${to.y}`;
|
|
482
|
+
}
|
|
483
|
+
function arrange(nodes, edges, { columnGap = 340, rowGap = 40 } = {}) {
|
|
484
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
485
|
+
const parents = new Map(nodes.map((n) => [n.id, []]));
|
|
486
|
+
for (const edge of edges) {
|
|
487
|
+
if (byId.has(edge.from) && byId.has(edge.to) && edge.from !== edge.to) {
|
|
488
|
+
parents.get(edge.from).push(edge.to);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const rank = /* @__PURE__ */ new Map();
|
|
492
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
493
|
+
const rankOf = (id) => {
|
|
494
|
+
if (rank.has(id)) return rank.get(id);
|
|
495
|
+
if (visiting.has(id)) return 0;
|
|
496
|
+
visiting.add(id);
|
|
497
|
+
const value = parents.get(id).length ? Math.max(...parents.get(id).map(rankOf)) + 1 : 0;
|
|
498
|
+
visiting.delete(id);
|
|
499
|
+
rank.set(id, value);
|
|
500
|
+
return value;
|
|
501
|
+
};
|
|
502
|
+
for (const node of nodes) rankOf(node.id);
|
|
503
|
+
const columns = /* @__PURE__ */ new Map();
|
|
504
|
+
for (const node of nodes) {
|
|
505
|
+
const column = rank.get(node.id) ?? 0;
|
|
506
|
+
if (!columns.has(column)) columns.set(column, []);
|
|
507
|
+
columns.get(column).push(node);
|
|
508
|
+
}
|
|
509
|
+
const positions = /* @__PURE__ */ new Map();
|
|
510
|
+
for (const [column, members] of [...columns].sort((a, b) => a[0] - b[0])) {
|
|
511
|
+
members.sort((a, b) => a.label.localeCompare(b.label));
|
|
512
|
+
let y = 0;
|
|
513
|
+
for (const node of members) {
|
|
514
|
+
positions.set(node.id, { x: column * columnGap, y });
|
|
515
|
+
y += (node.height || 120) + rowGap;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return positions;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export {
|
|
522
|
+
CanvasAdapter,
|
|
523
|
+
OBSTACLE_ROUTING_NODE_LIMIT,
|
|
524
|
+
routeEdge,
|
|
525
|
+
footAnchor,
|
|
526
|
+
crowsFoot,
|
|
527
|
+
cardinality,
|
|
528
|
+
selfLoop,
|
|
529
|
+
returnEdge,
|
|
530
|
+
arrange
|
|
531
|
+
};
|
|
532
|
+
//# sourceMappingURL=chunk-SN4LSOAR.js.map
|