@unofficialbox/box-open-elements 0.35.0 → 0.36.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 +13 -13
- package/dist/components/forms/code-editor.d.ts +1 -0
- package/dist/components/forms/code-editor.js +18 -1
- package/dist/patterns/process-modeler/automatic-routing.d.ts +22 -0
- package/dist/patterns/process-modeler/automatic-routing.js +329 -0
- package/dist/patterns/process-modeler/bridge.js +2 -0
- package/dist/patterns/process-modeler/design.d.ts +1 -1
- package/dist/patterns/process-modeler/design.js +69 -22
- package/dist/patterns/process-modeler/geometry.js +48 -10
- package/dist/patterns/process-modeler/model.d.ts +54 -2
- package/dist/patterns/process-modeler/model.js +23 -3
- package/dist/patterns/process-modeler/process-modeler.d.ts +10 -0
- package/dist/patterns/process-modeler/process-modeler.js +747 -184
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
Browse the full catalog on the **[live docs site](https://unofficialbox.github.io/box-open-elements)** — live previews, an events/properties inspector, per-framework code snippets (React / Angular / Vue / Svelte / HTML), and the foundations pages.
|
|
14
14
|
|
|
15
|
-
The library is organized around three layers (see [taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
15
|
+
The library is organized around three layers (see [taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/taxonomy.md) for the canonical model):
|
|
16
16
|
|
|
17
17
|
- **Foundations** — design decisions as data: tokens, color, typography, iconography, accessibility, theming
|
|
18
18
|
- **Components** — accessible Web Components for single controls, organized by category
|
|
@@ -77,7 +77,7 @@ selection.toggleSelection("1");
|
|
|
77
77
|
- optimized component entrypoints: `@unofficialbox/box-open-elements/<name>`
|
|
78
78
|
- patterns: `@unofficialbox/box-open-elements/patterns/<area>` and `@unofficialbox/box-open-elements/patterns/<area>/<module>`
|
|
79
79
|
|
|
80
|
-
See the versioned [API guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
80
|
+
See the versioned [API guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/api-guidelines.md).
|
|
81
81
|
|
|
82
82
|
## Developing
|
|
83
83
|
|
|
@@ -91,17 +91,17 @@ bun run docs # build + serve the docs site at http://localhost:4600
|
|
|
91
91
|
|
|
92
92
|
## Documentation
|
|
93
93
|
|
|
94
|
-
The versioned docs index is at [docs/README.md](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
95
|
-
|
|
96
|
-
- [Taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
97
|
-
- [Architecture](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
98
|
-
- [API Guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
99
|
-
- [Components catalog](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
100
|
-
- [Using with React, Angular, Vue, and Svelte](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
101
|
-
- [Design Tokens](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
102
|
-
- [Content Explorer](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
103
|
-
- [Box Server Integration](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
104
|
-
- [Agent UI guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.
|
|
94
|
+
The versioned docs index is at [docs/README.md](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/README.md). The most important entries:
|
|
95
|
+
|
|
96
|
+
- [Taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/taxonomy.md)
|
|
97
|
+
- [Architecture](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/architecture.md)
|
|
98
|
+
- [API Guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/api-guidelines.md)
|
|
99
|
+
- [Components catalog](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/components/catalog.md) and [patterns catalog](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/patterns/catalog.md)
|
|
100
|
+
- [Using with React, Angular, Vue, and Svelte](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/integration/frameworks.md)
|
|
101
|
+
- [Design Tokens](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/foundations/tokens.md)
|
|
102
|
+
- [Content Explorer](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/patterns/content-explorer.md)
|
|
103
|
+
- [Box Server Integration](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/integration/box-server.md)
|
|
104
|
+
- [Agent UI guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/patterns/agent-ui-guidelines.md), [building blocks](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/patterns/agent-ui.md), and [motion](https://github.com/unofficialbox/box-open-elements/blob/v0.36.0/docs/foundations/motion.md)
|
|
105
105
|
|
|
106
106
|
## Contributing
|
|
107
107
|
|
|
@@ -74,6 +74,7 @@ export class CodeEditor extends BaseElement {
|
|
|
74
74
|
problemsInternal = [];
|
|
75
75
|
completionsInternal = [];
|
|
76
76
|
view;
|
|
77
|
+
foldPartObserver;
|
|
77
78
|
languageConfig = new Compartment();
|
|
78
79
|
readonlyConfig = new Compartment();
|
|
79
80
|
completionsConfig = new Compartment();
|
|
@@ -280,6 +281,8 @@ export class CodeEditor extends BaseElement {
|
|
|
280
281
|
super.connectedCallback();
|
|
281
282
|
}
|
|
282
283
|
disconnectedCallback() {
|
|
284
|
+
this.foldPartObserver?.disconnect();
|
|
285
|
+
this.foldPartObserver = undefined;
|
|
283
286
|
this.flushChange();
|
|
284
287
|
this.savedSelection = this.selection;
|
|
285
288
|
this.view?.destroy();
|
|
@@ -293,7 +296,11 @@ export class CodeEditor extends BaseElement {
|
|
|
293
296
|
.cm-editor{background:var(--boe-code-background,var(--boe-token-surface-surface,#fff));color:var(--boe-code-foreground,var(--boe-token-text-text,#222))}
|
|
294
297
|
[part=editor]:focus-within{box-shadow:0 0 0 3px var(--boe-token-surface-surface-brand,#0061d5)}
|
|
295
298
|
.cm-editor.cm-focused{outline:none}
|
|
296
|
-
.cm-
|
|
299
|
+
.cm-content{padding:var(--boe-code-content-padding,4px 0)!important}
|
|
300
|
+
.cm-lineNumbers .cm-gutterElement{padding:var(--boe-code-line-number-padding,0 3px 0 5px)!important}
|
|
301
|
+
.cm-foldGutter .cm-gutterElement{padding:var(--boe-code-fold-gutter-padding,0)!important}
|
|
302
|
+
|
|
303
|
+
.cm-scroller{max-height:var(--boe-code-editor-height,420px);min-height:160px;overflow:auto;font-family:var(--boe-code-font-family,monospace)!important;font-size:var(--boe-code-font-size,14px);line-height:var(--boe-code-line-height,1.6)}
|
|
297
304
|
.cm-editor .cm-scroller{line-height:var(--boe-code-line-height,1.4)!important}
|
|
298
305
|
:host([fill-height]){height:100%}
|
|
299
306
|
:host([fill-height]) [part=editor],:host([fill-height]) .cm-editor,:host([fill-height]) .cm-scroller{height:100%;max-height:none;min-height:0}
|
|
@@ -499,6 +506,16 @@ export class CodeEditor extends BaseElement {
|
|
|
499
506
|
],
|
|
500
507
|
}),
|
|
501
508
|
});
|
|
509
|
+
const markFoldParts = () => {
|
|
510
|
+
this.shadowRoot.querySelectorAll('.cm-foldGutter .cm-gutterElement > span').forEach(marker => {
|
|
511
|
+
// basicSetup supplies these stable default marker characters. Keep
|
|
512
|
+
// their text/title and interaction; export presentation-only parts.
|
|
513
|
+
marker.setAttribute('part', `fold-marker ${marker.textContent === '⌄' ? 'fold-marker-open' : 'fold-marker-closed'}`);
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
markFoldParts();
|
|
517
|
+
this.foldPartObserver = new MutationObserver(markFoldParts);
|
|
518
|
+
this.foldPartObserver.observe(this.view.dom, { subtree: true, childList: true, characterData: true });
|
|
502
519
|
this.selection = this.savedSelection;
|
|
503
520
|
}
|
|
504
521
|
else {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Automatic event endpoint policy from the pinned Process Modeler design.
|
|
2
|
+
* Candidate ports use heading-aware orthogonal route cost; a shared task side
|
|
3
|
+
* spreads incoming and outgoing lines together before the final route is built.
|
|
4
|
+
* Other ordinary routes and authored pins/waypoints keep their existing policy.
|
|
5
|
+
*/
|
|
6
|
+
import type { ProcessPoint } from './geometry.js';
|
|
7
|
+
import { type ProcessLayout, type ProcessLine, type ProcessProjection, type ProcessSide } from './model.js';
|
|
8
|
+
/** Native automatic event ports. Authored pins/waypoints retain the existing routing contract. */
|
|
9
|
+
export declare function automaticEventPorts(line: ProcessLine, layout: ProcessLayout, projection: ProcessProjection): {
|
|
10
|
+
start: ProcessPoint;
|
|
11
|
+
end: ProcessPoint;
|
|
12
|
+
fromSide: ProcessSide;
|
|
13
|
+
toSide: ProcessSide;
|
|
14
|
+
obstacles: {
|
|
15
|
+
endpoint: boolean;
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
w: number;
|
|
19
|
+
h: number;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
export declare function automaticEventRoute(line: ProcessLine, layout: ProcessLayout, projection: ProcessProjection): ProcessPoint[];
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { isFlowLine } from './model.js';
|
|
2
|
+
const DIRS = { north: { x: 0, y: -1 }, east: { x: 1, y: 0 }, south: { x: 0, y: 1 }, west: { x: -1, y: 0 } };
|
|
3
|
+
const center = (r) => ({ x: r.x + r.w / 2, y: r.y + r.h / 2 });
|
|
4
|
+
const inflate = (r, m) => ({ x: r.x - m, y: r.y - m, w: r.w + 2 * m, h: r.h + 2 * m });
|
|
5
|
+
const contains = (r, p) => p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h;
|
|
6
|
+
const BEND = 36;
|
|
7
|
+
function simplify(points) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const point of points) {
|
|
10
|
+
const last = out.at(-1);
|
|
11
|
+
if (last && Math.abs(last.x - point.x) < .01 && Math.abs(last.y - point.y) < .01)
|
|
12
|
+
continue;
|
|
13
|
+
out.push({ ...point });
|
|
14
|
+
while (out.length >= 3) {
|
|
15
|
+
const [a, b, c] = out.slice(-3);
|
|
16
|
+
if ((a.x === b.x && b.x === c.x) || (a.y === b.y && b.y === c.y))
|
|
17
|
+
out.splice(out.length - 2, 1);
|
|
18
|
+
else
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function port(box, r, side, offset = 0) {
|
|
25
|
+
const c = center(r), inset = box.shape === 'event' ? 8 : 0;
|
|
26
|
+
if (side === 'north')
|
|
27
|
+
return { x: c.x + offset, y: r.y + inset };
|
|
28
|
+
if (side === 'south')
|
|
29
|
+
return { x: c.x + offset, y: r.y + r.h - inset };
|
|
30
|
+
if (side === 'west')
|
|
31
|
+
return { x: r.x + inset, y: c.y + offset };
|
|
32
|
+
return { x: r.x + r.w - inset, y: c.y + offset };
|
|
33
|
+
}
|
|
34
|
+
function routeOrthC(p1, sa, p2, sb, obstacles) {
|
|
35
|
+
const STUB = 20, M = 12;
|
|
36
|
+
const d1 = DIRS[sa], d2 = DIRS[sb];
|
|
37
|
+
const s = { x: p1.x + d1.x * STUB, y: p1.y + d1.y * STUB };
|
|
38
|
+
const t = { x: p2.x + d2.x * STUB, y: p2.y + d2.y * STUB };
|
|
39
|
+
// Region of interest.
|
|
40
|
+
const pad = 200;
|
|
41
|
+
let rx1 = Math.min(s.x, t.x) - pad, rx2 = Math.max(s.x, t.x) + pad;
|
|
42
|
+
let ry1 = Math.min(s.y, t.y) - pad, ry2 = Math.max(s.y, t.y) + pad;
|
|
43
|
+
const obs = [];
|
|
44
|
+
for (const o of obstacles) {
|
|
45
|
+
const r = inflate(o, M);
|
|
46
|
+
if (r.x + r.w < rx1 || r.x > rx2 || r.y + r.h < ry1 || r.y > ry2)
|
|
47
|
+
continue;
|
|
48
|
+
if (contains({ x: r.x + 0.5, y: r.y + 0.5, w: r.w - 1, h: r.h - 1 }, s)
|
|
49
|
+
|| contains({ x: r.x + 0.5, y: r.y + 0.5, w: r.w - 1, h: r.h - 1 }, t)) {
|
|
50
|
+
// Endpoint bodies may surround an inset port; unrelated obstacles cannot be crossed.
|
|
51
|
+
if (o.endpoint)
|
|
52
|
+
continue;
|
|
53
|
+
return { pts: [], cost: Infinity };
|
|
54
|
+
}
|
|
55
|
+
obs.push(r);
|
|
56
|
+
}
|
|
57
|
+
for (const r of obs) {
|
|
58
|
+
rx1 = Math.min(rx1, r.x - 24);
|
|
59
|
+
rx2 = Math.max(rx2, r.x + r.w + 24);
|
|
60
|
+
ry1 = Math.min(ry1, r.y - 24);
|
|
61
|
+
ry2 = Math.max(ry2, r.y + r.h + 24);
|
|
62
|
+
}
|
|
63
|
+
const xsSet = new Set([s.x, t.x, rx1, rx2]), ysSet = new Set([s.y, t.y, ry1, ry2]);
|
|
64
|
+
for (const r of obs) {
|
|
65
|
+
xsSet.add(r.x);
|
|
66
|
+
xsSet.add(r.x + r.w);
|
|
67
|
+
ysSet.add(r.y);
|
|
68
|
+
ysSet.add(r.y + r.h);
|
|
69
|
+
}
|
|
70
|
+
const addMids = (set) => {
|
|
71
|
+
const a = [...set].sort((p, q) => p - q);
|
|
72
|
+
for (let i = 0; i < a.length - 1; i++)
|
|
73
|
+
if (a[i + 1] - a[i] > 24)
|
|
74
|
+
set.add((a[i] + a[i + 1]) / 2);
|
|
75
|
+
return [...set].sort((p, q) => p - q);
|
|
76
|
+
};
|
|
77
|
+
const xs = addMids(xsSet), ys = addMids(ysSet);
|
|
78
|
+
const nx = xs.length, ny = ys.length;
|
|
79
|
+
const blockedP = new Uint8Array(nx * ny), blockH = new Uint8Array(nx * ny), blockV = new Uint8Array(nx * ny);
|
|
80
|
+
const inside = (v, a, b) => v > a + 0.01 && v < b - 0.01;
|
|
81
|
+
for (const r of obs) {
|
|
82
|
+
const x1 = r.x, x2 = r.x + r.w, y1 = r.y, y2 = r.y + r.h;
|
|
83
|
+
let i0 = 0;
|
|
84
|
+
while (i0 < nx && xs[i0] < x1)
|
|
85
|
+
i0++;
|
|
86
|
+
let j0 = 0;
|
|
87
|
+
while (j0 < ny && ys[j0] < y1)
|
|
88
|
+
j0++;
|
|
89
|
+
for (let i = Math.max(0, i0 - 1); i < nx && xs[i] <= x2; i++) {
|
|
90
|
+
for (let j = Math.max(0, j0 - 1); j < ny && ys[j] <= y2; j++) {
|
|
91
|
+
const k = j * nx + i;
|
|
92
|
+
if (inside(xs[i], x1, x2) && inside(ys[j], y1, y2))
|
|
93
|
+
blockedP[k] = 1;
|
|
94
|
+
if (i < nx - 1 && inside((xs[i] + xs[i + 1]) / 2, x1, x2) && inside(ys[j], y1, y2))
|
|
95
|
+
blockH[k] = 1;
|
|
96
|
+
if (j < ny - 1 && inside(xs[i], x1, x2) && inside((ys[j] + ys[j + 1]) / 2, y1, y2))
|
|
97
|
+
blockV[k] = 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const si = xs.indexOf(s.x), sj = ys.indexOf(s.y), ti = xs.indexOf(t.x), tj = ys.indexOf(t.y);
|
|
102
|
+
const dirIndex = (d) => (d.x === 1 ? 0 : d.y === 1 ? 1 : d.x === -1 ? 2 : 3);
|
|
103
|
+
const DX = [1, 0, -1, 0], DY = [0, 1, 0, -1];
|
|
104
|
+
const startDir = dirIndex(d1), needDir = dirIndex({ x: -d2.x, y: -d2.y });
|
|
105
|
+
const total = nx * ny * 4;
|
|
106
|
+
const g = new Float64Array(total).fill(Infinity);
|
|
107
|
+
const prev = new Int32Array(total).fill(-1);
|
|
108
|
+
const heap = [];
|
|
109
|
+
const push = (f, st) => {
|
|
110
|
+
heap.push([f, st]);
|
|
111
|
+
let i = heap.length - 1;
|
|
112
|
+
while (i > 0) {
|
|
113
|
+
const p = (i - 1) >> 1;
|
|
114
|
+
if (heap[p][0] <= heap[i][0])
|
|
115
|
+
break;
|
|
116
|
+
[heap[p], heap[i]] = [heap[i], heap[p]];
|
|
117
|
+
i = p;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const pop = () => {
|
|
121
|
+
const top = heap[0], last = heap.pop();
|
|
122
|
+
if (heap.length) {
|
|
123
|
+
heap[0] = last;
|
|
124
|
+
let i = 0;
|
|
125
|
+
for (;;) {
|
|
126
|
+
const l = 2 * i + 1, r = l + 1;
|
|
127
|
+
let m = i;
|
|
128
|
+
if (l < heap.length && heap[l][0] < heap[m][0])
|
|
129
|
+
m = l;
|
|
130
|
+
if (r < heap.length && heap[r][0] < heap[m][0])
|
|
131
|
+
m = r;
|
|
132
|
+
if (m === i)
|
|
133
|
+
break;
|
|
134
|
+
[heap[m], heap[i]] = [heap[i], heap[m]];
|
|
135
|
+
i = m;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return top;
|
|
139
|
+
};
|
|
140
|
+
const h = (i, j) => Math.abs(xs[i] - t.x) + Math.abs(ys[j] - t.y);
|
|
141
|
+
const s0 = (sj * nx + si) * 4 + startDir;
|
|
142
|
+
g[s0] = 0;
|
|
143
|
+
push(h(si, sj), s0);
|
|
144
|
+
let goal = -1, goalCost = Infinity;
|
|
145
|
+
let guard = 0;
|
|
146
|
+
while (heap.length && guard++ < 60000) {
|
|
147
|
+
const [f, st] = pop();
|
|
148
|
+
if (f >= goalCost)
|
|
149
|
+
break;
|
|
150
|
+
const dir = st & 3, k = st >> 2, i = k % nx, j = (k / nx) | 0;
|
|
151
|
+
const gc = g[st];
|
|
152
|
+
if (i === ti && j === tj) {
|
|
153
|
+
if (dir === needDir && gc < goalCost) {
|
|
154
|
+
goalCost = gc;
|
|
155
|
+
goal = st;
|
|
156
|
+
}
|
|
157
|
+
else if (dir !== ((needDir + 2) & 3) && gc + BEND < goalCost) {
|
|
158
|
+
goalCost = gc + BEND;
|
|
159
|
+
goal = st;
|
|
160
|
+
}
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
for (let nd = 0; nd < 4; nd++) {
|
|
164
|
+
if (nd === ((dir + 2) & 3))
|
|
165
|
+
continue;
|
|
166
|
+
const ni = i + DX[nd], nj = j + DY[nd];
|
|
167
|
+
if (ni < 0 || nj < 0 || ni >= nx || nj >= ny)
|
|
168
|
+
continue;
|
|
169
|
+
const nk = nj * nx + ni;
|
|
170
|
+
if (blockedP[nk])
|
|
171
|
+
continue;
|
|
172
|
+
if (nd === 0 && blockH[k])
|
|
173
|
+
continue;
|
|
174
|
+
if (nd === 2 && blockH[nk])
|
|
175
|
+
continue;
|
|
176
|
+
if (nd === 1 && blockV[k])
|
|
177
|
+
continue;
|
|
178
|
+
if (nd === 3 && blockV[nk])
|
|
179
|
+
continue;
|
|
180
|
+
const cost = gc + Math.abs(xs[ni] - xs[i]) + Math.abs(ys[nj] - ys[j]) + (nd === dir ? 0 : BEND);
|
|
181
|
+
const ns = nk * 4 + nd;
|
|
182
|
+
if (cost < g[ns]) {
|
|
183
|
+
g[ns] = cost;
|
|
184
|
+
prev[ns] = st;
|
|
185
|
+
push(cost + h(ni, nj), ns);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
let pts, cost;
|
|
190
|
+
if (goal < 0) {
|
|
191
|
+
pts = [];
|
|
192
|
+
cost = Infinity;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const chain = [];
|
|
196
|
+
for (let st = goal; st >= 0; st = prev[st]) {
|
|
197
|
+
const k = st >> 2;
|
|
198
|
+
chain.push({ x: xs[k % nx], y: ys[(k / nx) | 0] });
|
|
199
|
+
}
|
|
200
|
+
chain.reverse();
|
|
201
|
+
pts = [p1, ...chain, p2];
|
|
202
|
+
cost = goalCost;
|
|
203
|
+
}
|
|
204
|
+
return { pts: simplify(pts), cost };
|
|
205
|
+
}
|
|
206
|
+
/** Native automatic event ports. Authored pins/waypoints retain the existing routing contract. */
|
|
207
|
+
export function automaticEventPorts(line, layout, projection) {
|
|
208
|
+
const boxes = new Map(projection.boxes.map(box => [box.id, box]));
|
|
209
|
+
const rects = new Map(projection.boxes.filter(box => layout.boxes[box.id]).map(box => { const p = layout.boxes[box.id], compact = box.shape === 'event' || box.shape === 'gateway'; return [box.id, { x: p.x, y: p.y, w: p.width ?? (box.frame ? 320 : compact ? 56 : box.role === 'note' ? 208 : 224), h: p.height ?? (box.frame ? 180 : compact ? 56 : 64) }]; }));
|
|
210
|
+
const candidates = projection.lines.some(edge => edge.id === line.id) ? projection.lines.map(edge => edge.id === line.id ? line : edge) : [...projection.lines, line];
|
|
211
|
+
const flows = candidates.filter(edge => isFlowLine(edge) && rects.has(edge.from) && rects.has(edge.to));
|
|
212
|
+
const obstacles = projection.boxes.filter(box => rects.has(box.id) && box.kind !== 'section').map(box => ({ id: box.id, r: rects.get(box.id) }));
|
|
213
|
+
const ancestors = (id) => {
|
|
214
|
+
const ids = new Set();
|
|
215
|
+
let parent = boxes.get(id)?.parentId;
|
|
216
|
+
while (parent && !ids.has(parent)) {
|
|
217
|
+
ids.add(parent);
|
|
218
|
+
parent = boxes.get(parent)?.parentId;
|
|
219
|
+
}
|
|
220
|
+
return ids;
|
|
221
|
+
};
|
|
222
|
+
const edgeObstacles = (edge) => { const skip = new Set([...ancestors(edge.from), ...ancestors(edge.to)]); return obstacles.filter(o => !skip.has(o.id)).map(o => ({ ...o.r, endpoint: o.id === edge.from || o.id === edge.to })); };
|
|
223
|
+
const sides = new Map();
|
|
224
|
+
for (const edge of flows) {
|
|
225
|
+
const a = boxes.get(edge.from), b = boxes.get(edge.to), ra = rects.get(a.id), rb = rects.get(b.id), ca = center(ra), cb = center(rb), dy = cb.y - ca.y;
|
|
226
|
+
const fan = a.shape === 'gateway' && flows.filter(e => e.from === a.id).length > 1, merge = b.shape === 'gateway' && flows.filter(e => e.to === b.id).length > 1;
|
|
227
|
+
const ha = edge.fromSide ?? (fan && Math.abs(dy) > 8 && cb.x > ca.x ? (dy < 0 ? 'north' : 'south') : rb.x >= ra.x + ra.w + 8 ? 'east' : rb.y >= ra.y + ra.h + 8 ? 'south' : rb.y + rb.h <= ra.y - 8 ? 'north' : rb.x + rb.w <= ra.x - 8 ? 'south' : 'east');
|
|
228
|
+
const hb = edge.toSide ?? (merge && Math.abs(dy) > 8 && ca.x < cb.x ? (dy > 0 ? 'north' : 'south') : ra.x + ra.w <= rb.x - 8 ? 'west' : ra.y + ra.h <= rb.y - 8 ? 'north' : ra.y >= rb.y + rb.h + 8 ? 'south' : ra.x >= rb.x + rb.w + 8 ? 'south' : 'west');
|
|
229
|
+
const facing = (a, b) => [b.x >= a.x ? 'east' : 'west', b.y >= a.y ? 'south' : 'north'];
|
|
230
|
+
const A = edge.fromSide ? [edge.fromSide] : [...new Set([ha, ...facing(ca, cb)])], B = edge.toSide ? [edge.toSide] : [...new Set([hb, ...facing(cb, ca)])];
|
|
231
|
+
let best = { cost: Infinity, sa: ha, sb: hb };
|
|
232
|
+
for (const sa of A)
|
|
233
|
+
for (const sb of B) {
|
|
234
|
+
const route = routeOrthC(port(a, ra, sa), sa, port(b, rb, sb), sb, edgeObstacles(edge));
|
|
235
|
+
const cost = route.cost - (sa === ha && sb === hb ? 12 : 0);
|
|
236
|
+
if (cost < best.cost)
|
|
237
|
+
best = { cost, sa, sb };
|
|
238
|
+
}
|
|
239
|
+
// Authored edges use the legacy nearest-edge contract. Reserve those actual
|
|
240
|
+
// sides, rather than a side the automatic candidate search would choose.
|
|
241
|
+
const points = layout.lines?.[edge.id] ?? edge.points;
|
|
242
|
+
if (edge.fromSide || edge.toSide || points?.length) {
|
|
243
|
+
const nearest = (r, toward) => {
|
|
244
|
+
const x = Math.max(r.x, Math.min(toward.x, r.x + r.w)), y = Math.max(r.y, Math.min(toward.y, r.y + r.h));
|
|
245
|
+
const candidates = [['east', { x: r.x + r.w, y }], ['west', { x: r.x, y }], ['south', { x, y: r.y + r.h }], ['north', { x, y: r.y }]];
|
|
246
|
+
return candidates.reduce((a, b) => Math.hypot(b[1].x - toward.x, b[1].y - toward.y) < Math.hypot(a[1].x - toward.x, a[1].y - toward.y) ? b : a)[0];
|
|
247
|
+
};
|
|
248
|
+
best.sa = edge.fromSide ?? nearest(ra, points?.[0] ?? cb);
|
|
249
|
+
best.sb = edge.toSide ?? nearest(rb, points?.at(-1) ?? ca);
|
|
250
|
+
}
|
|
251
|
+
sides.set(edge.id, [best.sa, best.sb]);
|
|
252
|
+
}
|
|
253
|
+
const groups = new Map();
|
|
254
|
+
const add = (box, side, key, other) => { const id = box.id + '|' + side; const group = groups.get(id) ?? { box, side, ends: [] }; group.ends.push({ key, other }); groups.set(id, group); };
|
|
255
|
+
for (const edge of flows) {
|
|
256
|
+
const [sa, sb] = sides.get(edge.id);
|
|
257
|
+
add(boxes.get(edge.from), sa, edge.id + ':a', edge.to);
|
|
258
|
+
add(boxes.get(edge.to), sb, edge.id + ':b', edge.from);
|
|
259
|
+
}
|
|
260
|
+
const fixed = new Map();
|
|
261
|
+
for (const edge of flows) {
|
|
262
|
+
const points = layout.lines?.[edge.id] ?? edge.points;
|
|
263
|
+
if (!edge.fromSide && !edge.toSide && !points?.length)
|
|
264
|
+
continue;
|
|
265
|
+
const [sa, sb] = sides.get(edge.id);
|
|
266
|
+
for (const [id, side, key, toward] of [
|
|
267
|
+
[edge.from, sa, edge.id + ':a', points?.[0] ?? center(rects.get(edge.to))],
|
|
268
|
+
[edge.to, sb, edge.id + ':b', points?.at(-1) ?? center(rects.get(edge.from))]
|
|
269
|
+
]) {
|
|
270
|
+
const r = rects.get(id), c = center(r);
|
|
271
|
+
const authoredSide = key.endsWith(':a') ? edge.fromSide : edge.toSide;
|
|
272
|
+
const offset = authoredSide ? 0 : side === 'north' || side === 'south'
|
|
273
|
+
? Math.max(r.x, Math.min(toward.x, r.x + r.w)) - c.x
|
|
274
|
+
: Math.max(r.y, Math.min(toward.y, r.y + r.h)) - c.y;
|
|
275
|
+
fixed.set(key, offset);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const offsets = new Map();
|
|
279
|
+
for (const { box, side, ends } of groups.values()) {
|
|
280
|
+
if (ends.length === 1 || box.shape === 'gateway' || box.shape === 'event') {
|
|
281
|
+
ends.forEach(end => offsets.set(end.key, 0));
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const r = rects.get(box.id), c = center(r), horizontal = side === 'north' || side === 'south', half = (horizontal ? r.w : r.h) / 2, max = Math.max(0, Math.floor((half - 12) / 8) * 8), gap = Math.min(16, 2 * max / (ends.length - 1));
|
|
285
|
+
const list = ends.map(end => { const other = center(rects.get(end.other)); return { ...end, pref: Math.max(-max, Math.min(max, Math.round((horizontal ? other.x - c.x : other.y - c.y) / 8) * 8)) }; }).sort((a, b) => a.pref - b.pref || (a.key < b.key ? -1 : 1));
|
|
286
|
+
if (list.some(end => fixed.has(end.key))) {
|
|
287
|
+
const reserved = list.filter(end => fixed.has(end.key)).map(end => fixed.get(end.key));
|
|
288
|
+
const movable = list.filter(end => !fixed.has(end.key));
|
|
289
|
+
let allocated = [];
|
|
290
|
+
// Authored ports remain at their actual positions. Spread automatic siblings
|
|
291
|
+
// around them, relaxing spacing only when a crowded side cannot fit 16px.
|
|
292
|
+
for (const spacing of [16, 8, 4, 2, 1, .5, 0]) {
|
|
293
|
+
const used = [...reserved];
|
|
294
|
+
allocated = [];
|
|
295
|
+
for (const end of movable) {
|
|
296
|
+
const slots = [];
|
|
297
|
+
for (let value = -max; value <= max; value += .5)
|
|
298
|
+
if (used.every(other => Math.abs(value - other) >= spacing))
|
|
299
|
+
slots.push(value);
|
|
300
|
+
slots.sort((a, b) => Math.abs(a - end.pref) - Math.abs(b - end.pref) || a - b);
|
|
301
|
+
if (!slots.length)
|
|
302
|
+
break;
|
|
303
|
+
used.push(slots[0]);
|
|
304
|
+
allocated.push({ key: end.key, value: slots[0] });
|
|
305
|
+
}
|
|
306
|
+
if (allocated.length === movable.length)
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
for (const end of list)
|
|
310
|
+
if (fixed.has(end.key))
|
|
311
|
+
offsets.set(end.key, fixed.get(end.key));
|
|
312
|
+
for (const end of allocated)
|
|
313
|
+
offsets.set(end.key, end.value);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const positions = list.map(end => end.pref);
|
|
317
|
+
for (let i = 1; i < positions.length; i++)
|
|
318
|
+
positions[i] = Math.max(positions[i], positions[i - 1] + gap);
|
|
319
|
+
let shift = list.reduce((sum, end) => sum + end.pref, 0) / list.length - positions.reduce((sum, n) => sum + n, 0) / positions.length;
|
|
320
|
+
shift = Math.max(Math.min(shift, max - positions.at(-1)), -max - positions[0]);
|
|
321
|
+
list.forEach((end, i) => offsets.set(end.key, Math.round((positions[i] + shift) * 2) / 2));
|
|
322
|
+
}
|
|
323
|
+
const [sa, sb] = sides.get(line.id), a = boxes.get(line.from), b = boxes.get(line.to);
|
|
324
|
+
return { start: port(a, rects.get(a.id), sa, offsets.get(line.id + ':a')), end: port(b, rects.get(b.id), sb, offsets.get(line.id + ':b')), fromSide: sa, toSide: sb, obstacles: edgeObstacles(line) };
|
|
325
|
+
}
|
|
326
|
+
export function automaticEventRoute(line, layout, projection) {
|
|
327
|
+
const ports = automaticEventPorts(line, layout, projection);
|
|
328
|
+
return routeOrthC(ports.start, ports.fromSide, ports.end, ports.toSide, ports.obstacles).pts;
|
|
329
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isFlowBox, isFlowLine } from "./model.js";
|
|
1
2
|
/** Detach graph presentation data without cloning the host-owned node payload. */
|
|
2
3
|
export function snapshotProcessProjection(projection) {
|
|
3
4
|
return {
|
|
@@ -74,6 +75,7 @@ const branchKinds = new Set(["decision", "choice", "weighted", "weighted-choice"
|
|
|
74
75
|
const parallelKinds = new Set(["parallel", "fork"]);
|
|
75
76
|
/** Generic topology only. Hosts own workflow vocabulary and semantic validation. */
|
|
76
77
|
export function graphChecks(projection) {
|
|
78
|
+
projection = { boxes: projection.boxes.filter(isFlowBox), lines: projection.lines.filter(isFlowLine) };
|
|
77
79
|
const boxes = new Map(projection.boxes.map(box => [box.id, box]));
|
|
78
80
|
const outgoing = new Map(projection.boxes.map(box => [box.id, []]));
|
|
79
81
|
const incoming = new Map(projection.boxes.map(box => [box.id, []]));
|