@weasel-js/diagram 1.10.1 → 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,1436 +1,1058 @@
1
- import { layered, insetOf, portsOf, portOf, outlinePath, diagramNodeOf, expandGroupEdges, forceRelaxation, center, force, tree, DIAGRAM_TRAIT_KEY, axesFor, ranksOf, backEdges, DEFAULT_GROUP_INSET, sizeToBody, measureBody, buildBody, DEFAULT_NODE_GAP } from './chunk-JZ4CVLAB.js';
2
- export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, expandGroupEdges, force, forceRelaxation, forestOf, graphOrder, insetOf, isPinned, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, portOf, portsOf, portsOnOutline, rankGaps, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree, untilSettled } from './chunk-JZ4CVLAB.js';
3
- import { dragPanContribution, WeaselProvider, useScene, useSelection, useLatest, reconcileSpecs, SceneCanvas, fitViewToBounds, AUTO_POSE_DESCRIPTOR, translatePoseViaDescriptor, pointAlongPath, polylineFromPoints, registerNodeShape, effectivePose, usePoseRun, RECT_POSE_DESCRIPTOR, commandFill, commandStroke, composeAffordanceLayer } from '@weasel-js/core';
4
- import { portCurvePoints } from '@weasel-js/geom';
5
- import { AUTO_POSE_DESCRIPTOR as AUTO_POSE_DESCRIPTOR$1 } from '@weasel-js/core/math';
6
- import { forwardRef, useEffect, useMemo, useRef, useState, useImperativeHandle, useCallback } from 'react';
7
- import '@weasel-js/animator';
8
- import { jsx } from 'react/jsx-runtime';
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';
9
13
 
10
- function traitOf(node) {
11
- const data = node.data;
12
- if (data === null || typeof data !== "object") return null;
13
- const trait = data[DIAGRAM_TRAIT_KEY];
14
- return trait !== null && typeof trait === "object" ? trait : null;
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
+ });
15
28
  }
16
- function diagramGroupOf(node) {
17
- const group = traitOf(node)?.group;
18
- return group !== null && typeof group === "object" ? group : null;
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
+ );
19
36
  }
20
- function diagramAnchorOf(node) {
21
- const anchor = traitOf(node)?.anchor;
22
- return anchor !== null && typeof anchor === "object" && "at" in anchor ? anchor : null;
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;
23
70
  }
24
- function groupDerivePose(opts = {}) {
25
- const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
26
- return (node, deps) => {
27
- const group = diagramGroupOf(node);
28
- if (group === null) return null;
29
- let x0 = Infinity;
30
- let y0 = Infinity;
31
- let x1 = -Infinity;
32
- let y1 = -Infinity;
33
- for (const dep of deps) {
34
- if (dep === void 0) continue;
35
- const b = geometry.getBounds(dep.pose);
36
- x0 = Math.min(x0, b.x);
37
- y0 = Math.min(y0, b.y);
38
- x1 = Math.max(x1, b.x + b.width);
39
- y1 = Math.max(y1, b.y + b.height);
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
40
100
  }
41
- if (!Number.isFinite(x0)) return null;
42
- return geometry.fromBounds(boxAround({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }, group), node.pose);
43
- };
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 };
44
107
  }
45
- function boxAround(union, group) {
46
- const inset = insetOf(group.inset);
47
- const width = union.width + inset.left + inset.right;
48
- const grow = Math.max(0, (group.minWidth ?? 0) - width);
49
- return {
50
- x: union.x - inset.left - grow / 2,
51
- y: union.y - inset.top,
52
- width: width + grow,
53
- height: union.height + inset.top + inset.bottom
54
- };
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
+ });
55
120
  }
56
- function anchorDerivePose(opts = {}) {
57
- const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
58
- return (node, deps) => {
59
- const anchor = diagramAnchorOf(node);
60
- const dep = deps[0];
61
- if (anchor === null || dep === void 0) return null;
62
- const target = geometry.getBounds(dep.pose);
63
- const own = geometry.getBounds(node.pose);
64
- const self = anchor.self ?? anchor.at;
65
- const x = target.x + anchor.at.u * target.width + (anchor.offset?.x ?? 0) - self.u * own.width;
66
- const y = target.y + anchor.at.v * target.height + (anchor.offset?.y ?? 0) - self.v * own.height;
67
- return translatePoseViaDescriptor(node.pose, x - own.x, y - own.y, geometry);
68
- };
121
+ function useStencilTool(stencil) {
122
+ return useMemo(() => stencilTool(stencil), [stencil]);
69
123
  }
70
- var DIAGRAM_GROUP = "diagram:group";
71
- var DIAGRAM_ANCHOR = "diagram:anchor";
72
- var GROUP_DERIVE_POSE = groupDerivePose();
73
- var ANCHOR_DERIVE_POSE = anchorDerivePose();
74
- var NAMED = { start: 0, mid: 0.5, end: 1 };
75
- function diagramLabelOf(node) {
76
- const data = node.data;
77
- if (data === null || typeof data !== "object") return null;
78
- const trait = data.diagram;
79
- if (trait === null || typeof trait !== "object") return null;
80
- const label = trait.label;
81
- if (label === null || typeof label !== "object") return null;
82
- return "at" in label ? label : null;
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
+ });
83
182
  }
84
- function labelDerivePose(opts = {}) {
85
- const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
86
- return (node, deps) => {
87
- const label = diagramLabelOf(node);
88
- if (label === null) return null;
89
- const path = deps[0]?.path ?? null;
90
- if (path === null) return null;
91
- const t = typeof label.at === "number" ? label.at : NAMED[label.at] ?? 0.5;
92
- const station = pointAlongPath(path, t);
93
- if (station === null) return null;
94
- const bounds = geometry.getBounds(node.pose);
95
- const nx = station.tangent.y;
96
- const ny = -station.tangent.x;
97
- const offset = label.offset ?? 0;
98
- const reach = offset + Math.sign(offset) * (Math.abs(nx) * bounds.width + Math.abs(ny) * bounds.height) / 2;
99
- const x = station.point.x + nx * reach;
100
- const y = station.point.y + ny * reach;
101
- return translatePoseViaDescriptor(
102
- node.pose,
103
- x - (bounds.x + bounds.width / 2),
104
- y - (bounds.y + bounds.height / 2),
105
- geometry
106
- );
107
- };
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 };
108
188
  }
109
- var DIAGRAM_LABEL = "diagram:label";
110
- var LABEL_DERIVE_POSE = labelDerivePose();
111
-
112
- // src/selfLoop.ts
113
- var dot = (a, b) => a.x * b.x + a.y * b.y;
114
- function selfLoop(ports, standoff) {
115
- const facing = ports.filter((p) => p.normal !== null);
116
- const most = (dir, among) => among.reduce(
117
- (best, p) => best === void 0 || dot(p.normal, dir) > dot(best.normal, dir) ? p : best,
118
- void 0
119
- );
120
- const from = most({ x: 1, y: 0 }, facing);
121
- if (from === void 0) return null;
122
- const to = most({ x: 0, y: -1 }, facing.filter((p) => Math.abs(dot(p.normal, from.normal)) < 0.5));
123
- if (to === void 0) return null;
124
- const out = { x: from.point.x + from.normal.x * standoff, y: from.point.y + from.normal.y * standoff };
125
- const reach = dot({ x: to.point.x - out.x, y: to.point.y - out.y }, to.normal) + standoff;
126
- return { from, to, via: { x: out.x + to.normal.x * reach, y: out.y + to.normal.y * reach } };
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 } : {} };
127
194
  }
128
-
129
- // src/edge.ts
130
- var straight = (req) => [req.from.point, ...req.waypoints, req.to.point];
131
- var orthogonal = (req) => {
132
- const stops = [req.from.point, ...req.waypoints, req.to.point];
133
- const out = [stops[0]];
134
- for (let i = 1; i < stops.length; i++) {
135
- const a = out[out.length - 1];
136
- const b = stops[i];
137
- if (a.x === b.x || a.y === b.y) {
138
- out.push(b);
139
- continue;
140
- }
141
- const leaveHorizontal = i === 1 ? isHorizontal(req.from.normal) : out.length > 1 && out[out.length - 2].y === a.y;
142
- const arriving = i === stops.length - 1 ? req.to.normal : null;
143
- const arriveHorizontal = arriving == null ? !leaveHorizontal : isHorizontal(arriving);
144
- if (leaveHorizontal !== arriveHorizontal) {
145
- out.push(leaveHorizontal ? { x: b.x, y: a.y } : { x: a.x, y: b.y });
146
- out.push(b);
147
- continue;
148
- }
149
- const prev = out.length > 1 ? out[out.length - 2] : null;
150
- if (leaveHorizontal) {
151
- const mid = (a.x + b.x) / 2;
152
- if (prev !== null && (mid - a.x) * (a.x - prev.x) < 0) out.push({ x: a.x, y: b.y }, b);
153
- else out.push({ x: mid, y: a.y }, { x: mid, y: b.y }, b);
154
- } else {
155
- const mid = (a.y + b.y) / 2;
156
- if (prev !== null && (mid - a.y) * (a.y - prev.y) < 0) out.push({ x: b.x, y: a.y }, b);
157
- else out.push({ x: a.x, y: mid }, { x: b.x, y: mid }, b);
158
- }
159
- }
160
- return out;
161
- };
162
- function isHorizontal(normal) {
163
- return Math.abs(normal?.x ?? 1) >= Math.abs(normal?.y ?? 0);
164
- }
165
- var bezier = (req) => {
166
- const stops = [req.from.point, ...req.waypoints, req.to.point];
167
- const out = [stops[0]];
168
- for (let i = 1; i < stops.length; i++) {
169
- const a = stops[i - 1];
170
- const b = stops[i];
171
- const outN = i === 1 ? req.from.normal : through(stops, i - 1);
172
- const inN = i === stops.length - 1 ? req.to.normal : negated(through(stops, i));
173
- const leg = portCurvePoints(
174
- a.x,
175
- a.y,
176
- b.x,
177
- b.y,
178
- outN == null ? null : [outN.x, outN.y],
179
- inN == null ? null : [inN.x, inN.y],
180
- BEZIER_SAMPLES
181
- );
182
- for (const [x, y] of leg) out.push({ x, y });
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) } });
183
210
  }
184
211
  return out;
185
- };
186
- var BEZIER_SAMPLES = 16;
187
- function through(stops, i) {
188
- const dx = stops[i + 1].x - stops[i - 1].x;
189
- const dy = stops[i + 1].y - stops[i - 1].y;
190
- const len = Math.hypot(dx, dy);
191
- return len === 0 ? null : { x: dx / len, y: dy / len };
192
- }
193
- var negated = (v) => v === null ? null : { x: -v.x, y: -v.y };
194
- var ROUTERS = Object.freeze({
195
- straight,
196
- orthogonal,
197
- bezier
198
- });
199
- function diagramEdgeOf(node) {
200
- const data = node.data;
201
- if (data === null || typeof data !== "object") return null;
202
- const edge = data.diagram;
203
- if (edge === null || typeof edge !== "object") return null;
204
- return "from" in edge && "to" in edge ? edge : null;
205
- }
206
- function resolveEnd(end, dep, toward, opts, allPorts) {
207
- if (end.port !== void 0) {
208
- const named = portOf(dep.node, dep.pose, end.port, opts);
209
- if (named !== void 0) return named;
210
- }
211
- let best;
212
- let bestScore = -Infinity;
213
- for (const port of allPorts(dep)) {
214
- const dx = toward.x - port.point.x;
215
- const dy = toward.y - port.point.y;
216
- const len = Math.hypot(dx, dy) || 1;
217
- const facing = port.normal === null ? 0 : (port.normal.x * dx + port.normal.y * dy) / len;
218
- const score = facing - len / 1e6;
219
- if (score > bestScore) {
220
- bestScore = score;
221
- best = port;
222
- }
223
- }
224
- return best;
225
212
  }
226
- function edgeDerivePath(opts = {}) {
227
- const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
228
- const portOpts = { geometry, ...opts.read ? { read: opts.read } : {} };
229
- const allPorts = (d) => portsOf(d.node, d.pose, portOpts);
230
- const centerOf = (d) => {
231
- const b = geometry.getBounds(d.pose);
232
- return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
233
- };
234
- const standoffOf = (d) => {
235
- const b = geometry.getBounds(d.pose);
236
- return Math.min(b.width, b.height) / 2;
237
- };
238
- return (node, deps) => {
239
- const edge = diagramEdgeOf(node);
240
- if (edge === null) return null;
241
- const [fromDep, toDep] = deps;
242
- if (fromDep === void 0 || toDep === void 0) return null;
243
- const loop = fromDep.node.id === toDep.node.id && edge.from.port === void 0 && edge.to.port === void 0 ? selfLoop(allPorts(fromDep), opts.selfLoopStandoff ?? standoffOf(fromDep)) : null;
244
- const from = loop?.from ?? resolveEnd(edge.from, fromDep, centerOf(toDep), portOpts, allPorts);
245
- const to = loop?.to ?? resolveEnd(edge.to, toDep, centerOf(fromDep), portOpts, allPorts);
246
- if (from === void 0 || to === void 0) return null;
247
- const authored = edge.waypoints ?? [];
248
- const waypoints = loop !== null && authored.length === 0 ? [loop.via] : authored;
249
- const routers = opts.routers ?? ROUTERS;
250
- const router = edge.router !== void 0 && routers[edge.router] || straight;
251
- const points = router({ from, to, waypoints });
252
- return points.length < 2 ? null : polylineFromPoints(points);
253
- };
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;
254
219
  }
255
- var DIAGRAM_EDGE = "diagram:edge";
256
- var EDGE_DERIVE_PATH = edgeDerivePath();
257
- function withDiagramRegistry(registry = {}) {
220
+ function waypointFollower(scene, geometry = AUTO_POSE_DESCRIPTOR) {
221
+ const sc = scene;
222
+ const geo = geometry;
258
223
  return {
259
- ...registry,
260
- derivePath: {
261
- [DIAGRAM_EDGE]: EDGE_DERIVE_PATH,
262
- ...registry.derivePath
263
- },
264
- derivePose: {
265
- [DIAGRAM_LABEL]: LABEL_DERIVE_POSE,
266
- [DIAGRAM_GROUP]: GROUP_DERIVE_POSE,
267
- [DIAGRAM_ANCHOR]: ANCHOR_DERIVE_POSE,
268
- ...registry.derivePose
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];
269
235
  }
270
236
  };
271
237
  }
272
238
 
273
- // src/portAffordance.ts
274
- var PORT_LAYER_ID = "diagram-ports";
275
- var PORT_AFFORDANCE_KIND = `layer:${PORT_LAYER_ID}`;
276
- var DEFAULT_HIT_PX = 8;
277
- var DEFAULT_SIZE_PX = 7;
278
- function portScratchOf(hit) {
279
- const payload = hit?.payload;
280
- return payload !== void 0 && typeof payload.portId === "string" ? payload : null;
281
- }
282
- function createPortAffordance(participants, opts = {}) {
283
- const {
284
- id = PORT_LAYER_ID,
285
- hitRadiusPx = DEFAULT_HIT_PX,
286
- sizePx = DEFAULT_SIZE_PX,
287
- strength = "exclusive"
288
- } = opts;
289
- const portOpts = {
290
- ...opts.geometry ? { geometry: opts.geometry } : {},
291
- ...opts.read ? { read: opts.read } : {}
292
- };
293
- const paint = sizePx <= 0 ? void 0 : {
294
- kind: "square",
295
- sizePx,
296
- ...opts.fill ? { fill: opts.fill } : {},
297
- ...opts.stroke ? { stroke: opts.stroke } : {}
298
- };
299
- return {
300
- id,
301
- regions(state) {
302
- const out = [];
303
- for (const { node, pose } of participants()) {
304
- if (opts.shows !== void 0 && !opts.shows(node, state)) continue;
305
- for (const port of portsOf(node, pose, portOpts)) {
306
- out.push({
307
- id: `${node.id}:${port.id}`,
308
- targetId: null,
309
- shape: { kind: "point", x: port.point.x, y: port.point.y, hitRadiusPx },
310
- ...paint ? { paint } : {},
311
- ...opts.cursor !== void 0 ? { cursor: opts.cursor } : {},
312
- strength,
313
- claimedKinds: ["pointer"],
314
- bind: () => ({
315
- initialScratch: {
316
- targetId: node.id,
317
- nodeId: node.id,
318
- portId: port.id,
319
- port
320
- }
321
- })
322
- });
323
- }
324
- }
325
- return out;
326
- }
327
- };
328
- }
239
+ // src/passOrder.ts
240
+ var DIAGRAM_PASS_ORDER = { offPage: 100, avoid: 200, labels: 300, hops: 400 };
329
241
 
330
- // src/connect.ts
331
- var CONNECT_ACTION_ID = "diagram.connect";
332
- var defaultCanConnect = (from, to) => {
333
- if (from.nodeId === to.nodeId && from.id === to.id) return false;
334
- if (from.type === void 0 || to.type === void 0) return true;
335
- return from.type === to.type;
336
- };
337
- var DEFAULT_SNAP = 16;
338
- var DEFAULT_EDGE_STROKE = {
339
- paint: { color: "#7ba7c7" },
340
- width: 2,
341
- markerEnd: "arrow"
342
- };
343
- var DEFAULT_ROUTER = "straight";
344
- function connectBinding(actionId = CONNECT_ACTION_ID) {
345
- return {
346
- spec: { kind: "drag", target: `affordance:${PORT_AFFORDANCE_KIND}` },
347
- actionId
348
- };
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 } };
349
246
  }
350
- var GRAB_PORT_ACTION_ID = "diagram.grabPort";
351
- function grabPortAction(id = GRAB_PORT_ACTION_ID) {
352
- return {
353
- id,
354
- label: "Grab port",
355
- group: "diagram",
356
- invoker: { timing: "immediate", run: () => {
357
- } },
358
- enabled: () => true
359
- };
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;
360
258
  }
361
- function grabPortBindings(actionId = GRAB_PORT_ACTION_ID) {
362
- return [
363
- { spec: { kind: "pointerDown", target: `affordance:${PORT_AFFORDANCE_KIND}` }, actionId },
364
- { spec: { kind: "click", target: `affordance:${PORT_AFFORDANCE_KIND}` }, actionId }
365
- ];
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 };
366
264
  }
367
- function commitEdgeToScene(edge, ctx) {
368
- const scene = ctx.deps.scene;
369
- if (scene === void 0) return;
370
- const trait = {
371
- from: { port: edge.from.id },
372
- to: { port: edge.to.id },
373
- router: edge.router
374
- };
375
- const source = scene.get(edge.from.nodeId);
376
- scene.add({
377
- kind: "leaf",
378
- layer: source?.layer ?? scene.layers[0].id,
379
- pose: { x: 0, y: 0, width: 0, height: 0 },
380
- data: { diagram: trait, stroke: DEFAULT_EDGE_STROKE },
381
- dependsOn: [edge.from.nodeId, edge.to.nodeId],
382
- // Without this the edge re-resolves on every dependency move and paints
383
- // nothing. `EDGE_DERIVE_PATH` rather than a fresh `edgeDerivePath()` so the
384
- // scene can find its registry key and the edge survives `toJSON`.
385
- derivePath: EDGE_DERIVE_PATH
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 };
386
270
  });
271
+ return { obstacles: input.obstacles, edges };
387
272
  }
388
- function createConnectAction(opts) {
389
- const {
390
- id = CONNECT_ACTION_ID,
391
- canConnect = defaultCanConnect,
392
- snapRadius = DEFAULT_SNAP,
393
- router: routerKey = DEFAULT_ROUTER,
394
- commit = commitEdgeToScene
395
- } = opts;
396
- const routers = opts.routers ?? ROUTERS;
397
- const portOpts = {
398
- ...opts.geometry ? { geometry: opts.geometry } : {},
399
- ...opts.read ? { read: opts.read } : {}
400
- };
401
- const route = routers[routerKey] ?? routers["straight"] ?? ROUTERS["straight"];
402
- function targetPort(from, at) {
403
- let best = null;
404
- let bestD2 = snapRadius * snapRadius;
405
- for (const { node, pose } of opts.participants()) {
406
- for (const port of portsOf(node, pose, portOpts)) {
407
- if (!canConnect(from, port)) continue;
408
- const d2 = (port.point.x - at.x) ** 2 + (port.point.y - at.y) ** 2;
409
- if (d2 <= bestD2) {
410
- bestD2 = d2;
411
- best = port;
412
- }
413
- }
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;
414
285
  }
415
- return best;
416
- }
417
- function endAt(from, at) {
418
- const port = targetPort(from, at);
419
- return {
420
- port,
421
- end: port ?? { id: "", nodeId: "", point: at, normal: null }
422
- };
423
- }
286
+ return true;
287
+ };
424
288
  return {
425
- id,
426
- label: "Connect",
427
- group: "diagram",
428
- requires: ["scene"],
429
- invoker: {
430
- timing: "ongoing",
431
- start(ctx) {
432
- const pressed = portScratchOf(ctx.drag?.affordance);
433
- if (pressed === null) return {};
434
- const scratch = {
435
- from: pressed.port,
436
- current: { x: ctx.world.x, y: ctx.world.y },
437
- open: true,
438
- deps: ctx.deps
439
- };
440
- return {
441
- kind: "diagram-connect",
442
- onMove(moveCtx) {
443
- scratch.current = { x: moveCtx.world.x, y: moveCtx.world.y };
444
- },
445
- overlay() {
446
- if (!scratch.open) return null;
447
- const { end } = endAt(scratch.from, scratch.current);
448
- const points = route({ from: scratch.from, to: end, waypoints: [] });
449
- if (points.length < 2) return null;
450
- return { kind: "polyline", points, role: "connector" };
451
- },
452
- onEnd(endCtx, reason) {
453
- scratch.open = false;
454
- if (reason === "cancel") return;
455
- const { port } = endAt(scratch.from, { x: endCtx.world.x, y: endCtx.world.y });
456
- if (port === null) return;
457
- commit(
458
- { from: scratch.from, to: port, router: routerKey },
459
- { ...endCtx, deps: scratch.deps }
460
- );
461
- }
462
- };
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) });
463
301
  }
302
+ last = { frame, out };
303
+ return out;
464
304
  },
465
- enabled: () => true
305
+ finish: (ops) => [...ops]
466
306
  };
467
307
  }
468
- function sceneParticipants(scene) {
469
- return function* () {
470
- for (const node of scene.renderOrderNodes()) {
471
- yield {
472
- node,
473
- pose: effectivePose(scene, node)
474
- };
475
- }
476
- };
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 };
477
318
  }
478
- function portLayer(participants, opts = {}) {
479
- return composeAffordanceLayer(
480
- opts.id ?? PORT_LAYER_ID,
481
- "Diagram ports",
482
- [createPortAffordance(participants, opts)]
483
- );
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);
484
333
  }
485
334
 
486
- // src/contribution.ts
487
- function createDiagramContribution(opts) {
488
- const actionId = opts.id ?? CONNECT_ACTION_ID;
335
+ // src/hops/follower.ts
336
+ function hopFollower(scene) {
337
+ const sc = scene;
489
338
  return {
490
- id: "weasel-diagram",
491
- eligibility: { claimed: true },
492
- actions: [createConnectAction({ ...opts, id: actionId }), grabPortAction()],
493
- bindings: [connectBinding(actionId), ...grabPortBindings()]
494
- };
495
- }
496
- function diagramPorts(opts) {
497
- return {
498
- layer: portLayer(opts.participants, opts),
499
- contribution: createDiagramContribution(opts)
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]
500
358
  };
501
359
  }
502
360
 
503
- // src/groupTree.ts
504
- function nestGroups(declared, isNode) {
505
- const groupIds = new Set(declared.map((g) => g.id));
506
- const parentOf = /* @__PURE__ */ new Map();
507
- const claimedNodes = /* @__PURE__ */ new Set();
508
- const direct = /* @__PURE__ */ new Map();
509
- const isAncestorOrSelf = (candidate, of) => {
510
- for (let at = of; at !== void 0; at = parentOf.get(at)) {
511
- if (at === candidate) return true;
512
- }
513
- return false;
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));
514
401
  };
515
- for (const group of declared) {
516
- const own = [];
517
- for (const id of group.members) {
518
- if (isNode(id)) {
519
- if (claimedNodes.has(id)) continue;
520
- claimedNodes.add(id);
521
- own.push(id);
522
- } else if (groupIds.has(id) && !parentOf.has(id) && !isAncestorOrSelf(id, group.id)) {
523
- parentOf.set(id, group.id);
524
- own.push(id);
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);
525
426
  }
526
427
  }
527
- direct.set(group.id, own);
528
- }
529
- const flat = /* @__PURE__ */ new Map();
530
- const flatten = (id) => {
531
- const known = flat.get(id);
532
- if (known !== void 0) return known;
533
- const out = [];
534
- for (const member of direct.get(id)) {
535
- if (groupIds.has(member)) out.push(...flatten(member));
536
- else out.push(member);
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);
537
433
  }
538
- flat.set(id, out);
539
- return out;
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;
540
444
  };
541
- const live = new Set(declared.filter((g) => flatten(g.id).length > 0).map((g) => g.id));
542
- return declared.filter((g) => live.has(g.id)).map((g) => {
543
- const parent = parentOf.get(g.id);
544
- return {
545
- id: g.id,
546
- direct: direct.get(g.id).filter((id) => !groupIds.has(id) || live.has(id)),
547
- members: flat.get(g.id),
548
- ...parent !== void 0 ? { parent } : {}
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;
549
462
  };
550
- });
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 };
551
481
  }
552
482
 
553
- // src/graph.ts
554
- function buildGraph(source, opts = {}) {
555
- const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR$1;
556
- const nodes = [];
557
- const byId = /* @__PURE__ */ new Map();
558
- const candidates = [];
559
- const groupNodes = [];
560
- for (const { node, pose } of source()) {
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 : [];
561
497
  if (diagramEdgeOf(node) !== null) {
562
- const deps = node.dependsOn;
563
- if (Array.isArray(deps) && deps.length === 2) {
564
- candidates.push({ id: node.id, from: deps[0], to: deps[1] });
565
- }
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 });
566
501
  continue;
567
502
  }
568
- const trait = diagramNodeOf(node, opts.read);
569
- if (trait === null) continue;
570
- if (trait.group !== void 0) {
571
- const deps = node.dependsOn;
572
- groupNodes.push({
573
- id: node.id,
574
- members: Array.isArray(deps) ? deps : [],
575
- inset: insetOf(trait.group.inset),
576
- ...trait.group.minWidth !== void 0 ? { minWidth: trait.group.minWidth } : {}
577
- });
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 });
578
507
  continue;
579
508
  }
580
- const entry = {
581
- id: node.id,
582
- bounds: geometry.getBounds(pose),
583
- pinned: trait.pinned === true
584
- };
585
- nodes.push(entry);
586
- byId.set(entry.id, entry);
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)) });
587
511
  }
588
- const declared = new Map(groupNodes.map((g) => [g.id, g]));
589
- const live = nestGroups(groupNodes, (id) => byId.has(id)).map((g) => {
590
- for (const id of g.direct) {
591
- const node = byId.get(id);
592
- if (node !== void 0) node.group = g.id;
593
- }
594
- return {
595
- id: g.id,
596
- members: g.members,
597
- inset: declared.get(g.id).inset,
598
- ...declared.get(g.id).minWidth !== void 0 ? { minWidth: declared.get(g.id).minWidth } : {},
599
- ...g.parent !== void 0 ? { parent: g.parent } : {}
600
- };
601
- });
602
- const known = (id) => byId.has(id) || live.some((g) => g.id === id);
603
- const edges = expandGroupEdges(candidates.filter((e) => known(e.from) && known(e.to)), live);
604
- const out = /* @__PURE__ */ new Map();
605
- const into = /* @__PURE__ */ new Map();
606
- for (const edge of edges) {
607
- push(out, edge.from, edge);
608
- push(into, edge.to, edge);
609
- }
610
- const NONE = Object.freeze([]);
611
- return {
612
- nodes,
613
- edges,
614
- groups: live,
615
- node: (id) => byId.get(id),
616
- outgoing: (id) => out.get(id) ?? NONE,
617
- incoming: (id) => into.get(id) ?? NONE
618
- };
619
- }
620
- function push(index, key, edge) {
621
- const list = index.get(key);
622
- if (list === void 0) index.set(key, [edge]);
623
- else list.push(edge);
512
+ return out;
624
513
  }
625
-
626
- // src/dataGroups.ts
627
- function resolveGroups(groups, ids, nodeIds, opts) {
628
- for (const group of groups) {
629
- if (ids.has(group.id)) throw new Error(`diagramScene: group id "${group.id}" is already a node or group id`);
630
- ids.add(group.id);
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;
631
522
  }
632
- const declared = new Map(groups.map((g) => [g.id, g]));
633
- const nested = nestGroups(groups, (id) => nodeIds.has(id));
634
- const depthOf = /* @__PURE__ */ new Map();
635
- const byId = new Map(nested.map((g) => [g.id, g]));
636
- const depth = (id) => {
637
- const parent = byId.get(id).parent;
638
- let d = depthOf.get(id);
639
- if (d === void 0) depthOf.set(id, d = parent === void 0 ? 0 : depth(parent) + 1);
640
- return d;
641
- };
642
- return nested.map((g, i) => ({ g, i })).sort((a, b) => depth(a.g.id) - depth(b.g.id) || a.i - b.i).map(({ g }) => {
643
- const group = declared.get(g.id);
644
- const pad = group.padding ?? opts.padding;
645
- const title = group.label ? opts.title(group.label) : null;
646
- const inset = { top: pad + (title ? title.height + pad / 2 : 0), right: pad, bottom: pad, left: pad };
647
- return {
648
- group,
649
- direct: g.direct,
650
- members: g.members,
651
- inset,
652
- pad,
653
- title,
654
- ...title ? { minWidth: title.width + 2 * pad } : {},
655
- ...g.parent !== void 0 ? { parent: g.parent } : {}
656
- };
657
- });
523
+ return false;
658
524
  }
659
- function groupBoxes(groups, placedAt) {
660
- const byId = new Map(groups.map((g) => [g.group.id, g]));
661
- const boxes = /* @__PURE__ */ new Map();
662
- const boxOf = (id) => {
663
- const known = boxes.get(id);
664
- if (known !== void 0) return known;
665
- const g = byId.get(id);
666
- const rs = g.direct.map((m) => byId.has(m) ? boxOf(m) : placedAt.get(m));
667
- const x = Math.min(...rs.map((r) => r.x));
668
- const y = Math.min(...rs.map((r) => r.y));
669
- const box = boxAround({
670
- x,
671
- y,
672
- width: Math.max(...rs.map((r) => r.x + r.width)) - x,
673
- height: Math.max(...rs.map((r) => r.y + r.height)) - y
674
- }, g);
675
- boxes.set(id, box);
676
- return box;
677
- };
678
- for (const g of groups) boxOf(g.group.id);
679
- return boxes;
680
- }
681
- function groupSpecs(groups, placedAt, styleOf) {
682
- const boxes = groupBoxes(groups, placedAt);
683
- const specs = [];
684
- for (const g of groups) {
685
- const { style, text } = styleOf(g.group);
686
- const { x, y, width, height } = boxes.get(g.group.id);
687
- specs.push({
688
- id: g.group.id,
689
- kind: "leaf",
690
- layer: "main",
691
- pose: { x, y, width, height },
692
- data: {
693
- diagram: { outline: "rect", group: { inset: g.inset, ...g.minWidth !== void 0 ? { minWidth: g.minWidth } : {} } },
694
- fill: { paint: { color: style.fill } },
695
- stroke: {
696
- paint: { color: style.stroke },
697
- width: style.strokeWidth ?? 1,
698
- ...style.dash ? { dash: style.dash } : {}
699
- }
700
- },
701
- dependsOn: g.direct,
702
- derivePose: GROUP_DERIVE_POSE
703
- });
704
- if (g.title === null || !g.group.label) continue;
705
- const anchor = { at: { u: 0, v: 0 }, offset: { x: g.pad, y: g.pad } };
706
- specs.push({
707
- id: `${g.group.id}/label`,
708
- kind: "leaf",
709
- layer: "main",
710
- pickable: false,
711
- pose: { x: x + g.pad, y: y + g.pad, width: g.title.width, height: g.title.height },
712
- data: { diagram: { anchor }, text: g.group.label, style: text, fill: { paint: { color: style.text } } },
713
- dependsOn: [g.group.id],
714
- derivePose: ANCHOR_DERIVE_POSE
715
- });
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) ?? []);
716
535
  }
717
- return specs;
718
- }
719
-
720
- // src/route.ts
721
- var FACES = {
722
- down: { out: "s", in: "n" },
723
- up: { out: "n", in: "s" },
724
- right: { out: "e", in: "w" },
725
- left: { out: "w", in: "e" }
726
- };
727
- function layeredRoutes(input) {
728
- const { graph, boxes, members, compass, clearance } = input;
729
- const direction = input.direction ?? "down";
730
- const axes = axesFor(direction);
731
- const faces = FACES[direction];
732
- const ranks = ranksOf(graph, backEdges(graph));
733
- const span = (id) => {
734
- const ids = members.get(id) ?? [id];
735
- const rs = ids.map((m) => ranks.get(m)).filter((r) => r !== void 0);
736
- return rs.length === 0 ? null : [Math.min(...rs), Math.max(...rs)];
737
- };
738
- const lo = (r, axis) => axis === "x" ? r.x : r.y;
739
- const hi = (r, axis) => axis === "x" ? r.x + r.width : r.y + r.height;
740
- const mid = (r, axis) => (lo(r, axis) + hi(r, axis)) / 2;
741
- const byRank = /* @__PURE__ */ new Map();
742
- for (const node of graph.nodes) {
743
- const rank = ranks.get(node.id);
744
- const box = boxes.get(node.id);
745
- if (rank === void 0 || box === void 0) continue;
746
- byRank.set(rank, [...byRank.get(rank) ?? [], box]);
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);
747
542
  }
748
- const routes = /* @__PURE__ */ new Map();
749
- for (const edge of input.edges) {
750
- const from = span(edge.from);
751
- const to = span(edge.to);
752
- const a = boxes.get(edge.from);
753
- const b = boxes.get(edge.to);
754
- if (from === null || to === null || a === void 0 || b === void 0 || from[1] >= to[0]) continue;
755
- const route = {};
756
- if (compass.has(edge.from)) route.fromPort = faces.out;
757
- if (compass.has(edge.to)) route.toPort = faces.in;
758
- const near = axes.sign === 1 ? hi : lo;
759
- const far = axes.sign === 1 ? lo : hi;
760
- const start = { cross: mid(a, axes.cross), rank: near(a, axes.rank) };
761
- const end = { cross: mid(b, axes.cross), rank: far(b, axes.rank) };
762
- const ends = /* @__PURE__ */ new Set([edge.from, edge.to, ...members.get(edge.from) ?? [], ...members.get(edge.to) ?? []]);
763
- const groupsInWay = [...members].filter(([g, ms]) => !ends.has(g) && !ms.some((m) => ends.has(m)));
764
- const stops = [];
765
- let blocked = false;
766
- for (let k = from[1] + 1; k < to[0]; k++) {
767
- const row = byRank.get(k) ?? [];
768
- if (row.length === 0) continue;
769
- const bandLo = Math.min(...row.map((r) => lo(r, axes.rank)));
770
- const bandHi = Math.max(...row.map((r) => hi(r, axes.rank)));
771
- const obstacles = [...row];
772
- for (const [g] of groupsInWay) {
773
- const box = boxes.get(g);
774
- if (box !== void 0 && lo(box, axes.rank) < bandHi && hi(box, axes.rank) > bandLo) obstacles.push(box);
775
- }
776
- const t = ((bandLo + bandHi) / 2 - start.rank) / (end.rank - start.rank);
777
- const ideal = start.cross + (end.cross - start.cross) * t;
778
- const cross = freeAt(ideal, obstacles.map((r) => [lo(r, axes.cross) - clearance, hi(r, axes.cross) + clearance]));
779
- if (cross !== ideal) blocked = true;
780
- const [first, last] = axes.sign === 1 ? [bandLo, bandHi] : [bandHi, bandLo];
781
- stops.push({ cross, rank: first }, { cross, rank: last });
782
- }
783
- if (blocked) {
784
- route.waypoints = stops.map((s) => axes.rank === "y" ? { x: s.cross, y: s.rank } : { x: s.rank, y: s.cross });
785
- }
786
- if (route.fromPort !== void 0 || route.toPort !== void 0 || route.waypoints !== void 0) {
787
- routes.set(edge.id, route);
788
- }
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) ?? []);
789
547
  }
790
- return routes;
791
- }
792
- function authoredOver(edges, input) {
793
- const routes = layeredRoutes({ ...input, edges });
794
- return new Map(edges.map((e) => {
795
- const r = routes.get(e.id);
796
- const fromPort = e.fromPort ?? r?.fromPort;
797
- const toPort = e.toPort ?? r?.toPort;
798
- const waypoints = e.waypoints ?? r?.waypoints;
799
- return [e.id, { ...fromPort ? { fromPort } : {}, ...toPort ? { toPort } : {}, ...waypoints ? { waypoints } : {} }];
800
- }));
548
+ return { touched: touched2, edges, routes, shapes };
801
549
  }
802
- function freeAt(at, spans) {
803
- const sorted = [...spans].sort((p, q) => p[0] - q[0]);
804
- const merged = [];
805
- for (const [s, e] of sorted) {
806
- const top = merged[merged.length - 1];
807
- if (top !== void 0 && s < top[1]) top[1] = Math.max(top[1], e);
808
- else merged.push([s, e]);
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));
809
558
  }
810
- for (const [s, e] of merged) {
811
- if (at > s && at < e) return at - s <= e - at ? s : e;
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);
812
569
  }
813
- return at;
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;
814
577
  }
815
-
816
- // src/fromData.ts
817
- var FONT = { fontFamily: "sans-serif", fontSize: 12 };
818
- var NODE = { fill: "#16222c", stroke: "#7ba7c7", text: "#dbe7f2" };
819
- var EDGE = { stroke: "#7ba7c7" };
820
- var GROUP = { fill: "rgba(123, 167, 199, 0.08)", stroke: "#7ba7c7", text: "#9fbdd6", strokeWidth: 1, dash: [4, 3] };
821
- var LABEL_PLACEMENT = { at: "mid", offset: 9 };
822
- var LINE = 1.4;
823
- var GLYPH = 0.6;
824
- var MIN_WIDTH = 64;
825
- var LAYOUTS = { layered, tree, force };
826
- var edgeIdOf = (e) => `edge:${e.from}${e.fromPort ? `.${e.fromPort}` : ""}->${e.to}${e.toPort ? `.${e.toPort}` : ""}${e.label ? `:${e.label}` : ""}`;
827
- function graphOf(nodes, edges, groups = []) {
828
- const byId = new Map(nodes.map((n) => [n.id, n]));
829
- const out = /* @__PURE__ */ new Map();
830
- const inc = /* @__PURE__ */ new Map();
831
- for (const e of edges) {
832
- out.set(e.from, [...out.get(e.from) ?? [], e]);
833
- inc.set(e.to, [...inc.get(e.to) ?? [], e]);
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" }));
834
601
  }
835
- return {
836
- nodes,
837
- edges,
838
- groups,
839
- node: (id) => byId.get(id),
840
- outgoing: (id) => out.get(id) ?? [],
841
- incoming: (id) => inc.get(id) ?? []
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)]))
842
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 };
843
618
  }
844
- function routed(data, edges, graph, groups, placedAt, layoutOptions) {
845
- return authoredOver(edges.map((e) => ({ ...e, id: edgeIdOf(e) })), {
846
- graph,
847
- boxes: new Map([...placedAt, ...groupBoxes(groups, placedAt)]),
848
- members: new Map(groups.map((g) => [g.group.id, g.members])),
849
- compass: /* @__PURE__ */ new Set([...data.nodes.filter((n) => n.ports === void 0).map((n) => n.id), ...groups.map((g) => g.group.id)]),
850
- clearance: (layoutOptions.nodeGap ?? DEFAULT_NODE_GAP) / 2,
851
- ...layoutOptions.direction ? { direction: layoutOptions.direction } : {}
852
- });
619
+ function useLabelPlacement(scene, opts = {}) {
620
+ const latest = useLatest(opts);
621
+ useEffect(() => installLabelPlacement(scene, latest.current).dispose, [scene, latest]);
853
622
  }
854
- function placed(data, seeds, moved) {
855
- return new Map(data.nodes.map((n, i) => {
856
- const seed = seeds[i].bounds;
857
- const at = moved.get(n.id) ?? seed;
858
- return [n.id, { x: at.x, y: at.y, width: seed.width, height: seed.height }];
859
- }));
623
+ function documentBoundsOf(scene, node, geometry = AUTO_POSE_DESCRIPTOR) {
624
+ return geometry.getBounds(documentPose(scene, node));
860
625
  }
861
- function diagramScene(data, opts = {}) {
862
- const font = opts.font ?? FONT;
863
- const styleOf = (s) => ({
864
- fontFamily: s?.fontFamily ?? font.fontFamily,
865
- fontSize: s?.fontSize ?? font.fontSize,
866
- ...s?.bold === true ? { fontWeight: 700 } : {}
867
- });
868
- const measure = opts.measure ? (text, s) => opts.measure(text, { ...s, fontFamily: s.fontFamily ?? font.fontFamily, fontSize: s.fontSize ?? font.fontSize }) : (text, s) => {
869
- const size = s.fontSize ?? font.fontSize;
870
- return { width: text.length * GLYPH * size, height: Math.ceil(LINE * size) };
871
- };
872
- const ids = /* @__PURE__ */ new Set();
873
- for (const n of data.nodes) {
874
- if (ids.has(n.id)) throw new Error(`diagramScene: node id "${n.id}" appears more than once`);
875
- ids.add(n.id);
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]);
876
638
  }
877
- const nodeIds = new Set(ids);
878
- const titleStyle = { ...font, fontSize: font.fontSize - 1 };
879
- const groups = resolveGroups(data.groups ?? [], ids, nodeIds, {
880
- padding: opts.groupPadding ?? DEFAULT_GROUP_INSET,
881
- title: (text) => measure(text, titleStyle)
882
- });
883
- const seen = /* @__PURE__ */ new Set();
884
- const edges = data.edges.filter((e) => {
885
- const id = edgeIdOf(e);
886
- if (seen.has(id) || !ids.has(e.from) || !ids.has(e.to)) return false;
887
- seen.add(id);
888
- return true;
889
- });
890
- const bodies = data.nodes.map((n) => ({
891
- outline: n.outline ?? opts.outline ?? "rect",
892
- rows: n.rows ?? (n.lines ?? []).map((text) => ({ kind: "label", text })),
893
- padding: n.padding ?? opts.padding ?? 8,
894
- gap: n.gap ?? opts.gap ?? 4,
895
- verticalAlign: n.verticalAlign ?? opts.verticalAlign ?? "center",
896
- ...n.ports ? { ports: n.ports } : {}
897
- }));
898
- const seeds = data.nodes.map((n, i) => {
899
- const pose = sizeToBody(
900
- { ...n.at ?? { x: i, y: i }, width: n.width ?? opts.minWidth ?? MIN_WIDTH, height: n.height ?? 0 },
901
- measureBody(bodies[i], measure)
902
- );
903
- return { id: n.id, bounds: pose, pinned: n.pinned === true };
904
- });
905
- const graphGroups = groups.map((g) => ({
906
- id: g.group.id,
907
- members: g.members,
908
- inset: g.inset,
909
- ...g.minWidth !== void 0 ? { minWidth: g.minWidth } : {},
910
- ...g.parent !== void 0 ? { parent: g.parent } : {}
911
- }));
912
- const seedOf = new Map(seeds.map((n) => [n.id, n]));
913
- for (const g of groups) {
914
- for (const m of g.direct) {
915
- const seed = seedOf.get(m);
916
- if (seed !== void 0) seed.group = g.group.id;
917
- }
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);
918
643
  }
919
- const graph = graphOf(
920
- seeds,
921
- expandGroupEdges(edges.map((e) => ({ id: edgeIdOf(e), from: e.from, to: e.to })), graphGroups),
922
- graphGroups
923
- );
924
- const layout = typeof opts.layout === "function" ? opts.layout : opts.layout === "none" ? null : LAYOUTS[opts.layout ?? "layered"];
925
- const layoutOptions = { order: "barycenter", ...opts.layoutOptions };
926
- const moved = layout === null ? /* @__PURE__ */ new Map() : layout(graph, layoutOptions);
927
- const specs = [];
928
- const placedAt = placed(data, seeds, moved);
929
- const routes = layout === layered ? routed(data, edges, graph, groups, placedAt, layoutOptions) : null;
930
- specs.push(...groupSpecs(groups, placedAt, (g) => ({
931
- style: opts.groupStyle?.(g) ?? GROUP,
932
- text: styleOf(titleStyle)
933
- })));
934
- for (const [i, n] of data.nodes.entries()) {
935
- const seed = seeds[i].bounds;
936
- const at = placedAt.get(n.id);
937
- const style = opts.nodeStyle?.(n) ?? NODE;
938
- const textData = (text, s) => ({ text, style: styleOf(s), fill: { paint: { color: style.text } } });
939
- const built = buildBody(
940
- bodies[i],
941
- { x: at.x, y: at.y, width: seed.width, height: seed.height },
942
- {
943
- id: n.id,
944
- layer: "main",
945
- measure,
946
- body: (trait) => ({
947
- diagram: n.pinned === true ? { ...trait, pinned: true } : trait,
948
- fill: { paint: { color: style.fill } },
949
- stroke: {
950
- paint: { color: style.stroke },
951
- width: style.strokeWidth ?? 2,
952
- ...style.dash ? { dash: style.dash } : {}
953
- }
954
- }),
955
- row: (text, box) => text === "" ? null : textData(text, box.row.kind === "label" || box.row.kind === "field" ? box.row.style : void 0),
956
- portLabel: (text) => textData(text, void 0)
957
- }
958
- );
959
- for (const [k, s] of built.specs.entries()) {
960
- specs.push({ ...s, ...k === 0 ? {} : { id: `${n.id}/${k}` } });
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;
961
656
  }
962
- }
963
- for (const e of edges) {
964
- const id = edgeIdOf(e);
965
- const { fromPort, toPort, waypoints } = routes?.get(id) ?? e;
966
- const style = opts.edgeStyle?.(e) ?? EDGE;
967
- const markerStart = style.markerStart ?? null;
968
- const markerEnd = style.markerEnd === void 0 ? "arrow" : style.markerEnd;
969
- specs.push({
970
- id,
971
- kind: "leaf",
972
- layer: "main",
973
- pickable: false,
974
- pose: { x: 0, y: 0, width: 0, height: 0 },
975
- data: {
976
- diagram: {
977
- from: fromPort ? { port: fromPort } : {},
978
- to: toPort ? { port: toPort } : {},
979
- router: e.router ?? opts.router ?? "bezier",
980
- ...waypoints ? { waypoints } : {}
981
- },
982
- stroke: {
983
- paint: { color: style.stroke },
984
- width: style.width ?? 1.5,
985
- ...style.dash ? { dash: style.dash } : {},
986
- ...markerStart !== null ? { markerStart } : {},
987
- ...markerEnd !== null ? { markerEnd } : {}
988
- }
989
- },
990
- dependsOn: [e.from, e.to],
991
- derivePath: EDGE_DERIVE_PATH
992
- });
993
- if (e.label) {
994
- const labelStyle = { ...font, fontSize: font.fontSize - 2 };
995
- specs.push({
996
- id: `${id}/label`,
997
- kind: "leaf",
998
- layer: "main",
999
- pickable: false,
1000
- pose: { x: 0, y: 0, width: measure(e.label, labelStyle).width, height: labelStyle.fontSize },
1001
- data: {
1002
- diagram: { label: e.labelPlacement ?? opts.labelPlacement ?? LABEL_PLACEMENT },
1003
- text: e.label,
1004
- style: labelStyle,
1005
- fill: { paint: { color: style.label ?? style.stroke } }
1006
- },
1007
- dependsOn: [id],
1008
- derivePose: LABEL_DERIVE_POSE
1009
- });
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" }));
1010
673
  }
1011
674
  }
1012
- return specs;
1013
- }
1014
- var LAYOUTS2 = Object.freeze({
1015
- layered,
1016
- tree,
1017
- force
1018
- });
1019
- function applyLayout(scene, result, opts = {}) {
1020
- const poses = layoutPoses(scene, result, opts.geometry);
1021
- if (poses.size === 0) return 0;
1022
- scene.batch(opts.label ?? "Layout", () => {
1023
- for (const [id, pose] of poses) scene.setPose(id, pose);
1024
- });
1025
- let moved = 0;
1026
- for (const id of result.keys()) if (poses.has(id)) moved++;
1027
- return moved;
675
+ return ops;
1028
676
  }
1029
- function layoutPoses(scene, result, geometry = AUTO_POSE_DESCRIPTOR) {
1030
- const out = /* @__PURE__ */ new Map();
1031
- for (const [id, at] of result) {
1032
- const node = scene.get(id);
1033
- if (node === void 0) continue;
1034
- const bounds = geometry.getBounds(node.pose);
1035
- const dx = at.x - bounds.x;
1036
- const dy = at.y - bounds.y;
1037
- if (dx === 0 && dy === 0) continue;
1038
- out.set(id, translatePoseViaDescriptor(node.pose, dx, dy, geometry));
1039
- for (const descendant of subtreeOf(scene, id)) {
1040
- const child = scene.get(descendant);
1041
- if (child === void 0) continue;
1042
- out.set(descendant, translatePoseViaDescriptor(child.pose, dx, dy, geometry));
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;
1043
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 });
1044
701
  }
1045
- return out;
1046
- }
1047
- function subtreeOf(scene, id) {
1048
- const out = [];
1049
- const stack = [...scene.childrenOf(id)];
1050
- while (stack.length > 0) {
1051
- const next = stack.pop();
1052
- out.push(next);
1053
- stack.push(...scene.childrenOf(next));
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));
1054
711
  }
1055
- return out;
712
+ return ops;
1056
713
  }
1057
- var LAYOUT_ACTION_ID = "diagram.layout";
1058
- function createLayoutAction(opts) {
1059
- const algorithm = typeof opts.algorithm === "function" ? opts.algorithm : LAYOUTS2[opts.algorithm ?? "layered"] ?? layered;
1060
- const build = {
1061
- ...opts.read ? { read: opts.read } : {},
1062
- ...opts.geometry ? { geometry: opts.geometry } : {}
1063
- };
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));
1064
755
  return {
1065
- id: opts.id ?? LAYOUT_ACTION_ID,
1066
- label: opts.label ?? "Lay out diagram",
1067
- group: opts.group ?? "diagram",
1068
- requires: ["scene"],
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"] } },
1069
761
  invoker: {
1070
762
  timing: "immediate",
1071
- run: (deps) => {
1072
- const scene = deps["scene"];
1073
- if (scene === void 0) return;
1074
- const graph = buildGraph(opts.source, build);
1075
- applyLayout(scene, algorithm(graph, opts.layout), {
1076
- ...opts.geometry ? { geometry: opts.geometry } : {},
1077
- ...opts.label ? { label: opts.label } : {}
1078
- });
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 });
1079
768
  }
1080
769
  },
1081
- enabled: () => true
770
+ enabled: (deps, at) => tagUnder(deps, at) !== null ? true : ActionDisabledReason.NotApplicable
1082
771
  };
1083
772
  }
1084
-
1085
- // src/live.ts
1086
- var EASE_FRAMES = 24;
1087
- var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
1088
- var REST_DISTANCE = 0.1;
1089
- var REST_TICKS = 5;
1090
- function forceProducer(graph, opts = {}) {
1091
- const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
1092
- const relaxation = forceRelaxation(graph, opts);
1093
- const dragAlpha = opts.dragAlpha ?? 0.3;
1094
- const restDistance = opts.restDistance ?? REST_DISTANCE;
1095
- const restTicks = opts.restTicks ?? REST_TICKS;
1096
- const was = relaxation.bodies.map((b) => ({ x: b.x, y: b.y }));
1097
- let still = 0;
1098
- return ({ pinned }) => {
1099
- for (const node of graph.nodes) {
1100
- const body = relaxation.byId.get(node.id);
1101
- if (body === void 0) continue;
1102
- const held = pinned.get(node.id);
1103
- if (held !== void 0) {
1104
- const bounds = geometry.getBounds(held);
1105
- body.fx = center(bounds, "x");
1106
- body.fy = center(bounds, "y");
1107
- } else if (!relaxation.pinned.has(node.id)) {
1108
- delete body.fx;
1109
- delete body.fy;
1110
- }
1111
- }
1112
- relaxation.sim.alphaTarget(pinned.size > 0 ? dragAlpha : 0);
1113
- relaxation.sim.tick();
1114
- let moved = 0;
1115
- relaxation.bodies.forEach((body, i) => {
1116
- const at = was[i];
1117
- moved = Math.max(moved, Math.hypot(body.x - at.x, body.y - at.y));
1118
- at.x = body.x;
1119
- at.y = body.y;
1120
- });
1121
- still = pinned.size === 0 && moved < restDistance ? still + 1 : 0;
1122
- const done = still >= restTicks || relaxation.sim.isSettled();
1123
- return { result: relaxation.placed(), done };
1124
- };
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 };
1125
776
  }
1126
- function easedProducer(graph, algorithm, opts = {}) {
1127
- const frames = opts.frames ?? EASE_FRAMES;
1128
- const easing = opts.easing ?? easeInOut;
1129
- const target = algorithm(graph, opts.layout);
1130
- const from = /* @__PURE__ */ new Map();
1131
- for (const node of graph.nodes) from.set(node.id, { x: node.bounds.x, y: node.bounds.y });
1132
- return ({ pinned, frame }) => {
1133
- const t = Math.min((frame + 1) / frames, 1);
1134
- const u = easing(t);
1135
- const result = /* @__PURE__ */ new Map();
1136
- for (const [id, to] of target) {
1137
- if (pinned.has(id)) continue;
1138
- const start = from.get(id);
1139
- if (start === void 0) continue;
1140
- result.set(id, {
1141
- x: start.x + (to.x - start.x) * u,
1142
- y: start.y + (to.y - start.y) * u
1143
- });
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()]
1144
784
  }
1145
- return { result, done: t >= 1 };
1146
785
  };
1147
786
  }
1148
- function shapeOf(graph) {
1149
- const nodes = graph.nodes.map((n) => n.id).sort().join(",");
1150
- const edges = graph.edges.map((e) => `${e.from}>${e.to}`).sort().join(",");
1151
- return `${nodes}|${edges}`;
1152
- }
1153
- function useLiveLayout(opts) {
1154
- const optsRef = useLatest(opts);
1155
- const producer = useRef(null);
1156
- const producerFrame = useRef(0);
1157
- const shape = useRef("");
1158
- const build = useCallback(() => {
1159
- const o = optsRef.current;
1160
- const graph = buildGraph(o.source, {
1161
- ...o.read ? { read: o.read } : {},
1162
- ...o.geometry ? { geometry: o.geometry } : {}
1163
- });
1164
- shape.current = shapeOf(graph);
1165
- producerFrame.current = 0;
1166
- const algorithm = o.algorithm ?? "force";
1167
- if (algorithm === "force") {
1168
- producer.current = forceProducer(graph, {
1169
- ...o.layout,
1170
- ...o.force,
1171
- ...o.geometry ? { geometry: o.geometry } : {}
1172
- });
1173
- return;
1174
- }
1175
- const fn = typeof algorithm === "function" ? algorithm : LAYOUTS2[algorithm] ?? layered;
1176
- producer.current = easedProducer(graph, fn, {
1177
- ...o.layout ? { layout: o.layout } : {},
1178
- ...o.frames === void 0 ? {} : { frames: o.frames },
1179
- ...o.easing ? { easing: o.easing } : {}
1180
- });
1181
- }, [optsRef]);
1182
- const step = useCallback((ctx) => {
1183
- const o = optsRef.current;
1184
- if (producer.current === null) return { poses: [], done: true };
1185
- const { result, done } = producer.current({
1186
- pinned: ctx.pinned,
1187
- frame: producerFrame.current++
1188
- });
1189
- let placed2 = result;
1190
- if (ctx.pinned.size > 0) {
1191
- const held = new Map(result);
1192
- for (const id of ctx.pinned.keys()) held.delete(id);
1193
- placed2 = held;
1194
- }
1195
- const poses = layoutPoses(o.scene, placed2, o.geometry);
1196
- return { poses: [...poses], done };
1197
- }, [optsRef]);
1198
- const run = usePoseRun({
1199
- scene: opts.scene,
1200
- step,
1201
- onCommit: (moved) => optsRef.current.onSettle?.(moved),
1202
- ...opts.label ? { label: opts.label } : {},
1203
- ...opts.requestFrame ? { requestFrame: opts.requestFrame } : {},
1204
- ...opts.cancelFrame ? { cancelFrame: opts.cancelFrame } : {}
1205
- });
1206
- useEffect(() => {
1207
- if (opts.reheatOnGraphChange === false) return;
1208
- return opts.scene.subscribe(() => {
1209
- if (!run.isRunning()) return;
1210
- const o = optsRef.current;
1211
- const graph = buildGraph(o.source, {
1212
- ...o.read ? { read: o.read } : {},
1213
- ...o.geometry ? { geometry: o.geometry } : {}
1214
- });
1215
- if (shapeOf(graph) === shape.current) return;
1216
- build();
1217
- });
1218
- }, [opts.scene, opts.reheatOnGraphChange, run, build, optsRef]);
1219
- return useMemo(() => ({
1220
- start() {
1221
- if (run.isRunning()) return;
1222
- build();
1223
- run.start();
1224
- },
1225
- stop: () => {
1226
- run.stop();
1227
- },
1228
- cancel: () => {
1229
- run.cancel();
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)
1230
813
  },
1231
- restart() {
1232
- run.cancel();
1233
- build();
1234
- run.start();
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
+ ]
1235
824
  },
1236
- isRunning: () => run.isRunning()
1237
- }), [run, build]);
1238
- }
1239
- var EASED_LAYOUTS = Object.freeze({
1240
- layered,
1241
- tree
1242
- });
1243
- function diagramShape(opts = {}) {
1244
- const geometry = opts.geometry ?? RECT_POSE_DESCRIPTOR;
1245
- const outlineOf = (node) => diagramNodeOf(node, opts.read)?.outline;
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;
1246
848
  return {
1247
- id: opts.id ?? "diagram:outline",
1248
- matches: (node) => outlineOf(node) !== void 0,
1249
- paint: (node, pose) => {
1250
- const outline = outlineOf(node);
1251
- if (outline === void 0) return [];
1252
- const d = node.data;
1253
- const box = geometry.getBounds(pose);
1254
- const declaredFill = d.fill === void 0 ? opts.defaultFill : d.fill;
1255
- const declaredStroke = d.stroke === void 0 ? opts.defaultStroke : d.stroke;
1256
- const fill = declaredFill && commandFill(declaredFill, box);
1257
- const stroke = declaredStroke && commandStroke(declaredStroke, box);
1258
- return [{
1259
- kind: "path",
1260
- path: outlinePath(outline, box),
1261
- ...fill ? { fill } : {},
1262
- ...stroke ? { stroke } : {}
1263
- }];
1264
- },
1265
- // The drawn boundary, so picking and clipping follow the diamond rather
1266
- // than its bounding box.
1267
- silhouette: (node, pose) => {
1268
- const outline = outlineOf(node);
1269
- return outline === void 0 ? null : outlinePath(outline, geometry.getBounds(pose));
1270
- }
849
+ kind,
850
+ ...role !== void 0 ? { role } : {},
851
+ own: trait.ownStyle ?? [],
852
+ ownFont: trait.ownFont ?? []
1271
853
  };
1272
854
  }
1273
- function registerDiagramShape(opts = {}) {
1274
- return registerNodeShape(diagramShape(opts), { priority: "high" });
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);
1275
863
  }
1276
- function useMirroredSelection(selection, selected, onSelect) {
1277
- const want = selected ?? null;
1278
- const pushed = useRef(void 0);
1279
- useEffect(() => {
1280
- if (pushed.current === want) return;
1281
- pushed.current = want;
1282
- if ((selection.get()[0] ?? null) !== want) selection.set(want === null ? [] : [want]);
1283
- }, [want]);
1284
- const onSelectRef = useLatest(onSelect);
1285
- return useCallback(() => {
1286
- onSelectRef.current?.(selection.get()[0] ?? null);
1287
- }, [selection, onSelectRef]);
1288
- }
1289
- var AMBIENT = [dragPanContribution()];
1290
- var NO_MARQUEE = { areaSelect: null };
1291
- var DiagramView = forwardRef(function DiagramView2(props, ref) {
1292
- return /* @__PURE__ */ jsx(WeaselProvider, { children: /* @__PURE__ */ jsx(Inner, { ...props, apiRef: ref }) });
1293
- });
1294
- function Inner({
1295
- specs,
1296
- width,
1297
- height,
1298
- selected,
1299
- onSelect,
1300
- onMove,
1301
- onConnect,
1302
- enableKeybindings,
1303
- canConnect,
1304
- portOptions,
1305
- live: liveOpts,
1306
- minScale,
1307
- maxScale,
1308
- anchor,
1309
- fitPadding,
1310
- background,
1311
- view,
1312
- onViewChange,
1313
- className,
1314
- apiRef
1315
- }) {
1316
- useEffect(() => registerDiagramShape(), []);
1317
- const registry = useMemo(() => withDiagramRegistry(), []);
1318
- const scene = useScene({
1319
- systemLayers: [{ id: "main" }],
1320
- initial: specs,
1321
- registry
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;
1322
877
  });
1323
- const selection = useSelection({ scene, mode: "single" });
1324
- const onClick = useMirroredSelection(selection, selected, onSelect);
1325
- const onMoveRef = useLatest(onMove);
1326
- const syncing = useRef(false);
1327
- const applied = useRef(specs);
1328
- const known = useRef(/* @__PURE__ */ new Map());
1329
- useEffect(() => {
1330
- const read = () => {
1331
- const out = /* @__PURE__ */ new Map();
1332
- for (const n of scene.renderOrderNodes()) {
1333
- if (n.kind === "container") out.set(n.id, { x: n.pose.x, y: n.pose.y });
1334
- }
1335
- return out;
1336
- };
1337
- known.current = read();
1338
- return scene.subscribe(() => {
1339
- const now = read();
1340
- const moves = [];
1341
- if (!syncing.current) {
1342
- for (const [id, at] of now) {
1343
- const was = known.current.get(id);
1344
- if (was !== void 0 && (was.x !== at.x || was.y !== at.y)) moves.push({ id, ...at });
1345
- }
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) } });
1346
911
  }
1347
- known.current = now;
1348
- if (moves.length > 0) onMoveRef.current?.(moves);
1349
- });
1350
- }, [scene, onMoveRef]);
1351
- useEffect(() => {
1352
- if (applied.current === specs) return;
1353
- syncing.current = true;
1354
- try {
1355
- reconcileSpecs(scene, applied.current, specs);
1356
- } finally {
1357
- syncing.current = false;
1358
912
  }
1359
- applied.current = specs;
1360
- }, [scene, specs]);
1361
- const source = useMemo(() => sceneParticipants(scene), [scene]);
1362
- const [run, setRun] = useState(null);
1363
- const live = useLiveLayout({ ...liveOpts, scene, source, algorithm: run?.algorithm ?? "force" });
1364
- useEffect(() => {
1365
- if (run !== null) live.restart();
1366
- }, [run, live]);
1367
- useImperativeHandle(apiRef, () => ({
1368
- layout: (algorithm = "force") => setRun({ algorithm }),
1369
- live
1370
- }), [live]);
1371
- const onConnectRef = useLatest(onConnect);
1372
- const connecting = onConnect !== void 0;
1373
- const ports = useMemo(() => connecting ? diagramPorts({
1374
- ...portOptions,
1375
- participants: source,
1376
- ...canConnect ? { canConnect } : {},
1377
- commit: ({ from, to }) => onConnectRef.current?.({
1378
- from: from.nodeId,
1379
- fromPort: from.id,
1380
- to: to.nodeId,
1381
- toPort: to.id
1382
- })
1383
- }) : null, [connecting, source, canConnect, portOptions, onConnectRef]);
1384
- const canvasRef = useRef(null);
1385
- useEffect(() => ports ? canvasRef.current?.registerLayer(ports.layer) : void 0, [ports]);
1386
- const ambient = useMemo(() => ports ? [...AMBIENT, ports.contribution] : AMBIENT, [ports]);
1387
- const controlled = view !== void 0;
1388
- const defaultView = useMemo(
1389
- () => controlled ? void 0 : fitDiagram(specs, { width, height }, { minScale, maxScale, anchor, padding: fitPadding }),
1390
- // The fit is where the view opens; a later `specs` keeps the view it has.
1391
- // eslint-disable-next-line react-hooks/exhaustive-deps
1392
- [controlled, width, height, minScale, maxScale, anchor, fitPadding]
1393
- );
1394
- return /* @__PURE__ */ jsx(
1395
- SceneCanvas,
1396
- {
1397
- ref: canvasRef,
1398
- scene,
1399
- selection,
1400
- features: onMove ? ["view", "pick", "move"] : ["view", "pick"],
1401
- enableKeybindings: enableKeybindings ?? (onMove !== void 0 || onConnect !== void 0),
1402
- ambient,
1403
- actions: NO_MARQUEE,
1404
- onClick,
1405
- width,
1406
- height,
1407
- className,
1408
- ...defaultView ? { defaultView } : {},
1409
- ...view ? { view } : {},
1410
- ...onViewChange ? { onViewChange } : {},
1411
- ...background ? { backgroundFill: background } : {}
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
+ }
1412
928
  }
1413
- );
929
+ writeAll(scene, writes, opts.measure);
930
+ });
1414
931
  }
1415
- function fitDiagram(specs, size, fit = {}) {
1416
- const boxes = specs.filter((s) => s.kind === "container" || diagramGroupOf(s) !== null).map((s) => s.pose);
1417
- if (boxes.length === 0) return void 0;
1418
- const x = Math.min(...boxes.map((b) => b.x));
1419
- const y = Math.min(...boxes.map((b) => b.y));
1420
- const bounds = {
1421
- x,
1422
- y,
1423
- width: Math.max(...boxes.map((b) => b.x + b.width)) - x,
1424
- height: Math.max(...boxes.map((b) => b.y + b.height)) - y
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
1425
972
  };
1426
- return fitViewToBounds(bounds, size, { x: 0, y: 0, scale: { x: 1, y: 1 } }, {
1427
- maxScale: fit.maxScale ?? 1,
1428
- ...fit.minScale !== void 0 ? { minScale: fit.minScale } : {},
1429
- ...fit.anchor !== void 0 ? { anchor: fit.anchor } : {},
1430
- ...fit.padding !== void 0 ? { padding: fit.padding } : {}
1431
- });
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 };
1432
1054
  }
1433
1055
 
1434
- 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, LAYOUTS2 as 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, diagramPorts, diagramScene, diagramShape, easedProducer, edgeDerivePath, edgeIdOf, forceProducer, grabPortAction, grabPortBindings, groupDerivePose, labelDerivePose, layeredRoutes, layoutPoses, orthogonal, portLayer, portScratchOf, registerDiagramShape, resolveEnd, sceneParticipants, selfLoop, straight, useLiveLayout, useMirroredSelection, withDiagramRegistry };
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 };
1435
1057
  //# sourceMappingURL=index.js.map
1436
1058
  //# sourceMappingURL=index.js.map