@floegence/floe-webapp-core 0.82.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/_virtual/_commonjs-dynamic-modules.js +6 -0
- package/dist/_virtual/_commonjsHelpers.js +6 -0
- package/dist/_virtual/elk-api.js +7 -0
- package/dist/_virtual/elk-api2.js +4 -0
- package/dist/assets/elk-worker-CrDP6jPz.js +47082 -0
- package/dist/components/graph/GraphCanvas.d.ts +27 -0
- package/dist/components/graph/GraphCanvas.js +193 -0
- package/dist/components/graph/engine.d.ts +7 -0
- package/dist/components/graph/engine.js +35 -0
- package/dist/components/graph/geometry.d.ts +10 -0
- package/dist/components/graph/geometry.js +45 -0
- package/dist/components/graph/layout.d.ts +5 -0
- package/dist/components/graph/layout.js +125 -0
- package/dist/components/graph/positions.d.ts +3 -0
- package/dist/components/graph/positions.js +134 -0
- package/dist/components/graph/types.d.ts +76 -0
- package/dist/components/ui/InfiniteCanvas.d.ts +1 -1
- package/dist/components/ui/InfiniteCanvas.js +58 -55
- package/dist/graph.css +111 -0
- package/dist/graph.d.ts +4 -0
- package/dist/graph.js +11 -0
- package/dist/licenses/elkjs.txt +263 -0
- package/dist/licenses/graph-notice.txt +5 -0
- package/dist/node_modules/.pnpm/elkjs@0.12.0/node_modules/elkjs/lib/elk-api.js +186 -0
- package/package.json +8 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type JSX } from 'solid-js';
|
|
2
|
+
import type { GraphLayout, GraphLayoutNode, GraphObjectEvent, GraphObjectRef, GraphViewport } from './types';
|
|
3
|
+
export interface GraphNodeRenderContext {
|
|
4
|
+
selected: () => boolean;
|
|
5
|
+
/** Use on a touch-accessible menu trigger; floating UI remains host-owned. */
|
|
6
|
+
openMenu: (event: MouseEvent) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface GraphCanvasProps {
|
|
9
|
+
layout: GraphLayout;
|
|
10
|
+
viewport: GraphViewport;
|
|
11
|
+
onViewportChange: (viewport: GraphViewport) => void;
|
|
12
|
+
ariaLabel: string;
|
|
13
|
+
renderNode: (node: GraphLayoutNode, context: GraphNodeRenderContext) => JSX.Element;
|
|
14
|
+
renderGroup?: (node: GraphLayoutNode, context: GraphNodeRenderContext) => JSX.Element;
|
|
15
|
+
selected?: GraphObjectRef | null;
|
|
16
|
+
/** Undefined follows pointer/focus. Null explicitly clears highlighting. */
|
|
17
|
+
highlight?: GraphObjectRef | null;
|
|
18
|
+
onActivate?: (event: GraphObjectEvent) => void;
|
|
19
|
+
onContextMenu?: (event: GraphObjectEvent) => void;
|
|
20
|
+
onHover?: (object: GraphObjectRef | null) => void;
|
|
21
|
+
onInteractionStart?: () => void;
|
|
22
|
+
/** Overlay content should use shared SurfaceFloatingLayer for point placement. */
|
|
23
|
+
overlay?: JSX.Element;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Controlled graph surface. Hosts own documents and selection; Floe owns geometry and input. */
|
|
27
|
+
export declare function GraphCanvas(props: GraphCanvasProps): JSX.Element;
|
|
@@ -0,0 +1,193 @@
|
|
|
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
|
+
width: 0,
|
|
12
|
+
height: 0
|
|
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
|
+
if (e?.kind !== "node") return null;
|
|
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
|
+
const {
|
|
23
|
+
width: t,
|
|
24
|
+
height: r
|
|
25
|
+
} = O();
|
|
26
|
+
if (!t || !r) return i.layout.nodes;
|
|
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
|
+
const a = t.getBoundingClientRect();
|
|
33
|
+
return {
|
|
34
|
+
object: e,
|
|
35
|
+
owner: t,
|
|
36
|
+
position: r && (r.clientX !== 0 || r.clientY !== 0) ? {
|
|
37
|
+
x: r.clientX,
|
|
38
|
+
y: r.clientY
|
|
39
|
+
} : {
|
|
40
|
+
x: a.left + a.width / 2,
|
|
41
|
+
y: a.top + Math.min(32, a.height / 2)
|
|
42
|
+
}
|
|
43
|
+
};
|
|
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
|
+
};
|
|
49
|
+
re(() => {
|
|
50
|
+
if (!g) return;
|
|
51
|
+
const e = () => U({
|
|
52
|
+
width: g.clientWidth,
|
|
53
|
+
height: g.clientHeight
|
|
54
|
+
});
|
|
55
|
+
e();
|
|
56
|
+
const t = new ResizeObserver(e);
|
|
57
|
+
t.observe(g), ae(() => t.disconnect());
|
|
58
|
+
});
|
|
59
|
+
const G = (e) => {
|
|
60
|
+
const t = {
|
|
61
|
+
kind: "node",
|
|
62
|
+
id: e.id
|
|
63
|
+
}, r = {
|
|
64
|
+
selected: () => A(t),
|
|
65
|
+
openMenu: (a) => R(t, a)
|
|
66
|
+
};
|
|
67
|
+
return (() => {
|
|
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
|
+
return () => n() ? i.renderGroup(e, r) : i.renderNode(e, r);
|
|
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
|
+
}, {
|
|
78
|
+
e: void 0,
|
|
79
|
+
t: void 0,
|
|
80
|
+
a: void 0,
|
|
81
|
+
o: void 0,
|
|
82
|
+
i: void 0,
|
|
83
|
+
n: void 0,
|
|
84
|
+
s: void 0,
|
|
85
|
+
h: void 0
|
|
86
|
+
}), a;
|
|
87
|
+
})();
|
|
88
|
+
};
|
|
89
|
+
return (() => {
|
|
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
|
+
get viewport() {
|
|
95
|
+
return i.viewport;
|
|
96
|
+
},
|
|
97
|
+
minScale: 0.1,
|
|
98
|
+
maxScale: 3,
|
|
99
|
+
get ariaLabel() {
|
|
100
|
+
return i.ariaLabel;
|
|
101
|
+
},
|
|
102
|
+
get onViewportChange() {
|
|
103
|
+
return i.onViewportChange;
|
|
104
|
+
},
|
|
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" : oe(r),
|
|
107
|
+
onCanvasPointerDown: (r) => {
|
|
108
|
+
!(r.target instanceof Element) || r.target.closest("[data-graph-object]") || i.onActivate?.(x(null, g, r));
|
|
109
|
+
},
|
|
110
|
+
onCanvasContextMenu: (r) => i.onContextMenu?.({
|
|
111
|
+
object: null,
|
|
112
|
+
owner: g,
|
|
113
|
+
position: {
|
|
114
|
+
x: r.clientX,
|
|
115
|
+
y: r.clientY
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
118
|
+
children: (r) => {
|
|
119
|
+
const a = $(() => Z(r())), n = $(() => a().filter((c) => c.kind === "group")), b = $(() => a().filter((c) => c.kind !== "group"));
|
|
120
|
+
return [w(D, {
|
|
121
|
+
get each() {
|
|
122
|
+
return n();
|
|
123
|
+
},
|
|
124
|
+
children: G
|
|
125
|
+
}), (() => {
|
|
126
|
+
var c = ue(), d = c.firstChild, P = d.firstChild;
|
|
127
|
+
return l(P, "id", H), y(c, w(D, {
|
|
128
|
+
get each() {
|
|
129
|
+
return i.layout.edges;
|
|
130
|
+
},
|
|
131
|
+
children: (s) => {
|
|
132
|
+
const f = {
|
|
133
|
+
kind: "edge",
|
|
134
|
+
id: s.id
|
|
135
|
+
}, C = () => s.sections.map(ce).join(" "), I = () => le(s.sections);
|
|
136
|
+
return w(F, {
|
|
137
|
+
get when() {
|
|
138
|
+
return !Y() || Y().has(s.id);
|
|
139
|
+
},
|
|
140
|
+
get children() {
|
|
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;
|
|
162
|
+
}, {
|
|
163
|
+
e: void 0,
|
|
164
|
+
t: void 0,
|
|
165
|
+
a: void 0,
|
|
166
|
+
o: void 0,
|
|
167
|
+
i: void 0,
|
|
168
|
+
n: void 0
|
|
169
|
+
}), k;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}), null), S(() => l(c, "aria-label", i.ariaLabel)), c;
|
|
174
|
+
})(), w(D, {
|
|
175
|
+
get each() {
|
|
176
|
+
return b();
|
|
177
|
+
},
|
|
178
|
+
children: G
|
|
179
|
+
})];
|
|
180
|
+
}
|
|
181
|
+
}), null), y(e, () => i.overlay, null), S((r) => {
|
|
182
|
+
var a = `floe-graph ${i.class ?? ""}`, n = i.ariaLabel;
|
|
183
|
+
return a !== r.e && V(e, r.e = a), n !== r.t && l(e, "aria-label", r.t = n), r;
|
|
184
|
+
}, {
|
|
185
|
+
e: void 0,
|
|
186
|
+
t: void 0
|
|
187
|
+
}), e;
|
|
188
|
+
})();
|
|
189
|
+
}
|
|
190
|
+
te(["focusin", "focusout", "click", "contextmenu", "keydown"]);
|
|
191
|
+
export {
|
|
192
|
+
ke as GraphCanvas
|
|
193
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GraphInput, GraphLayout, GraphLayoutOptions } from './types';
|
|
2
|
+
export interface GraphLayoutEngine {
|
|
3
|
+
layout(input: GraphInput, options?: GraphLayoutOptions): Promise<GraphLayout>;
|
|
4
|
+
dispose(): void;
|
|
5
|
+
}
|
|
6
|
+
/** One worker per mounted graph. Dispose on unmount; failures are never hidden. */
|
|
7
|
+
export declare function createGraphLayoutEngine(): GraphLayoutEngine;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import m from "../../_virtual/elk-api.js";
|
|
2
|
+
import { computeGraphLayout as u } from "./layout.js";
|
|
3
|
+
function c() {
|
|
4
|
+
const r = new Worker(new URL(
|
|
5
|
+
/* @vite-ignore */
|
|
6
|
+
"" + new URL("../../assets/elk-worker-CrDP6jPz.js", import.meta.url).href,
|
|
7
|
+
import.meta.url
|
|
8
|
+
), { type: "module" }), p = new m({ workerFactory: () => r });
|
|
9
|
+
let o;
|
|
10
|
+
const t = /* @__PURE__ */ new Set(), n = (e) => {
|
|
11
|
+
o = e, r.terminate();
|
|
12
|
+
for (const a of t) a.reject(e);
|
|
13
|
+
t.clear();
|
|
14
|
+
};
|
|
15
|
+
return r.addEventListener(
|
|
16
|
+
"error",
|
|
17
|
+
(e) => n(new Error(e.message || "Graph layout worker failed"))
|
|
18
|
+
), r.addEventListener(
|
|
19
|
+
"messageerror",
|
|
20
|
+
() => n(new Error("Invalid graph layout worker response"))
|
|
21
|
+
), {
|
|
22
|
+
layout(e, a) {
|
|
23
|
+
return o ? Promise.reject(o) : new Promise((l, s) => {
|
|
24
|
+
const i = { reject: s };
|
|
25
|
+
t.add(i), u(e, a ?? {}, p).then(l, s).finally(() => t.delete(i));
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
dispose() {
|
|
29
|
+
n(new Error("Graph layout engine disposed"));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
c as createGraphLayoutEngine
|
|
35
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GraphBounds, GraphLayout, GraphPoint, GraphViewport } from './types';
|
|
2
|
+
export declare function graphSectionPath(points: readonly GraphPoint[]): string;
|
|
3
|
+
export declare function fitGraphViewport(bounds: GraphBounds, size: {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}, padding?: number): GraphViewport;
|
|
7
|
+
/** Includes descendants so highlighting a group traces all of its direct routes. */
|
|
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;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function c(e) {
|
|
2
|
+
return e.map((n, h) => `${h ? "L" : "M"}${n.x},${n.y}`).join(" ");
|
|
3
|
+
}
|
|
4
|
+
function s(e, n, h = 48) {
|
|
5
|
+
const a = Math.max(
|
|
6
|
+
0.1,
|
|
7
|
+
Math.min(
|
|
8
|
+
1,
|
|
9
|
+
(n.width - h * 2) / Math.max(1, e.width),
|
|
10
|
+
(n.height - h * 2) / Math.max(1, e.height)
|
|
11
|
+
)
|
|
12
|
+
);
|
|
13
|
+
return {
|
|
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
|
+
};
|
|
18
|
+
}
|
|
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
|
+
};
|
|
27
|
+
return r(n), new Set(
|
|
28
|
+
e.edges.filter((t) => h.has(t.source) || h.has(t.target)).map((t) => t.id)
|
|
29
|
+
);
|
|
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
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
s as fitGraphViewport,
|
|
42
|
+
g as graphEdgeLabelPosition,
|
|
43
|
+
f as graphRelatedEdges,
|
|
44
|
+
c as graphSectionPath
|
|
45
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ELK } from 'elkjs/lib/elk-api';
|
|
2
|
+
import type { GraphInput, GraphLayout, GraphLayoutOptions } from './types';
|
|
3
|
+
export declare function validateGraphInput(input: GraphInput): void;
|
|
4
|
+
/** Normalizes geometry around the supplied ELK engine; browser layout runs in its worker. */
|
|
5
|
+
export declare function computeGraphLayout(input: GraphInput, options: GraphLayoutOptions, engine: Pick<ELK, 'layout'>): Promise<GraphLayout>;
|
|
@@ -0,0 +1,125 @@
|
|
|
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) {
|
|
8
|
+
if (p(e.id), ![e.width, e.height].every((r) => Number.isFinite(r) && r > 0))
|
|
9
|
+
throw new Error(`Invalid dimensions for graph node: ${e.id}`);
|
|
10
|
+
for (const r of e.ports ?? []) p(r.id);
|
|
11
|
+
const a = /* @__PURE__ */ new Set([e.id]);
|
|
12
|
+
let d = e.parentId;
|
|
13
|
+
for (; d !== void 0; ) {
|
|
14
|
+
const r = s.get(d);
|
|
15
|
+
if (!r || r.kind !== "group")
|
|
16
|
+
throw new Error(`Invalid parent for graph node: ${e.id}`);
|
|
17
|
+
if (a.has(d)) throw new Error(`Cyclic graph containment: ${e.id}`);
|
|
18
|
+
a.add(d), d = r.parentId;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const e of i.edges) {
|
|
22
|
+
p(e.id);
|
|
23
|
+
for (const [a, d] of [
|
|
24
|
+
[e.source, e.sourcePort],
|
|
25
|
+
[e.target, e.targetPort]
|
|
26
|
+
]) {
|
|
27
|
+
const r = s.get(a);
|
|
28
|
+
if (!r || d !== void 0 && !r.ports?.some((c) => c.id === d))
|
|
29
|
+
throw new Error(`Invalid endpoint for graph edge: ${e.id}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
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))
|
|
37
|
+
throw new Error("Graph layout spacing must be finite and nonnegative");
|
|
38
|
+
const e = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const t of i.nodes)
|
|
40
|
+
e.set(t.id, {
|
|
41
|
+
id: t.id,
|
|
42
|
+
width: t.width,
|
|
43
|
+
height: t.height,
|
|
44
|
+
...t.kind === "group" ? { children: [] } : {},
|
|
45
|
+
ports: t.ports?.map((o) => ({
|
|
46
|
+
id: o.id,
|
|
47
|
+
width: 1,
|
|
48
|
+
height: 1,
|
|
49
|
+
layoutOptions: { "elk.port.side": o.side }
|
|
50
|
+
})),
|
|
51
|
+
layoutOptions: {
|
|
52
|
+
"elk.portConstraints": "FIXED_ORDER",
|
|
53
|
+
"elk.padding": `[top=${s.top},left=${s.left},bottom=${s.bottom},right=${s.right}]`
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const a = [];
|
|
57
|
+
for (const t of i.nodes)
|
|
58
|
+
(t.parentId ? e.get(t.parentId).children : a).push(e.get(t.id));
|
|
59
|
+
const d = /* @__PURE__ */ new Set([
|
|
60
|
+
...i.nodes.flatMap((t) => [t.id, ...(t.ports ?? []).map((o) => o.id)]),
|
|
61
|
+
...i.edges.map((t) => t.id)
|
|
62
|
+
]);
|
|
63
|
+
let r = "$graph";
|
|
64
|
+
for (; d.has(r); ) r += "$";
|
|
65
|
+
const c = await p.layout({
|
|
66
|
+
id: r,
|
|
67
|
+
children: a,
|
|
68
|
+
edges: i.edges.map((t) => ({
|
|
69
|
+
id: t.id,
|
|
70
|
+
sources: [t.sourcePort ?? t.source],
|
|
71
|
+
targets: [t.targetPort ?? t.target]
|
|
72
|
+
})),
|
|
73
|
+
layoutOptions: {
|
|
74
|
+
"elk.algorithm": "layered",
|
|
75
|
+
"elk.direction": n.direction ?? "RIGHT",
|
|
76
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
77
|
+
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
|
78
|
+
"elk.spacing.nodeNode": String(n.spacing ?? 64),
|
|
79
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": String(n.spacing ?? 64),
|
|
80
|
+
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
81
|
+
"elk.randomSeed": "1"
|
|
82
|
+
}
|
|
83
|
+
}), f = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(), I = (t) => {
|
|
84
|
+
for (const o of t.edges ?? []) {
|
|
85
|
+
const h = o.container ?? t.id, l = f.get(h) ?? { x: 0, y: 0 };
|
|
86
|
+
x.set(
|
|
87
|
+
o.id,
|
|
88
|
+
(o.sections ?? []).map(
|
|
89
|
+
(g) => [g.startPoint, ...g.bendPoints ?? [], g.endPoint].map((w) => ({
|
|
90
|
+
x: w.x + l.x,
|
|
91
|
+
y: w.y + l.y
|
|
92
|
+
}))
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
for (const o of t.children ?? []) I(o);
|
|
97
|
+
}, E = (t, o, h) => {
|
|
98
|
+
const l = o + (t.x ?? 0), g = h + (t.y ?? 0);
|
|
99
|
+
f.set(t.id, { x: l, y: g, width: t.width ?? 0, height: t.height ?? 0 });
|
|
100
|
+
for (const w of t.children ?? []) E(w, l, g);
|
|
101
|
+
};
|
|
102
|
+
for (const t of c.children ?? []) E(t, 0, 0);
|
|
103
|
+
I(c);
|
|
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
|
+
);
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
O as computeGraphLayout,
|
|
124
|
+
$ as validateGraphInput
|
|
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
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { InfiniteCanvasPoint } from '../ui/InfiniteCanvas';
|
|
2
|
+
export interface GraphPoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
export interface GraphBounds extends GraphPoint {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
export type GraphViewport = InfiniteCanvasPoint;
|
|
11
|
+
/** Port IDs, node IDs, and edge IDs share one namespace. */
|
|
12
|
+
export interface GraphPort {
|
|
13
|
+
id: string;
|
|
14
|
+
side: 'NORTH' | 'EAST' | 'SOUTH' | 'WEST';
|
|
15
|
+
}
|
|
16
|
+
export interface GraphNode {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
kind?: 'node' | 'group';
|
|
20
|
+
parentId?: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
ports?: readonly GraphPort[];
|
|
24
|
+
}
|
|
25
|
+
export interface GraphEdge {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
source: string;
|
|
29
|
+
target: string;
|
|
30
|
+
sourcePort?: string;
|
|
31
|
+
targetPort?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface GraphInput {
|
|
34
|
+
nodes: readonly GraphNode[];
|
|
35
|
+
edges: readonly GraphEdge[];
|
|
36
|
+
}
|
|
37
|
+
export interface GraphLayoutNode extends GraphNode, GraphBounds {
|
|
38
|
+
}
|
|
39
|
+
export interface GraphLayoutEdge extends GraphEdge {
|
|
40
|
+
/** Separate sections preserve branches and self loops without inventing joins. */
|
|
41
|
+
sections: readonly (readonly GraphPoint[])[];
|
|
42
|
+
}
|
|
43
|
+
export interface GraphLayout {
|
|
44
|
+
nodes: readonly GraphLayoutNode[];
|
|
45
|
+
edges: readonly GraphLayoutEdge[];
|
|
46
|
+
bounds: GraphBounds;
|
|
47
|
+
}
|
|
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
|
+
})[];
|
|
53
|
+
direction?: 'RIGHT' | 'DOWN' | 'LEFT' | 'UP';
|
|
54
|
+
spacing?: number;
|
|
55
|
+
groupPadding?: {
|
|
56
|
+
top: number;
|
|
57
|
+
right: number;
|
|
58
|
+
bottom: number;
|
|
59
|
+
left: number;
|
|
60
|
+
};
|
|
61
|
+
/** Keep the object under inspection at its previous world position after layout. */
|
|
62
|
+
anchor?: {
|
|
63
|
+
nodeId: string;
|
|
64
|
+
position: GraphPoint;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export type GraphObjectRef = {
|
|
68
|
+
kind: 'node' | 'edge';
|
|
69
|
+
id: string;
|
|
70
|
+
};
|
|
71
|
+
/** Client coordinates go directly to SurfaceFloatingLayer; never rescale them. */
|
|
72
|
+
export interface GraphObjectEvent {
|
|
73
|
+
object: GraphObjectRef | null;
|
|
74
|
+
position: GraphPoint;
|
|
75
|
+
owner: Element;
|
|
76
|
+
}
|
|
@@ -14,7 +14,7 @@ export interface InfiniteCanvasContextMenuEvent {
|
|
|
14
14
|
worldY: number;
|
|
15
15
|
}
|
|
16
16
|
export interface InfiniteCanvasProps {
|
|
17
|
-
children: JSX.Element;
|
|
17
|
+
children: JSX.Element | ((viewport: Accessor<InfiniteCanvasPoint>) => JSX.Element);
|
|
18
18
|
overlay?: (viewport: Accessor<InfiniteCanvasPoint>) => JSX.Element;
|
|
19
19
|
viewport: InfiniteCanvasPoint;
|
|
20
20
|
onViewportChange?: (viewport: InfiniteCanvasPoint) => void;
|