@floegence/floe-webapp-core 0.83.1 → 0.84.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/dist/components/graph/GraphCanvas.js +93 -78
- package/dist/components/graph/geometry.d.ts +2 -0
- package/dist/components/graph/geometry.js +29 -19
- package/dist/components/graph/layout.js +39 -35
- package/dist/components/graph/positions.d.ts +3 -0
- package/dist/components/graph/positions.js +134 -0
- package/dist/components/graph/types.d.ts +4 -0
- package/dist/graph.css +18 -0
- package/dist/graph.js +7 -6
- package/package.json +1 -1
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { insert as
|
|
2
|
-
import { createUniqueId as
|
|
3
|
-
import { InfiniteCanvas as
|
|
4
|
-
import { resolveSurfaceInteractionTargetRole as
|
|
5
|
-
import { graphRelatedEdges as
|
|
6
|
-
var
|
|
7
|
-
function
|
|
8
|
-
const
|
|
9
|
-
let
|
|
10
|
-
const [
|
|
1
|
+
import { insert as y, createComponent as w, setAttribute as l, effect as S, className as V, template as _, memo as p, setStyleProperty as T, use as ee, delegateEvents as te } from "solid-js/web";
|
|
2
|
+
import { createUniqueId as ne, createSignal as B, createMemo as $, onMount as re, For as D, Show as F, onCleanup as ae } from "solid-js";
|
|
3
|
+
import { InfiniteCanvas as ie } from "../ui/InfiniteCanvas.js";
|
|
4
|
+
import { resolveSurfaceInteractionTargetRole as oe } from "../ui/localInteractionSurface.js";
|
|
5
|
+
import { graphRelatedEdges as W, graphEdgeLabelPosition as le, graphSectionPath as ce } from "./geometry.js";
|
|
6
|
+
var de = /* @__PURE__ */ _("<div data-floe-canvas-pan-surface=true role=group tabindex=0>"), se = /* @__PURE__ */ _("<div role=region tabindex=0>"), ue = /* @__PURE__ */ _('<svg class=floe-graph__edges width=1 height=1><defs><marker viewBox="0 0 8 8"refX=7 refY=4 markerWidth=7 markerHeight=7 orient=auto-start-reverse><path d="M1 1 L7 4 L1 7"fill=none stroke=context-stroke stroke-width=1.2>'), he = /* @__PURE__ */ _("<svg><g><path class=floe-graph__edge></path><path class=floe-graph__edge-target tabindex=0 role=button data-floe-canvas-pan-surface=true></svg>", !1, !0, !1), ge = /* @__PURE__ */ _("<svg><text class=floe-graph__edge-label aria-hidden=true></svg>", !1, !0, !1);
|
|
7
|
+
function ke(i) {
|
|
8
|
+
const H = `graph-arrow-${ne()}`;
|
|
9
|
+
let g;
|
|
10
|
+
const [q, K] = B(null), [O, U] = B({
|
|
11
11
|
width: 0,
|
|
12
12
|
height: 0
|
|
13
|
-
}),
|
|
14
|
-
const e =
|
|
15
|
-
return e?.kind === "node" ?
|
|
16
|
-
}),
|
|
17
|
-
const e =
|
|
13
|
+
}), X = () => i.highlight === void 0 ? q() : i.highlight, J = $(() => new Map(i.layout.nodes.map((e) => [e.id, e]))), Q = $(() => {
|
|
14
|
+
const e = X();
|
|
15
|
+
return e?.kind === "node" ? W(i.layout, e.id) : new Set(e ? [e.id] : []);
|
|
16
|
+
}), Y = $(() => {
|
|
17
|
+
const e = X();
|
|
18
18
|
if (e?.kind !== "node") return null;
|
|
19
|
-
const t =
|
|
20
|
-
return r ?
|
|
21
|
-
}),
|
|
19
|
+
const t = J().get(e.id), r = t?.kind === "group" ? t.id : t?.parentId;
|
|
20
|
+
return r ? W(i.layout, r) : null;
|
|
21
|
+
}), A = (e) => i.selected?.kind === e.kind && i.selected.id === e.id, Z = (e) => {
|
|
22
22
|
const {
|
|
23
23
|
width: t,
|
|
24
24
|
height: r
|
|
25
|
-
} =
|
|
25
|
+
} = O();
|
|
26
26
|
if (!t || !r) return i.layout.nodes;
|
|
27
|
-
const a = -e.x / e.scale - 240, n = -e.y / e.scale - 240,
|
|
28
|
-
return i.layout.nodes.filter((d) => d.id === i.selected?.id || d.x + d.width >= a && d.x <=
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
},
|
|
27
|
+
const a = -e.x / e.scale - 240, n = -e.y / e.scale - 240, b = a + t / e.scale + 480, c = n + r / e.scale + 480;
|
|
28
|
+
return i.layout.nodes.filter((d) => d.id === i.selected?.id || d.x + d.width >= a && d.x <= b && d.y + d.height >= n && d.y <= c);
|
|
29
|
+
}, v = (e) => {
|
|
30
|
+
K(e), i.onHover?.(e);
|
|
31
|
+
}, x = (e, t, r) => {
|
|
32
32
|
const a = t.getBoundingClientRect();
|
|
33
33
|
return {
|
|
34
34
|
object: e,
|
|
@@ -41,39 +41,39 @@ function me(i) {
|
|
|
41
41
|
y: a.top + Math.min(32, a.height / 2)
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
|
-
},
|
|
45
|
-
i.onContextMenu && (t.preventDefault(), t.stopPropagation(), i.onContextMenu(
|
|
46
|
-
},
|
|
47
|
-
t.target === t.currentTarget && (t.key === "ContextMenu" || t.shiftKey && t.key === "F10" ? (t.preventDefault(), t.stopPropagation(), i.onContextMenu?.(
|
|
44
|
+
}, R = (e, t) => {
|
|
45
|
+
i.onContextMenu && (t.preventDefault(), t.stopPropagation(), i.onContextMenu(x(e, t.currentTarget, t)));
|
|
46
|
+
}, j = (e, t) => {
|
|
47
|
+
t.target === t.currentTarget && (t.key === "ContextMenu" || t.shiftKey && t.key === "F10" ? (t.preventDefault(), t.stopPropagation(), i.onContextMenu?.(x(e, t.currentTarget))) : t.key === "Enter" ? (t.preventDefault(), t.stopPropagation(), i.onActivate?.(x(e, t.currentTarget))) : t.key === "Escape" && (v(null), i.onActivate?.(x(null, g))));
|
|
48
48
|
};
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
const e = () =>
|
|
52
|
-
width:
|
|
53
|
-
height:
|
|
49
|
+
re(() => {
|
|
50
|
+
if (!g) return;
|
|
51
|
+
const e = () => U({
|
|
52
|
+
width: g.clientWidth,
|
|
53
|
+
height: g.clientHeight
|
|
54
54
|
});
|
|
55
55
|
e();
|
|
56
56
|
const t = new ResizeObserver(e);
|
|
57
|
-
t.observe(
|
|
57
|
+
t.observe(g), ae(() => t.disconnect());
|
|
58
58
|
});
|
|
59
|
-
const
|
|
59
|
+
const G = (e) => {
|
|
60
60
|
const t = {
|
|
61
61
|
kind: "node",
|
|
62
62
|
id: e.id
|
|
63
63
|
}, r = {
|
|
64
|
-
selected: () =>
|
|
65
|
-
openMenu: (a) =>
|
|
64
|
+
selected: () => A(t),
|
|
65
|
+
openMenu: (a) => R(t, a)
|
|
66
66
|
};
|
|
67
67
|
return (() => {
|
|
68
|
-
var a =
|
|
69
|
-
return a.$$keydown = (n) =>
|
|
70
|
-
n.target.closest("button, a, input, select, textarea, [data-floe-canvas-interactive]") || (n.stopPropagation(), i.onActivate?.(
|
|
71
|
-
}, a.$$focusout = () =>
|
|
72
|
-
var n =
|
|
68
|
+
var a = de();
|
|
69
|
+
return a.$$keydown = (n) => j(t, n), a.$$contextmenu = (n) => R(t, n), a.$$click = (n) => {
|
|
70
|
+
n.target.closest("button, a, input, select, textarea, [data-floe-canvas-interactive]") || (n.stopPropagation(), i.onActivate?.(x(t, n.currentTarget, n)));
|
|
71
|
+
}, a.$$focusout = () => v(null), a.$$focusin = () => v(t), a.addEventListener("pointerleave", () => v(null)), a.addEventListener("pointerenter", () => v(t)), y(a, (() => {
|
|
72
|
+
var n = p(() => !!(e.kind === "group" && i.renderGroup));
|
|
73
73
|
return () => n() ? i.renderGroup(e, r) : i.renderNode(e, r);
|
|
74
|
-
})()),
|
|
75
|
-
var
|
|
76
|
-
return
|
|
74
|
+
})()), S((n) => {
|
|
75
|
+
var b = e.kind === "group" ? "floe-graph__group" : "floe-graph__node", c = e.id, d = A(t), P = `${e.x}px`, s = `${e.y}px`, f = `${e.width}px`, C = `${e.height}px`, I = e.label;
|
|
76
|
+
return b !== n.e && V(a, n.e = b), c !== n.t && l(a, "data-graph-object", n.t = c), d !== n.a && l(a, "data-selected", n.a = d), P !== n.o && T(a, "left", n.o = P), s !== n.i && T(a, "top", n.i = s), f !== n.n && T(a, "width", n.n = f), C !== n.s && T(a, "height", n.s = C), I !== n.h && l(a, "aria-label", n.h = I), n;
|
|
77
77
|
}, {
|
|
78
78
|
e: void 0,
|
|
79
79
|
t: void 0,
|
|
@@ -87,10 +87,10 @@ function me(i) {
|
|
|
87
87
|
})();
|
|
88
88
|
};
|
|
89
89
|
return (() => {
|
|
90
|
-
var e =
|
|
91
|
-
e.$$keydown = (r) =>
|
|
92
|
-
var t =
|
|
93
|
-
return typeof t == "function" ?
|
|
90
|
+
var e = se();
|
|
91
|
+
e.$$keydown = (r) => j(null, r);
|
|
92
|
+
var t = g;
|
|
93
|
+
return typeof t == "function" ? ee(t, e) : g = e, y(e, w(ie, {
|
|
94
94
|
get viewport() {
|
|
95
95
|
return i.viewport;
|
|
96
96
|
},
|
|
@@ -103,47 +103,62 @@ function me(i) {
|
|
|
103
103
|
return i.onViewportChange;
|
|
104
104
|
},
|
|
105
105
|
onViewportInteractionStart: () => i.onInteractionStart?.(),
|
|
106
|
-
resolveTargetRole: (r) => (r.target instanceof Element ? r.target : null)?.closest('button, a, input, select, textarea, [data-floe-canvas-interactive="true"]') ? "local_surface" :
|
|
106
|
+
resolveTargetRole: (r) => (r.target instanceof Element ? r.target : null)?.closest('button, a, input, select, textarea, [data-floe-canvas-interactive="true"]') ? "local_surface" : oe(r),
|
|
107
107
|
onCanvasPointerDown: (r) => {
|
|
108
|
-
!(r.target instanceof Element) || r.target.closest("[data-graph-object]") || i.onActivate?.(
|
|
108
|
+
!(r.target instanceof Element) || r.target.closest("[data-graph-object]") || i.onActivate?.(x(null, g, r));
|
|
109
109
|
},
|
|
110
110
|
onCanvasContextMenu: (r) => i.onContextMenu?.({
|
|
111
111
|
object: null,
|
|
112
|
-
owner:
|
|
112
|
+
owner: g,
|
|
113
113
|
position: {
|
|
114
114
|
x: r.clientX,
|
|
115
115
|
y: r.clientY
|
|
116
116
|
}
|
|
117
117
|
}),
|
|
118
118
|
children: (r) => {
|
|
119
|
-
const a =
|
|
120
|
-
return [
|
|
119
|
+
const a = $(() => Z(r())), n = $(() => a().filter((c) => c.kind === "group")), b = $(() => a().filter((c) => c.kind !== "group"));
|
|
120
|
+
return [w(D, {
|
|
121
121
|
get each() {
|
|
122
122
|
return n();
|
|
123
123
|
},
|
|
124
|
-
children:
|
|
124
|
+
children: G
|
|
125
125
|
}), (() => {
|
|
126
|
-
var
|
|
127
|
-
return
|
|
126
|
+
var c = ue(), d = c.firstChild, P = d.firstChild;
|
|
127
|
+
return l(P, "id", H), y(c, w(D, {
|
|
128
128
|
get each() {
|
|
129
129
|
return i.layout.edges;
|
|
130
130
|
},
|
|
131
|
-
children: (
|
|
132
|
-
const
|
|
131
|
+
children: (s) => {
|
|
132
|
+
const f = {
|
|
133
133
|
kind: "edge",
|
|
134
|
-
id:
|
|
135
|
-
},
|
|
136
|
-
return
|
|
134
|
+
id: s.id
|
|
135
|
+
}, C = () => s.sections.map(ce).join(" "), I = () => le(s.sections);
|
|
136
|
+
return w(F, {
|
|
137
137
|
get when() {
|
|
138
|
-
return !
|
|
138
|
+
return !Y() || Y().has(s.id);
|
|
139
139
|
},
|
|
140
140
|
get children() {
|
|
141
|
-
var
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
var k = he(), z = k.firstChild, u = z.nextSibling;
|
|
142
|
+
return l(z, "marker-end", `url(#${H})`), y(k, w(F, {
|
|
143
|
+
get when() {
|
|
144
|
+
return I();
|
|
145
|
+
},
|
|
146
|
+
children: (o) => (() => {
|
|
147
|
+
var m = ge();
|
|
148
|
+
return y(m, () => s.label), S((h) => {
|
|
149
|
+
var E = /^\d+$/.test(s.label), L = o().x, M = o().y - 6;
|
|
150
|
+
return E !== h.e && l(m, "data-count", h.e = E), L !== h.t && l(m, "x", h.t = L), M !== h.a && l(m, "y", h.a = M), h;
|
|
151
|
+
}, {
|
|
152
|
+
e: void 0,
|
|
153
|
+
t: void 0,
|
|
154
|
+
a: void 0
|
|
155
|
+
}), m;
|
|
156
|
+
})()
|
|
157
|
+
}), u), u.$$keydown = (o) => j(f, o), u.$$contextmenu = (o) => R(f, o), u.$$click = (o) => {
|
|
158
|
+
o.stopPropagation(), i.onActivate?.(x(f, o.currentTarget, o));
|
|
159
|
+
}, u.addEventListener("blur", () => v(null)), u.addEventListener("focus", () => v(f)), u.addEventListener("pointerleave", () => v(null)), u.addEventListener("pointerenter", () => v(f)), S((o) => {
|
|
160
|
+
var m = s.id, h = Q().has(s.id), E = A(f), L = C(), M = C(), N = s.label;
|
|
161
|
+
return m !== o.e && l(k, "data-graph-object", o.e = m), h !== o.t && l(k, "data-emphasized", o.t = h), E !== o.a && l(k, "data-selected", o.a = E), L !== o.o && l(z, "d", o.o = L), M !== o.i && l(u, "d", o.i = M), N !== o.n && l(u, "aria-label", o.n = N), o;
|
|
147
162
|
}, {
|
|
148
163
|
e: void 0,
|
|
149
164
|
t: void 0,
|
|
@@ -151,28 +166,28 @@ function me(i) {
|
|
|
151
166
|
o: void 0,
|
|
152
167
|
i: void 0,
|
|
153
168
|
n: void 0
|
|
154
|
-
}),
|
|
169
|
+
}), k;
|
|
155
170
|
}
|
|
156
171
|
});
|
|
157
172
|
}
|
|
158
|
-
}), null),
|
|
159
|
-
})(),
|
|
173
|
+
}), null), S(() => l(c, "aria-label", i.ariaLabel)), c;
|
|
174
|
+
})(), w(D, {
|
|
160
175
|
get each() {
|
|
161
|
-
return
|
|
176
|
+
return b();
|
|
162
177
|
},
|
|
163
|
-
children:
|
|
178
|
+
children: G
|
|
164
179
|
})];
|
|
165
180
|
}
|
|
166
|
-
}), null),
|
|
181
|
+
}), null), y(e, () => i.overlay, null), S((r) => {
|
|
167
182
|
var a = `floe-graph ${i.class ?? ""}`, n = i.ariaLabel;
|
|
168
|
-
return a !== r.e &&
|
|
183
|
+
return a !== r.e && V(e, r.e = a), n !== r.t && l(e, "aria-label", r.t = n), r;
|
|
169
184
|
}, {
|
|
170
185
|
e: void 0,
|
|
171
186
|
t: void 0
|
|
172
187
|
}), e;
|
|
173
188
|
})();
|
|
174
189
|
}
|
|
175
|
-
|
|
190
|
+
te(["focusin", "focusout", "click", "contextmenu", "keydown"]);
|
|
176
191
|
export {
|
|
177
|
-
|
|
192
|
+
ke as GraphCanvas
|
|
178
193
|
};
|
|
@@ -6,3 +6,5 @@ export declare function fitGraphViewport(bounds: GraphBounds, size: {
|
|
|
6
6
|
}, padding?: number): GraphViewport;
|
|
7
7
|
/** Includes descendants so highlighting a group traces all of its direct routes. */
|
|
8
8
|
export declare function graphRelatedEdges(layout: GraphLayout, nodeId: string): ReadonlySet<string>;
|
|
9
|
+
/** Put the short edge label on its longest routed segment, away from arrowheads. */
|
|
10
|
+
export declare function graphEdgeLabelPosition(sections: readonly (readonly GraphPoint[])[]): GraphPoint | null;
|
|
@@ -1,35 +1,45 @@
|
|
|
1
|
-
function c(
|
|
2
|
-
return
|
|
1
|
+
function c(e) {
|
|
2
|
+
return e.map((n, h) => `${h ? "L" : "M"}${n.x},${n.y}`).join(" ");
|
|
3
3
|
}
|
|
4
|
-
function
|
|
5
|
-
const
|
|
4
|
+
function s(e, n, h = 48) {
|
|
5
|
+
const a = Math.max(
|
|
6
6
|
0.1,
|
|
7
7
|
Math.min(
|
|
8
8
|
1,
|
|
9
|
-
(
|
|
10
|
-
(
|
|
9
|
+
(n.width - h * 2) / Math.max(1, e.width),
|
|
10
|
+
(n.height - h * 2) / Math.max(1, e.height)
|
|
11
11
|
)
|
|
12
12
|
);
|
|
13
13
|
return {
|
|
14
|
-
x: (
|
|
15
|
-
y: (
|
|
16
|
-
scale:
|
|
14
|
+
x: (n.width - e.width * a) / 2 - e.x * a,
|
|
15
|
+
y: (n.height - e.height * a) / 2 - e.y * a,
|
|
16
|
+
scale: a
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
function f(
|
|
20
|
-
const
|
|
21
|
-
for (const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
for (const
|
|
25
|
-
|
|
19
|
+
function f(e, n) {
|
|
20
|
+
const h = /* @__PURE__ */ new Set([n]), a = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const t of e.nodes)
|
|
22
|
+
t.parentId && a.set(t.parentId, [...a.get(t.parentId) ?? [], t.id]);
|
|
23
|
+
const r = (t) => {
|
|
24
|
+
for (const i of a.get(t) ?? [])
|
|
25
|
+
h.has(i) || (h.add(i), r(i));
|
|
26
26
|
};
|
|
27
|
-
return n
|
|
28
|
-
|
|
27
|
+
return r(n), new Set(
|
|
28
|
+
e.edges.filter((t) => h.has(t.source) || h.has(t.target)).map((t) => t.id)
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
|
+
function g(e) {
|
|
32
|
+
let n = null, h = 0;
|
|
33
|
+
for (const a of e)
|
|
34
|
+
for (let r = 1; r < a.length; r++) {
|
|
35
|
+
const t = a[r - 1], i = a[r], o = Math.abs(i.x - t.x) + Math.abs(i.y - t.y);
|
|
36
|
+
o > h && (h = o, n = { x: (t.x + i.x) / 2, y: (t.y + i.y) / 2 });
|
|
37
|
+
}
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
31
40
|
export {
|
|
32
|
-
|
|
41
|
+
s as fitGraphViewport,
|
|
42
|
+
g as graphEdgeLabelPosition,
|
|
33
43
|
f as graphRelatedEdges,
|
|
34
44
|
c as graphSectionPath
|
|
35
45
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { applyGraphPositions as v } from "./positions.js";
|
|
2
|
+
function $(i) {
|
|
3
|
+
const n = /* @__PURE__ */ new Set(), p = (e) => {
|
|
4
|
+
if (!e || n.has(e)) throw new Error(`Duplicate or empty graph ID: ${e}`);
|
|
5
|
+
n.add(e);
|
|
6
|
+
}, s = new Map(i.nodes.map((e) => [e.id, e]));
|
|
7
|
+
for (const e of i.nodes) {
|
|
7
8
|
if (p(e.id), ![e.width, e.height].every((r) => Number.isFinite(r) && r > 0))
|
|
8
9
|
throw new Error(`Invalid dimensions for graph node: ${e.id}`);
|
|
9
10
|
for (const r of e.ports ?? []) p(r.id);
|
|
@@ -17,7 +18,7 @@ function k(n) {
|
|
|
17
18
|
a.add(d), d = r.parentId;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
|
-
for (const e of
|
|
21
|
+
for (const e of i.edges) {
|
|
21
22
|
p(e.id);
|
|
22
23
|
for (const [a, d] of [
|
|
23
24
|
[e.source, e.sourcePort],
|
|
@@ -29,13 +30,13 @@ function k(n) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
async function
|
|
33
|
-
|
|
34
|
-
const s =
|
|
35
|
-
if (![...Object.values(s),
|
|
33
|
+
async function O(i, n, p) {
|
|
34
|
+
$(i);
|
|
35
|
+
const s = n.groupPadding ?? { top: 64, right: 28, bottom: 28, left: 28 };
|
|
36
|
+
if (![...Object.values(s), n.spacing ?? 64].every((t) => Number.isFinite(t) && t >= 0))
|
|
36
37
|
throw new Error("Graph layout spacing must be finite and nonnegative");
|
|
37
38
|
const e = /* @__PURE__ */ new Map();
|
|
38
|
-
for (const t of
|
|
39
|
+
for (const t of i.nodes)
|
|
39
40
|
e.set(t.id, {
|
|
40
41
|
id: t.id,
|
|
41
42
|
width: t.width,
|
|
@@ -53,29 +54,29 @@ async function N(n, i, p) {
|
|
|
53
54
|
}
|
|
54
55
|
});
|
|
55
56
|
const a = [];
|
|
56
|
-
for (const t of
|
|
57
|
+
for (const t of i.nodes)
|
|
57
58
|
(t.parentId ? e.get(t.parentId).children : a).push(e.get(t.id));
|
|
58
59
|
const d = /* @__PURE__ */ new Set([
|
|
59
|
-
...
|
|
60
|
-
...
|
|
60
|
+
...i.nodes.flatMap((t) => [t.id, ...(t.ports ?? []).map((o) => o.id)]),
|
|
61
|
+
...i.edges.map((t) => t.id)
|
|
61
62
|
]);
|
|
62
63
|
let r = "$graph";
|
|
63
64
|
for (; d.has(r); ) r += "$";
|
|
64
65
|
const c = await p.layout({
|
|
65
66
|
id: r,
|
|
66
67
|
children: a,
|
|
67
|
-
edges:
|
|
68
|
+
edges: i.edges.map((t) => ({
|
|
68
69
|
id: t.id,
|
|
69
70
|
sources: [t.sourcePort ?? t.source],
|
|
70
71
|
targets: [t.targetPort ?? t.target]
|
|
71
72
|
})),
|
|
72
73
|
layoutOptions: {
|
|
73
74
|
"elk.algorithm": "layered",
|
|
74
|
-
"elk.direction":
|
|
75
|
+
"elk.direction": n.direction ?? "RIGHT",
|
|
75
76
|
"elk.edgeRouting": "ORTHOGONAL",
|
|
76
77
|
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
|
77
|
-
"elk.spacing.nodeNode": String(
|
|
78
|
-
"elk.layered.spacing.nodeNodeBetweenLayers": String(
|
|
78
|
+
"elk.spacing.nodeNode": String(n.spacing ?? 64),
|
|
79
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": String(n.spacing ?? 64),
|
|
79
80
|
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
80
81
|
"elk.randomSeed": "1"
|
|
81
82
|
}
|
|
@@ -100,22 +101,25 @@ async function N(n, i, p) {
|
|
|
100
101
|
};
|
|
101
102
|
for (const t of c.children ?? []) E(t, 0, 0);
|
|
102
103
|
I(c);
|
|
103
|
-
const y =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
const y = !n.positions?.length && n.anchor && f.get(n.anchor.nodeId), m = y ? n.anchor.position.x - y.x : 0, u = y ? n.anchor.position.y - y.y : 0, k = i.nodes.map((t) => {
|
|
105
|
+
const o = f.get(t.id);
|
|
106
|
+
return { ...t, ...o, x: o.x + m, y: o.y + u };
|
|
107
|
+
});
|
|
108
|
+
return v(
|
|
109
|
+
{
|
|
110
|
+
nodes: k,
|
|
111
|
+
edges: i.edges.map((t) => ({
|
|
112
|
+
...t,
|
|
113
|
+
sections: (x.get(t.id) ?? []).map(
|
|
114
|
+
(o) => o.map((h) => ({ x: h.x + m, y: h.y + u }))
|
|
115
|
+
)
|
|
116
|
+
})),
|
|
117
|
+
bounds: { x: m, y: u, width: c.width ?? 0, height: c.height ?? 0 }
|
|
118
|
+
},
|
|
119
|
+
n
|
|
120
|
+
);
|
|
117
121
|
}
|
|
118
122
|
export {
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
O as computeGraphLayout,
|
|
124
|
+
$ as validateGraphInput
|
|
121
125
|
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { GraphLayout, GraphLayoutOptions } from './types';
|
|
2
|
+
/** Apply optional absolute world positions, retaining unpinned ELK geometry and group containment. */
|
|
3
|
+
export declare function applyGraphPositions(layout: GraphLayout, options: GraphLayoutOptions): GraphLayout;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const P = (s, e = 0) => ({
|
|
2
|
+
left: s.x - e,
|
|
3
|
+
top: s.y - e,
|
|
4
|
+
right: s.x + s.width + e,
|
|
5
|
+
bottom: s.y + s.height + e
|
|
6
|
+
}), b = (s, e) => s.x > e.left && s.x < e.right && s.y > e.top && s.y < e.bottom, N = (s, e, y) => !y.some(
|
|
7
|
+
(o) => s.x === e.x ? s.x > o.left && s.x < o.right && Math.max(s.y, e.y) > o.top && Math.min(s.y, e.y) < o.bottom : s.y > o.top && s.y < o.bottom && Math.max(s.x, e.x) > o.left && Math.min(s.x, e.x) < o.right
|
|
8
|
+
);
|
|
9
|
+
function k(s, e, y) {
|
|
10
|
+
if (y.some((i) => b(s, i) || b(e, i)))
|
|
11
|
+
throw new Error("Pinned graph positions obstruct an edge endpoint");
|
|
12
|
+
const o = [
|
|
13
|
+
.../* @__PURE__ */ new Set([s.x, e.x, ...y.flatMap((i) => [i.left, i.right])])
|
|
14
|
+
].sort((i, r) => i - r), d = [
|
|
15
|
+
.../* @__PURE__ */ new Set([s.y, e.y, ...y.flatMap((i) => [i.top, i.bottom])])
|
|
16
|
+
].sort((i, r) => i - r), u = (i) => ({ x: o[i % o.length], y: d[Math.floor(i / o.length)] }), v = d.indexOf(s.y) * o.length + o.indexOf(s.x), E = d.indexOf(e.y) * o.length + o.indexOf(e.x), w = /* @__PURE__ */ new Map([[v, 0]]), I = /* @__PURE__ */ new Map(), h = [], T = (i) => {
|
|
17
|
+
let r = h.length;
|
|
18
|
+
for (h.push(i); r > 0; ) {
|
|
19
|
+
const c = r - 1 >> 1;
|
|
20
|
+
if (h[c].score <= i.score) break;
|
|
21
|
+
h[r] = h[c], r = c;
|
|
22
|
+
}
|
|
23
|
+
h[r] = i;
|
|
24
|
+
}, t = () => {
|
|
25
|
+
const i = h[0], r = h.pop();
|
|
26
|
+
if (h.length) {
|
|
27
|
+
let c = 0;
|
|
28
|
+
for (; c * 2 + 1 < h.length; ) {
|
|
29
|
+
let a = c * 2 + 1;
|
|
30
|
+
if (a + 1 < h.length && h[a + 1].score < h[a].score && a++, h[a].score >= r.score) break;
|
|
31
|
+
h[c] = h[a], c = a;
|
|
32
|
+
}
|
|
33
|
+
h[c] = r;
|
|
34
|
+
}
|
|
35
|
+
return i;
|
|
36
|
+
};
|
|
37
|
+
T({ id: v, score: 0, cost: 0 });
|
|
38
|
+
let g = 0;
|
|
39
|
+
for (; h.length && g++ < 5e4; ) {
|
|
40
|
+
const i = t();
|
|
41
|
+
if (i.cost !== w.get(i.id)) continue;
|
|
42
|
+
if (i.id === E) {
|
|
43
|
+
const x = [u(E)];
|
|
44
|
+
let f = E;
|
|
45
|
+
for (; f !== v; )
|
|
46
|
+
f = I.get(f), x.push(u(f));
|
|
47
|
+
return x.reverse(), x.filter((l, p) => {
|
|
48
|
+
const n = x[p - 1], M = x[p + 1];
|
|
49
|
+
return !n || !M || !(n.x === l.x && l.x === M.x || n.y === l.y && l.y === M.y);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const r = u(i.id), c = i.id % o.length, a = Math.floor(i.id / o.length), S = [
|
|
53
|
+
c ? i.id - 1 : -1,
|
|
54
|
+
c + 1 < o.length ? i.id + 1 : -1,
|
|
55
|
+
a ? i.id - o.length : -1,
|
|
56
|
+
a + 1 < d.length ? i.id + o.length : -1
|
|
57
|
+
];
|
|
58
|
+
for (const x of S) {
|
|
59
|
+
if (x < 0) continue;
|
|
60
|
+
const f = u(x), l = i.cost + Math.abs(r.x - f.x) + Math.abs(r.y - f.y);
|
|
61
|
+
l >= (w.get(x) ?? 1 / 0) || !N(r, f, y) || (w.set(x, l), I.set(x, i.id), T({ id: x, cost: l, score: l + Math.abs(f.x - e.x) + Math.abs(f.y - e.y) }));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new Error("Pinned graph positions cannot be routed within the layout budget");
|
|
65
|
+
}
|
|
66
|
+
function H(s, e) {
|
|
67
|
+
if (!e.positions?.length) return s;
|
|
68
|
+
const y = new Map(s.nodes.map((t) => [t.id, { ...t }])), o = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const t of e.positions) {
|
|
70
|
+
if (!y.has(t.nodeId) || o.has(t.nodeId) || !Number.isFinite(t.x) || !Number.isFinite(t.y))
|
|
71
|
+
throw new Error(`Invalid graph position: ${t.nodeId}`);
|
|
72
|
+
o.set(t.nodeId, t);
|
|
73
|
+
}
|
|
74
|
+
const d = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const t of y.values())
|
|
76
|
+
if (t.parentId) {
|
|
77
|
+
const g = d.get(t.parentId) ?? [];
|
|
78
|
+
g.push(t), d.set(t.parentId, g);
|
|
79
|
+
}
|
|
80
|
+
const u = e.groupPadding ?? { top: 64, left: 28, right: 28, bottom: 28 }, v = (t, g, i) => {
|
|
81
|
+
const r = { x: t.x, y: t.y }, c = o.get(t.id);
|
|
82
|
+
t.x = c?.x ?? t.x + g, t.y = c?.y ?? t.y + i;
|
|
83
|
+
const a = d.get(t.id) ?? [];
|
|
84
|
+
for (const p of a) v(p, t.x - r.x, t.y - r.y);
|
|
85
|
+
if (!a.length) return;
|
|
86
|
+
const S = Math.min(...a.map((p) => p.x)) - u.left, x = Math.min(...a.map((p) => p.y)) - u.top;
|
|
87
|
+
if (c && (S < t.x || x < t.y))
|
|
88
|
+
throw new Error(`Pinned child lies outside group content: ${t.id}`);
|
|
89
|
+
const f = Math.max(
|
|
90
|
+
t.x + t.width,
|
|
91
|
+
...a.map((p) => p.x + p.width + u.right)
|
|
92
|
+
), l = Math.max(
|
|
93
|
+
t.y + t.height,
|
|
94
|
+
...a.map((p) => p.y + p.height + u.bottom)
|
|
95
|
+
);
|
|
96
|
+
c || (t.x = Math.min(t.x, S), t.y = Math.min(t.y, x)), t.width = f - t.x, t.height = l - t.y;
|
|
97
|
+
};
|
|
98
|
+
for (const t of y.values()) t.parentId || v(t, 0, 0);
|
|
99
|
+
const E = s.edges.map((t) => {
|
|
100
|
+
const g = y.get(t.source), i = y.get(t.target), r = (n, M, m) => n.ports?.find((O) => O.id === M)?.side ?? m, c = g.x + g.width / 2 <= i.x + i.width / 2, a = (n, M) => {
|
|
101
|
+
const m = { x: n.x + n.width / 2, y: n.y + n.height / 2 };
|
|
102
|
+
return M === "NORTH" ? [
|
|
103
|
+
{ x: m.x, y: n.y },
|
|
104
|
+
{ x: m.x, y: n.y - 8 }
|
|
105
|
+
] : M === "SOUTH" ? [
|
|
106
|
+
{ x: m.x, y: n.y + n.height },
|
|
107
|
+
{ x: m.x, y: n.y + n.height + 8 }
|
|
108
|
+
] : M === "WEST" ? [
|
|
109
|
+
{ x: n.x, y: m.y },
|
|
110
|
+
{ x: n.x - 8, y: m.y }
|
|
111
|
+
] : [
|
|
112
|
+
{ x: n.x + n.width, y: m.y },
|
|
113
|
+
{ x: n.x + n.width + 8, y: m.y }
|
|
114
|
+
];
|
|
115
|
+
}, [S, x] = a(g, r(g, t.sourcePort, c ? "EAST" : "WEST")), [f, l] = a(
|
|
116
|
+
i,
|
|
117
|
+
r(i, t.targetPort, g === i ? "NORTH" : c ? "WEST" : "EAST")
|
|
118
|
+
), p = [...y.values()].filter((n) => n.kind !== "group").map((n) => P(n, 4));
|
|
119
|
+
return { ...t, sections: [[S, ...k(x, l, p), f]] };
|
|
120
|
+
}), w = [...y.values()], I = E.flatMap((t) => t.sections.flat()), h = Math.min(...w.map((t) => t.x), ...I.map((t) => t.x)), T = Math.min(...w.map((t) => t.y), ...I.map((t) => t.y));
|
|
121
|
+
return {
|
|
122
|
+
nodes: w,
|
|
123
|
+
edges: E,
|
|
124
|
+
bounds: {
|
|
125
|
+
x: h,
|
|
126
|
+
y: T,
|
|
127
|
+
width: Math.max(...w.map((t) => t.x + t.width), ...I.map((t) => t.x)) - h,
|
|
128
|
+
height: Math.max(...w.map((t) => t.y + t.height), ...I.map((t) => t.y)) - T
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
H as applyGraphPositions
|
|
134
|
+
};
|
|
@@ -46,6 +46,10 @@ export interface GraphLayout {
|
|
|
46
46
|
bounds: GraphBounds;
|
|
47
47
|
}
|
|
48
48
|
export interface GraphLayoutOptions {
|
|
49
|
+
/** Absolute world positions. Descendants move with pinned groups; impossible containment or routing fails explicitly. */
|
|
50
|
+
positions?: readonly (GraphPoint & {
|
|
51
|
+
nodeId: string;
|
|
52
|
+
})[];
|
|
49
53
|
direction?: 'RIGHT' | 'DOWN' | 'LEFT' | 'UP';
|
|
50
54
|
spacing?: number;
|
|
51
55
|
groupPadding?: {
|
package/dist/graph.css
CHANGED
|
@@ -91,3 +91,21 @@
|
|
|
91
91
|
outline-color: Highlight;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
.floe-graph__edge-label {
|
|
96
|
+
fill: var(--graph-edge, var(--muted-foreground));
|
|
97
|
+
stroke: var(--graph-background, var(--background));
|
|
98
|
+
stroke-width: 3px;
|
|
99
|
+
paint-order: stroke fill;
|
|
100
|
+
font: 11px var(--font-mono, monospace);
|
|
101
|
+
text-anchor: middle;
|
|
102
|
+
pointer-events: none;
|
|
103
|
+
opacity: 0;
|
|
104
|
+
}
|
|
105
|
+
.floe-graph__edge-label[data-count='true'] {
|
|
106
|
+
opacity: 0.7;
|
|
107
|
+
}
|
|
108
|
+
.floe-graph__edges [data-emphasized='true'] .floe-graph__edge-label,
|
|
109
|
+
.floe-graph__edges [data-selected='true'] .floe-graph__edge-label {
|
|
110
|
+
opacity: 1;
|
|
111
|
+
}
|
package/dist/graph.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { fitGraphViewport as e, graphRelatedEdges as t, graphSectionPath as
|
|
2
|
-
import { createGraphLayoutEngine as
|
|
3
|
-
import { GraphCanvas as
|
|
1
|
+
import { fitGraphViewport as e, graphEdgeLabelPosition as o, graphRelatedEdges as t, graphSectionPath as p } from "./components/graph/geometry.js";
|
|
2
|
+
import { createGraphLayoutEngine as g } from "./components/graph/engine.js";
|
|
3
|
+
import { GraphCanvas as n } from "./components/graph/GraphCanvas.js";
|
|
4
4
|
export {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
n as GraphCanvas,
|
|
6
|
+
g as createGraphLayoutEngine,
|
|
7
7
|
e as fitGraphViewport,
|
|
8
|
+
o as graphEdgeLabelPosition,
|
|
8
9
|
t as graphRelatedEdges,
|
|
9
|
-
|
|
10
|
+
p as graphSectionPath
|
|
10
11
|
};
|