@floegence/floe-webapp-core 0.82.1 → 0.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +178 -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 +8 -0
- package/dist/components/graph/geometry.js +35 -0
- package/dist/components/graph/layout.d.ts +5 -0
- package/dist/components/graph/layout.js +121 -0
- package/dist/components/graph/types.d.ts +72 -0
- package/dist/components/ui/InfiniteCanvas.d.ts +1 -1
- package/dist/components/ui/InfiniteCanvas.js +58 -55
- package/dist/graph.css +93 -0
- package/dist/graph.d.ts +4 -0
- package/dist/graph.js +10 -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,178 @@
|
|
|
1
|
+
import { insert as w, createComponent as y, setAttribute as c, effect as C, className as N, template as M, memo as Q, setStyleProperty as E, use as Z, delegateEvents as p } from "solid-js/web";
|
|
2
|
+
import { createUniqueId as ee, createSignal as V, createMemo as k, onMount as te, For as P, Show as ne, onCleanup as re } from "solid-js";
|
|
3
|
+
import { InfiniteCanvas as ae } from "../ui/InfiniteCanvas.js";
|
|
4
|
+
import { resolveSurfaceInteractionTargetRole as ie } from "../ui/localInteractionSurface.js";
|
|
5
|
+
import { graphRelatedEdges as B, graphSectionPath as oe } from "./geometry.js";
|
|
6
|
+
var le = /* @__PURE__ */ M("<div data-floe-canvas-pan-surface=true role=group tabindex=0>"), ce = /* @__PURE__ */ M("<div role=region tabindex=0>"), de = /* @__PURE__ */ M('<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>'), ue = /* @__PURE__ */ M("<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);
|
|
7
|
+
function me(i) {
|
|
8
|
+
const T = `graph-arrow-${ee()}`;
|
|
9
|
+
let u;
|
|
10
|
+
const [F, W] = V(null), [q, K] = V({
|
|
11
|
+
width: 0,
|
|
12
|
+
height: 0
|
|
13
|
+
}), A = () => i.highlight === void 0 ? F() : i.highlight, O = k(() => new Map(i.layout.nodes.map((e) => [e.id, e]))), U = k(() => {
|
|
14
|
+
const e = A();
|
|
15
|
+
return e?.kind === "node" ? B(i.layout, e.id) : new Set(e ? [e.id] : []);
|
|
16
|
+
}), R = k(() => {
|
|
17
|
+
const e = A();
|
|
18
|
+
if (e?.kind !== "node") return null;
|
|
19
|
+
const t = O().get(e.id), r = t?.kind === "group" ? t.id : t?.parentId;
|
|
20
|
+
return r ? B(i.layout, r) : null;
|
|
21
|
+
}), S = (e) => i.selected?.kind === e.kind && i.selected.id === e.id, J = (e) => {
|
|
22
|
+
const {
|
|
23
|
+
width: t,
|
|
24
|
+
height: r
|
|
25
|
+
} = q();
|
|
26
|
+
if (!t || !r) return i.layout.nodes;
|
|
27
|
+
const a = -e.x / e.scale - 240, n = -e.y / e.scale - 240, x = a + t / e.scale + 480, l = n + r / e.scale + 480;
|
|
28
|
+
return i.layout.nodes.filter((d) => d.id === i.selected?.id || d.x + d.width >= a && d.x <= x && d.y + d.height >= n && d.y <= l);
|
|
29
|
+
}, s = (e) => {
|
|
30
|
+
W(e), i.onHover?.(e);
|
|
31
|
+
}, m = (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
|
+
}, L = (e, t) => {
|
|
45
|
+
i.onContextMenu && (t.preventDefault(), t.stopPropagation(), i.onContextMenu(m(e, t.currentTarget, t)));
|
|
46
|
+
}, _ = (e, t) => {
|
|
47
|
+
t.target === t.currentTarget && (t.key === "ContextMenu" || t.shiftKey && t.key === "F10" ? (t.preventDefault(), t.stopPropagation(), i.onContextMenu?.(m(e, t.currentTarget))) : t.key === "Enter" ? (t.preventDefault(), t.stopPropagation(), i.onActivate?.(m(e, t.currentTarget))) : t.key === "Escape" && (s(null), i.onActivate?.(m(null, u))));
|
|
48
|
+
};
|
|
49
|
+
te(() => {
|
|
50
|
+
if (!u) return;
|
|
51
|
+
const e = () => K({
|
|
52
|
+
width: u.clientWidth,
|
|
53
|
+
height: u.clientHeight
|
|
54
|
+
});
|
|
55
|
+
e();
|
|
56
|
+
const t = new ResizeObserver(e);
|
|
57
|
+
t.observe(u), re(() => t.disconnect());
|
|
58
|
+
});
|
|
59
|
+
const j = (e) => {
|
|
60
|
+
const t = {
|
|
61
|
+
kind: "node",
|
|
62
|
+
id: e.id
|
|
63
|
+
}, r = {
|
|
64
|
+
selected: () => S(t),
|
|
65
|
+
openMenu: (a) => L(t, a)
|
|
66
|
+
};
|
|
67
|
+
return (() => {
|
|
68
|
+
var a = le();
|
|
69
|
+
return a.$$keydown = (n) => _(t, n), a.$$contextmenu = (n) => L(t, n), a.$$click = (n) => {
|
|
70
|
+
n.target.closest("button, a, input, select, textarea, [data-floe-canvas-interactive]") || (n.stopPropagation(), i.onActivate?.(m(t, n.currentTarget, n)));
|
|
71
|
+
}, a.$$focusout = () => s(null), a.$$focusin = () => s(t), a.addEventListener("pointerleave", () => s(null)), a.addEventListener("pointerenter", () => s(t)), w(a, (() => {
|
|
72
|
+
var n = Q(() => !!(e.kind === "group" && i.renderGroup));
|
|
73
|
+
return () => n() ? i.renderGroup(e, r) : i.renderNode(e, r);
|
|
74
|
+
})()), C((n) => {
|
|
75
|
+
var x = e.kind === "group" ? "floe-graph__group" : "floe-graph__node", l = e.id, d = S(t), $ = `${e.x}px`, h = `${e.y}px`, g = `${e.width}px`, b = `${e.height}px`, v = e.label;
|
|
76
|
+
return x !== n.e && N(a, n.e = x), l !== n.t && c(a, "data-graph-object", n.t = l), d !== n.a && c(a, "data-selected", n.a = d), $ !== n.o && E(a, "left", n.o = $), h !== n.i && E(a, "top", n.i = h), g !== n.n && E(a, "width", n.n = g), b !== n.s && E(a, "height", n.s = b), v !== n.h && c(a, "aria-label", n.h = v), 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 = ce();
|
|
91
|
+
e.$$keydown = (r) => _(null, r);
|
|
92
|
+
var t = u;
|
|
93
|
+
return typeof t == "function" ? Z(t, e) : u = e, w(e, y(ae, {
|
|
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" : ie(r),
|
|
107
|
+
onCanvasPointerDown: (r) => {
|
|
108
|
+
!(r.target instanceof Element) || r.target.closest("[data-graph-object]") || i.onActivate?.(m(null, u, r));
|
|
109
|
+
},
|
|
110
|
+
onCanvasContextMenu: (r) => i.onContextMenu?.({
|
|
111
|
+
object: null,
|
|
112
|
+
owner: u,
|
|
113
|
+
position: {
|
|
114
|
+
x: r.clientX,
|
|
115
|
+
y: r.clientY
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
118
|
+
children: (r) => {
|
|
119
|
+
const a = k(() => J(r())), n = k(() => a().filter((l) => l.kind === "group")), x = k(() => a().filter((l) => l.kind !== "group"));
|
|
120
|
+
return [y(P, {
|
|
121
|
+
get each() {
|
|
122
|
+
return n();
|
|
123
|
+
},
|
|
124
|
+
children: j
|
|
125
|
+
}), (() => {
|
|
126
|
+
var l = de(), d = l.firstChild, $ = d.firstChild;
|
|
127
|
+
return c($, "id", T), w(l, y(P, {
|
|
128
|
+
get each() {
|
|
129
|
+
return i.layout.edges;
|
|
130
|
+
},
|
|
131
|
+
children: (h) => {
|
|
132
|
+
const g = {
|
|
133
|
+
kind: "edge",
|
|
134
|
+
id: h.id
|
|
135
|
+
}, b = () => h.sections.map(oe).join(" ");
|
|
136
|
+
return y(ne, {
|
|
137
|
+
get when() {
|
|
138
|
+
return !R() || R().has(h.id);
|
|
139
|
+
},
|
|
140
|
+
get children() {
|
|
141
|
+
var v = ue(), I = v.firstChild, f = I.nextSibling;
|
|
142
|
+
return c(I, "marker-end", `url(#${T})`), f.$$keydown = (o) => _(g, o), f.$$contextmenu = (o) => L(g, o), f.$$click = (o) => {
|
|
143
|
+
o.stopPropagation(), i.onActivate?.(m(g, o.currentTarget, o));
|
|
144
|
+
}, f.addEventListener("blur", () => s(null)), f.addEventListener("focus", () => s(g)), f.addEventListener("pointerleave", () => s(null)), f.addEventListener("pointerenter", () => s(g)), C((o) => {
|
|
145
|
+
var z = h.id, D = U().has(h.id), H = S(g), X = b(), Y = b(), G = h.label;
|
|
146
|
+
return z !== o.e && c(v, "data-graph-object", o.e = z), D !== o.t && c(v, "data-emphasized", o.t = D), H !== o.a && c(v, "data-selected", o.a = H), X !== o.o && c(I, "d", o.o = X), Y !== o.i && c(f, "d", o.i = Y), G !== o.n && c(f, "aria-label", o.n = G), o;
|
|
147
|
+
}, {
|
|
148
|
+
e: void 0,
|
|
149
|
+
t: void 0,
|
|
150
|
+
a: void 0,
|
|
151
|
+
o: void 0,
|
|
152
|
+
i: void 0,
|
|
153
|
+
n: void 0
|
|
154
|
+
}), v;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}), null), C(() => c(l, "aria-label", i.ariaLabel)), l;
|
|
159
|
+
})(), y(P, {
|
|
160
|
+
get each() {
|
|
161
|
+
return x();
|
|
162
|
+
},
|
|
163
|
+
children: j
|
|
164
|
+
})];
|
|
165
|
+
}
|
|
166
|
+
}), null), w(e, () => i.overlay, null), C((r) => {
|
|
167
|
+
var a = `floe-graph ${i.class ?? ""}`, n = i.ariaLabel;
|
|
168
|
+
return a !== r.e && N(e, r.e = a), n !== r.t && c(e, "aria-label", r.t = n), r;
|
|
169
|
+
}, {
|
|
170
|
+
e: void 0,
|
|
171
|
+
t: void 0
|
|
172
|
+
}), e;
|
|
173
|
+
})();
|
|
174
|
+
}
|
|
175
|
+
p(["focusin", "focusout", "click", "contextmenu", "keydown"]);
|
|
176
|
+
export {
|
|
177
|
+
me as GraphCanvas
|
|
178
|
+
};
|
|
@@ -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,8 @@
|
|
|
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>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function c(t) {
|
|
2
|
+
return t.map((h, a) => `${a ? "L" : "M"}${h.x},${h.y}`).join(" ");
|
|
3
|
+
}
|
|
4
|
+
function o(t, h, a = 48) {
|
|
5
|
+
const i = Math.max(
|
|
6
|
+
0.1,
|
|
7
|
+
Math.min(
|
|
8
|
+
1,
|
|
9
|
+
(h.width - a * 2) / Math.max(1, t.width),
|
|
10
|
+
(h.height - a * 2) / Math.max(1, t.height)
|
|
11
|
+
)
|
|
12
|
+
);
|
|
13
|
+
return {
|
|
14
|
+
x: (h.width - t.width * i) / 2 - t.x * i,
|
|
15
|
+
y: (h.height - t.height * i) / 2 - t.y * i,
|
|
16
|
+
scale: i
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function f(t, h) {
|
|
20
|
+
const a = /* @__PURE__ */ new Set([h]), i = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const e of t.nodes)
|
|
22
|
+
e.parentId && i.set(e.parentId, [...i.get(e.parentId) ?? [], e.id]);
|
|
23
|
+
const n = (e) => {
|
|
24
|
+
for (const r of i.get(e) ?? [])
|
|
25
|
+
a.has(r) || (a.add(r), n(r));
|
|
26
|
+
};
|
|
27
|
+
return n(h), new Set(
|
|
28
|
+
t.edges.filter((e) => a.has(e.source) || a.has(e.target)).map((e) => e.id)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
o as fitGraphViewport,
|
|
33
|
+
f as graphRelatedEdges,
|
|
34
|
+
c as graphSectionPath
|
|
35
|
+
};
|
|
@@ -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,121 @@
|
|
|
1
|
+
function k(n) {
|
|
2
|
+
const i = /* @__PURE__ */ new Set(), p = (e) => {
|
|
3
|
+
if (!e || i.has(e)) throw new Error(`Duplicate or empty graph ID: ${e}`);
|
|
4
|
+
i.add(e);
|
|
5
|
+
}, s = new Map(n.nodes.map((e) => [e.id, e]));
|
|
6
|
+
for (const e of n.nodes) {
|
|
7
|
+
if (p(e.id), ![e.width, e.height].every((r) => Number.isFinite(r) && r > 0))
|
|
8
|
+
throw new Error(`Invalid dimensions for graph node: ${e.id}`);
|
|
9
|
+
for (const r of e.ports ?? []) p(r.id);
|
|
10
|
+
const a = /* @__PURE__ */ new Set([e.id]);
|
|
11
|
+
let d = e.parentId;
|
|
12
|
+
for (; d !== void 0; ) {
|
|
13
|
+
const r = s.get(d);
|
|
14
|
+
if (!r || r.kind !== "group")
|
|
15
|
+
throw new Error(`Invalid parent for graph node: ${e.id}`);
|
|
16
|
+
if (a.has(d)) throw new Error(`Cyclic graph containment: ${e.id}`);
|
|
17
|
+
a.add(d), d = r.parentId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const e of n.edges) {
|
|
21
|
+
p(e.id);
|
|
22
|
+
for (const [a, d] of [
|
|
23
|
+
[e.source, e.sourcePort],
|
|
24
|
+
[e.target, e.targetPort]
|
|
25
|
+
]) {
|
|
26
|
+
const r = s.get(a);
|
|
27
|
+
if (!r || d !== void 0 && !r.ports?.some((c) => c.id === d))
|
|
28
|
+
throw new Error(`Invalid endpoint for graph edge: ${e.id}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function N(n, i, p) {
|
|
33
|
+
k(n);
|
|
34
|
+
const s = i.groupPadding ?? { top: 64, right: 28, bottom: 28, left: 28 };
|
|
35
|
+
if (![...Object.values(s), i.spacing ?? 64].every((t) => Number.isFinite(t) && t >= 0))
|
|
36
|
+
throw new Error("Graph layout spacing must be finite and nonnegative");
|
|
37
|
+
const e = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const t of n.nodes)
|
|
39
|
+
e.set(t.id, {
|
|
40
|
+
id: t.id,
|
|
41
|
+
width: t.width,
|
|
42
|
+
height: t.height,
|
|
43
|
+
...t.kind === "group" ? { children: [] } : {},
|
|
44
|
+
ports: t.ports?.map((o) => ({
|
|
45
|
+
id: o.id,
|
|
46
|
+
width: 1,
|
|
47
|
+
height: 1,
|
|
48
|
+
layoutOptions: { "elk.port.side": o.side }
|
|
49
|
+
})),
|
|
50
|
+
layoutOptions: {
|
|
51
|
+
"elk.portConstraints": "FIXED_ORDER",
|
|
52
|
+
"elk.padding": `[top=${s.top},left=${s.left},bottom=${s.bottom},right=${s.right}]`
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const a = [];
|
|
56
|
+
for (const t of n.nodes)
|
|
57
|
+
(t.parentId ? e.get(t.parentId).children : a).push(e.get(t.id));
|
|
58
|
+
const d = /* @__PURE__ */ new Set([
|
|
59
|
+
...n.nodes.flatMap((t) => [t.id, ...(t.ports ?? []).map((o) => o.id)]),
|
|
60
|
+
...n.edges.map((t) => t.id)
|
|
61
|
+
]);
|
|
62
|
+
let r = "$graph";
|
|
63
|
+
for (; d.has(r); ) r += "$";
|
|
64
|
+
const c = await p.layout({
|
|
65
|
+
id: r,
|
|
66
|
+
children: a,
|
|
67
|
+
edges: n.edges.map((t) => ({
|
|
68
|
+
id: t.id,
|
|
69
|
+
sources: [t.sourcePort ?? t.source],
|
|
70
|
+
targets: [t.targetPort ?? t.target]
|
|
71
|
+
})),
|
|
72
|
+
layoutOptions: {
|
|
73
|
+
"elk.algorithm": "layered",
|
|
74
|
+
"elk.direction": i.direction ?? "RIGHT",
|
|
75
|
+
"elk.edgeRouting": "ORTHOGONAL",
|
|
76
|
+
"elk.hierarchyHandling": "INCLUDE_CHILDREN",
|
|
77
|
+
"elk.spacing.nodeNode": String(i.spacing ?? 64),
|
|
78
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": String(i.spacing ?? 64),
|
|
79
|
+
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
80
|
+
"elk.randomSeed": "1"
|
|
81
|
+
}
|
|
82
|
+
}), f = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(), I = (t) => {
|
|
83
|
+
for (const o of t.edges ?? []) {
|
|
84
|
+
const h = o.container ?? t.id, l = f.get(h) ?? { x: 0, y: 0 };
|
|
85
|
+
x.set(
|
|
86
|
+
o.id,
|
|
87
|
+
(o.sections ?? []).map(
|
|
88
|
+
(g) => [g.startPoint, ...g.bendPoints ?? [], g.endPoint].map((w) => ({
|
|
89
|
+
x: w.x + l.x,
|
|
90
|
+
y: w.y + l.y
|
|
91
|
+
}))
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
for (const o of t.children ?? []) I(o);
|
|
96
|
+
}, E = (t, o, h) => {
|
|
97
|
+
const l = o + (t.x ?? 0), g = h + (t.y ?? 0);
|
|
98
|
+
f.set(t.id, { x: l, y: g, width: t.width ?? 0, height: t.height ?? 0 });
|
|
99
|
+
for (const w of t.children ?? []) E(w, l, g);
|
|
100
|
+
};
|
|
101
|
+
for (const t of c.children ?? []) E(t, 0, 0);
|
|
102
|
+
I(c);
|
|
103
|
+
const y = i.anchor && f.get(i.anchor.nodeId), u = y ? i.anchor.position.x - y.x : 0, m = y ? i.anchor.position.y - y.y : 0;
|
|
104
|
+
return {
|
|
105
|
+
nodes: n.nodes.map((t) => {
|
|
106
|
+
const o = f.get(t.id);
|
|
107
|
+
return { ...t, ...o, x: o.x + u, y: o.y + m };
|
|
108
|
+
}),
|
|
109
|
+
edges: n.edges.map((t) => ({
|
|
110
|
+
...t,
|
|
111
|
+
sections: (x.get(t.id) ?? []).map(
|
|
112
|
+
(o) => o.map((h) => ({ x: h.x + u, y: h.y + m }))
|
|
113
|
+
)
|
|
114
|
+
})),
|
|
115
|
+
bounds: { x: u, y: m, width: c.width ?? 0, height: c.height ?? 0 }
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
N as computeGraphLayout,
|
|
120
|
+
k as validateGraphInput
|
|
121
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
direction?: 'RIGHT' | 'DOWN' | 'LEFT' | 'UP';
|
|
50
|
+
spacing?: number;
|
|
51
|
+
groupPadding?: {
|
|
52
|
+
top: number;
|
|
53
|
+
right: number;
|
|
54
|
+
bottom: number;
|
|
55
|
+
left: number;
|
|
56
|
+
};
|
|
57
|
+
/** Keep the object under inspection at its previous world position after layout. */
|
|
58
|
+
anchor?: {
|
|
59
|
+
nodeId: string;
|
|
60
|
+
position: GraphPoint;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export type GraphObjectRef = {
|
|
64
|
+
kind: 'node' | 'edge';
|
|
65
|
+
id: string;
|
|
66
|
+
};
|
|
67
|
+
/** Client coordinates go directly to SurfaceFloatingLayer; never rescale them. */
|
|
68
|
+
export interface GraphObjectEvent {
|
|
69
|
+
object: GraphObjectRef | null;
|
|
70
|
+
position: GraphPoint;
|
|
71
|
+
owner: Element;
|
|
72
|
+
}
|
|
@@ -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;
|