@oh-just-another/state 0.59.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +189 -0
- package/README.md +6 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/actions/actionArrange.js +4 -4
- package/dist/actions/actionArrange.js.map +1 -1
- package/dist/actions/actionClipboard.d.ts.map +1 -1
- package/dist/actions/actionClipboard.js +5 -0
- package/dist/actions/actionClipboard.js.map +1 -1
- package/dist/actions/actionKeyboard.d.ts.map +1 -1
- package/dist/actions/actionKeyboard.js +79 -11
- package/dist/actions/actionKeyboard.js.map +1 -1
- package/dist/actions/actionMode.d.ts +2 -0
- package/dist/actions/actionMode.d.ts.map +1 -1
- package/dist/actions/actionMode.js +81 -27
- package/dist/actions/actionMode.js.map +1 -1
- package/dist/actions/actionSelection.d.ts.map +1 -1
- package/dist/actions/actionSelection.js +1 -0
- package/dist/actions/actionSelection.js.map +1 -1
- package/dist/actions/actionView.d.ts +6 -0
- package/dist/actions/actionView.d.ts.map +1 -1
- package/dist/actions/actionView.js +19 -1
- package/dist/actions/actionView.js.map +1 -1
- package/dist/actions/actionZoom.d.ts.map +1 -1
- package/dist/actions/actionZoom.js +5 -0
- package/dist/actions/actionZoom.js.map +1 -1
- package/dist/actions/index.d.ts +3 -3
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +3 -3
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/registry.d.ts.map +1 -1
- package/dist/actions/registry.js +13 -0
- package/dist/actions/registry.js.map +1 -1
- package/dist/actions/types.d.ts +10 -1
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js.map +1 -1
- package/dist/built-in-handlers.d.ts +8 -0
- package/dist/built-in-handlers.d.ts.map +1 -1
- package/dist/built-in-handlers.js +31 -16
- package/dist/built-in-handlers.js.map +1 -1
- package/dist/clipboard.d.ts +0 -14
- package/dist/clipboard.d.ts.map +1 -1
- package/dist/clipboard.js +1 -1
- package/dist/clipboard.js.map +1 -1
- package/dist/constants.d.ts +253 -18
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +255 -2
- package/dist/constants.js.map +1 -1
- package/dist/dom-events.d.ts +1 -2
- package/dist/dom-events.d.ts.map +1 -1
- package/dist/dom-events.js.map +1 -1
- package/dist/editor/animation-scene.d.ts +15 -0
- package/dist/editor/animation-scene.d.ts.map +1 -1
- package/dist/editor/animation-scene.js +159 -0
- package/dist/editor/animation-scene.js.map +1 -1
- package/dist/editor/applies/create.d.ts +0 -6
- package/dist/editor/applies/create.d.ts.map +1 -1
- package/dist/editor/applies/create.js +1 -1
- package/dist/editor/applies/create.js.map +1 -1
- package/dist/editor/applies/link-move.d.ts +0 -8
- package/dist/editor/applies/link-move.d.ts.map +1 -1
- package/dist/editor/applies/link-move.js +1 -1
- package/dist/editor/applies/link-move.js.map +1 -1
- package/dist/editor/event-fanout.d.ts +6 -6
- package/dist/editor/event-fanout.d.ts.map +1 -1
- package/dist/editor/event-fanout.js +8 -8
- package/dist/editor/event-fanout.js.map +1 -1
- package/dist/editor/hit-test.d.ts +8 -0
- package/dist/editor/hit-test.d.ts.map +1 -1
- package/dist/editor/hit-test.js +31 -16
- package/dist/editor/hit-test.js.map +1 -1
- package/dist/editor/interaction-state.d.ts +184 -0
- package/dist/editor/interaction-state.d.ts.map +1 -0
- package/dist/editor/interaction-state.js +139 -0
- package/dist/editor/interaction-state.js.map +1 -0
- package/dist/editor/link-handle-drag.d.ts +172 -0
- package/dist/editor/link-handle-drag.d.ts.map +1 -0
- package/dist/editor/link-handle-drag.js +369 -0
- package/dist/editor/link-handle-drag.js.map +1 -0
- package/dist/editor/pointer-binding.d.ts +9 -11
- package/dist/editor/pointer-binding.d.ts.map +1 -1
- package/dist/editor/pointer-binding.js +1247 -909
- package/dist/editor/pointer-binding.js.map +1 -1
- package/dist/editor/public/brush.d.ts +93 -13
- package/dist/editor/public/brush.d.ts.map +1 -1
- package/dist/editor/public/brush.js +240 -22
- package/dist/editor/public/brush.js.map +1 -1
- package/dist/editor/public/cursor.d.ts +1 -15
- package/dist/editor/public/cursor.d.ts.map +1 -1
- package/dist/editor/public/cursor.js +17 -7
- package/dist/editor/public/cursor.js.map +1 -1
- package/dist/editor/public/eraser.d.ts +55 -0
- package/dist/editor/public/eraser.d.ts.map +1 -0
- package/dist/editor/public/eraser.js +65 -0
- package/dist/editor/public/eraser.js.map +1 -0
- package/dist/editor/public/laser.d.ts +38 -0
- package/dist/editor/public/laser.d.ts.map +1 -0
- package/dist/editor/public/laser.js +42 -0
- package/dist/editor/public/laser.js.map +1 -0
- package/dist/editor/public/placement.d.ts.map +1 -1
- package/dist/editor/public/placement.js +9 -1
- package/dist/editor/public/placement.js.map +1 -1
- package/dist/editor/public/selection-ops.d.ts +12 -0
- package/dist/editor/public/selection-ops.d.ts.map +1 -1
- package/dist/editor/public/selection-ops.js +19 -1
- package/dist/editor/public/selection-ops.js.map +1 -1
- package/dist/editor/public/stroke-eraser-coverage.d.ts +37 -0
- package/dist/editor/public/stroke-eraser-coverage.d.ts.map +1 -0
- package/dist/editor/public/stroke-eraser-coverage.js +168 -0
- package/dist/editor/public/stroke-eraser-coverage.js.map +1 -0
- package/dist/editor/public/stroke-eraser.d.ts +50 -0
- package/dist/editor/public/stroke-eraser.d.ts.map +1 -0
- package/dist/editor/public/stroke-eraser.js +176 -0
- package/dist/editor/public/stroke-eraser.js.map +1 -0
- package/dist/editor/public/stroke-smoothing.d.ts +21 -0
- package/dist/editor/public/stroke-smoothing.d.ts.map +1 -0
- package/dist/editor/public/stroke-smoothing.js +37 -0
- package/dist/editor/public/stroke-smoothing.js.map +1 -0
- package/dist/editor/public/tool-ops.d.ts +130 -0
- package/dist/editor/public/tool-ops.d.ts.map +1 -0
- package/dist/editor/public/tool-ops.js +392 -0
- package/dist/editor/public/tool-ops.js.map +1 -0
- package/dist/editor/public/z-order.d.ts +1 -12
- package/dist/editor/public/z-order.d.ts.map +1 -1
- package/dist/editor/public/z-order.js +1 -1
- package/dist/editor/public/z-order.js.map +1 -1
- package/dist/editor/public/zoom-pan.d.ts +9 -0
- package/dist/editor/public/zoom-pan.d.ts.map +1 -1
- package/dist/editor/public/zoom-pan.js +31 -0
- package/dist/editor/public/zoom-pan.js.map +1 -1
- package/dist/editor/render-orchestrator.d.ts +180 -5
- package/dist/editor/render-orchestrator.d.ts.map +1 -1
- package/dist/editor/render-orchestrator.js +516 -353
- package/dist/editor/render-orchestrator.js.map +1 -1
- package/dist/editor/text-edit.d.ts +136 -0
- package/dist/editor/text-edit.d.ts.map +1 -0
- package/dist/editor/text-edit.js +328 -0
- package/dist/editor/text-edit.js.map +1 -0
- package/dist/editor-events.d.ts +5 -5
- package/dist/editor-events.d.ts.map +1 -1
- package/dist/editor.d.ts +560 -250
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1650 -743
- package/dist/editor.js.map +1 -1
- package/dist/handle.d.ts +0 -2
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +0 -6
- package/dist/handle.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/modes.d.ts +34 -1
- package/dist/modes.d.ts.map +1 -1
- package/dist/modes.js.map +1 -1
- package/dist/overlay.d.ts +98 -21
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +442 -116
- package/dist/overlay.js.map +1 -1
- package/dist/search.d.ts +37 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +45 -0
- package/dist/search.js.map +1 -0
- package/package.json +5 -4
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { getLink, getLinkPath, getLinkCurvePoints, getLinkWaypointMidpoints, getElement, getElementWorldBounds, getElementOutline, getWorldToScreen, getDropZonesWorld, strokeOutsideExtent, isFrame, isImage, } from "@oh-just-another/scene";
|
|
2
|
-
import { DEFAULT_LOD, renderLinks, renderGrid, renderScene } from "@oh-just-another/renderer-core";
|
|
1
|
+
import { getLink, getLinkPath, getLinkCurvePoints, getLinkWaypointMidpoints, getElement, getElementWorldBounds, getElementOutline, getWorldToScreen, getDropZonesWorld, strokeOutsideExtent, isFrame, isImage, getElbowSegmentHandles, } from "@oh-just-another/scene";
|
|
2
|
+
import { DEFAULT_LOD, renderLinks, renderGrid, renderScene, setAnimationClock, } from "@oh-just-another/renderer-core";
|
|
3
3
|
import { renderOverlay, paintElementSelectionHalo, } from "../overlay.js";
|
|
4
4
|
import { anchorOverlayPoints } from "./anchor-points.js";
|
|
5
5
|
import { hitZoneVisibility } from "./hit-test.js";
|
|
6
6
|
import { buildElementForCreate, buildEdgePreviewLink } from "./applies/create.js";
|
|
7
|
-
import { ANCHOR_DOT_ACTIVE_RADIUS, ANCHOR_DOT_RADIUS, ANCHOR_DOT_HOVER_GROW_RADIUS, ANCHOR_DOT_HOVER_MAX_RADIUS, DEFAULT_SNAP_THRESHOLD, GHOST_PREVIEW_OPACITY,
|
|
7
|
+
import { ANCHOR_DOT_ACTIVE_RADIUS, ANCHOR_DOT_RADIUS, ANCHOR_DOT_HOVER_GROW_RADIUS, ANCHOR_DOT_HOVER_MAX_RADIUS, DEFAULT_SNAP_THRESHOLD, FLOWCHART_PREVIEW_OPACITY, GHOST_PREVIEW_OPACITY, ERASE_DIM_OPACITY, ISOLATION_DIM_OPACITY, LINK_START_ANCHOR_OUTSET, LINK_ATTACH_ANCHOR_OUTSET, } from "../constants.js";
|
|
8
8
|
/**
|
|
9
9
|
* Stable throwaway id for the transient shape-draw preview element. Never
|
|
10
10
|
* enters the scene / history — it exists only for the duration of a single
|
|
@@ -14,13 +14,78 @@ const DRAW_PREVIEW_ELEMENT_ID = "__draw-preview__";
|
|
|
14
14
|
/** Throwaway id for the live draw-edge connector preview link. */
|
|
15
15
|
const DRAW_PREVIEW_LINK_ID = "__draw-preview-link__";
|
|
16
16
|
import { req } from "../util.js";
|
|
17
|
+
const overlayMemoByTarget = new WeakMap();
|
|
18
|
+
/**
|
|
19
|
+
* Cheap identity signature of every input the overlay-options build reads.
|
|
20
|
+
* Element-wise `Object.is`-compared against the previous frame's; a full match
|
|
21
|
+
* means the bag would rebuild identically, so the prior object is reused.
|
|
22
|
+
*
|
|
23
|
+
* Reference-stable interaction fields (selection, hover, drag state, peer
|
|
24
|
+
* arrays, scene) compare by identity; primitives by value. Two derived values
|
|
25
|
+
* are deliberately absent — `combinedSelectionBounds` and `aspectLocked` are
|
|
26
|
+
* pure functions of `scene` + `selection` (+ `selectedLinks`), all already
|
|
27
|
+
* here, so their freshly-allocated results can't diverge while those match.
|
|
28
|
+
* `gifBadges` is excluded on purpose (its playback-pause input advances outside
|
|
29
|
+
* the signature) and recomputed every frame. `editingText` (fresh each frame
|
|
30
|
+
* while editing) is included, so the memo simply doesn't engage during text
|
|
31
|
+
* editing — not an idle path.
|
|
32
|
+
*/
|
|
33
|
+
const buildOverlaySignature = (e) => [
|
|
34
|
+
e.scene,
|
|
35
|
+
e.selection,
|
|
36
|
+
e.selectedLinks,
|
|
37
|
+
e.selectedLink,
|
|
38
|
+
e.selectedAnnotation,
|
|
39
|
+
e.mode,
|
|
40
|
+
e.activeLayerId,
|
|
41
|
+
e.cropFrame,
|
|
42
|
+
e.cropGhost,
|
|
43
|
+
e.flowchartPreview,
|
|
44
|
+
e.strokeErasePreview,
|
|
45
|
+
e.lassoPreview,
|
|
46
|
+
e.drawingPreview,
|
|
47
|
+
e.edgePreview,
|
|
48
|
+
e.linkDragFromAnchor,
|
|
49
|
+
e.hoveredLinkTarget,
|
|
50
|
+
e.panGesture,
|
|
51
|
+
e.pinchActive,
|
|
52
|
+
e.gestureActive,
|
|
53
|
+
e.linkEndpointDrag,
|
|
54
|
+
e.linkSegmentDrag,
|
|
55
|
+
e.linkWaypointDrag,
|
|
56
|
+
e.hoverCursorWorld,
|
|
57
|
+
e.anchorStartHitSlop,
|
|
58
|
+
e.anchorClickRadius,
|
|
59
|
+
e.containerHover,
|
|
60
|
+
e.brushStroke,
|
|
61
|
+
e.laserStrokes,
|
|
62
|
+
e.eraserTrail,
|
|
63
|
+
e.eraserCursor,
|
|
64
|
+
e.peerCursors,
|
|
65
|
+
e.peerSelections,
|
|
66
|
+
e.debugHitZones,
|
|
67
|
+
e.readOnly,
|
|
68
|
+
e.groupMoveOrigin,
|
|
69
|
+
e.editingText,
|
|
70
|
+
];
|
|
71
|
+
const overlaySigEqual = (a, b) => {
|
|
72
|
+
if (a.length !== b.length)
|
|
73
|
+
return false;
|
|
74
|
+
for (let i = 0; i < a.length; i++) {
|
|
75
|
+
if (!Object.is(a[i], b[i]))
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
17
80
|
/**
|
|
18
81
|
* Render orchestrator: background grid pass, tile-cache vs full renderScene
|
|
19
82
|
* path, and the overlay options builder (drawing / lasso preview, edge
|
|
20
83
|
* preview, hovered ports, group handles, container drop zone, brush stroke,
|
|
21
|
-
* edge endpoint drag, peer cursors, annotations).
|
|
22
|
-
*
|
|
23
|
-
* site is editor.ts's own private `render()`
|
|
84
|
+
* edge endpoint drag, peer cursors, annotations). Consumes a flat
|
|
85
|
+
* {@link RenderSnapshot} the Editor builds each frame — no coupling to the
|
|
86
|
+
* `Editor` class. The single call site is editor.ts's own private `render()`
|
|
87
|
+
* wrapper. Side-effect free apart from the paint itself: the caller owns the
|
|
88
|
+
* `lastRendered*` bookkeeping and the tile-dirty reset.
|
|
24
89
|
*/
|
|
25
90
|
export const renderEditor = (editor) => {
|
|
26
91
|
// Background layer (grid + selection halo), when the host gave us a
|
|
@@ -34,434 +99,532 @@ export const renderEditor = (editor) => {
|
|
|
34
99
|
// Grid pass also clears the background layer each frame. When the grid is
|
|
35
100
|
// toggled off, still clear it so no stale grid lingers under the halos.
|
|
36
101
|
if (editor.gridEnabled)
|
|
37
|
-
renderGrid(editor.
|
|
102
|
+
renderGrid(editor.scene, editor.backgroundTarget);
|
|
38
103
|
else
|
|
39
104
|
editor.backgroundTarget.clear();
|
|
40
105
|
const halos = [];
|
|
41
|
-
for (const id of editor.
|
|
42
|
-
const shape = getElement(editor.
|
|
106
|
+
for (const id of editor.selection) {
|
|
107
|
+
const shape = getElement(editor.scene, id);
|
|
43
108
|
if (!shape)
|
|
44
109
|
continue;
|
|
45
110
|
const style = shape.style ?? {};
|
|
46
111
|
halos.push({
|
|
47
|
-
loops: getElementOutline(editor.
|
|
112
|
+
loops: getElementOutline(editor.scene, shape),
|
|
48
113
|
outsetWorld: strokeOutsideExtent(style),
|
|
49
114
|
});
|
|
50
115
|
}
|
|
51
116
|
if (halos.length > 0) {
|
|
52
|
-
paintElementSelectionHalo(editor.backgroundTarget, getWorldToScreen(editor.
|
|
117
|
+
paintElementSelectionHalo(editor.backgroundTarget, getWorldToScreen(editor.scene.viewport), halos, editor.scene.viewport.zoom || 1);
|
|
53
118
|
}
|
|
54
119
|
}
|
|
55
120
|
// World-space viewport rect — used by `renderScene` to skip off-screen
|
|
56
121
|
// shapes. Computed by mapping the screen viewport corners through the
|
|
57
122
|
// inverse projection. Slightly inflated so geometry near the edge
|
|
58
123
|
// does not flicker during pan.
|
|
59
|
-
const viewportWorld = editor.
|
|
60
|
-
const dirtyWorld = editor.
|
|
61
|
-
const dimElements = editor.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
124
|
+
const viewportWorld = editor.viewportWorld;
|
|
125
|
+
const dirtyWorld = editor.dirtyWorld;
|
|
126
|
+
const dimElements = editor.dimElements;
|
|
127
|
+
const hideElements = editor.hideElements;
|
|
128
|
+
// Group isolation (dim) can't be honoured by the tile cache: tiles bake
|
|
129
|
+
// every shape at full opacity, and neither a scrim nor an overlay can
|
|
130
|
+
// reproduce `renderScene`'s per-element dim without re-rasterising most
|
|
131
|
+
// tiles (isolation dims almost everything), which would defeat the cache.
|
|
132
|
+
// While the dim set is non-empty we take the full `renderScene` path.
|
|
133
|
+
// Per-element HIDE stays on the tile path: the sets are small (stroke-eraser
|
|
134
|
+
// preview, visibility), so the compositor honours them directly — it bakes
|
|
135
|
+
// tiles with the set applied and re-rasterises only the tiles a shape
|
|
136
|
+
// touches when it enters/leaves the set.
|
|
137
|
+
const dimActive = dimElements !== undefined && dimElements.size > 0;
|
|
138
|
+
if (editor.tileComposeFn && viewportWorld && !dimActive) {
|
|
139
|
+
// Tile-cache path: clear main once, then composite cached tiles.
|
|
140
|
+
// The compositor rasterises tiles through its own `renderScene` calls,
|
|
141
|
+
// which we can't hand a render context — bridge this editor's per-instance
|
|
142
|
+
// clock onto the process-global fallback so animated tiles still honour
|
|
143
|
+
// per-shape playback. Scoped to this opt-in path (rare); the common paths
|
|
144
|
+
// no longer touch the module global.
|
|
145
|
+
setAnimationClock(editor.animationClock);
|
|
70
146
|
editor.mainTarget.clear();
|
|
71
|
-
|
|
147
|
+
// Feed the persistent spatial index (shared with the hit-test path) when
|
|
148
|
+
// the editor keeps one, so the compositor's per-tile element selection
|
|
149
|
+
// queries the index instead of scanning every shape in every layer.
|
|
150
|
+
editor.tileComposeFn(editor.scene, editor.mainTarget, {
|
|
72
151
|
viewport: viewportWorld,
|
|
73
152
|
changedElements: editor.tileDirtyElements,
|
|
74
|
-
zoomBucket: editor.
|
|
75
|
-
|
|
76
|
-
|
|
153
|
+
zoomBucket: editor.scene.viewport.zoom > 0 ? 2 ** Math.round(Math.log2(editor.scene.viewport.zoom)) : 1,
|
|
154
|
+
...(hideElements && hideElements.size > 0 ? { hideElements } : {}),
|
|
155
|
+
...(editor.sharedIndex ? { index: editor.sharedIndex } : {}),
|
|
77
156
|
});
|
|
78
|
-
editor.
|
|
79
|
-
renderLinks(editor._scene, editor.mainTarget, { viewportWorld });
|
|
157
|
+
renderLinks(editor.scene, editor.mainTarget, { viewportWorld });
|
|
80
158
|
}
|
|
81
159
|
else {
|
|
82
160
|
// For very large scenes share the same SpatialGrid the hit-test path
|
|
83
161
|
// already maintains — `renderScene` uses it to skip the per-shape AABB
|
|
84
162
|
// cull on shapes outside the viewport.
|
|
85
|
-
const sharedIndex = editor.
|
|
86
|
-
renderScene(editor.
|
|
163
|
+
const sharedIndex = editor.sharedIndex;
|
|
164
|
+
renderScene(editor.scene, editor.mainTarget, {
|
|
87
165
|
...(viewportWorld ? { viewport: viewportWorld } : {}),
|
|
88
166
|
...(dirtyWorld ? { dirtyWorld } : {}),
|
|
89
167
|
boundsCache: editor.boundsCache,
|
|
168
|
+
clock: editor.animationClock,
|
|
90
169
|
lod: DEFAULT_LOD,
|
|
91
|
-
...(dimElements
|
|
170
|
+
...(dimElements
|
|
171
|
+
? {
|
|
172
|
+
dimElements,
|
|
173
|
+
dimOpacity: editor.eraseActive ? ERASE_DIM_OPACITY : ISOLATION_DIM_OPACITY,
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
92
176
|
...(hideElements ? { hideElements } : {}),
|
|
93
177
|
...(sharedIndex ? { spatialIndex: sharedIndex } : {}),
|
|
94
178
|
});
|
|
95
|
-
renderLinks(editor.
|
|
179
|
+
renderLinks(editor.scene, editor.mainTarget, {
|
|
96
180
|
...(viewportWorld ? { viewportWorld } : {}),
|
|
97
181
|
...(dirtyWorld ? { dirtyWorld } : {}),
|
|
98
182
|
});
|
|
99
183
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
let
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
184
|
+
// Overlay options bag — rebuilding it allocates dozens of objects (port
|
|
185
|
+
// sets, link halos, handle midpoints, debug zones). On idle / animation /
|
|
186
|
+
// peer-update frames none of its inputs changed, so memoize the whole bag by
|
|
187
|
+
// a cheap identity signature per overlay target and reuse the prior object.
|
|
188
|
+
// `gifBadges` — whose input (per-shape GIF playback pause) advances on its
|
|
189
|
+
// own timeline, outside the signature — is recomputed every frame below.
|
|
190
|
+
const overlaySig = buildOverlaySignature(editor);
|
|
191
|
+
let overlayMemo = overlayMemoByTarget.get(editor.overlayTarget);
|
|
192
|
+
if (overlayMemo === undefined || !overlaySigEqual(overlayMemo.sig, overlaySig)) {
|
|
193
|
+
const overlayOpts = {};
|
|
194
|
+
// Read-only: keep selection outlines but drop every interactive handle.
|
|
195
|
+
if (editor.readOnly)
|
|
196
|
+
overlayOpts.readOnly = true;
|
|
197
|
+
// Non-select tool active: same handle suppression — the chrome is not
|
|
198
|
+
// pressable (pickPressTarget gates it), so don't advertise it. `hand` is
|
|
199
|
+
// navigation-only and keeps the chrome visible for context.
|
|
200
|
+
if (editor.mode !== "select" && editor.mode !== "hand")
|
|
201
|
+
overlayOpts.suppressHandles = true;
|
|
202
|
+
// Image-crop chrome (crop mode) — dashed accent quad over the pending
|
|
203
|
+
// window, 8 grab handles, and the faint full-image ghost behind them.
|
|
204
|
+
if (editor.cropFrame)
|
|
205
|
+
overlayOpts.cropFrame = editor.cropFrame;
|
|
206
|
+
if (editor.cropGhost)
|
|
207
|
+
overlayOpts.cropGhost = editor.cropGhost;
|
|
208
|
+
// Throwaway scene holding the click-create ghost connector — rendered
|
|
209
|
+
// through the real link renderer (faded) AFTER the overlay, so the ghost
|
|
210
|
+
// connector matches the link that will be created (routing / arrowhead /
|
|
211
|
+
// style), not a dashed preview line. Set in the start-dot hover branch.
|
|
212
|
+
let ghostScene = null;
|
|
213
|
+
// The lasso (select-mode rubber-band) keeps the plain dashed rect. A shape
|
|
214
|
+
// draw (draw-rect / draw-ellipse) shows a preview of the would-be element
|
|
215
|
+
// rendered through its real renderer — the user sees the actual shape +
|
|
216
|
+
// default style they'll get on release, not just a dashed box. Both
|
|
217
|
+
// gestures can't run at once, so one slot is set.
|
|
218
|
+
if (editor.lassoPreview) {
|
|
219
|
+
overlayOpts.drawingPreview = editor.lassoPreview;
|
|
120
220
|
}
|
|
121
|
-
else {
|
|
122
|
-
|
|
221
|
+
else if (editor.drawingPreview) {
|
|
222
|
+
const kind = editor.mode === "draw-rect" ? "rect" : editor.mode === "draw-ellipse" ? "ellipse" : null;
|
|
223
|
+
if (kind) {
|
|
224
|
+
overlayOpts.drawingPreviewElement = buildElementForCreate(editor.scene, kind, editor.drawingPreview, DRAW_PREVIEW_ELEMENT_ID, editor.activeLayerId, () => "");
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
overlayOpts.drawingPreview = editor.drawingPreview;
|
|
228
|
+
}
|
|
123
229
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
links: new Map([[DRAW_PREVIEW_LINK_ID, previewLink]]),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
// Connection anchors. Two roles: link-start (on selection) and link-attach
|
|
138
|
-
// (on hover/proximity). During a drag started FROM a start-anchor (select
|
|
139
|
-
// mode, no tool switch) BOTH are shown: the source keeps its start dots
|
|
140
|
-
// while the target shows its attach dots under the cursor.
|
|
141
|
-
if (editor.mode !== "brush" && editor.mode !== "hand") {
|
|
142
|
-
const zoom = editor._scene.viewport.zoom || 1;
|
|
143
|
-
// Build one overlay port-set for a shape. The free outline-attach point
|
|
144
|
-
// (`outlinePoint`, link-attach only) is appended un-offset — it is the
|
|
145
|
-
// real landing point. `activeAnchorName` highlights the snap target if it
|
|
146
|
-
// is one of the named dots.
|
|
147
|
-
const buildPortSet = (shapeId, role, activeAnchorName, outlinePoint) => {
|
|
148
|
-
const shape = getElement(editor._scene, shapeId);
|
|
149
|
-
if (!shape)
|
|
150
|
-
return null;
|
|
151
|
-
const outsetPx = role === "link-start" ? LINK_START_ANCHOR_OUTSET : LINK_ATTACH_ANCHOR_OUTSET;
|
|
152
|
-
const { names, worldPoints: anchorPts } = anchorOverlayPoints(shape, outsetPx / zoom);
|
|
153
|
-
const worldPoints = [...anchorPts];
|
|
154
|
-
const namedActive = activeAnchorName !== null ? names.indexOf(activeAnchorName) : -1;
|
|
155
|
-
if (role === "link-attach" && outlinePoint)
|
|
156
|
-
worldPoints.push(outlinePoint);
|
|
157
|
-
const activeIndex = namedActive >= 0
|
|
158
|
-
? namedActive
|
|
159
|
-
: role === "link-attach" && outlinePoint
|
|
160
|
-
? worldPoints.length - 1
|
|
161
|
-
: -1;
|
|
162
|
-
return {
|
|
163
|
-
worldPoints,
|
|
164
|
-
...(activeIndex >= 0 ? { activeIndex } : {}),
|
|
165
|
-
role,
|
|
230
|
+
// Draw-edge connector preview: render the would-be link through the real
|
|
231
|
+
// link renderer (solid, default arrowheads, full colour) so the dragged
|
|
232
|
+
// preview looks exactly like the link that'll be created — same default
|
|
233
|
+
// object as commit (`buildLinkForCreate`), not a faded/dashed stand-in.
|
|
234
|
+
let edgePreviewScene = null;
|
|
235
|
+
if (editor.edgePreview) {
|
|
236
|
+
const previewLink = buildEdgePreviewLink(editor.scene, editor.edgePreview, DRAW_PREVIEW_LINK_ID, editor.activeLayerId);
|
|
237
|
+
edgePreviewScene = {
|
|
238
|
+
...editor.scene,
|
|
239
|
+
links: new Map([[DRAW_PREVIEW_LINK_ID, previewLink]]),
|
|
166
240
|
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
241
|
+
}
|
|
242
|
+
// Flowchart-create preview: the pending nodes are drawn through the overlay
|
|
243
|
+
// (faded, `flowchartPreviewElements`); the pending links are drawn through
|
|
244
|
+
// the real link renderer onto a throwaway scene that also carries the
|
|
245
|
+
// pending nodes so both endpoints resolve. Both at reduced opacity below.
|
|
246
|
+
let flowchartPreviewScene = null;
|
|
247
|
+
if (editor.flowchartPreview && editor.flowchartPreview.elements.length > 0) {
|
|
248
|
+
overlayOpts.flowchartPreviewElements = editor.flowchartPreview.elements;
|
|
249
|
+
const previewElements = new Map(editor.scene.elements);
|
|
250
|
+
for (const el of editor.flowchartPreview.elements)
|
|
251
|
+
previewElements.set(el.id, el);
|
|
252
|
+
const previewLinks = new Map();
|
|
253
|
+
for (const link of editor.flowchartPreview.links)
|
|
254
|
+
previewLinks.set(link.id, link);
|
|
255
|
+
flowchartPreviewScene = {
|
|
256
|
+
...editor.scene,
|
|
257
|
+
elements: previewElements,
|
|
258
|
+
links: previewLinks,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// Stroke-erase preview: the would-be brush fragments for every touched
|
|
262
|
+
// brush, drawn per-element on the overlay (the originals are hidden in the
|
|
263
|
+
// main pass). Uses the `flowchartPreviewElements`-style per-element paint —
|
|
264
|
+
// NOT `renderScene`, which clears the overlay and would wipe the eraser
|
|
265
|
+
// cursor ring / trail mid-drag.
|
|
266
|
+
if (editor.strokeErasePreview && editor.strokeErasePreview.elements.length > 0) {
|
|
267
|
+
overlayOpts.strokeErasePreviewElements = editor.strokeErasePreview.elements;
|
|
268
|
+
}
|
|
269
|
+
// Connection anchors. Two roles: link-start (on selection) and link-attach
|
|
270
|
+
// (on hover/proximity). During a drag started FROM a start-anchor (select
|
|
271
|
+
// mode, no tool switch) BOTH are shown: the source keeps its start dots
|
|
272
|
+
// while the target shows its attach dots under the cursor.
|
|
273
|
+
// Read-only never creates links, so the whole port/ghost overlay is off —
|
|
274
|
+
// no anchor dots on hover, no click-create ghost element/connector.
|
|
275
|
+
if (editor.mode !== "brush" && editor.mode !== "hand" && !editor.readOnly) {
|
|
276
|
+
const zoom = editor.scene.viewport.zoom || 1;
|
|
277
|
+
// Build one overlay port-set for a shape. The free outline-attach point
|
|
278
|
+
// (`outlinePoint`, link-attach only) is appended un-offset — it is the
|
|
279
|
+
// real landing point. `activeAnchorName` highlights the snap target if it
|
|
280
|
+
// is one of the named dots.
|
|
281
|
+
const buildPortSet = (shapeId, role, activeAnchorName, outlinePoint) => {
|
|
282
|
+
const shape = getElement(editor.scene, shapeId);
|
|
283
|
+
if (!shape)
|
|
284
|
+
return null;
|
|
285
|
+
const outsetPx = role === "link-start" ? LINK_START_ANCHOR_OUTSET : LINK_ATTACH_ANCHOR_OUTSET;
|
|
286
|
+
const { names, worldPoints: anchorPts } = anchorOverlayPoints(shape, outsetPx / zoom);
|
|
287
|
+
const worldPoints = [...anchorPts];
|
|
288
|
+
const namedActive = activeAnchorName !== null ? names.indexOf(activeAnchorName) : -1;
|
|
289
|
+
if (role === "link-attach" && outlinePoint)
|
|
290
|
+
worldPoints.push(outlinePoint);
|
|
291
|
+
const activeIndex = namedActive >= 0
|
|
292
|
+
? namedActive
|
|
293
|
+
: role === "link-attach" && outlinePoint
|
|
294
|
+
? worldPoints.length - 1
|
|
295
|
+
: -1;
|
|
296
|
+
return {
|
|
297
|
+
worldPoints,
|
|
298
|
+
...(activeIndex >= 0 ? { activeIndex } : {}),
|
|
299
|
+
role,
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
const portSets = [];
|
|
303
|
+
if (editor.linkDragFromAnchor) {
|
|
304
|
+
// Drag from a start-anchor: keep the source's start dots visible…
|
|
305
|
+
const startSet = buildPortSet(editor.linkDragFromAnchor.fromElement, "link-start", null);
|
|
306
|
+
if (startSet)
|
|
307
|
+
portSets.push(startSet);
|
|
308
|
+
// …and show the target's attach dots under the cursor.
|
|
309
|
+
if (editor.hoveredLinkTarget) {
|
|
310
|
+
const t = editor.hoveredLinkTarget;
|
|
311
|
+
const attachSet = buildPortSet(t.elementId, "link-attach", t.activeAnchor, t.outlinePoint);
|
|
312
|
+
if (attachSet)
|
|
313
|
+
portSets.push(attachSet);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (editor.hoveredLinkTarget) {
|
|
317
|
+
// Proximity snap while a link is drawn with the draw-edge tool.
|
|
176
318
|
const t = editor.hoveredLinkTarget;
|
|
177
319
|
const attachSet = buildPortSet(t.elementId, "link-attach", t.activeAnchor, t.outlinePoint);
|
|
178
320
|
if (attachSet)
|
|
179
321
|
portSets.push(attachSet);
|
|
180
322
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
bestI
|
|
323
|
+
else if (!editor.panGesture &&
|
|
324
|
+
!editor.pinchActive &&
|
|
325
|
+
!editor.gestureActive && // hide only during a real drag (tx opens on first move-patch), not on a bare press
|
|
326
|
+
!editor.edgePreview && // don't show start-anchors if we are already drawing a link
|
|
327
|
+
!editor.linkEndpointDrag // or dragging an existing endpoint
|
|
328
|
+
) {
|
|
329
|
+
// At rest — show link-start anchors for the single selected element, and
|
|
330
|
+
// only while the cursor is over it or within reach of its dots: dots sit
|
|
331
|
+
// just OUTSIDE the edges, so the hover zone is the element bounds
|
|
332
|
+
// expanded by the outset + dot grab radius. Keeps them visible as the
|
|
333
|
+
// cursor travels out to a dot. Dots are NOT shown on unselected
|
|
334
|
+
// elements — select first, then connect. The dot nearest the cursor
|
|
335
|
+
// grows (`ANCHOR_DOT_HOVER_GROW_RADIUS`).
|
|
336
|
+
const cursor = editor.hoverCursorWorld;
|
|
337
|
+
if (editor.selection.size === 1 && cursor) {
|
|
338
|
+
const id = req([...editor.selection][0]);
|
|
339
|
+
const shape = getElement(editor.scene, id);
|
|
340
|
+
if (shape) {
|
|
341
|
+
const b = getElementWorldBounds(shape);
|
|
342
|
+
const pad = (LINK_START_ANCHOR_OUTSET + ANCHOR_DOT_ACTIVE_RADIUS + editor.anchorStartHitSlop) /
|
|
343
|
+
zoom;
|
|
344
|
+
const near = cursor.x >= b.x - pad &&
|
|
345
|
+
cursor.x <= b.x + b.width + pad &&
|
|
346
|
+
cursor.y >= b.y - pad &&
|
|
347
|
+
cursor.y <= b.y + b.height + pad;
|
|
348
|
+
if (near) {
|
|
349
|
+
const set = buildPortSet(id, "link-start", null);
|
|
350
|
+
if (set) {
|
|
351
|
+
const r = ANCHOR_DOT_HOVER_GROW_RADIUS / zoom;
|
|
352
|
+
let bestI = -1;
|
|
353
|
+
let bestD2 = r * r;
|
|
354
|
+
set.worldPoints.forEach((p, i) => {
|
|
355
|
+
const dx = p.x - cursor.x;
|
|
356
|
+
const dy = p.y - cursor.y;
|
|
357
|
+
const d2 = dx * dx + dy * dy;
|
|
358
|
+
if (d2 <= bestD2) {
|
|
359
|
+
bestD2 = d2;
|
|
360
|
+
bestI = i;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
if (bestI >= 0) {
|
|
364
|
+
// Smooth proximity grow: scale the nearest dot from its resting
|
|
365
|
+
// radius up to the max as the cursor closes in (t = 1 at the
|
|
366
|
+
// dot, 0 at the edge of the grow radius).
|
|
367
|
+
const t = r > 0 ? Math.max(0, 1 - Math.sqrt(bestD2) / r) : 0;
|
|
368
|
+
const activeRadius = ANCHOR_DOT_RADIUS + (ANCHOR_DOT_HOVER_MAX_RADIUS - ANCHOR_DOT_RADIUS) * t;
|
|
369
|
+
portSets.push({ ...set, activeIndex: bestI, activeRadius });
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
portSets.push(set);
|
|
228
373
|
}
|
|
229
|
-
});
|
|
230
|
-
if (bestI >= 0) {
|
|
231
|
-
// Smooth proximity grow: scale the nearest dot from its resting
|
|
232
|
-
// radius up to the max as the cursor closes in (t = 1 at the
|
|
233
|
-
// dot, 0 at the edge of the grow radius).
|
|
234
|
-
const t = r > 0 ? Math.max(0, 1 - Math.sqrt(bestD2) / r) : 0;
|
|
235
|
-
const activeRadius = ANCHOR_DOT_RADIUS + (ANCHOR_DOT_HOVER_MAX_RADIUS - ANCHOR_DOT_RADIUS) * t;
|
|
236
|
-
portSets.push({ ...set, activeIndex: bestI, activeRadius });
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
portSets.push(set);
|
|
240
374
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
375
|
+
// Hovering ON a dot (within the click radius) → ghost preview of
|
|
376
|
+
// what a click would create (copy element + connector).
|
|
377
|
+
const { names, worldPoints } = anchorOverlayPoints(shape, LINK_START_ANCHOR_OUTSET / zoom);
|
|
378
|
+
const clickR2 = (editor.anchorClickRadius / zoom) ** 2;
|
|
379
|
+
let hoveredName = null;
|
|
380
|
+
for (let i = 0; i < worldPoints.length; i++) {
|
|
381
|
+
const p = req(worldPoints[i]);
|
|
382
|
+
if ((p.x - cursor.x) ** 2 + (p.y - cursor.y) ** 2 <= clickR2) {
|
|
383
|
+
hoveredName = req(names[i]);
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
252
386
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
387
|
+
if (hoveredName) {
|
|
388
|
+
const preview = editor.previewClickCreate(id, hoveredName);
|
|
389
|
+
if (preview) {
|
|
390
|
+
overlayOpts.ghostElement = preview.bounds;
|
|
391
|
+
overlayOpts.ghostElementShape = preview.element;
|
|
392
|
+
ghostScene = preview.ghostScene;
|
|
393
|
+
}
|
|
260
394
|
}
|
|
261
395
|
}
|
|
262
396
|
}
|
|
263
397
|
}
|
|
264
398
|
}
|
|
399
|
+
if (portSets.length === 1)
|
|
400
|
+
overlayOpts.ports = req(portSets[0]);
|
|
401
|
+
else if (portSets.length > 1)
|
|
402
|
+
overlayOpts.ports = portSets;
|
|
403
|
+
// Float-attach feedback: when the endpoint will attach to the whole
|
|
404
|
+
// element (not a specific dot), highlight that element so the user knows
|
|
405
|
+
// it'll float vs fix to a point.
|
|
406
|
+
const hov = editor.hoveredLinkTarget;
|
|
407
|
+
if (hov?.mode === "element") {
|
|
408
|
+
const tshape = getElement(editor.scene, hov.elementId);
|
|
409
|
+
if (tshape)
|
|
410
|
+
overlayOpts.linkAttachHighlight = getElementWorldBounds(tshape);
|
|
411
|
+
}
|
|
265
412
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const tshape = getElement(editor._scene, hov.elementId);
|
|
276
|
-
if (tshape)
|
|
277
|
-
overlayOpts.linkAttachHighlight = getElementWorldBounds(tshape);
|
|
413
|
+
// Group-handle overlay: a multi-object selection (elements + links) OR a
|
|
414
|
+
// single group-typed shape. A lone link keeps its endpoint handles, not a
|
|
415
|
+
// resize box. Aspect-locked groups flag the overlay for corner-only handles.
|
|
416
|
+
if (editor.selection.size + editor.selectedLinks.size > 1 || editor.aspectLocked) {
|
|
417
|
+
const combined = editor.combinedSelectionBounds;
|
|
418
|
+
if (combined)
|
|
419
|
+
overlayOpts.groupBounds = combined;
|
|
420
|
+
if (editor.aspectLocked)
|
|
421
|
+
overlayOpts.groupAspectLocked = true;
|
|
278
422
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
423
|
+
if (editor.containerHover) {
|
|
424
|
+
overlayOpts.containerDropZone = editor.containerHover.dropZone;
|
|
425
|
+
}
|
|
426
|
+
if (editor.brushStroke) {
|
|
427
|
+
// The preview stroke is precomputed by `buildRenderSnapshot`: Catmull-Rom
|
|
428
|
+
// smoothed (same resampler the commit uses) with the real paint colour and
|
|
429
|
+
// opacity, so it matches the committed stroke exactly — no geometry / colour
|
|
430
|
+
// / opacity snap when the pointer is lifted.
|
|
431
|
+
overlayOpts.brushPreview = {
|
|
432
|
+
origin: editor.brushStroke.origin,
|
|
433
|
+
points: editor.brushStroke.points,
|
|
434
|
+
fill: editor.brushStroke.fill,
|
|
435
|
+
opacity: editor.brushStroke.opacity,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (editor.laserStrokes.length > 0) {
|
|
439
|
+
overlayOpts.laserStrokes = editor.laserStrokes;
|
|
440
|
+
}
|
|
441
|
+
if (editor.eraserTrail.length > 0) {
|
|
442
|
+
overlayOpts.eraserTrail = editor.eraserTrail;
|
|
443
|
+
}
|
|
444
|
+
if (editor.eraserCursor) {
|
|
445
|
+
overlayOpts.eraserCursor = editor.eraserCursor;
|
|
446
|
+
}
|
|
447
|
+
// Persistent halo around EVERY selected link (multi-select). Curve-aware
|
|
448
|
+
// so the halo follows the drawn path, matching the hover highlight.
|
|
449
|
+
if (editor.selectedLinks.size > 0) {
|
|
450
|
+
const halos = [];
|
|
451
|
+
for (const id of editor.selectedLinks) {
|
|
452
|
+
const edge = getLink(editor.scene, id);
|
|
453
|
+
if (!edge)
|
|
454
|
+
continue;
|
|
455
|
+
const hpath = getLinkCurvePoints(editor.scene, edge);
|
|
456
|
+
if (hpath && hpath.length >= 2) {
|
|
457
|
+
halos.push({ path: hpath, width: edge.style.strokeWidth ?? 1 });
|
|
458
|
+
}
|
|
311
459
|
}
|
|
460
|
+
if (halos.length > 0)
|
|
461
|
+
overlayOpts.selectedLinkPaths = halos;
|
|
312
462
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
for
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
463
|
+
// Endpoint / bend handles only for the SOLE selected link (no elements);
|
|
464
|
+
// a multi/mixed selection hides them to stay uncluttered.
|
|
465
|
+
const soleSelectedLink = editor.selectedLink;
|
|
466
|
+
if (soleSelectedLink) {
|
|
467
|
+
const edge = getLink(editor.scene, soleSelectedLink);
|
|
468
|
+
if (edge) {
|
|
469
|
+
const path = getLinkPath(editor.scene, edge);
|
|
470
|
+
if (path && path.length >= 2) {
|
|
471
|
+
// During an endpoint-rebind drag the dragged end is re-pointed live in
|
|
472
|
+
// the scene (the whole link follows the cursor), so `path` already
|
|
473
|
+
// reflects the cursor position — handles ride along with it.
|
|
474
|
+
const from = req(path[0]);
|
|
475
|
+
const to = req(path[path.length - 1]);
|
|
476
|
+
// Bend-point handles: existing waypoints (solid) + segment-midpoint
|
|
477
|
+
// "add" handles along the logical [from, ...waypoints, to] chain.
|
|
478
|
+
// Midpoints are hidden during an active waypoint drag to declutter.
|
|
479
|
+
// ELBOW (orthogonal) links don't expose free bend handles — their
|
|
480
|
+
// points are router output, not user-placed (segment-drag editing is
|
|
481
|
+
// a separate mechanic). Only straight / bezier show free waypoints.
|
|
482
|
+
const isElbow = (edge.routing ?? "straight") === "orthogonal";
|
|
483
|
+
if (isElbow) {
|
|
484
|
+
// Segment handles on the slidable segments of the routed chain —
|
|
485
|
+
// positions shared with the pointer hit-test (and slid out from
|
|
486
|
+
// under the caption pill). Hidden during an active segment /
|
|
487
|
+
// endpoint drag.
|
|
488
|
+
const midpoints = editor.linkSegmentDrag || editor.linkEndpointDrag
|
|
489
|
+
? []
|
|
490
|
+
: getElbowSegmentHandles(editor.scene, edge, path).map((h) => h.point);
|
|
491
|
+
overlayOpts.edgeSelection = { from, to, midpoints };
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
const waypoints = [...(edge.waypoints ?? [])];
|
|
495
|
+
// "Add waypoint" handles sit at the VISUAL middle of each span — on
|
|
496
|
+
// the drawn arc for bezier (t=0.5 of the span's cubic), on the chord
|
|
497
|
+
// for straight. The raw chord midpoint would put bezier handles off
|
|
498
|
+
// the curve.
|
|
499
|
+
const midpoints = editor.linkWaypointDrag
|
|
500
|
+
? []
|
|
501
|
+
: (getLinkWaypointMidpoints(editor.scene, edge) ?? []);
|
|
502
|
+
overlayOpts.edgeSelection = { from, to, waypoints, midpoints };
|
|
352
503
|
}
|
|
353
|
-
overlayOpts.edgeSelection = { from, to, midpoints };
|
|
354
504
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (editor.peerCursors.length > 0)
|
|
508
|
+
overlayOpts.peerCursors = editor.peerCursors;
|
|
509
|
+
if (editor.peerSelections.length > 0)
|
|
510
|
+
overlayOpts.peerSelections = editor.peerSelections;
|
|
511
|
+
if (editor.scene.annotations.size > 0) {
|
|
512
|
+
overlayOpts.annotations = [...editor.scene.annotations.values()];
|
|
513
|
+
overlayOpts.selectedAnnotation = editor.selectedAnnotation;
|
|
514
|
+
}
|
|
515
|
+
// In-canvas text editing: caret + selection highlight for the shape
|
|
516
|
+
// under edit (null when not editing).
|
|
517
|
+
const editingText = editor.editingText;
|
|
518
|
+
if (editingText)
|
|
519
|
+
overlayOpts.editingText = editingText;
|
|
520
|
+
if (editor.debugHitZones) {
|
|
521
|
+
overlayOpts.debugHitZones = true;
|
|
522
|
+
// One decision point for which hit-zone categories are actionable now
|
|
523
|
+
// (see `hitZoneVisibility`): while a link endpoint is being placed only
|
|
524
|
+
// the link-attach drop-zones are live; at rest the resize / body / handle
|
|
525
|
+
// / start-dot zones are. The overlay paints only the visible ones.
|
|
526
|
+
const linkDragActive = editor.linkDragFromAnchor !== null ||
|
|
527
|
+
editor.edgePreview !== null ||
|
|
528
|
+
editor.linkEndpointDrag !== null;
|
|
529
|
+
// An element move is in flight: position snapshot taken (groupMoveOrigin)
|
|
530
|
+
// AND a real drag transaction opened (gestureTx opens only past the drag
|
|
531
|
+
// threshold, so a bare press doesn't count). Resize uses groupResizeOrigin
|
|
532
|
+
// instead, so this stays move-only.
|
|
533
|
+
const elementDragActive = editor.groupMoveOrigin !== null && editor.gestureActive;
|
|
534
|
+
const visibility = hitZoneVisibility({ linkDragActive, elementDragActive });
|
|
535
|
+
overlayOpts.debugHitZoneVisibility = visibility;
|
|
536
|
+
if (visibility.attachDropZones) {
|
|
537
|
+
// Link-attach drop-zones (anchor catchments + edge bands + floating body)
|
|
538
|
+
// the snap engine resolves against, for every element except the drag
|
|
539
|
+
// source — shows where a drop lands ON a point vs ON an edge vs the body.
|
|
540
|
+
const srcId = editor.linkDragFromAnchor?.fromElement;
|
|
541
|
+
const anchors = [];
|
|
542
|
+
const outlineLoops = [];
|
|
543
|
+
for (const shape of editor.scene.elements.values()) {
|
|
544
|
+
if (shape.id === srcId)
|
|
545
|
+
continue;
|
|
546
|
+
anchors.push(...anchorOverlayPoints(shape, 0).worldPoints);
|
|
547
|
+
for (const loop of getElementOutline(editor.scene, shape))
|
|
548
|
+
outlineLoops.push(loop);
|
|
549
|
+
}
|
|
550
|
+
overlayOpts.debugAttachZones = {
|
|
551
|
+
anchors,
|
|
552
|
+
outlineLoops,
|
|
553
|
+
thresholdWorld: DEFAULT_SNAP_THRESHOLD,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (visibility.containerDropZones) {
|
|
557
|
+
// Element drop-zones for ALL frames + containers at once. Frames use
|
|
558
|
+
// centroid-in-body (full world bounds); containers use
|
|
559
|
+
// cursor-in-drop-zone (resolved zone). The dragged elements themselves
|
|
560
|
+
// are skipped — you can't drop into self.
|
|
561
|
+
const dragged = editor.groupMoveOrigin;
|
|
562
|
+
const frames = [];
|
|
563
|
+
const containers = [];
|
|
564
|
+
for (const shape of editor.scene.elements.values()) {
|
|
565
|
+
if (dragged?.has(shape.id))
|
|
566
|
+
continue;
|
|
567
|
+
if (isFrame(shape)) {
|
|
568
|
+
frames.push(getElementWorldBounds(shape));
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
// All drop-zones (every lane of a multi-lane container), not just the
|
|
572
|
+
// single attach zone — so a swim-lane shows a region per lane.
|
|
573
|
+
containers.push(...getDropZonesWorld(shape));
|
|
365
574
|
}
|
|
575
|
+
overlayOpts.debugContainerZones = { frames, containers };
|
|
366
576
|
}
|
|
367
577
|
}
|
|
578
|
+
overlayMemo = {
|
|
579
|
+
sig: overlaySig,
|
|
580
|
+
opts: overlayOpts,
|
|
581
|
+
ghostScene,
|
|
582
|
+
edgePreviewScene,
|
|
583
|
+
flowchartPreviewScene,
|
|
584
|
+
};
|
|
585
|
+
overlayMemoByTarget.set(editor.overlayTarget, overlayMemo);
|
|
368
586
|
}
|
|
369
|
-
|
|
370
|
-
overlayOpts.peerCursors = editor._peerCursors;
|
|
371
|
-
if (editor._peerSelections.length > 0)
|
|
372
|
-
overlayOpts.peerSelections = editor._peerSelections;
|
|
373
|
-
if (editor._scene.annotations.size > 0) {
|
|
374
|
-
overlayOpts.annotations = [...editor._scene.annotations.values()];
|
|
375
|
-
overlayOpts.selectedAnnotation = editor._selectedAnnotation;
|
|
376
|
-
}
|
|
587
|
+
const overlayOpts = overlayMemo.opts;
|
|
377
588
|
// "Play" badge on paused animated (GIF) shapes — auto-stopped or held under
|
|
378
|
-
// prefers-reduced-motion.
|
|
589
|
+
// prefers-reduced-motion. Recomputed every frame (and applied onto the
|
|
590
|
+
// possibly-memoized bag): playback pause state is not part of the overlay
|
|
591
|
+
// signature — it advances on its own timeline — so a memo hit must still
|
|
592
|
+
// refresh the badge set.
|
|
379
593
|
const gifBadges = [];
|
|
380
|
-
for (const shape of editor.
|
|
594
|
+
for (const shape of editor.scene.elements.values()) {
|
|
381
595
|
if (isImage(shape) && shape.animationKind && editor.isPlaybackPaused(shape.id)) {
|
|
382
596
|
gifBadges.push(getElementWorldBounds(shape));
|
|
383
597
|
}
|
|
384
598
|
}
|
|
385
599
|
if (gifBadges.length > 0)
|
|
386
600
|
overlayOpts.gifBadges = gifBadges;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (editingText)
|
|
391
|
-
overlayOpts.editingText = editingText;
|
|
392
|
-
if (editor.debugHitZones) {
|
|
393
|
-
overlayOpts.debugHitZones = true;
|
|
394
|
-
// One decision point for which hit-zone categories are actionable now
|
|
395
|
-
// (see `hitZoneVisibility`): while a link endpoint is being placed only
|
|
396
|
-
// the link-attach drop-zones are live; at rest the resize / body / handle
|
|
397
|
-
// / start-dot zones are. The overlay paints only the visible ones.
|
|
398
|
-
const linkDragActive = editor.linkDragFromAnchor !== null ||
|
|
399
|
-
editor.edgePreview !== null ||
|
|
400
|
-
editor.linkEndpointDrag !== null;
|
|
401
|
-
// An element move is in flight: position snapshot taken (groupMoveOrigin)
|
|
402
|
-
// AND a real drag transaction opened (gestureTx opens only past the drag
|
|
403
|
-
// threshold, so a bare press doesn't count). Resize uses groupResizeOrigin
|
|
404
|
-
// instead, so this stays move-only.
|
|
405
|
-
const elementDragActive = editor.groupMoveOrigin !== null && editor.gestureTx !== null;
|
|
406
|
-
const visibility = hitZoneVisibility({ linkDragActive, elementDragActive });
|
|
407
|
-
overlayOpts.debugHitZoneVisibility = visibility;
|
|
408
|
-
if (visibility.attachDropZones) {
|
|
409
|
-
// Link-attach drop-zones (anchor catchments + edge bands + floating body)
|
|
410
|
-
// the snap engine resolves against, for every element except the drag
|
|
411
|
-
// source — shows where a drop lands ON a point vs ON an edge vs the body.
|
|
412
|
-
const srcId = editor.linkDragFromAnchor?.fromElement;
|
|
413
|
-
const anchors = [];
|
|
414
|
-
const outlineLoops = [];
|
|
415
|
-
for (const shape of editor._scene.elements.values()) {
|
|
416
|
-
if (shape.id === srcId)
|
|
417
|
-
continue;
|
|
418
|
-
anchors.push(...anchorOverlayPoints(shape, 0).worldPoints);
|
|
419
|
-
for (const loop of getElementOutline(editor._scene, shape))
|
|
420
|
-
outlineLoops.push(loop);
|
|
421
|
-
}
|
|
422
|
-
overlayOpts.debugAttachZones = {
|
|
423
|
-
anchors,
|
|
424
|
-
outlineLoops,
|
|
425
|
-
thresholdWorld: DEFAULT_SNAP_THRESHOLD,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
if (visibility.containerDropZones) {
|
|
429
|
-
// Element drop-zones for ALL frames + containers at once. Frames use
|
|
430
|
-
// centroid-in-body (full world bounds); containers use
|
|
431
|
-
// cursor-in-drop-zone (resolved zone). The dragged elements themselves
|
|
432
|
-
// are skipped — you can't drop into self.
|
|
433
|
-
const dragged = editor.groupMoveOrigin;
|
|
434
|
-
const frames = [];
|
|
435
|
-
const containers = [];
|
|
436
|
-
for (const shape of editor._scene.elements.values()) {
|
|
437
|
-
if (dragged?.has(shape.id))
|
|
438
|
-
continue;
|
|
439
|
-
if (isFrame(shape)) {
|
|
440
|
-
frames.push(getElementWorldBounds(shape));
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
// All drop-zones (every lane of a multi-lane container), not just the
|
|
444
|
-
// single attach zone — so a swim-lane shows a region per lane.
|
|
445
|
-
containers.push(...getDropZonesWorld(shape));
|
|
446
|
-
}
|
|
447
|
-
overlayOpts.debugContainerZones = { frames, containers };
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
renderOverlay(editor._scene, editor._selection, editor.overlayTarget, overlayOpts);
|
|
601
|
+
else if (overlayOpts.gifBadges !== undefined)
|
|
602
|
+
delete overlayOpts.gifBadges;
|
|
603
|
+
renderOverlay(editor.scene, editor.selection, editor.overlayTarget, overlayOpts);
|
|
451
604
|
// Ghost connector (click-create hover) — drawn through the REAL link
|
|
452
605
|
// renderer onto the overlay, faded, AFTER the overlay chrome so it sits on
|
|
453
606
|
// top of the ghost element (matches scene z-order: links over shapes). Uses
|
|
454
607
|
// the would-be link's actual routing / arrowhead / style, not a dashed line.
|
|
455
|
-
if (ghostScene) {
|
|
608
|
+
if (overlayMemo.ghostScene) {
|
|
456
609
|
editor.overlayTarget.save();
|
|
457
610
|
editor.overlayTarget.setOpacity(GHOST_PREVIEW_OPACITY);
|
|
458
|
-
renderLinks(ghostScene, editor.overlayTarget, {});
|
|
611
|
+
renderLinks(overlayMemo.ghostScene, editor.overlayTarget, {});
|
|
459
612
|
editor.overlayTarget.restore();
|
|
460
613
|
}
|
|
461
614
|
// Live draw-edge connector preview — real link renderer, FULL opacity (the
|
|
462
615
|
// preview must look identical to the committed link, not faded/dashed).
|
|
463
|
-
if (edgePreviewScene) {
|
|
464
|
-
renderLinks(edgePreviewScene, editor.overlayTarget, {});
|
|
616
|
+
if (overlayMemo.edgePreviewScene) {
|
|
617
|
+
renderLinks(overlayMemo.edgePreviewScene, editor.overlayTarget, {});
|
|
618
|
+
}
|
|
619
|
+
// Flowchart-create connectors — pending links drawn through the real link
|
|
620
|
+
// renderer at reduced opacity (the pending nodes are painted by the overlay's
|
|
621
|
+
// `flowchartPreviewElements` at the same opacity). Drawn last so connectors
|
|
622
|
+
// sit over the nodes, matching scene z-order.
|
|
623
|
+
if (overlayMemo.flowchartPreviewScene) {
|
|
624
|
+
editor.overlayTarget.save();
|
|
625
|
+
editor.overlayTarget.setOpacity(FLOWCHART_PREVIEW_OPACITY);
|
|
626
|
+
renderLinks(overlayMemo.flowchartPreviewScene, editor.overlayTarget, {});
|
|
627
|
+
editor.overlayTarget.restore();
|
|
465
628
|
}
|
|
466
629
|
};
|
|
467
630
|
//# sourceMappingURL=render-orchestrator.js.map
|