@weasel-js/diagram 1.11.0 → 1.11.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/index.js CHANGED
@@ -1,4 +1,1058 @@
1
- export { ANCHOR_DERIVE_POSE, CONNECT_ACTION_ID, DEFAULT_EDGE_STROKE, DIAGRAM_ANCHOR, DIAGRAM_EDGE, DIAGRAM_GROUP, DIAGRAM_LABEL, DiagramView, EASED_LAYOUTS, EDGE_DERIVE_PATH, GRAB_PORT_ACTION_ID, GROUP_DERIVE_POSE, LABEL_DERIVE_POSE, LAYOUTS, LAYOUT_ACTION_ID, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, ROUTERS, anchorDerivePose, applyLayout, bezier, boxAround, buildGraph, commitEdgeToScene, connectBinding, createConnectAction, createDiagramContribution, createLayoutAction, createPortAffordance, defaultCanConnect, diagramAnchorOf, diagramEdgeOf, diagramGroupOf, diagramLabelOf, diagramLayoutOf, diagramMarkOf, diagramPorts, diagramScene, diagramShape, easedProducer, edgeDerivePath, edgeIdOf, forceProducer, grabPortAction, grabPortBindings, groupDerivePose, labelDerivePose, layoutOwnsPort, layoutOwnsRoute, layoutPoses, markShape, orthogonal, portLayer, portScratchOf, registerDiagramShape, registerMarkShape, resolveEnd, routeWrites, routesOf, sceneParticipants, selfLoop, straight, useLiveLayout, useMirroredSelection, withDiagramRegistry } from './chunk-C4NAI6NV.js';
2
- export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, PORT_LABEL_GAP, anchorOffset, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, chain, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, expandGroupEdges, footprintNode, force, forceRelaxation, forestOf, graphOrder, insetOf, isPinned, layered, layeredPlan, layeredRoutes, layoutBody, layoutRowPorts, marginOf, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, planOf, plannerPlan, portLabelBox, portLabelsOf, portOf, portSitesOf, portsOf, portsOnOutline, rankGaps, ranksByLength, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree, untilSettled } from './chunk-NUMMW5BI.js';
1
+ import './chunk-WDIE35AK.js';
2
+ import { createConnectAction, avoidSceneInput, sceneEdges, inOrUnder, withHopsData, edgePolylines, themeWrites, hopDataOps, diagramDropParentAt, containerBuild } from './chunk-CI3I3I6T.js';
3
+ export { ADD_LANE_ACTION_ID, CONNECT_ACTION_ID, CONTAINER_STENCILS, DEFAULT_EDGE_STROKE, DiagramView, EASED_LAYOUTS, EDGE_END_AFFORDANCE_KIND, EDGE_END_LAYER_ID, EDIT_CONTAINER_TITLE_ACTION_ID, GRAB_EDGE_END_ACTION_ID, GRAB_LABEL_ACTION_ID, GRAB_PORT_ACTION_ID, GRAB_WAYPOINT_ACTION_ID, LANE_AFFORDANCE_KIND, LANE_LAYER_ID, LANE_RESIZE_ACTION_ID, LAYOUTS, LAYOUT_ACTION_ID, MOVE_LABEL_ACTION_ID, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, RECONNECT_ACTION_ID, RELEASE_MEMBERS_ACTION_ID, REMOVE_WAYPOINT_ACTION_ID, REORDER_ACTION_ID, RESET_LABEL_ACTION_ID, RESET_ROUTE_ACTION_ID, ROUTER_LABELS, ROUTE_EDIT_ACTION_ID, SELECT_LABEL_EDGE_ACTION_ID, SET_HOPS_ACTION_ID, SET_ROUTER_ACTION_IDS, STEP_REORDER_ACTION_ID, WAYPOINT_AFFORDANCE_KIND, WAYPOINT_HIT_PX, WAYPOINT_LAYER_ID, addLane, applyLayout, avoidSceneInput, bodyAt, commitEdgeToScene, commitLabelToScene, commitLaneResizeToScene, commitReconnectToScene, commitRouteToScene, commitRouterToScene, connectBinding, containerBuild, createAddLaneAction, createConnectAction, createDiagramContribution, createEdgeEndAffordance, createLabelDragAction, createLaneAffordance, createLaneResizeAction, createLayoutAction, createPortAffordance, createReconnectAction, createReleaseMembersAction, createRemoveWaypointAction, createReorderAction, createResetLabelAction, createResetRouteAction, createRouteEditAction, createSetHopsAction, createSetRouterAction, createSpecHops, createSpecLabelPlacer, createSpecRouter, createStepReorderAction, createWaypointAffordance, defaultCanConnect, diagramCanDropInto, diagramContainers, diagramDropParentAt, diagramEdgeEnds, diagramLabelDrag, diagramLayoutOf, diagramPorts, diagramResizable, diagramShape, diagramWaypoints, dragSegment, dragVertex, easedProducer, edgeEndLayer, edgeEndScratchOf, editContainerTitleAction, editContainerTitleBinding, forceProducer, grabPortAction, grabPortBindings, groupReorderContribution, groupReorderSlots, headerIdOf, insertWaypoint, isBodyEnd, laneLayer, laneResizeFrame, laneScratchOf, lanesOf, layoutPoses, movedLabelData, pinSegment, poolAxisOf, poolsOf, portLayer, portScratchOf, reconnectBinding, regionHeaderAt, registerDiagramShape, releaseMembers, removeSegment, removeWaypoint, reorderBinding, reorderContribution, routeHandles, routeStateOf, routeWrites, routedData, routerActions, routesOf, sceneParticipants, segmentAxis, sequenceSlots, themeWrites, unpinSegment, useLiveLayout, useMirroredSelection, waypointLayer, waypointScratchOf, withRouter } from './chunk-CI3I3I6T.js';
4
+ import { createAvoidRouter, HOPS_DOC_KEY, edgeHops, placeLabels } from './chunk-T6DQT553.js';
5
+ export { AVOID_DEFAULTS, DEFAULT_HOP_SIZE, HOPS_DOC_KEY, createAvoidRouter, edgeHops, placeLabels, simplifyStops } from './chunk-T6DQT553.js';
6
+ import { NODE, NODE_STROKE_WIDTH, rowTextStyle, DEFAULT_FONT, bodyLayoutOf, offPageTagLook, diagramThemeOf, offPageSplits, offPageTagOf, DIAGRAM_THEMES, DIAGRAM_THEME_KEY, themeDocValue, DIAGRAM_ROLES, diagramLabelOf, diagramContainerOf, diagramGroupOf, offPageTagId, offPageFarText, offPageTagSpec, offPageTagData, offPageTagSize, relayoutBodies, labelSize, labelStyleOf } from './chunk-43LDZ6L4.js';
7
+ export { ANCHOR_DERIVE_POSE, CONTAINER_HEADER, CONTAINER_LAYOUTS, DEFAULT_LANE_SIZE, DEFAULT_POOL_ACROSS, DIAGRAM_ANCHOR, DIAGRAM_CONTAINER_LAYOUT, DIAGRAM_EDGE, DIAGRAM_GROUP, DIAGRAM_LABEL, DIAGRAM_LANE_COLUMNS_LAYOUT, DIAGRAM_LANE_ROWS_LAYOUT, DIAGRAM_OFF_PAGE_TAG, DIAGRAM_POOL_COLUMNS_LAYOUT, DIAGRAM_POOL_ROWS_LAYOUT, DIAGRAM_ROLES, DIAGRAM_THEMES, DIAGRAM_THEME_KEY, EDGE_DERIVE_PATH, EDGE_STROKE_WIDTH, EDIT_EDGE_LABEL_ACTION_ID, GROUP_DERIVE_POSE, LABEL_DERIVE_POSE, LABEL_DRAG_AFFORDANCE_KIND, LABEL_DRAG_LAYER_ID, LABEL_PLACEMENT, LANE_HEADER_SIZE, LANE_STROKE_WIDTH, MIN_LANE_SIZE, NODE_STROKE_WIDTH, OFF_PAGE_STUB, OFF_PAGE_TAG_DERIVE_POSE, OFF_PAGE_TAG_PADDING, POOL_HEADER_SIZE, ROUTERS, anchorDerivePose, avoid, bezier, bodyBox, boxAround, buildGraph, containerShape, dataEdgeStyle, dataNodeStyle, dataOffPage, dataRegionStyle, diagramAnchorOf, diagramContainerLayout, diagramContainerOf, diagramGroupOf, diagramLabelOf, diagramMarkOf, diagramPages, diagramScene, diagramThemeFrom, diagramThemeOf, draggableLabels, dropEmptyEdgeLabel, edgeDerivePath, edgeIdOf, edgeLabelSpec, edgeRouter, editEdgeLabelAction, editEdgeLabelBinding, firstLanes, groupDerivePose, headerBox, labelDerivePose, labelDragLayer, labelScratchOf, labelSize, labelUnder, laneHeader, laneStackLayout, markShape, nextOffPageRef, offPageFarText, offPageSplits, offPageTagData, offPageTagDerivePose, offPageTagId, offPageTagLook, offPageTagOf, offPageTagSize, offPageTagSpec, offPageTagSpecs, offPageTagText, orthogonal, poolHeader, regionSpecs, registerMarkShape, relayoutBodies, resolveEnd, roleEdgeStyle, roleStyle, rowTextStyle, selfLoop, stackedLanes, straight, themeDocValue, themeEdgeStroke, themeFont, themeLabelFont, themeLabelStyle, titleIdOf, titlePose, withDiagramRegistry, withHops } from './chunk-43LDZ6L4.js';
8
+ import { buildBody, diagramEdgeOf, samePoints, diagramNodeOf, labelCenter } from './chunk-IM6DHWZO.js';
9
+ export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, PORT_LABEL_GAP, anchorOffset, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, chain, contentBox, createDiagramNodes, dataKeyReader, diagramEdgeOf, diagramNodeOf, expandGroupEdges, footprintNode, force, forceRelaxation, forestOf, graphOrder, insetOf, isPinned, labelCenter, layered, layeredPlan, layeredRoutes, layoutBody, layoutOwnsPort, layoutOwnsRoute, layoutRowPorts, layoutWritesRoute, marginOf, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, planOf, plannerPlan, portLabelBox, portLabelsOf, portOf, portSitesOf, portsOf, portsOnOutline, rankGaps, ranksByLength, ranksOf, rayHit, routeHolds, seededOrder, settle, sizeToBody, subpathOf, translated, tree, untilSettled } from './chunk-IM6DHWZO.js';
10
+ import { useMemo, useRef, useEffect } from 'react';
11
+ import { bodyPredicate, AUTO_POSE_DESCRIPTOR, defineTool, createInsertOp, INSERT_REPOSITION_BINDING, useDepRegistry, useLatest, useDepSource, commitOps, createSetDataOp, useOptionalDepRegistry, useTransformFollower, documentPose, placedPagesOf, PAGES_DOC_KEY, ActionDisabledReason, measureTextBounds, writtenBy, documentDepOf, createDeleteOp, createTransformOp, fitViewToPage, parseColor } from '@weasel-js/core';
12
+ import { PATH_M } from '@weasel-js/core/math';
13
+
14
+ var CONNECTOR_TOOL_ID = "diagram.connector";
15
+ var CONNECTOR_ACTION_ID = "diagram.connectorDraw";
16
+ function connectorTool(opts) {
17
+ return defineTool({
18
+ id: CONNECTOR_TOOL_ID,
19
+ hookName: "useConnectorTool",
20
+ cursor: "crosshair",
21
+ presentation: { label: "Connector", icon: (
22
+ /* @icon */
23
+ "core:arrow"
24
+ ), group: "draw" },
25
+ actions: [createConnectAction({ ...opts, id: CONNECTOR_ACTION_ID, startFrom: "body", bodies: true })],
26
+ bindings: [{ spec: { kind: "drag" }, actionId: CONNECTOR_ACTION_ID }]
27
+ });
28
+ }
29
+ function useConnectorTool(opts) {
30
+ const { participants, read: read2, geometry, canConnect, showsPort, snapRadius, router, routers, commit, edgeStroke } = opts;
31
+ return useMemo(
32
+ () => connectorTool(opts),
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `opts` is the fields listed
34
+ [participants, read2, geometry, canConnect, showsPort, snapRadius, router, routers, commit, edgeStroke]
35
+ );
36
+ }
37
+ var FLOWCHART_STENCILS = [
38
+ { id: "process", label: "Process", outline: "rect", size: { width: 120, height: 60 }, icon: (
39
+ /* @icon */
40
+ "shape:rectangle"
41
+ ) },
42
+ { id: "decision", label: "Decision", outline: "diamond", size: { width: 120, height: 80 }, icon: (
43
+ /* @icon */
44
+ "shape:diamond"
45
+ ) },
46
+ { id: "terminal", label: "Terminal", outline: "stadium", size: { width: 120, height: 50 }, icon: (
47
+ /* @icon */
48
+ "shape:stadium"
49
+ ) },
50
+ { id: "data", label: "Data", outline: "parallelogram", size: { width: 130, height: 60 }, icon: (
51
+ /* @icon */
52
+ "shape:parallelogram"
53
+ ) },
54
+ { id: "manual", label: "Manual", outline: "trapezoid", size: { width: 130, height: 60 }, icon: (
55
+ /* @icon */
56
+ "shape:trapezoidSide"
57
+ ) },
58
+ { id: "preparation", label: "Preparation", outline: "hexagon", size: { width: 130, height: 60 }, icon: (
59
+ /* @icon */
60
+ "shape:hexagonFlat"
61
+ ) },
62
+ { id: "connector", label: "Connector", outline: "ellipse", size: { width: 50, height: 50 }, icon: (
63
+ /* @icon */
64
+ "shape:circle"
65
+ ) }
66
+ ];
67
+ var stencilKind = (id) => `stencil:${id}`;
68
+ function stencilIdOf(kind) {
69
+ return kind.startsWith("stencil:") ? kind.slice("stencil:".length) : null;
70
+ }
71
+ var DEFAULT_ROWS = [{ kind: "label", text: "" }];
72
+ function placedBounds(stencil, b) {
73
+ if (b.width > 0 && b.height > 0) return b;
74
+ const { width, height } = stencil.size;
75
+ return { x: b.x + b.width / 2 - width / 2, y: b.y + b.height / 2 - height / 2, width, height };
76
+ }
77
+ function stencilBuild(stencil, bounds, opts) {
78
+ const id = opts.newId();
79
+ const style = opts.style ?? NODE;
80
+ const at = placedBounds(stencil, bounds);
81
+ const spec = { outline: stencil.outline, rows: stencil.rows ?? DEFAULT_ROWS };
82
+ const built = buildBody(
83
+ spec,
84
+ at,
85
+ {
86
+ id,
87
+ layer: opts.layer,
88
+ ...opts.measure ? { measure: opts.measure } : {},
89
+ body: (trait) => ({
90
+ diagram: { ...trait, ...bodyLayoutOf(spec, trait) },
91
+ fill: { paint: { color: style.fill } },
92
+ stroke: { paint: { color: style.stroke }, width: style.strokeWidth ?? NODE_STROKE_WIDTH, ...style.dash ? { dash: style.dash } : {} }
93
+ }),
94
+ row: (text, box) => box.row.kind === "label" || box.row.kind === "field" ? {
95
+ text,
96
+ style: rowTextStyle(box.row, opts.font ?? DEFAULT_FONT),
97
+ fill: { paint: { color: style.text } },
98
+ bodyRow: box.index
99
+ } : null
100
+ }
101
+ );
102
+ const ops = built.specs.map((spec2, k) => createInsertOp({
103
+ node: { ...spec2, id: spec2.id ?? `${id}/${k}`, parent: spec2.parent ?? opts.parent ?? null },
104
+ label: `Insert ${stencil.label}`
105
+ }));
106
+ return { id, ops };
107
+ }
108
+ function stencilTool(stencil) {
109
+ return defineTool({
110
+ id: stencilKind(stencil.id),
111
+ hookName: "useStencilTool",
112
+ capabilities: ["creates-shapes"],
113
+ cursor: "crosshair",
114
+ presentation: { label: stencil.label, icon: stencil.icon, group: "stencil" },
115
+ bindings: [
116
+ { spec: { kind: "drag" }, actionId: "insert", opts: { params: { kind: stencilKind(stencil.id) } } },
117
+ INSERT_REPOSITION_BINDING
118
+ ]
119
+ });
120
+ }
121
+ function useStencilTool(stencil) {
122
+ return useMemo(() => stencilTool(stencil), [stencil]);
123
+ }
124
+ function useStencilInsert(stencils, opts = {}) {
125
+ const registry = useDepRegistry();
126
+ const stencilsRef = useLatest(stencils);
127
+ const optsRef = useLatest(opts);
128
+ useDepSource("insert", (below) => {
129
+ const scene = registry.get("scene");
130
+ const fallback = below();
131
+ const layer = () => optsRef.current.layer ?? fallback?.layer?.() ?? scene?.layers[0]?.id ?? "default";
132
+ const stencilOf = (kind) => {
133
+ const id = stencilIdOf(kind);
134
+ if (id === null) return void 0;
135
+ return stencilsRef.current.find((s) => s.id === id) ?? optsRef.current.containers?.find((s) => s.id === id);
136
+ };
137
+ const builtAt = (stencil, bounds) => {
138
+ if (scene === void 0) return null;
139
+ const center = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
140
+ const role = "role" in stencil ? stencil.role : null;
141
+ const { style, measure, font, containerStyle, laneStyles } = optsRef.current;
142
+ const common = {
143
+ layer: layer(),
144
+ newId: () => scene.mintId(),
145
+ parent: diagramDropParentAt(scene, center, role)
146
+ };
147
+ if ("role" in stencil) {
148
+ const boxStyle = containerStyle ?? style;
149
+ return containerBuild(stencil, bounds, {
150
+ ...common,
151
+ ...boxStyle ? { style: boxStyle } : {},
152
+ ...laneStyles ? { laneStyles } : {}
153
+ });
154
+ }
155
+ return stencilBuild(stencil, bounds, {
156
+ ...common,
157
+ ...style ? { style } : {},
158
+ ...measure ? { measure } : {},
159
+ ...font ? { font } : {}
160
+ });
161
+ };
162
+ const build = (bounds, extras, ends) => {
163
+ const stencil = stencilOf(extras.kind);
164
+ if (stencil === void 0) return fallback?.build?.(bounds, extras, ends) ?? null;
165
+ const built = builtAt(stencil, bounds);
166
+ return built === null ? null : { id: built.id, ops: built.ops };
167
+ };
168
+ return {
169
+ layer,
170
+ build,
171
+ commit(bounds, extras, ends) {
172
+ const stencil = stencilOf(extras.kind);
173
+ if (stencil === void 0) return fallback?.commit(bounds, extras, ends) ?? null;
174
+ const built = build(bounds, extras, ends);
175
+ if (built === null) return null;
176
+ const deps = { scene, applyOps: registry.get("applyOps"), selection: registry.get("selection") };
177
+ commitOps(deps, built.ops, `Insert ${stencil.label}`);
178
+ return built.id;
179
+ }
180
+ };
181
+ });
182
+ }
183
+ var EPS = 1e-6;
184
+ function offsetOf(geometry, before, after) {
185
+ const a = geometry.getBounds(before);
186
+ const b = geometry.getBounds(after);
187
+ return { x: b.x - a.x, y: b.y - a.y };
188
+ }
189
+ function carried(edge, d) {
190
+ const move = (points) => points.map((p) => ({ x: p.x + d.x, y: p.y + d.y }));
191
+ const waypoints = move(edge.waypoints ?? []);
192
+ const owned = edge.laidOut !== void 0 && samePoints(edge.waypoints ?? [], edge.laidOut);
193
+ return { ...edge, waypoints, ...owned ? { laidOut: waypoints } : {} };
194
+ }
195
+ function followers(sc, geometry, frame) {
196
+ const out = [];
197
+ for (const node of sc.renderOrderNodes()) {
198
+ const edge = diagramEdgeOf(node);
199
+ const ends = node.dependsOn;
200
+ if (edge === null || (edge.waypoints ?? []).length === 0 || !Array.isArray(ends) || ends.length !== 2) continue;
201
+ const [from, to] = ends.map((id) => sc.get(id));
202
+ const fromAfter = frame.get(ends[0]);
203
+ const toAfter = frame.get(ends[1]);
204
+ if (from === void 0 || to === void 0 || fromAfter === void 0 || toAfter === void 0) continue;
205
+ const d = offsetOf(geometry, from.pose, fromAfter);
206
+ const e = offsetOf(geometry, to.pose, toAfter);
207
+ if (Math.abs(d.x - e.x) > EPS || Math.abs(d.y - e.y) > EPS || d.x === 0 && d.y === 0) continue;
208
+ const data = node.data;
209
+ out.push({ id: node.id, pose: node.pose, data: { ...data, diagram: carried(edge, d) } });
210
+ }
211
+ return out;
212
+ }
213
+ function posesMovedBy(sc, ops) {
214
+ const written = writtenBy(sc, ops);
215
+ if (written === null) return null;
216
+ const moved = /* @__PURE__ */ new Map();
217
+ for (const [id, w] of written) if (w.pose !== sc.get(id)?.pose) moved.set(id, w.pose);
218
+ return moved;
219
+ }
220
+ function waypointFollower(scene, geometry = AUTO_POSE_DESCRIPTOR) {
221
+ const sc = scene;
222
+ const geo = geometry;
223
+ return {
224
+ preview: (frame) => frame.size < 2 ? [] : followers(sc, geo, frame),
225
+ finish(ops, label) {
226
+ const moved = posesMovedBy(sc, ops);
227
+ if (moved === null || moved.size < 2) return [...ops];
228
+ const changes = followers(sc, geo, moved).map((c) => createSetDataOp({
229
+ id: c.id,
230
+ from: sc.get(c.id).data,
231
+ to: c.data,
232
+ label
233
+ }));
234
+ return [...ops, ...changes];
235
+ }
236
+ };
237
+ }
238
+
239
+ // src/passOrder.ts
240
+ var DIAGRAM_PASS_ORDER = { offPage: 100, avoid: 200, labels: 300, hops: 400 };
241
+
242
+ // src/avoid/install.ts
243
+ function withRoute(data, edge, route) {
244
+ const { route: _r, ...rest } = edge;
245
+ return { ...data, diagram: route === null ? rest : { ...rest, route } };
246
+ }
247
+ function routeDataOps(scene, routes) {
248
+ const ops = [];
249
+ for (const [id, route] of routes) {
250
+ const node = scene.get(id);
251
+ const edge = node === void 0 ? null : diagramEdgeOf(node);
252
+ if (node === void 0 || edge === null) continue;
253
+ const had = edge.route;
254
+ if (route === null ? had === void 0 : had !== void 0 && samePoints(had, route)) continue;
255
+ ops.push(createSetDataOp({ id, from: node.data, to: withRoute(node.data, edge, route), label: "Route" }));
256
+ }
257
+ return ops;
258
+ }
259
+ var touched = (s) => s.added.size + s.removed.size + s.posed.size + s.reparented.size + s.dataChanged.size > 0;
260
+ function offsetOf2(geometry, before, after) {
261
+ const a = geometry.getBounds(before);
262
+ const b = geometry.getBounds(after);
263
+ return { x: b.x - a.x, y: b.y - a.y };
264
+ }
265
+ function carried2(data, input) {
266
+ const edges = input.edges.map((e) => {
267
+ const shown = data.get(e.id);
268
+ const waypoints = shown === void 0 ? void 0 : diagramEdgeOf({ data: shown })?.waypoints;
269
+ return waypoints === void 0 ? e : { ...e, waypoints };
270
+ });
271
+ return { obstacles: input.obstacles, edges };
272
+ }
273
+ function avoidFollower(scene, router, opts) {
274
+ const sc = scene;
275
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
276
+ let last = null;
277
+ const still = (frame) => {
278
+ if (last === null || last.frame.size !== frame.size) return false;
279
+ const px = opts.pixelSize?.() ?? 1;
280
+ for (const [id, pose] of frame) {
281
+ const was = last.frame.get(id);
282
+ if (was === void 0) return false;
283
+ const d = offsetOf2(geometry, was, pose);
284
+ if (Math.abs(d.x) >= px || Math.abs(d.y) >= px) return false;
285
+ }
286
+ return true;
287
+ };
288
+ return {
289
+ preview(frame, data) {
290
+ if (frame.size === 0) return [];
291
+ if (still(frame)) return last.out;
292
+ const input = carried2(data, avoidSceneInput(scene, opts, (id) => frame.get(id)));
293
+ const routes = router.update(input, { preview: true, budgetMs: opts.frameBudgetMs ?? 4 });
294
+ const out = [];
295
+ for (const [id, route] of routes) {
296
+ const node = sc.get(id);
297
+ const shown = data.get(id) ?? node?.data;
298
+ const edge = node === void 0 ? null : diagramEdgeOf({ data: shown });
299
+ if (node === void 0 || edge === null) continue;
300
+ out.push({ id, pose: node.pose, data: withRoute(shown, edge, route) });
301
+ }
302
+ last = { frame, out };
303
+ return out;
304
+ },
305
+ finish: (ops) => [...ops]
306
+ };
307
+ }
308
+ function installAvoidRouting(scene, opts = {}) {
309
+ const sc = scene;
310
+ const router = createAvoidRouter(opts);
311
+ router.seed(avoidSceneInput(scene, opts));
312
+ const dispose = scene.addEditPass((summary) => {
313
+ if (!touched(summary)) return null;
314
+ const ops = routeDataOps(sc, router.update(avoidSceneInput(scene, opts)));
315
+ return ops.length === 0 ? null : ops;
316
+ }, { order: DIAGRAM_PASS_ORDER.avoid });
317
+ return { router, follower: avoidFollower(scene, router, opts), dispose };
318
+ }
319
+ function useAvoidRouting(scene, opts = {}) {
320
+ const deps = useOptionalDepRegistry();
321
+ const latest = useLatest(opts);
322
+ const follower = useRef(null);
323
+ useEffect(() => {
324
+ const pixelSize = () => latest.current.pixelSize?.() ?? 1 / Math.abs(deps?.get("view")?.get().scale.x || 1);
325
+ const routing = installAvoidRouting(scene, { ...latest.current, pixelSize });
326
+ follower.current = routing.follower;
327
+ return () => {
328
+ routing.dispose();
329
+ follower.current = null;
330
+ };
331
+ }, [scene, deps, latest]);
332
+ useTransformFollower(() => follower.current);
333
+ }
334
+
335
+ // src/hops/follower.ts
336
+ function hopFollower(scene) {
337
+ const sc = scene;
338
+ return {
339
+ preview(frame, data) {
340
+ if (frame.size === 0) return [];
341
+ const edges = sceneEdges(sc);
342
+ const moved = /* @__PURE__ */ new Set();
343
+ for (const e of edges) {
344
+ if (frame.has(e.node.id) || e.ends.some((end) => inOrUnder(sc, end, frame))) moved.add(e.node.id);
345
+ }
346
+ const out = [];
347
+ for (const e of edges) {
348
+ const hops = e.trait.hops;
349
+ if (hops === void 0 || hops.length === 0) continue;
350
+ if (!moved.has(e.node.id) && !hops.some((h) => h.over.some((id) => moved.has(id)))) continue;
351
+ const shown = data.get(e.node.id) ?? e.node.data;
352
+ const trait = diagramEdgeOf({ data: shown }) ?? e.trait;
353
+ out.push({ id: e.node.id, pose: e.node.pose, data: withHopsData(shown, trait, []) });
354
+ }
355
+ return out;
356
+ },
357
+ finish: (ops) => [...ops]
358
+ };
359
+ }
360
+
361
+ // src/hops/install.ts
362
+ function boxOf(points, pad) {
363
+ const b = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity };
364
+ for (const p of points) {
365
+ b.x0 = Math.min(b.x0, p.x - pad);
366
+ b.y0 = Math.min(b.y0, p.y - pad);
367
+ b.x1 = Math.max(b.x1, p.x + pad);
368
+ b.y1 = Math.max(b.y1, p.y + pad);
369
+ }
370
+ return b;
371
+ }
372
+ var meets = (a, b) => a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1;
373
+ function reordered(before, now) {
374
+ const was = new Set(before);
375
+ const is = new Set(now);
376
+ const a = before.filter((id) => is.has(id));
377
+ const b = now.filter((id) => was.has(id));
378
+ return a.some((id, i) => id !== b[i]);
379
+ }
380
+ function allPolylines(edges, polylineOf) {
381
+ const out = /* @__PURE__ */ new Map();
382
+ for (const e of edges) {
383
+ const p = polylineOf(e);
384
+ if (p !== null) out.set(e.node.id, p);
385
+ }
386
+ return out;
387
+ }
388
+ function candidates(sc, edges, held, s) {
389
+ const changed = /* @__PURE__ */ new Set([...s.added, ...s.removed, ...s.posed, ...s.reparented, ...s.dataChanged]);
390
+ const derived = (id) => sc.get(id)?.derivePose !== void 0;
391
+ return edges.filter((e) => changed.has(e.node.id) || !held.polylines.has(e.node.id) || e.ends.some((end) => inOrUnder(sc, end, changed) || derived(end)));
392
+ }
393
+ function installHops(scene, opts = {}) {
394
+ const sc = scene;
395
+ const polylineOf = edgePolylines(sc, opts);
396
+ let held = null;
397
+ const full = (edges, r) => {
398
+ const order = edges.map((e) => e.node.id);
399
+ held = { polylines: allPolylines(edges, polylineOf), order, r };
400
+ return hopDataOps(sc, edgeHops(held.polylines, order, r));
401
+ };
402
+ const pass = (summary) => {
403
+ const value = sc.doc.get(HOPS_DOC_KEY);
404
+ const keyed = summary.docChanged.has(HOPS_DOC_KEY);
405
+ if (value === void 0) {
406
+ held = null;
407
+ return keyed ? hopDataOps(sc, new Map(sceneEdges(sc).map((e) => [e.node.id, []]))) : null;
408
+ }
409
+ const edges = sceneEdges(sc);
410
+ const order = edges.map((e) => e.node.id);
411
+ if (keyed || held === null || held.r !== value.size || reordered(held.order, order)) return full(edges, value.size);
412
+ const h = held;
413
+ const r = h.r;
414
+ const boxes = [];
415
+ const dirty = /* @__PURE__ */ new Set();
416
+ for (const e of candidates(sc, edges, h, summary)) {
417
+ const was = h.polylines.get(e.node.id);
418
+ const now = polylineOf(e);
419
+ if (was !== void 0 && now !== null && samePoints(was, now)) continue;
420
+ dirty.add(e.node.id);
421
+ if (was !== void 0) boxes.push(boxOf(was, r));
422
+ if (now === null) h.polylines.delete(e.node.id);
423
+ else {
424
+ boxes.push(boxOf(now, r));
425
+ h.polylines.set(e.node.id, now);
426
+ }
427
+ }
428
+ const present = new Set(order);
429
+ for (const [id, was] of h.polylines) {
430
+ if (present.has(id)) continue;
431
+ boxes.push(boxOf(was, r));
432
+ h.polylines.delete(id);
433
+ }
434
+ h.order = order;
435
+ if (boxes.length === 0) return null;
436
+ const affected = new Set(dirty);
437
+ for (const [id, points] of h.polylines) {
438
+ if (affected.has(id)) continue;
439
+ const box = boxOf(points, 0);
440
+ if (boxes.some((b) => meets(box, b))) affected.add(id);
441
+ }
442
+ const ops = hopDataOps(sc, edgeHops(h.polylines, order, r, affected));
443
+ return ops.length === 0 ? null : ops;
444
+ };
445
+ const on = sc.doc.get(HOPS_DOC_KEY);
446
+ if (on !== void 0) {
447
+ const edges = sceneEdges(sc);
448
+ held = { polylines: allPolylines(edges, polylineOf), order: edges.map((e) => e.node.id), r: on.size };
449
+ }
450
+ const dispose = scene.addEditPass(pass, { order: DIAGRAM_PASS_ORDER.hops });
451
+ return { follower: hopFollower(scene), dispose };
452
+ }
453
+ function useHops(scene, opts = {}) {
454
+ const latest = useLatest(opts);
455
+ const follower = useRef(null);
456
+ useEffect(() => {
457
+ const hops = installHops(scene, latest.current);
458
+ follower.current = hops.follower;
459
+ return () => {
460
+ hops.dispose();
461
+ follower.current = null;
462
+ };
463
+ }, [scene, latest]);
464
+ useTransformFollower(() => follower.current);
465
+ }
466
+ var isRect = (pose) => typeof pose === "object" && pose !== null && typeof pose.width === "number" && typeof pose.height === "number";
467
+ function labelFitOps(scene, summary, measure) {
468
+ const ops = [];
469
+ const sizes = /* @__PURE__ */ new Map();
470
+ for (const id of summary.dataChanged) {
471
+ const node = scene.get(id);
472
+ if (node === void 0 || diagramLabelOf(node) === null || !isRect(node.pose)) continue;
473
+ const data = node.data;
474
+ if (typeof data.text !== "string") continue;
475
+ const size = labelSize(data.text, data.style ?? labelStyleOf(DEFAULT_FONT), measure);
476
+ if (Math.abs(size.width - node.pose.width) < 1e-6 && Math.abs(size.height - node.pose.height) < 1e-6) continue;
477
+ sizes.set(id, size);
478
+ ops.push(createTransformOp({ id, from: node.pose, to: { ...node.pose, ...size }, label: "Fit label" }));
479
+ }
480
+ return { ops, sizes };
481
+ }
482
+
483
+ // src/labels/install.ts
484
+ function runsOf(path) {
485
+ if (path === null || path.kind !== "polygon") return [];
486
+ const runs = [];
487
+ for (let i = 0; i < path.commands.length; i++) {
488
+ if (path.commands[i] === PATH_M || runs.length === 0) runs.push([]);
489
+ runs[runs.length - 1].push({ x: path.coords[2 * i], y: path.coords[2 * i + 1] });
490
+ }
491
+ return runs.filter((r) => r.length >= 2);
492
+ }
493
+ function read(sc, geometry, reader, sizes) {
494
+ const out = { shapes: [], routes: /* @__PURE__ */ new Map(), labels: [] };
495
+ for (const node of sc.renderOrderNodes()) {
496
+ const ends = Array.isArray(node.dependsOn) ? node.dependsOn : [];
497
+ if (diagramEdgeOf(node) !== null) {
498
+ const path = documentDepOf(sc, node.id)?.path ?? null;
499
+ const [points, ...more] = runsOf(path);
500
+ if (path !== null && points !== void 0) out.routes.set(node.id, { id: node.id, points, more, path, ends });
501
+ continue;
502
+ }
503
+ const placement = diagramLabelOf(node);
504
+ if (placement !== null) {
505
+ const { width, height } = sizes?.get(node.id) ?? geometry.getBounds(node.pose);
506
+ if (ends[0] !== void 0) out.labels.push({ id: node.id, edge: ends[0], placement, size: { width, height }, node });
507
+ continue;
508
+ }
509
+ if (diagramContainerOf(node) !== null || diagramGroupOf(node) !== null || diagramNodeOf(node, reader) === null) continue;
510
+ out.shapes.push({ id: node.id, box: geometry.getBounds(documentPose(sc, node)) });
511
+ }
512
+ return out;
513
+ }
514
+ var grow = (b, by) => ({ x: b.x - by, y: b.y - by, width: b.width + 2 * by, height: b.height + 2 * by });
515
+ var meets2 = (a, b) => a.x <= b.x + b.width && b.x <= a.x + a.width && a.y <= b.y + b.height && b.y <= a.y + a.height;
516
+ function routeMeets(points, box) {
517
+ for (let i = 0; i + 1 < points.length; i++) {
518
+ const a = points[i];
519
+ const b = points[i + 1];
520
+ const seg = { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(a.x - b.x), height: Math.abs(a.y - b.y) };
521
+ if (meets2(seg, box)) return true;
522
+ }
523
+ return false;
524
+ }
525
+ var runsIn = (r) => [r.points, ...r.more ?? []];
526
+ function changesOf(scene, summary, seen) {
527
+ const touched2 = (id) => summary.added.has(id) || summary.posed.has(id) || summary.dataChanged.has(id);
528
+ const edges = /* @__PURE__ */ new Set();
529
+ const routes = [];
530
+ for (const r of scene.routes.values()) {
531
+ if (!touched2(r.id) && !r.ends.some((e) => touched2(e) || summary.removed.has(e))) continue;
532
+ edges.add(r.id);
533
+ routes.push(...runsIn(r));
534
+ routes.push(...seen.routes.get(r.id) ?? []);
535
+ }
536
+ const shapes = [];
537
+ for (const s of scene.shapes) {
538
+ if (!touched2(s.id)) continue;
539
+ shapes.push(s.box);
540
+ const was = seen.shapes.get(s.id);
541
+ if (was !== void 0) shapes.push(was);
542
+ }
543
+ for (const id of summary.removed) {
544
+ const shape = seen.shapes.get(id);
545
+ if (shape !== void 0) shapes.push(shape);
546
+ routes.push(...seen.routes.get(id) ?? []);
547
+ }
548
+ return { touched: touched2, edges, routes, shapes };
549
+ }
550
+ function boxesOf(scene, label, gap) {
551
+ const route = scene.routes.get(label.edge);
552
+ if (route === void 0) return [];
553
+ const { placed, ...preferred } = label.placement;
554
+ const out = [];
555
+ for (const placement of placed === void 0 ? [preferred] : [label.placement, preferred]) {
556
+ const c = labelCenter(route.path, placement, label.size);
557
+ if (c !== null) out.push(grow({ x: c.x - label.size.width / 2, y: c.y - label.size.height / 2, ...label.size }, gap));
558
+ }
559
+ return out;
560
+ }
561
+ function movingOf(scene, summary, seen, gap) {
562
+ const changed = changesOf(scene, summary, seen);
563
+ const boxes = new Map(scene.labels.map((l) => [l.id, boxesOf(scene, l, gap)]));
564
+ const first = /* @__PURE__ */ new Set();
565
+ for (const l of scene.labels) {
566
+ const own = boxes.get(l.id);
567
+ const hit = own.some((box) => changed.shapes.some((s) => meets2(s, box)) || changed.routes.some((r) => routeMeets(r, box)));
568
+ if (changed.edges.has(l.edge) || changed.touched(l.id) || hit) first.add(l.id);
569
+ }
570
+ const moving = new Set(first);
571
+ for (const l of scene.labels) {
572
+ if (moving.has(l.id)) continue;
573
+ const own = boxes.get(l.id);
574
+ for (const id of first) if (boxes.get(id).some((b) => own.some((o) => meets2(b, o)))) moving.add(l.id);
575
+ }
576
+ return moving;
577
+ }
578
+ var sameSpot = (a, b) => a === b || a !== void 0 && b !== void 0 && Math.abs(a.at - b.at) < 1e-9 && Math.abs(a.offset - b.offset) < 1e-9;
579
+ function withSpot(data, label, spot) {
580
+ const { placed: _p, ...rest } = label;
581
+ const diagram = data.diagram;
582
+ return { ...data, diagram: { ...diagram, label: spot === void 0 ? rest : { ...rest, placed: spot } } };
583
+ }
584
+ function placementOps(scene, summary, opts, seen) {
585
+ const sc = scene;
586
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
587
+ const fit = labelFitOps(sc, summary, opts.measure);
588
+ const reading = read(sc, geometry, opts.read, fit.sizes);
589
+ const moving = movingOf(reading, summary, seen, opts.gap ?? 2);
590
+ seen.shapes = new Map(reading.shapes.map((s) => [s.id, s.box]));
591
+ seen.routes = new Map([...reading.routes.values()].map((r) => [r.id, runsIn(r)]));
592
+ if (moving.size === 0) return fit.ops;
593
+ const labels = reading.labels.map((l) => ({ ...l, moving: moving.has(l.id) }));
594
+ const placed = placeLabels({ shapes: reading.shapes, routes: [...reading.routes.values()], labels }, opts);
595
+ const ops = [...fit.ops];
596
+ for (const l of labels) {
597
+ if (!placed.has(l.id)) continue;
598
+ const spot = placed.get(l.id) ?? void 0;
599
+ if (sameSpot(l.placement.placed, spot)) continue;
600
+ ops.push(createSetDataOp({ id: l.id, from: l.node.data, to: withSpot(l.node.data, l.placement, spot), label: "Place label" }));
601
+ }
602
+ return ops;
603
+ }
604
+ var any = (s) => s.added.size + s.removed.size + s.posed.size + s.reparented.size + s.dataChanged.size > 0;
605
+ function installLabelPlacement(scene, opts = {}) {
606
+ const sc = scene;
607
+ const first = read(sc, opts.geometry ?? AUTO_POSE_DESCRIPTOR, opts.read);
608
+ const seen = {
609
+ shapes: new Map(first.shapes.map((s) => [s.id, s.box])),
610
+ routes: new Map([...first.routes.values()].map((r) => [r.id, runsIn(r)]))
611
+ };
612
+ const dispose = scene.addEditPass((summary) => {
613
+ if (!any(summary)) return null;
614
+ const ops = placementOps(scene, summary, opts, seen);
615
+ return ops.length === 0 ? null : ops;
616
+ }, { order: DIAGRAM_PASS_ORDER.labels });
617
+ return { dispose };
618
+ }
619
+ function useLabelPlacement(scene, opts = {}) {
620
+ const latest = useLatest(opts);
621
+ useEffect(() => installLabelPlacement(scene, latest.current).dispose, [scene, latest]);
622
+ }
623
+ function documentBoundsOf(scene, node, geometry = AUTO_POSE_DESCRIPTOR) {
624
+ return geometry.getBounds(documentPose(scene, node));
625
+ }
626
+ var ENDS = ["from", "to"];
627
+ var sameSplit = (a, b) => a === void 0 ? b === null : b !== null && a.ref === b.ref && a.from === b.from && a.to === b.to;
628
+ function withSplit(data, edge, split) {
629
+ const { offPage: _o, ...rest } = edge;
630
+ return { ...data, diagram: split === null ? rest : { ...rest, offPage: split } };
631
+ }
632
+ function reached(scene, s) {
633
+ const ids = /* @__PURE__ */ new Set([...s.added, ...s.posed, ...s.reparented, ...s.dataChanged]);
634
+ const out = /* @__PURE__ */ new Set();
635
+ for (const id of s.removed) {
636
+ const tagged = /^(.*)\/offpage-(?:from|to)$/.exec(id);
637
+ if (tagged !== null) out.add(tagged[1]);
638
+ }
639
+ for (const node of scene.renderOrderNodes()) {
640
+ if (diagramEdgeOf(node) === null) continue;
641
+ const deps = node.dependsOn;
642
+ if (ids.has(node.id) || Array.isArray(deps) && deps.some((d) => ids.has(d))) out.add(node.id);
643
+ }
644
+ return out;
645
+ }
646
+ function tagOps(scene, edge, split, pages, look) {
647
+ const ops = [];
648
+ let lookNow = null;
649
+ const lookOf = () => lookNow ??= look();
650
+ for (const end of ENDS) {
651
+ const id = offPageTagId(edge.id, end);
652
+ const tag = scene.get(id);
653
+ if (split === null) {
654
+ if (tag !== void 0) ops.push(createDeleteOp({ node: tag, label: "Off-page" }));
655
+ continue;
656
+ }
657
+ const text = offPageFarText(pages, split, end);
658
+ if (tag === void 0) {
659
+ const spec = offPageTagSpec(edge.id, end, text, { ...lookOf(), layer: edge.layer });
660
+ ops.push(createInsertOp({ node: { ...spec, parent: null }, label: "Off-page" }));
661
+ } else if (tag.data.text !== text) {
662
+ const old = tag.data;
663
+ const kept = {
664
+ ...lookOf(),
665
+ ...old.style ? { style: old.style } : {},
666
+ ...old.fill?.paint?.color ? { fill: old.fill.paint.color } : {},
667
+ ...old.background?.paint?.color ? { background: old.background.paint.color } : {},
668
+ ...old.padding !== void 0 ? { padding: old.padding } : {}
669
+ };
670
+ ops.push(createSetDataOp({ id, from: tag.data, to: { ...tag.data, ...offPageTagData(edge.id, end, text, kept) }, label: "Off-page" }));
671
+ const pose = tag.pose;
672
+ ops.push(createTransformOp({ id, from: tag.pose, to: { ...pose, ...offPageTagSize(text, kept) }, label: "Off-page" }));
673
+ }
674
+ }
675
+ return ops;
676
+ }
677
+ function offPageOps(scene, edges, opts = {}) {
678
+ const sc = scene;
679
+ const geometry = opts.geometry;
680
+ const look = opts.look ?? (() => offPageTagLook(diagramThemeOf(sc, opts.theme)));
681
+ const wanted = edges === "all" ? null : new Set(edges);
682
+ const pages = placedPagesOf(sc);
683
+ const boxOf2 = (id) => {
684
+ const node = sc.get(id);
685
+ return node === void 0 ? null : documentBoundsOf(scene, node, geometry);
686
+ };
687
+ const inputs = [];
688
+ const used = [];
689
+ for (const node of sc.renderOrderNodes()) {
690
+ const trait = diagramEdgeOf(node);
691
+ if (trait === null) continue;
692
+ if (wanted !== null && !wanted.has(node.id)) {
693
+ if (trait.offPage !== void 0) used.push(trait.offPage.ref);
694
+ continue;
695
+ }
696
+ const deps = node.dependsOn;
697
+ const from = Array.isArray(deps) ? boxOf2(deps[0]) : null;
698
+ const to = Array.isArray(deps) ? boxOf2(deps[1]) : null;
699
+ if (from === null || to === null) continue;
700
+ inputs.push({ id: node.id, from, to, offPage: trait.offPage, node, trait });
701
+ }
702
+ if (inputs.length === 0) return [];
703
+ const splits = offPageSplits({ pages, edges: inputs, used });
704
+ const ops = [];
705
+ for (const edge of inputs) {
706
+ const split = splits.get(edge.id) ?? null;
707
+ if (!sameSplit(edge.trait.offPage, split)) {
708
+ ops.push(createSetDataOp({ id: edge.id, from: edge.node.data, to: withSplit(edge.node.data, edge.trait, split), label: "Off-page" }));
709
+ }
710
+ ops.push(...tagOps(sc, edge.node, split, pages, look));
711
+ }
712
+ return ops;
713
+ }
714
+ function installOffPageMarkers(scene, opts = {}) {
715
+ const sc = scene;
716
+ return scene.addEditPass((summary) => {
717
+ const edges = summary.docChanged.has(PAGES_DOC_KEY) ? "all" : reached(sc, summary);
718
+ if (edges !== "all" && edges.size === 0) return null;
719
+ const ops = offPageOps(scene, edges, opts);
720
+ return ops.length === 0 ? null : ops;
721
+ }, { order: DIAGRAM_PASS_ORDER.offPage });
722
+ }
723
+ function useOffPageMarkers(scene, opts = {}) {
724
+ const latest = useLatest(opts);
725
+ useEffect(() => installOffPageMarkers(scene, latest.current), [scene, latest]);
726
+ }
727
+ var FOLLOW_OFF_PAGE_ACTION_ID = "diagram.followOffPage";
728
+ function offPageTargetOf(scene, tagId) {
729
+ const node = scene.get(tagId);
730
+ const tag = node === void 0 ? null : offPageTagOf(node);
731
+ const edgeNode = tag === null ? void 0 : scene.get(tag.edge);
732
+ const split = edgeNode === void 0 ? void 0 : diagramEdgeOf(edgeNode)?.offPage;
733
+ const deps = edgeNode?.dependsOn;
734
+ if (tag === null || split === void 0 || !Array.isArray(deps)) return null;
735
+ const far = tag.end === "from" ? 1 : 0;
736
+ return { tag: tagId, edge: tag.edge, node: deps[far], page: far === 1 ? split.to : split.from };
737
+ }
738
+ function tagUnder(deps, at) {
739
+ if (deps?.scene === void 0 || at === void 0) return null;
740
+ const id = deps.nodeAtPoint?.(at);
741
+ return id == null ? null : offPageTargetOf(deps.scene, id);
742
+ }
743
+ function followDefault(target, ctx, padding) {
744
+ const { scene, selection, view } = ctx.deps;
745
+ if (scene === void 0) return;
746
+ selection?.set([target.node]);
747
+ const page = placedPagesOf(scene).find((p) => p.id === target.page);
748
+ const size = view?.hostSize?.();
749
+ if (page === void 0 || view === void 0 || size == null) return;
750
+ view.set(fitViewToPage(page, size, view.get(), { padding }));
751
+ }
752
+ function createFollowOffPageAction(opts = {}) {
753
+ const padding = opts.padding ?? 24;
754
+ const follow = opts.follow ?? ((target, ctx) => followDefault(target, ctx, padding));
755
+ return {
756
+ id: FOLLOW_OFF_PAGE_ACTION_ID,
757
+ label: "Go to the other page",
758
+ group: "diagram",
759
+ requires: ["scene", "nodeAtPoint", "selection", "view"],
760
+ eligible: { mode: { in: ["normal", "isolation"] } },
761
+ invoker: {
762
+ timing: "immediate",
763
+ run(deps, params) {
764
+ const world = { x: Number(params?.worldX), y: Number(params?.worldY) };
765
+ const target = tagUnder(deps, world);
766
+ if (target === null) return;
767
+ follow(target, { world, modifiers: { alt: false, shift: false, meta: false, ctrl: false }, deps });
768
+ }
769
+ },
770
+ enabled: (deps, at) => tagUnder(deps, at) !== null ? true : ActionDisabledReason.NotApplicable
771
+ };
772
+ }
773
+ var onBody = bodyPredicate((target) => target === "selected-body" || target === "unselected-body");
774
+ function followOffPageBinding(actionId = FOLLOW_OFF_PAGE_ACTION_ID) {
775
+ return { spec: { kind: "doubleClick", target: { kindOf: onBody } }, actionId };
776
+ }
777
+ function diagramOffPage(opts = {}) {
778
+ return {
779
+ contribution: {
780
+ id: "weasel-diagram-off-page",
781
+ eligibility: { always: true },
782
+ actions: [createFollowOffPageAction(opts)],
783
+ bindings: [followOffPageBinding()]
784
+ }
785
+ };
786
+ }
787
+
788
+ // src/theme/fromTokens.ts
789
+ function diagramThemeFromTokens(read2, opts = {}) {
790
+ const paper = DIAGRAM_THEMES.paper;
791
+ const t = (name, fallback) => read2(`--wzl-${name}`).trim() || fallback;
792
+ const fg = t("fg", paper.roles.default.text);
793
+ const muted = t("fg-muted", paper.roles.muted.text);
794
+ const raised = t("surface-raised", paper.roles.default.fill);
795
+ const sunken = t("surface-sunken", paper.roles.muted.fill);
796
+ const border = t("border-strong", t("border", paper.roles.default.stroke));
797
+ const accent = t("accent", paper.roles.accent.stroke);
798
+ const success = t("success", paper.roles.positive.stroke);
799
+ const warning = t("warning", paper.roles.warning.stroke);
800
+ const danger = t("danger", paper.roles.negative.stroke);
801
+ const size = Number.parseFloat(read2("--wzl-font-size"));
802
+ const shape = (stroke) => ({ fill: raised, stroke, text: fg });
803
+ return {
804
+ id: opts.id ?? "tokens",
805
+ name: opts.name ?? "Match chrome",
806
+ roles: {
807
+ default: shape(border),
808
+ accent: { fill: t("accent-soft", raised), stroke: accent, text: fg },
809
+ muted: { fill: sunken, stroke: border, text: muted },
810
+ positive: shape(success),
811
+ warning: shape(warning),
812
+ negative: shape(danger)
813
+ },
814
+ defaultRole: "default",
815
+ edge: { ...paper.edge, stroke: muted },
816
+ label: { fill: fg },
817
+ container: { ...paper.container, fill: sunken, stroke: border, text: muted },
818
+ lane: {
819
+ header: { fill: sunken, stroke: border, text: fg, strokeWidth: 1 },
820
+ bodies: [
821
+ { fill: t("surface", paper.page), stroke: border, text: fg, strokeWidth: 1 },
822
+ { fill: raised, stroke: border, text: fg, strokeWidth: 1 }
823
+ ]
824
+ },
825
+ page: t("surface", paper.page),
826
+ font: { family: t("font-ui", paper.font.family), size: Number.isFinite(size) && size > 0 ? size : paper.font.size },
827
+ series: [accent, success, warning, danger, t("accent-strong", accent), muted]
828
+ };
829
+ }
830
+ var FALLBACK_SIZE = 16;
831
+ var rendererMeasure = (text, style) => {
832
+ const fontSize = style.fontSize ?? FALLBACK_SIZE;
833
+ const { width } = measureTextBounds(text, {
834
+ fontSize,
835
+ ...style.fontFamily !== void 0 ? { fontFamily: style.fontFamily } : {},
836
+ ...style.bold === true ? { fontWeight: 700 } : {}
837
+ });
838
+ return { width: Math.ceil(width), height: Math.ceil(fontSize * 1.2) };
839
+ };
840
+
841
+ // src/theme/applyTheme.ts
842
+ function diagramStyleOf(node) {
843
+ const trait = node.data?.diagram;
844
+ if (trait === null || typeof trait !== "object") return null;
845
+ const kind = "from" in trait && "to" in trait ? "connector" : "outline" in trait && !("group" in trait) && !("container" in trait) ? "shape" : null;
846
+ if (kind === null) return null;
847
+ const role = trait.style;
848
+ return {
849
+ kind,
850
+ ...role !== void 0 ? { role } : {},
851
+ own: trait.ownStyle ?? [],
852
+ ownFont: trait.ownFont ?? []
853
+ };
854
+ }
855
+ function writeAll(scene, writes, measure) {
856
+ const bodies = [];
857
+ for (const [id, data] of writes) {
858
+ const node = scene.get(id);
859
+ if (node.data?.style?.fontSize !== data?.style?.fontSize && node.parent !== null) bodies.push(node.parent);
860
+ scene.update(id, { data });
861
+ }
862
+ relayoutBodies(scene, bodies, measure);
863
+ }
864
+ function applyDiagramTheme(scene, theme, label = `Theme: ${theme.name}`, opts = {}) {
865
+ const from = opts.from ?? diagramThemeOf(scene);
866
+ scene.batch(label, () => {
867
+ writeAll(scene, themeWrites(scene, theme, void 0, from), opts.measure);
868
+ scene.doc.set(DIAGRAM_THEME_KEY, themeDocValue(theme));
869
+ }, opts.coalesceKey !== void 0 ? { coalesceKey: opts.coalesceKey } : {});
870
+ }
871
+ var withTrait = (data, trait) => ({ ...data, diagram: trait });
872
+ function setDiagramStyle(scene, ids, role, opts = {}) {
873
+ const theme = opts.theme ?? diagramThemeOf(scene);
874
+ const styled = ids.filter((id) => {
875
+ const n = scene.get(id);
876
+ return n !== void 0 && diagramStyleOf(n) !== null;
877
+ });
878
+ if (styled.length === 0) return;
879
+ scene.batch(opts.label ?? "Set style", () => {
880
+ for (const id of styled) {
881
+ const data = scene.get(id).data;
882
+ const { ownStyle: _, ...trait } = data.diagram;
883
+ scene.update(id, { data: withTrait(data, { ...trait, style: role }) });
884
+ }
885
+ writeAll(scene, themeWrites(scene, theme, new Set(styled)));
886
+ });
887
+ }
888
+ var painted = (paint, color) => ({ ...paint, paint: { ...paint?.paint, color } });
889
+ function textNodesOf(scene, id, kind) {
890
+ if (kind === "shape") return scene.childrenOf(id).filter((c) => typeof scene.get(c)?.data?.text === "string");
891
+ return scene.renderOrderNodes().filter((n) => {
892
+ const deps = n.dependsOn;
893
+ return Array.isArray(deps) && deps.length === 1 && deps[0] === id && "label" in (n.data.diagram ?? {});
894
+ }).map((n) => n.id);
895
+ }
896
+ function setDiagramColor(scene, ids, field, color, opts = {}) {
897
+ scene.batch(opts.label ?? `Set ${field} color`, () => {
898
+ for (const id of ids) {
899
+ const node = scene.get(id);
900
+ const state = node === void 0 ? null : diagramStyleOf(node);
901
+ if (state === null || state.kind === "connector" && field === "fill") continue;
902
+ const data = node.data;
903
+ const own = state.own.includes(field) ? state.own : [...state.own, field];
904
+ const next = withTrait(data, { ...data.diagram, ownStyle: own });
905
+ if (field !== "text") next[field] = painted(data[field], color);
906
+ scene.update(id, { data: next });
907
+ if (field !== "text") continue;
908
+ for (const t of textNodesOf(scene, id, state.kind)) {
909
+ const text = scene.get(t).data;
910
+ scene.update(t, { data: { ...text, fill: painted(text.fill, color) } });
911
+ }
912
+ }
913
+ });
914
+ }
915
+ function setDiagramFontSize(scene, ids, size, opts = {}) {
916
+ scene.batch(opts.label ?? "Set text size", () => {
917
+ const writes = /* @__PURE__ */ new Map();
918
+ for (const id of ids) {
919
+ const node = scene.get(id);
920
+ const state = node === void 0 ? null : diagramStyleOf(node);
921
+ if (state === null) continue;
922
+ const data = node.data;
923
+ if (!state.ownFont.includes("size")) scene.update(id, { data: withTrait(data, { ...data.diagram, ownFont: [...state.ownFont, "size"] }) });
924
+ for (const t of textNodesOf(scene, id, state.kind)) {
925
+ const text = scene.get(t).data;
926
+ writes.set(t, { ...text, style: { ...text.style, fontSize: size } });
927
+ }
928
+ }
929
+ writeAll(scene, writes, opts.measure);
930
+ });
931
+ }
932
+
933
+ // src/theme/themeEdit.ts
934
+ var isShippedTheme = (theme) => DIAGRAM_THEMES[theme.id] === theme;
935
+ function forkTheme(theme) {
936
+ return { ...structuredClone(theme), id: `${theme.id}-edited`, name: `${theme.name} (edited)` };
937
+ }
938
+ var editableTheme = (theme) => isShippedTheme(theme) ? forkTheme(theme) : theme;
939
+ function themeValue(theme, path) {
940
+ let at = theme;
941
+ for (const key of path) at = at?.[key];
942
+ return at;
943
+ }
944
+ function setIn(at, path, value) {
945
+ if (path.length === 0) return value;
946
+ const [key, ...rest] = path;
947
+ if (Array.isArray(at)) {
948
+ const copy = [...at];
949
+ copy[key] = setIn(copy[key], rest, value);
950
+ return copy;
951
+ }
952
+ const obj = at ?? {};
953
+ return { ...obj, [key]: setIn(obj[key], rest, value) };
954
+ }
955
+ var setThemeValue = (theme, path, value) => setIn(theme, path, value);
956
+ function themeRoles(theme) {
957
+ const shipped = new Map(DIAGRAM_ROLES.map((r) => [r.id, r.label]));
958
+ const ids = [
959
+ ...DIAGRAM_ROLES.map((r) => r.id).filter((id) => id in theme.roles),
960
+ ...Object.keys(theme.roles).filter((id) => !shipped.has(id))
961
+ ];
962
+ return ids.map((id) => ({ id, label: theme.roleLabels?.[id] ?? shipped.get(id) ?? id }));
963
+ }
964
+ var slug = (label) => label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "role";
965
+ function addThemeRole(theme, label) {
966
+ const base = slug(label);
967
+ let id = base;
968
+ for (let n = 2; id in theme.roles; n++) id = `${base}-${n}`;
969
+ return {
970
+ theme: { ...theme, roles: { ...theme.roles, [id]: { ...theme.roles[theme.defaultRole] } }, roleLabels: { ...theme.roleLabels, [id]: label } },
971
+ id
972
+ };
973
+ }
974
+ var renameThemeRole = (theme, id, label) => id in theme.roles ? { ...theme, roleLabels: { ...theme.roleLabels, [id]: label } } : theme;
975
+ function removeThemeRole(theme, id) {
976
+ if (id === theme.defaultRole || !(id in theme.roles)) return theme;
977
+ const { [id]: _, ...roles } = theme.roles;
978
+ const { [id]: __, ...labels } = theme.roleLabels ?? {};
979
+ return { ...theme, roles, ...theme.roleLabels ? { roleLabels: labels } : {} };
980
+ }
981
+ var DIAGRAM_THEME_EXTENSION = ".weasel-theme.json";
982
+ var themeToJson = (theme) => `${JSON.stringify(theme, null, 2)}
983
+ `;
984
+ var isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
985
+ function isColor(v) {
986
+ if (typeof v !== "string") return false;
987
+ try {
988
+ parseColor(v);
989
+ return true;
990
+ } catch {
991
+ return false;
992
+ }
993
+ }
994
+ function checker(errors) {
995
+ const color = (path, v) => {
996
+ if (!isColor(v)) errors.push(`${path}: ${JSON.stringify(v)} is not a color`);
997
+ };
998
+ const box = (path, v) => {
999
+ if (!isObject(v)) return void errors.push(`${path}: missing`);
1000
+ for (const field of ["fill", "stroke", "text"]) color(`${path}.${field}`, v[field]);
1001
+ };
1002
+ const text = (path, v) => {
1003
+ if (typeof v !== "string" || v === "") errors.push(`${path}: missing`);
1004
+ };
1005
+ const font = (path, v) => {
1006
+ if (!isObject(v)) return void errors.push(`${path}: missing`);
1007
+ text(`${path}.family`, v.family);
1008
+ if (typeof v.size !== "number" || !(v.size > 0)) errors.push(`${path}.size: ${JSON.stringify(v.size)} is not a size`);
1009
+ };
1010
+ return { color, box, text, font };
1011
+ }
1012
+ function checkTheme(t, errors) {
1013
+ const { color, box, text, font } = checker(errors);
1014
+ text("id", t.id);
1015
+ text("name", t.name);
1016
+ if (!isObject(t.roles) || Object.keys(t.roles).length === 0) errors.push("roles: missing");
1017
+ else for (const [id, style] of Object.entries(t.roles)) box(`roles.${id}`, style);
1018
+ if (typeof t.defaultRole !== "string") errors.push("defaultRole: missing");
1019
+ else if (isObject(t.roles) && !(t.defaultRole in t.roles)) errors.push(`defaultRole: no role ${JSON.stringify(t.defaultRole)}`);
1020
+ if (t.roleLabels !== void 0) {
1021
+ if (!isObject(t.roleLabels)) errors.push("roleLabels: not an object");
1022
+ else for (const [id, label] of Object.entries(t.roleLabels)) text(`roleLabels.${id}`, label);
1023
+ }
1024
+ if (!isObject(t.edge)) errors.push("edge: missing");
1025
+ else color("edge.stroke", t.edge.stroke);
1026
+ if (!isObject(t.label)) errors.push("label: missing");
1027
+ else {
1028
+ color("label.fill", t.label.fill);
1029
+ if (t.label.font !== void 0) font("label.font", t.label.font);
1030
+ }
1031
+ box("container", t.container);
1032
+ if (!isObject(t.lane)) errors.push("lane: missing");
1033
+ else {
1034
+ box("lane.header", t.lane.header);
1035
+ if (!Array.isArray(t.lane.bodies) || t.lane.bodies.length === 0) errors.push("lane.bodies: missing");
1036
+ else t.lane.bodies.forEach((b, i) => box(`lane.bodies.${i}`, b));
1037
+ }
1038
+ color("page", t.page);
1039
+ font("font", t.font);
1040
+ if (!Array.isArray(t.series)) errors.push("series: missing");
1041
+ else t.series.forEach((c, i) => color(`series.${i}`, c));
1042
+ }
1043
+ function parseDiagramTheme(json) {
1044
+ let raw;
1045
+ try {
1046
+ raw = JSON.parse(json);
1047
+ } catch (err) {
1048
+ return { errors: [`not JSON: ${err.message}`] };
1049
+ }
1050
+ if (!isObject(raw)) return { errors: ["a theme is an object"] };
1051
+ const errors = [];
1052
+ checkTheme(raw, errors);
1053
+ return errors.length > 0 ? { errors } : { theme: raw };
1054
+ }
1055
+
1056
+ export { CONNECTOR_ACTION_ID, CONNECTOR_TOOL_ID, DIAGRAM_PASS_ORDER, DIAGRAM_THEME_EXTENSION, FLOWCHART_STENCILS, FOLLOW_OFF_PAGE_ACTION_ID, addThemeRole, applyDiagramTheme, connectorTool, createFollowOffPageAction, diagramOffPage, diagramStyleOf, diagramThemeFromTokens, documentBoundsOf, editableTheme, followOffPageBinding, forkTheme, hopFollower, installAvoidRouting, installHops, installLabelPlacement, installOffPageMarkers, isShippedTheme, offPageOps, offPageTargetOf, parseDiagramTheme, removeThemeRole, renameThemeRole, rendererMeasure, routeDataOps, setDiagramColor, setDiagramFontSize, setDiagramStyle, setThemeValue, stencilBuild, stencilIdOf, stencilKind, stencilTool, themeRoles, themeToJson, themeValue, useAvoidRouting, useConnectorTool, useHops, useLabelPlacement, useOffPageMarkers, useStencilInsert, useStencilTool, waypointFollower };
3
1057
  //# sourceMappingURL=index.js.map
4
1058
  //# sourceMappingURL=index.js.map