@oh-just-another/state 0.57.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 +18 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/actions/actionClipboard.d.ts +6 -0
- package/dist/actions/actionClipboard.d.ts.map +1 -0
- package/dist/actions/actionClipboard.js +26 -0
- package/dist/actions/actionClipboard.js.map +1 -0
- package/dist/actions/actionGrouping.d.ts +5 -0
- package/dist/actions/actionGrouping.d.ts.map +1 -0
- package/dist/actions/actionGrouping.js +26 -0
- package/dist/actions/actionGrouping.js.map +1 -0
- package/dist/actions/actionHistory.d.ts +5 -0
- package/dist/actions/actionHistory.d.ts.map +1 -0
- package/dist/actions/actionHistory.js +25 -0
- package/dist/actions/actionHistory.js.map +1 -0
- package/dist/actions/actionKeyboard.d.ts +3 -0
- package/dist/actions/actionKeyboard.d.ts.map +1 -0
- package/dist/actions/actionKeyboard.js +130 -0
- package/dist/actions/actionKeyboard.js.map +1 -0
- package/dist/actions/actionLayout.d.ts +3 -0
- package/dist/actions/actionLayout.d.ts.map +1 -0
- package/dist/actions/actionLayout.js +65 -0
- package/dist/actions/actionLayout.js.map +1 -0
- package/dist/actions/actionMode.d.ts +13 -0
- package/dist/actions/actionMode.d.ts.map +1 -0
- package/dist/actions/actionMode.js +116 -0
- package/dist/actions/actionMode.js.map +1 -0
- package/dist/actions/actionSelection.d.ts +9 -0
- package/dist/actions/actionSelection.d.ts.map +1 -0
- package/dist/actions/actionSelection.js +57 -0
- package/dist/actions/actionSelection.js.map +1 -0
- package/dist/actions/actionView.d.ts +7 -0
- package/dist/actions/actionView.d.ts.map +1 -0
- package/dist/actions/actionView.js +14 -0
- package/dist/actions/actionView.js.map +1 -0
- package/dist/actions/actionZOrder.d.ts +7 -0
- package/dist/actions/actionZOrder.d.ts.map +1 -0
- package/dist/actions/actionZOrder.js +42 -0
- package/dist/actions/actionZOrder.js.map +1 -0
- package/dist/actions/actionZoom.d.ts +8 -0
- package/dist/actions/actionZoom.d.ts.map +1 -0
- package/dist/actions/actionZoom.js +61 -0
- package/dist/actions/actionZoom.js.map +1 -0
- package/dist/actions/index.d.ts +12 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/registry.d.ts +68 -0
- package/dist/actions/registry.d.ts.map +1 -0
- package/dist/actions/registry.js +300 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/actions/types.d.ts +103 -0
- package/dist/actions/types.d.ts.map +1 -0
- package/dist/actions/types.js +10 -0
- package/dist/actions/types.js.map +1 -0
- package/dist/animation-tick.d.ts +28 -0
- package/dist/animation-tick.d.ts.map +1 -0
- package/dist/animation-tick.js +50 -0
- package/dist/animation-tick.js.map +1 -0
- package/dist/auto-compact.d.ts +27 -0
- package/dist/auto-compact.d.ts.map +1 -0
- package/dist/auto-compact.js +46 -0
- package/dist/auto-compact.js.map +1 -0
- package/dist/auto-layout-scheduler.d.ts +50 -0
- package/dist/auto-layout-scheduler.d.ts.map +1 -0
- package/dist/auto-layout-scheduler.js +94 -0
- package/dist/auto-layout-scheduler.js.map +1 -0
- package/dist/built-in-handlers.d.ts +24 -0
- package/dist/built-in-handlers.d.ts.map +1 -0
- package/dist/built-in-handlers.js +210 -0
- package/dist/built-in-handlers.js.map +1 -0
- package/dist/clipboard.d.ts +51 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +105 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/constants.d.ts +544 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +537 -0
- package/dist/constants.js.map +1 -0
- package/dist/dom-events.d.ts +13 -0
- package/dist/dom-events.d.ts.map +1 -0
- package/dist/dom-events.js +70 -0
- package/dist/dom-events.js.map +1 -0
- package/dist/editor/anchor-points.d.ts +22 -0
- package/dist/editor/anchor-points.d.ts.map +1 -0
- package/dist/editor/anchor-points.js +28 -0
- package/dist/editor/anchor-points.js.map +1 -0
- package/dist/editor/applies/create.d.ts +60 -0
- package/dist/editor/applies/create.d.ts.map +1 -0
- package/dist/editor/applies/create.js +107 -0
- package/dist/editor/applies/create.js.map +1 -0
- package/dist/editor/applies/edge.d.ts +32 -0
- package/dist/editor/applies/edge.d.ts.map +1 -0
- package/dist/editor/applies/edge.js +44 -0
- package/dist/editor/applies/edge.js.map +1 -0
- package/dist/editor/applies/link-move.d.ts +53 -0
- package/dist/editor/applies/link-move.d.ts.map +1 -0
- package/dist/editor/applies/link-move.js +160 -0
- package/dist/editor/applies/link-move.js.map +1 -0
- package/dist/editor/applies/move.d.ts +38 -0
- package/dist/editor/applies/move.d.ts.map +1 -0
- package/dist/editor/applies/move.js +83 -0
- package/dist/editor/applies/move.js.map +1 -0
- package/dist/editor/applies/resize.d.ts +68 -0
- package/dist/editor/applies/resize.d.ts.map +1 -0
- package/dist/editor/applies/resize.js +199 -0
- package/dist/editor/applies/resize.js.map +1 -0
- package/dist/editor/applies/selection.d.ts +34 -0
- package/dist/editor/applies/selection.d.ts.map +1 -0
- package/dist/editor/applies/selection.js +65 -0
- package/dist/editor/applies/selection.js.map +1 -0
- package/dist/editor/applies/snap-grid.d.ts +45 -0
- package/dist/editor/applies/snap-grid.d.ts.map +1 -0
- package/dist/editor/applies/snap-grid.js +90 -0
- package/dist/editor/applies/snap-grid.js.map +1 -0
- package/dist/editor/container-ops.d.ts +74 -0
- package/dist/editor/container-ops.d.ts.map +1 -0
- package/dist/editor/container-ops.js +262 -0
- package/dist/editor/container-ops.js.map +1 -0
- package/dist/editor/event-fanout.d.ts +48 -0
- package/dist/editor/event-fanout.d.ts.map +1 -0
- package/dist/editor/event-fanout.js +53 -0
- package/dist/editor/event-fanout.js.map +1 -0
- package/dist/editor/gesture-tx.d.ts +77 -0
- package/dist/editor/gesture-tx.d.ts.map +1 -0
- package/dist/editor/gesture-tx.js +93 -0
- package/dist/editor/gesture-tx.js.map +1 -0
- package/dist/editor/hit-test.d.ts +178 -0
- package/dist/editor/hit-test.d.ts.map +1 -0
- package/dist/editor/hit-test.js +460 -0
- package/dist/editor/hit-test.js.map +1 -0
- package/dist/editor/long-press.d.ts +34 -0
- package/dist/editor/long-press.d.ts.map +1 -0
- package/dist/editor/long-press.js +58 -0
- package/dist/editor/long-press.js.map +1 -0
- package/dist/editor/pinch.d.ts +37 -0
- package/dist/editor/pinch.d.ts.map +1 -0
- package/dist/editor/pinch.js +90 -0
- package/dist/editor/pinch.js.map +1 -0
- package/dist/editor/pointer-binding.d.ts +18 -0
- package/dist/editor/pointer-binding.d.ts.map +1 -0
- package/dist/editor/pointer-binding.js +1087 -0
- package/dist/editor/pointer-binding.js.map +1 -0
- package/dist/editor/public/annotations.d.ts +54 -0
- package/dist/editor/public/annotations.d.ts.map +1 -0
- package/dist/editor/public/annotations.js +106 -0
- package/dist/editor/public/annotations.js.map +1 -0
- package/dist/editor/public/arrange-group.d.ts +81 -0
- package/dist/editor/public/arrange-group.d.ts.map +1 -0
- package/dist/editor/public/arrange-group.js +193 -0
- package/dist/editor/public/arrange-group.js.map +1 -0
- package/dist/editor/public/brush.d.ts +33 -0
- package/dist/editor/public/brush.d.ts.map +1 -0
- package/dist/editor/public/brush.js +54 -0
- package/dist/editor/public/brush.js.map +1 -0
- package/dist/editor/public/clipboard.d.ts +31 -0
- package/dist/editor/public/clipboard.d.ts.map +1 -0
- package/dist/editor/public/clipboard.js +35 -0
- package/dist/editor/public/clipboard.js.map +1 -0
- package/dist/editor/public/image-insert.d.ts +30 -0
- package/dist/editor/public/image-insert.d.ts.map +1 -0
- package/dist/editor/public/image-insert.js +57 -0
- package/dist/editor/public/image-insert.js.map +1 -0
- package/dist/editor/public/layers.d.ts +56 -0
- package/dist/editor/public/layers.d.ts.map +1 -0
- package/dist/editor/public/layers.js +104 -0
- package/dist/editor/public/layers.js.map +1 -0
- package/dist/editor/public/link.d.ts +25 -0
- package/dist/editor/public/link.d.ts.map +1 -0
- package/dist/editor/public/link.js +74 -0
- package/dist/editor/public/link.js.map +1 -0
- package/dist/editor/public/placement.d.ts +60 -0
- package/dist/editor/public/placement.d.ts.map +1 -0
- package/dist/editor/public/placement.js +111 -0
- package/dist/editor/public/placement.js.map +1 -0
- package/dist/editor/public/selection-ops.d.ts +78 -0
- package/dist/editor/public/selection-ops.d.ts.map +1 -0
- package/dist/editor/public/selection-ops.js +226 -0
- package/dist/editor/public/selection-ops.js.map +1 -0
- package/dist/editor/public/text-edit.d.ts +17 -0
- package/dist/editor/public/text-edit.d.ts.map +1 -0
- package/dist/editor/public/text-edit.js +28 -0
- package/dist/editor/public/text-edit.js.map +1 -0
- package/dist/editor/public/z-order.d.ts +47 -0
- package/dist/editor/public/z-order.d.ts.map +1 -0
- package/dist/editor/public/z-order.js +151 -0
- package/dist/editor/public/z-order.js.map +1 -0
- package/dist/editor/public/zoom-pan.d.ts +44 -0
- package/dist/editor/public/zoom-pan.d.ts.map +1 -0
- package/dist/editor/public/zoom-pan.js +116 -0
- package/dist/editor/public/zoom-pan.js.map +1 -0
- package/dist/editor/render-orchestrator.d.ts +11 -0
- package/dist/editor/render-orchestrator.d.ts.map +1 -0
- package/dist/editor/render-orchestrator.js +473 -0
- package/dist/editor/render-orchestrator.js.map +1 -0
- package/dist/editor/resize-helpers.d.ts +30 -0
- package/dist/editor/resize-helpers.d.ts.map +1 -0
- package/dist/editor/resize-helpers.js +121 -0
- package/dist/editor/resize-helpers.js.map +1 -0
- package/dist/editor/shape-filters.d.ts +11 -0
- package/dist/editor/shape-filters.d.ts.map +1 -0
- package/dist/editor/shape-filters.js +19 -0
- package/dist/editor/shape-filters.js.map +1 -0
- package/dist/editor/shape-traits.d.ts +16 -0
- package/dist/editor/shape-traits.d.ts.map +1 -0
- package/dist/editor/shape-traits.js +29 -0
- package/dist/editor/shape-traits.js.map +1 -0
- package/dist/editor/viewport-helpers.d.ts +24 -0
- package/dist/editor/viewport-helpers.d.ts.map +1 -0
- package/dist/editor/viewport-helpers.js +63 -0
- package/dist/editor/viewport-helpers.js.map +1 -0
- package/dist/editor-events.d.ts +33 -0
- package/dist/editor-events.d.ts.map +1 -0
- package/dist/editor-events.js +2 -0
- package/dist/editor-events.js.map +1 -0
- package/dist/editor.d.ts +1841 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +4803 -0
- package/dist/editor.js.map +1 -0
- package/dist/file-drop.d.ts +99 -0
- package/dist/file-drop.d.ts.map +1 -0
- package/dist/file-drop.js +198 -0
- package/dist/file-drop.js.map +1 -0
- package/dist/frame-helpers.d.ts +30 -0
- package/dist/frame-helpers.d.ts.map +1 -0
- package/dist/frame-helpers.js +105 -0
- package/dist/frame-helpers.js.map +1 -0
- package/dist/group-helpers.d.ts +39 -0
- package/dist/group-helpers.d.ts.map +1 -0
- package/dist/group-helpers.js +116 -0
- package/dist/group-helpers.js.map +1 -0
- package/dist/handle.d.ts +73 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +184 -0
- package/dist/handle.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive.d.ts +21 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +11 -0
- package/dist/interactive.js.map +1 -0
- package/dist/link-selection.d.ts +21 -0
- package/dist/link-selection.d.ts.map +1 -0
- package/dist/link-selection.js +37 -0
- package/dist/link-selection.js.map +1 -0
- package/dist/machine.d.ts +322 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +568 -0
- package/dist/machine.js.map +1 -0
- package/dist/modes.d.ts +24 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +2 -0
- package/dist/modes.js.map +1 -0
- package/dist/overlay.d.ts +262 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +673 -0
- package/dist/overlay.js.map +1 -0
- package/dist/platform.d.ts +70 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +115 -0
- package/dist/platform.js.map +1 -0
- package/dist/selection.d.ts +18 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +31 -0
- package/dist/selection.js.map +1 -0
- package/package.json +59 -0
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { getAnnotationWorldPosition, getElementWorldBounds, getWorldToScreen, } from "@oh-just-another/scene";
|
|
2
|
+
import { bounds as B, matrix } from "@oh-just-another/math";
|
|
3
|
+
import { getElementRenderer, strokeRoundedPolyline, LINK_CORNER_RADIUS, } from "@oh-just-another/renderer-core";
|
|
4
|
+
import { ANCHOR_DOT_ACTIVE_RADIUS, ANCHOR_DOT_RADIUS, ANCHOR_DOT_STROKE_WIDTH, ANNOTATION_PIN_BADGE_FONT_SIZE, ANNOTATION_PIN_FILL, ANNOTATION_PIN_RADIUS, ANNOTATION_PIN_RESOLVED_FILL, ANNOTATION_PIN_STROKE, CURSOR_ARROW_SIZE, CURSOR_NAME_CHIP_OFFSET, CURSOR_NAME_CHIP_PADDING_X, CURSOR_NAME_CHIP_PADDING_Y, CURSOR_NAME_FONT_SIZE, DRAW_PREVIEW_OPACITY, GHOST_PREVIEW_OPACITY, LINK_ATTACH_ANCHOR_FILL, LINK_ATTACH_ANCHOR_STROKE, LINK_ENDPOINT_HANDLE_DRAW_RADIUS, LINK_MIDPOINT_HANDLE_DRAW_RADIUS, SELECTION_HALO_PEEK_PX, LINK_START_ANCHOR_FILL, LINK_START_ANCHOR_STROKE, PEER_SELECTION_DASH, PEER_SELECTION_PADDING, PEER_SELECTION_STROKE_WIDTH, TEXT_CARET_WIDTH_PX, TEXT_SELECTION_FILL, TEXT_SELECTION_OPACITY, } from "./constants.js";
|
|
5
|
+
import { CORNER_HANDLES, HANDLE_SIZE, handlePosition, } from "./handle.js";
|
|
6
|
+
import { isResizable } from "./editor/shape-traits.js";
|
|
7
|
+
import { drawHitZones, hitZoneVisibility, } from "./editor/hit-test.js";
|
|
8
|
+
/** Index-access helper: throws on out-of-range instead of returning `undefined`. */
|
|
9
|
+
const req = (v) => {
|
|
10
|
+
if (v === undefined)
|
|
11
|
+
throw new Error("packages/state: index out of range");
|
|
12
|
+
return v;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Union AABB of every direct child of `groupId` (recursive). Returns
|
|
16
|
+
* `null` for empty groups so callers can skip the outline pass. Used
|
|
17
|
+
* by the selection overlay to draw a halo around grouped shapes.
|
|
18
|
+
*/
|
|
19
|
+
const groupWorldBounds = (scene, groupId) => {
|
|
20
|
+
let acc = null;
|
|
21
|
+
for (const shape of scene.elements.values()) {
|
|
22
|
+
if (shape.parentId !== groupId)
|
|
23
|
+
continue;
|
|
24
|
+
const inner = shape.type === "group" ? groupWorldBounds(scene, shape.id) : getElementWorldBounds(shape);
|
|
25
|
+
if (!inner)
|
|
26
|
+
continue;
|
|
27
|
+
acc = acc ? B.union(acc, inner) : inner;
|
|
28
|
+
}
|
|
29
|
+
return acc;
|
|
30
|
+
};
|
|
31
|
+
export const DEFAULT_OVERLAY_STYLE = {
|
|
32
|
+
selectionStroke: "#1a73e8",
|
|
33
|
+
selectionStrokeWidth: 1,
|
|
34
|
+
handleFill: "#fff",
|
|
35
|
+
handleStroke: "#1a73e8",
|
|
36
|
+
drawingStroke: "#1a73e8",
|
|
37
|
+
drawingDash: [4, 4],
|
|
38
|
+
};
|
|
39
|
+
/** Translucent fill opacity for the under-shape selection halo. */
|
|
40
|
+
const SELECTION_HALO_OPACITY = 0.32;
|
|
41
|
+
/**
|
|
42
|
+
* Paint the contour selection halo for elements — a translucent stroke
|
|
43
|
+
* hugging each world-space outline loop. Drawn on the dedicated background
|
|
44
|
+
* layer (under the shapes) so it peeks out from behind them. The stroke is
|
|
45
|
+
* centred on the contour with width `2 × (outset + peek/zoom)`, so its outer
|
|
46
|
+
* edge lands exactly `peek` screen px beyond the element's VISIBLE edge
|
|
47
|
+
* (contour + border outset) at every zoom and border thickness. Sets the
|
|
48
|
+
* world transform itself; resets to identity at the end.
|
|
49
|
+
*/
|
|
50
|
+
export const paintElementSelectionHalo = (target, w2s, halos, zoom, style = DEFAULT_OVERLAY_STYLE) => {
|
|
51
|
+
if (halos.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const z = zoom || 1;
|
|
54
|
+
const peekWorld = SELECTION_HALO_PEEK_PX / z;
|
|
55
|
+
target.setTransform(w2s);
|
|
56
|
+
target.setStroke(style.selectionStroke);
|
|
57
|
+
target.setOpacity(SELECTION_HALO_OPACITY);
|
|
58
|
+
target.setDashArray(null);
|
|
59
|
+
// Miter join so the halo reproduces the element's own corners (sharp on a
|
|
60
|
+
// rectangle / polygon, pointed on a star); rounded corners come from the
|
|
61
|
+
// traced outline points (rounded-rect / ellipse), not the join.
|
|
62
|
+
target.setLineJoin("miter");
|
|
63
|
+
target.setLineCap("butt");
|
|
64
|
+
for (const { loops, outsetWorld } of halos) {
|
|
65
|
+
// Centred stroke → outer edge sits (width/2) past the contour. We want
|
|
66
|
+
// that to be outset + peek, so width = 2 × (outset + peek).
|
|
67
|
+
target.setStrokeWidth(2 * (outsetWorld + peekWorld));
|
|
68
|
+
for (const loop of loops) {
|
|
69
|
+
if (loop.length < 2)
|
|
70
|
+
continue;
|
|
71
|
+
target.beginPath();
|
|
72
|
+
let started = false;
|
|
73
|
+
for (const p of loop) {
|
|
74
|
+
if (started)
|
|
75
|
+
target.lineTo(p.x, p.y);
|
|
76
|
+
else {
|
|
77
|
+
target.moveTo(p.x, p.y);
|
|
78
|
+
started = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
target.closePath();
|
|
82
|
+
target.stroke();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
target.setOpacity(1);
|
|
86
|
+
target.setTransform(matrix.IDENTITY);
|
|
87
|
+
};
|
|
88
|
+
export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
89
|
+
const style = { ...DEFAULT_OVERLAY_STYLE, ...options.style };
|
|
90
|
+
target.clear();
|
|
91
|
+
// World → screen transform: handles draw at constant screen size.
|
|
92
|
+
const w2s = getWorldToScreen(scene.viewport);
|
|
93
|
+
const zoom = scene.viewport.zoom;
|
|
94
|
+
target.save();
|
|
95
|
+
target.setTransform(matrix.IDENTITY);
|
|
96
|
+
// 0. Debug hit-zones — drawn first so the real selection chrome sits
|
|
97
|
+
// on top. Visualises every element's mouse hit-targets.
|
|
98
|
+
if (options.debugHitZones) {
|
|
99
|
+
drawHitZones(target, {
|
|
100
|
+
scene,
|
|
101
|
+
w2s,
|
|
102
|
+
zoom,
|
|
103
|
+
selection,
|
|
104
|
+
visibility: options.debugHitZoneVisibility ??
|
|
105
|
+
hitZoneVisibility({ linkDragActive: false, elementDragActive: false }),
|
|
106
|
+
...(options.edgeSelection ? { edgeSelection: options.edgeSelection } : {}),
|
|
107
|
+
...(options.debugAttachZones ? { attach: options.debugAttachZones } : {}),
|
|
108
|
+
...(options.debugContainerZones ? { containers: options.debugContainerZones } : {}),
|
|
109
|
+
...(options.groupBounds ? { groupBounds: options.groupBounds } : {}),
|
|
110
|
+
...(options.groupAspectLocked !== undefined
|
|
111
|
+
? { groupAspectLocked: options.groupAspectLocked }
|
|
112
|
+
: {}),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// 1. Selection outlines (+ handles only when a single shape is
|
|
116
|
+
// selected). Multi-selection skips per-shape handles in favour of
|
|
117
|
+
// the combined group bbox handles drawn later — otherwise the
|
|
118
|
+
// overlay would look like a forest of corner squares and the user
|
|
119
|
+
// could grab a child handle, which `hitTest` also blocks.
|
|
120
|
+
const multiSelect = selection.size > 1;
|
|
121
|
+
for (const id of selection) {
|
|
122
|
+
const shape = scene.elements.get(id);
|
|
123
|
+
if (!shape)
|
|
124
|
+
continue;
|
|
125
|
+
// Groups have no intrinsic geometry — outline the union of their
|
|
126
|
+
// descendants instead. Otherwise the selection chrome would collapse
|
|
127
|
+
// to a zero-size point at the group's origin.
|
|
128
|
+
const worldBounds = shape.type === "group" ? groupWorldBounds(scene, id) : getElementWorldBounds(shape);
|
|
129
|
+
if (!worldBounds)
|
|
130
|
+
continue;
|
|
131
|
+
const screenBounds = projectBounds(worldBounds, w2s);
|
|
132
|
+
drawOutline(target, screenBounds, style);
|
|
133
|
+
// Locked element: no resize handles; show a small lock badge instead so
|
|
134
|
+
// the user sees why it won't move/resize (click still selects → unlock).
|
|
135
|
+
if (shape.locked === true) {
|
|
136
|
+
drawLockBadge(target, screenBounds);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (multiSelect || !isResizable(shape))
|
|
140
|
+
continue;
|
|
141
|
+
// Draw only the four CORNER dots. The edge-midpoint handles are gone from
|
|
142
|
+
// the chrome — edge resize is done by dragging the selection-box side
|
|
143
|
+
// (see `hitHandle`), so no midpoint dot is needed. `resizeHandlesFor`
|
|
144
|
+
// (ALL_HANDLES) still drives hit-test capability.
|
|
145
|
+
for (const handle of CORNER_HANDLES) {
|
|
146
|
+
const worldPoint = handlePosition(handle, worldBounds, zoom);
|
|
147
|
+
const screenPoint = matrix.applyToPoint(w2s, worldPoint);
|
|
148
|
+
drawHandle(target, screenPoint, style);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 2. Rubber-band drawing preview (already in world coords if drawn before transform reset)
|
|
152
|
+
if (options.drawingPreview) {
|
|
153
|
+
const screenBounds = projectBounds(options.drawingPreview, w2s);
|
|
154
|
+
drawDrawingPreview(target, screenBounds, style);
|
|
155
|
+
}
|
|
156
|
+
// 2.5 WYSIWYG shape-draw preview — render the would-be element through
|
|
157
|
+
// its real renderer (in the world→screen transform, like renderScene)
|
|
158
|
+
// so a drag shows the actual rect / ellipse + default style, not just
|
|
159
|
+
// a dashed box. Drawn faded so it reads as "not committed yet".
|
|
160
|
+
if (options.drawingPreviewElement) {
|
|
161
|
+
const el = options.drawingPreviewElement;
|
|
162
|
+
const renderer = getElementRenderer(el.type);
|
|
163
|
+
if (renderer) {
|
|
164
|
+
target.save();
|
|
165
|
+
target.setTransform(w2s);
|
|
166
|
+
target.setOpacity(DRAW_PREVIEW_OPACITY);
|
|
167
|
+
target.setDashArray(null);
|
|
168
|
+
target.translate(el.position.x, el.position.y);
|
|
169
|
+
if (el.rotation !== 0)
|
|
170
|
+
target.rotate(el.rotation);
|
|
171
|
+
if (el.scale.x !== 1 || el.scale.y !== 1)
|
|
172
|
+
target.scale(el.scale.x, el.scale.y);
|
|
173
|
+
renderer(el, target);
|
|
174
|
+
target.restore();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 3. Link-drawing preview — dashed line in screen space. An elbow preview
|
|
178
|
+
// carries the full orthogonal polyline (`points`); otherwise a straight
|
|
179
|
+
// from→to segment.
|
|
180
|
+
if (options.edgePreview) {
|
|
181
|
+
const pts = options.edgePreview.points;
|
|
182
|
+
if (pts && pts.length >= 2) {
|
|
183
|
+
drawLinkPreviewPath(target, pts.map((p) => matrix.applyToPoint(w2s, p)), style);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const from = matrix.applyToPoint(w2s, options.edgePreview.from);
|
|
187
|
+
const to = matrix.applyToPoint(w2s, options.edgePreview.to);
|
|
188
|
+
drawLinkPreview(target, from, to, style);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 3.4 Ghost preview for "click a start dot → create element". Just the
|
|
192
|
+
// would-be ELEMENT here (faded); its connector is drawn separately
|
|
193
|
+
// through the real link renderer by the orchestrator (so it matches
|
|
194
|
+
// the link that'll be created, not a dashed line).
|
|
195
|
+
if (options.ghostElement || options.ghostElementShape) {
|
|
196
|
+
target.setOpacity(GHOST_PREVIEW_OPACITY);
|
|
197
|
+
const ghostShape = options.ghostElementShape;
|
|
198
|
+
const ghostRenderer = ghostShape ? getElementRenderer(ghostShape.type) : undefined;
|
|
199
|
+
if (ghostShape && ghostRenderer) {
|
|
200
|
+
// Render the actual shape through its renderer (in the world→screen
|
|
201
|
+
// transform, like renderScene) so the ghost matches the real element.
|
|
202
|
+
target.save();
|
|
203
|
+
target.setTransform(w2s);
|
|
204
|
+
target.setDashArray(null); // shape is solid; don't inherit any dash state
|
|
205
|
+
target.translate(ghostShape.position.x, ghostShape.position.y);
|
|
206
|
+
if (ghostShape.rotation !== 0)
|
|
207
|
+
target.rotate(ghostShape.rotation);
|
|
208
|
+
if (ghostShape.scale.x !== 1 || ghostShape.scale.y !== 1) {
|
|
209
|
+
target.scale(ghostShape.scale.x, ghostShape.scale.y);
|
|
210
|
+
}
|
|
211
|
+
ghostRenderer(ghostShape, target);
|
|
212
|
+
target.restore();
|
|
213
|
+
}
|
|
214
|
+
else if (options.ghostElement) {
|
|
215
|
+
const g = projectBounds(options.ghostElement, w2s);
|
|
216
|
+
target.setStroke(style.selectionStroke);
|
|
217
|
+
target.setStrokeWidth(1.5);
|
|
218
|
+
target.setDashArray(null);
|
|
219
|
+
target.setFill(null);
|
|
220
|
+
target.beginPath();
|
|
221
|
+
target.rect(g.x, g.y, g.width, g.height);
|
|
222
|
+
target.stroke();
|
|
223
|
+
}
|
|
224
|
+
target.setOpacity(1);
|
|
225
|
+
}
|
|
226
|
+
// 3.5 Float-attach target highlight — the whole element a connector will
|
|
227
|
+
// attach to (drop on body, not a dot). Brand outline, under the dots.
|
|
228
|
+
if (options.linkAttachHighlight) {
|
|
229
|
+
const b = projectBounds(options.linkAttachHighlight, w2s);
|
|
230
|
+
const pad = 2;
|
|
231
|
+
target.setStroke(style.selectionStroke);
|
|
232
|
+
target.setStrokeWidth(2);
|
|
233
|
+
target.setDashArray(null);
|
|
234
|
+
target.setFill(null);
|
|
235
|
+
target.beginPath();
|
|
236
|
+
target.rect(b.x - pad, b.y - pad, b.width + pad * 2, b.height + pad * 2);
|
|
237
|
+
target.stroke();
|
|
238
|
+
}
|
|
239
|
+
// 4. Port dots — hover affordance in draw-edge mode. May be one set or
|
|
240
|
+
// several (source start-anchors + target attach-anchors at once).
|
|
241
|
+
if (options.ports) {
|
|
242
|
+
const portSets = Array.isArray(options.ports)
|
|
243
|
+
? options.ports
|
|
244
|
+
: [options.ports];
|
|
245
|
+
for (const set of portSets) {
|
|
246
|
+
for (let i = 0; i < set.worldPoints.length; i++) {
|
|
247
|
+
const screen = matrix.applyToPoint(w2s, req(set.worldPoints[i]));
|
|
248
|
+
const active = set.activeIndex === i;
|
|
249
|
+
drawPortDot(target, screen, style, active, set.role, active ? set.activeRadius : undefined);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// 4.6 Persistent selection halo around every selected link. Same world-
|
|
254
|
+
// space rounded-stroke technique as the hover halo, but with the
|
|
255
|
+
// selection colour and a touch more opacity so it reads as "selected".
|
|
256
|
+
if (options.selectedLinkPaths && options.selectedLinkPaths.length > 0) {
|
|
257
|
+
target.setTransform(w2s);
|
|
258
|
+
target.setStroke(style.selectionStroke);
|
|
259
|
+
target.setOpacity(0.32);
|
|
260
|
+
target.setDashArray(null);
|
|
261
|
+
target.setLineJoin("round");
|
|
262
|
+
target.setLineCap("round");
|
|
263
|
+
// A link's stroke is centred on its path, so its visible edge is
|
|
264
|
+
// `width/2` from the path. Halo width `= width + 2 × peek/zoom` →
|
|
265
|
+
// `2 × (width/2 + peek/zoom)` → peeks exactly `peek` screen px beyond the
|
|
266
|
+
// link's visible edge, same constant as elements.
|
|
267
|
+
const peekWorld = SELECTION_HALO_PEEK_PX / (zoom || 1);
|
|
268
|
+
for (const { path, width } of options.selectedLinkPaths) {
|
|
269
|
+
if (path.length < 2)
|
|
270
|
+
continue;
|
|
271
|
+
target.setStrokeWidth(width + 2 * peekWorld);
|
|
272
|
+
target.beginPath();
|
|
273
|
+
strokeRoundedPolyline(target, path, LINK_CORNER_RADIUS);
|
|
274
|
+
target.stroke();
|
|
275
|
+
}
|
|
276
|
+
target.setOpacity(1);
|
|
277
|
+
target.setLineJoin("miter");
|
|
278
|
+
target.setLineCap("butt");
|
|
279
|
+
target.setTransform(matrix.IDENTITY);
|
|
280
|
+
}
|
|
281
|
+
// 5. Selected-edge endpoint handles + bend-point (waypoint) handles.
|
|
282
|
+
if (options.edgeSelection) {
|
|
283
|
+
const from = matrix.applyToPoint(w2s, options.edgeSelection.from);
|
|
284
|
+
const to = matrix.applyToPoint(w2s, options.edgeSelection.to);
|
|
285
|
+
// Segment-midpoint "add waypoint" handles (drawn first, under the rest).
|
|
286
|
+
for (const m of options.edgeSelection.midpoints ?? []) {
|
|
287
|
+
drawLinkMidpointHandle(target, matrix.applyToPoint(w2s, m), style);
|
|
288
|
+
}
|
|
289
|
+
drawLinkEndpointHandle(target, from, style);
|
|
290
|
+
drawLinkEndpointHandle(target, to, style);
|
|
291
|
+
for (const w of options.edgeSelection.waypoints ?? []) {
|
|
292
|
+
drawLinkEndpointHandle(target, matrix.applyToPoint(w2s, w), style);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// 6. Peer selection halos — dashed outline around shapes selected
|
|
296
|
+
// by remote users, painted in their colour. Drawn before own-
|
|
297
|
+
// selection outlines so own selection stays on top.
|
|
298
|
+
if (options.peerSelections) {
|
|
299
|
+
for (const peer of options.peerSelections) {
|
|
300
|
+
for (const wb of peer.bounds) {
|
|
301
|
+
const sb = projectBounds(wb, w2s);
|
|
302
|
+
drawPeerSelection(target, sb, peer.color);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// 6.5. Live brush stroke preview — quad-strip with interpolated
|
|
307
|
+
// widths, same render path as the committed BrushElement. Runs
|
|
308
|
+
// in world coords so the stroke stays anchored to the cursor
|
|
309
|
+
// as the user zooms / pans mid-stroke.
|
|
310
|
+
if (options.brushPreview && options.brushPreview.points.length > 0) {
|
|
311
|
+
const bp = options.brushPreview;
|
|
312
|
+
target.save();
|
|
313
|
+
target.setTransform(w2s);
|
|
314
|
+
target.setFill(bp.fill);
|
|
315
|
+
target.setStroke(null);
|
|
316
|
+
const pts = bp.points;
|
|
317
|
+
const ox = bp.origin.x;
|
|
318
|
+
const oy = bp.origin.y;
|
|
319
|
+
if (pts.length === 1) {
|
|
320
|
+
const p = req(pts[0]);
|
|
321
|
+
target.beginPath();
|
|
322
|
+
target.ellipse(ox + p.x, oy + p.y, p.width, p.width);
|
|
323
|
+
target.fill();
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
327
|
+
const a = req(pts[i]);
|
|
328
|
+
const b = req(pts[i + 1]);
|
|
329
|
+
const ax = ox + a.x;
|
|
330
|
+
const ay = oy + a.y;
|
|
331
|
+
const bx = ox + b.x;
|
|
332
|
+
const by = oy + b.y;
|
|
333
|
+
const dx = bx - ax;
|
|
334
|
+
const dy = by - ay;
|
|
335
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
336
|
+
const nx = -dy / len;
|
|
337
|
+
const ny = dx / len;
|
|
338
|
+
target.beginPath();
|
|
339
|
+
target.moveTo(ax + nx * a.width, ay + ny * a.width);
|
|
340
|
+
target.lineTo(bx + nx * b.width, by + ny * b.width);
|
|
341
|
+
target.lineTo(bx - nx * b.width, by - ny * b.width);
|
|
342
|
+
target.lineTo(ax - nx * a.width, ay - ny * a.width);
|
|
343
|
+
target.closePath();
|
|
344
|
+
target.fill();
|
|
345
|
+
target.beginPath();
|
|
346
|
+
target.ellipse(bx, by, b.width, b.width);
|
|
347
|
+
target.fill();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
target.restore();
|
|
351
|
+
}
|
|
352
|
+
// 7.0. Container drop-zone highlight — drawn under selection chrome
|
|
353
|
+
// so handles stay legible. Dashed rect + soft fill — same
|
|
354
|
+
// visual language as the drawing preview.
|
|
355
|
+
if (options.containerDropZone) {
|
|
356
|
+
const zoneScreen = projectBounds(options.containerDropZone, w2s);
|
|
357
|
+
target.setFill("rgba(26, 115, 232, 0.10)");
|
|
358
|
+
target.setStroke("#1a73e8");
|
|
359
|
+
target.setStrokeWidth(2);
|
|
360
|
+
target.setDashArray([6, 3]);
|
|
361
|
+
target.beginPath();
|
|
362
|
+
target.rect(zoneScreen.x, zoneScreen.y, zoneScreen.width, zoneScreen.height);
|
|
363
|
+
target.fill();
|
|
364
|
+
target.stroke();
|
|
365
|
+
target.setDashArray(null);
|
|
366
|
+
}
|
|
367
|
+
// 7. Multi-selection / group-typed combined bounds — outline + handles.
|
|
368
|
+
// Only CORNER dots are drawn (edge resize = drag the box side); the
|
|
369
|
+
// edge handles stay hit-testable via `hitHandle` for non-aspect-locked
|
|
370
|
+
// groups.
|
|
371
|
+
if (options.groupBounds) {
|
|
372
|
+
const groupScreen = projectBounds(options.groupBounds, w2s);
|
|
373
|
+
drawOutline(target, groupScreen, style);
|
|
374
|
+
const handleSet = CORNER_HANDLES;
|
|
375
|
+
for (const handle of handleSet) {
|
|
376
|
+
const worldPoint = handlePosition(handle, options.groupBounds, zoom);
|
|
377
|
+
const screenPoint = matrix.applyToPoint(w2s, worldPoint);
|
|
378
|
+
drawHandle(target, screenPoint, style);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// 7.5. Annotation pins — drawn before peer cursors so cursors stay
|
|
382
|
+
// on top, but on top of selection handles. Each pin shows a comment
|
|
383
|
+
// count badge when the thread has > 0 replies.
|
|
384
|
+
if (options.annotations && options.annotations.length > 0) {
|
|
385
|
+
for (const ann of options.annotations) {
|
|
386
|
+
const world = getAnnotationWorldPosition(scene, ann);
|
|
387
|
+
const screen = matrix.applyToPoint(w2s, world);
|
|
388
|
+
drawAnnotationPin(target, screen, ann, options.selectedAnnotation ?? null);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// 8. Remote peer cursors — drawn last so they sit on top of every
|
|
392
|
+
// other overlay element (including own selection handles).
|
|
393
|
+
if (options.peerCursors) {
|
|
394
|
+
for (const cursor of options.peerCursors) {
|
|
395
|
+
const screen = matrix.applyToPoint(w2s, cursor.position);
|
|
396
|
+
drawPeerCursor(target, screen, cursor.color, cursor.name);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// 9. GIF "play" badges on paused animated shapes — drawn last
|
|
400
|
+
// so they sit above selection chrome.
|
|
401
|
+
if (options.gifBadges) {
|
|
402
|
+
for (const b of options.gifBadges) {
|
|
403
|
+
drawGifBadge(target, projectBounds(b, w2s));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// 10. In-canvas text editing: translucent selection highlight, then
|
|
407
|
+
// the caret bar on top. Both backends draw this via the shared
|
|
408
|
+
// RenderTarget primitives (rect + fill), so it's identical on
|
|
409
|
+
// Canvas2D and WebGL2.
|
|
410
|
+
if (options.editingText) {
|
|
411
|
+
const et = options.editingText;
|
|
412
|
+
if (et.selectionRects.length > 0) {
|
|
413
|
+
target.setFill(TEXT_SELECTION_FILL);
|
|
414
|
+
target.setOpacity(TEXT_SELECTION_OPACITY);
|
|
415
|
+
for (const r of et.selectionRects) {
|
|
416
|
+
const s = projectBounds(r, w2s);
|
|
417
|
+
target.beginPath();
|
|
418
|
+
target.rect(s.x, s.y, s.width, s.height);
|
|
419
|
+
target.fill();
|
|
420
|
+
}
|
|
421
|
+
target.setOpacity(1);
|
|
422
|
+
}
|
|
423
|
+
if (et.caret) {
|
|
424
|
+
const p = matrix.applyToPoint(w2s, { x: et.caret.x, y: et.caret.y });
|
|
425
|
+
target.setFill(et.caretColor);
|
|
426
|
+
target.setOpacity(1);
|
|
427
|
+
target.beginPath();
|
|
428
|
+
target.rect(p.x, p.y, TEXT_CARET_WIDTH_PX, et.caret.height * zoom);
|
|
429
|
+
target.fill();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
target.restore();
|
|
433
|
+
};
|
|
434
|
+
/** Rounded "gif" chip in a shape's top-left corner — signals a paused
|
|
435
|
+
* GIF the user can click (or hover) to resume. */
|
|
436
|
+
const GIF_BADGE_W = 30;
|
|
437
|
+
const GIF_BADGE_H = 16;
|
|
438
|
+
const GIF_BADGE_PAD = 4;
|
|
439
|
+
const GIF_BADGE_RADIUS = 4;
|
|
440
|
+
/** Small padlock badge at a selected locked element's top-right corner. */
|
|
441
|
+
const LOCK_BADGE_SIZE = 16;
|
|
442
|
+
const drawLockBadge = (target, b) => {
|
|
443
|
+
const s = LOCK_BADGE_SIZE;
|
|
444
|
+
const x = b.x + b.width - s - 2;
|
|
445
|
+
const y = b.y + 2;
|
|
446
|
+
const color = "#1a73e8";
|
|
447
|
+
// Shackle — a ring above the body; its lower half is hidden by the body so
|
|
448
|
+
// the visible top arc reads as the lock's bow.
|
|
449
|
+
target.setStroke(color);
|
|
450
|
+
target.setFill(null);
|
|
451
|
+
target.setStrokeWidth(2);
|
|
452
|
+
target.setDashArray(null);
|
|
453
|
+
target.setOpacity(1);
|
|
454
|
+
target.beginPath();
|
|
455
|
+
target.ellipse(x + s / 2, y + s * 0.46, s * 0.22, s * 0.26);
|
|
456
|
+
target.stroke();
|
|
457
|
+
// Body — solid rounded block.
|
|
458
|
+
target.setStroke(null);
|
|
459
|
+
target.setFill(color);
|
|
460
|
+
target.beginPath();
|
|
461
|
+
target.rect(x + s * 0.2, y + s * 0.44, s * 0.6, s * 0.48);
|
|
462
|
+
target.fill();
|
|
463
|
+
// Keyhole.
|
|
464
|
+
target.setFill("#fff");
|
|
465
|
+
target.beginPath();
|
|
466
|
+
target.ellipse(x + s / 2, y + s * 0.64, s * 0.07, s * 0.07);
|
|
467
|
+
target.fill();
|
|
468
|
+
};
|
|
469
|
+
const drawGifBadge = (target, screen) => {
|
|
470
|
+
const x = screen.x + GIF_BADGE_PAD;
|
|
471
|
+
const y = screen.y + GIF_BADGE_PAD;
|
|
472
|
+
const w = GIF_BADGE_W;
|
|
473
|
+
const h = GIF_BADGE_H;
|
|
474
|
+
const r = GIF_BADGE_RADIUS;
|
|
475
|
+
// Rounded-rect background.
|
|
476
|
+
target.setStroke(null);
|
|
477
|
+
target.setFill("rgba(0,0,0,0.65)");
|
|
478
|
+
target.beginPath();
|
|
479
|
+
target.moveTo(x + r, y);
|
|
480
|
+
target.lineTo(x + w - r, y);
|
|
481
|
+
target.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
482
|
+
target.lineTo(x + w, y + h - r);
|
|
483
|
+
target.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
484
|
+
target.lineTo(x + r, y + h);
|
|
485
|
+
target.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
486
|
+
target.lineTo(x, y + r);
|
|
487
|
+
target.quadraticCurveTo(x, y, x + r, y);
|
|
488
|
+
target.closePath();
|
|
489
|
+
target.fill();
|
|
490
|
+
// "gif" label.
|
|
491
|
+
target.setFill("#ffffff");
|
|
492
|
+
target.setFont("system-ui, sans-serif", 10);
|
|
493
|
+
target.setTextAlign("center");
|
|
494
|
+
target.setTextBaseline("middle");
|
|
495
|
+
target.fillText("gif", x + w / 2, y + h / 2);
|
|
496
|
+
};
|
|
497
|
+
const projectBounds = (b, w2s) => {
|
|
498
|
+
const tl = matrix.applyToPoint(w2s, { x: b.x, y: b.y });
|
|
499
|
+
const br = matrix.applyToPoint(w2s, { x: b.x + b.width, y: b.y + b.height });
|
|
500
|
+
const x = Math.min(tl.x, br.x);
|
|
501
|
+
const y = Math.min(tl.y, br.y);
|
|
502
|
+
return { x, y, width: Math.abs(br.x - tl.x), height: Math.abs(br.y - tl.y) };
|
|
503
|
+
};
|
|
504
|
+
const drawOutline = (target, b, style) => {
|
|
505
|
+
target.setStroke(style.selectionStroke);
|
|
506
|
+
target.setStrokeWidth(style.selectionStrokeWidth);
|
|
507
|
+
target.setDashArray(null);
|
|
508
|
+
target.beginPath();
|
|
509
|
+
target.rect(b.x, b.y, b.width, b.height);
|
|
510
|
+
target.stroke();
|
|
511
|
+
};
|
|
512
|
+
const drawHandle = (target, center, style) => {
|
|
513
|
+
// Circle of radius HANDLE_SIZE — visually equivalent to a rounded
|
|
514
|
+
// square with maximum corner radius, but renders via `ellipse`
|
|
515
|
+
// which every RenderTarget already supports (canvas, svg). A rounded
|
|
516
|
+
// shape reads as "draggable handle" more clearly than a sharp rectangle.
|
|
517
|
+
target.setFill(style.handleFill);
|
|
518
|
+
target.setStroke(style.handleStroke);
|
|
519
|
+
target.setStrokeWidth(1);
|
|
520
|
+
target.setDashArray(null);
|
|
521
|
+
target.beginPath();
|
|
522
|
+
target.ellipse(center.x, center.y, HANDLE_SIZE, HANDLE_SIZE);
|
|
523
|
+
target.fill();
|
|
524
|
+
target.stroke();
|
|
525
|
+
};
|
|
526
|
+
const drawDrawingPreview = (target, b, style) => {
|
|
527
|
+
target.setStroke(style.drawingStroke);
|
|
528
|
+
target.setStrokeWidth(1);
|
|
529
|
+
target.setDashArray(style.drawingDash);
|
|
530
|
+
target.beginPath();
|
|
531
|
+
target.rect(b.x, b.y, b.width, b.height);
|
|
532
|
+
target.stroke();
|
|
533
|
+
};
|
|
534
|
+
const drawLinkPreview = (target, from, to, style) => {
|
|
535
|
+
target.setStroke(style.drawingStroke);
|
|
536
|
+
target.setStrokeWidth(1.5);
|
|
537
|
+
target.setDashArray(style.drawingDash);
|
|
538
|
+
target.beginPath();
|
|
539
|
+
target.moveTo(from.x, from.y);
|
|
540
|
+
target.lineTo(to.x, to.y);
|
|
541
|
+
target.stroke();
|
|
542
|
+
};
|
|
543
|
+
/** Dashed polyline preview (elbow) in screen space. */
|
|
544
|
+
const drawLinkPreviewPath = (target, pts, style) => {
|
|
545
|
+
target.setStroke(style.drawingStroke);
|
|
546
|
+
target.setStrokeWidth(1.5);
|
|
547
|
+
target.setDashArray(style.drawingDash);
|
|
548
|
+
target.beginPath();
|
|
549
|
+
const first = req(pts[0]);
|
|
550
|
+
target.moveTo(first.x, first.y);
|
|
551
|
+
for (let i = 1; i < pts.length; i++) {
|
|
552
|
+
const p = req(pts[i]);
|
|
553
|
+
target.lineTo(p.x, p.y);
|
|
554
|
+
}
|
|
555
|
+
target.stroke();
|
|
556
|
+
};
|
|
557
|
+
const drawPortDot = (target, center, style, active, role = "link-start", activeRadius) => {
|
|
558
|
+
const radius = active ? (activeRadius ?? ANCHOR_DOT_ACTIVE_RADIUS) : ANCHOR_DOT_RADIUS;
|
|
559
|
+
const isStart = role === "link-start";
|
|
560
|
+
// When active (snapped), use the inverse fill of the resting state
|
|
561
|
+
// to highlight the dot.
|
|
562
|
+
const fill = active
|
|
563
|
+
? (isStart ? LINK_START_ANCHOR_STROKE : LINK_ATTACH_ANCHOR_STROKE)
|
|
564
|
+
: (isStart ? LINK_START_ANCHOR_FILL : LINK_ATTACH_ANCHOR_FILL);
|
|
565
|
+
const stroke = isStart ? LINK_START_ANCHOR_STROKE : LINK_ATTACH_ANCHOR_STROKE;
|
|
566
|
+
target.setStroke(stroke);
|
|
567
|
+
target.setStrokeWidth(ANCHOR_DOT_STROKE_WIDTH);
|
|
568
|
+
target.setDashArray(null);
|
|
569
|
+
target.setFill(fill);
|
|
570
|
+
target.beginPath();
|
|
571
|
+
target.ellipse(center.x, center.y, radius, radius);
|
|
572
|
+
target.fill();
|
|
573
|
+
target.stroke();
|
|
574
|
+
};
|
|
575
|
+
const drawLinkEndpointHandle = (target, center, style) => {
|
|
576
|
+
const radius = LINK_ENDPOINT_HANDLE_DRAW_RADIUS;
|
|
577
|
+
target.setStroke(style.selectionStroke);
|
|
578
|
+
target.setStrokeWidth(2);
|
|
579
|
+
target.setDashArray(null);
|
|
580
|
+
target.setFill(style.handleFill);
|
|
581
|
+
target.beginPath();
|
|
582
|
+
target.ellipse(center.x, center.y, radius, radius);
|
|
583
|
+
target.fill();
|
|
584
|
+
target.stroke();
|
|
585
|
+
};
|
|
586
|
+
// Smaller, semi-transparent dot on a segment midpoint — the "drag to add a
|
|
587
|
+
// bend point" affordance. Lighter than a real waypoint handle so it reads
|
|
588
|
+
// as secondary.
|
|
589
|
+
const drawLinkMidpointHandle = (target, center, style) => {
|
|
590
|
+
const radius = LINK_MIDPOINT_HANDLE_DRAW_RADIUS;
|
|
591
|
+
target.setOpacity(0.55);
|
|
592
|
+
target.setStroke(style.selectionStroke);
|
|
593
|
+
target.setStrokeWidth(1.5);
|
|
594
|
+
target.setDashArray(null);
|
|
595
|
+
target.setFill(style.handleFill);
|
|
596
|
+
target.beginPath();
|
|
597
|
+
target.ellipse(center.x, center.y, radius, radius);
|
|
598
|
+
target.fill();
|
|
599
|
+
target.stroke();
|
|
600
|
+
target.setOpacity(1);
|
|
601
|
+
};
|
|
602
|
+
const drawPeerSelection = (target, b, color) => {
|
|
603
|
+
const pad = PEER_SELECTION_PADDING;
|
|
604
|
+
target.setStroke(color);
|
|
605
|
+
target.setStrokeWidth(PEER_SELECTION_STROKE_WIDTH);
|
|
606
|
+
target.setDashArray(PEER_SELECTION_DASH);
|
|
607
|
+
target.beginPath();
|
|
608
|
+
target.rect(b.x - pad, b.y - pad, b.width + 2 * pad, b.height + 2 * pad);
|
|
609
|
+
target.stroke();
|
|
610
|
+
};
|
|
611
|
+
const drawPeerCursor = (target, tip, color, name) => {
|
|
612
|
+
// Arrow glyph — a triangle anchored at the cursor tip. Coordinates
|
|
613
|
+
// are relative to the tip; the canonical macOS pointer leans down-
|
|
614
|
+
// right.
|
|
615
|
+
const size = CURSOR_ARROW_SIZE;
|
|
616
|
+
target.setFill(color);
|
|
617
|
+
target.setStroke("#fff");
|
|
618
|
+
target.setStrokeWidth(1);
|
|
619
|
+
target.setDashArray(null);
|
|
620
|
+
target.beginPath();
|
|
621
|
+
target.moveTo(tip.x, tip.y);
|
|
622
|
+
target.lineTo(tip.x + size * 0.7, tip.y + size * 0.25);
|
|
623
|
+
target.lineTo(tip.x + size * 0.35, tip.y + size * 0.4);
|
|
624
|
+
target.lineTo(tip.x + size * 0.5, tip.y + size * 0.95);
|
|
625
|
+
target.lineTo(tip.x + size * 0.35, tip.y + size * 1.05);
|
|
626
|
+
target.lineTo(tip.x + size * 0.2, tip.y + size * 0.5);
|
|
627
|
+
target.lineTo(tip.x, tip.y + size * 0.65);
|
|
628
|
+
target.closePath();
|
|
629
|
+
target.fill();
|
|
630
|
+
target.stroke();
|
|
631
|
+
// Name chip — anchored down-right of the tip.
|
|
632
|
+
target.setFont("sans-serif", CURSOR_NAME_FONT_SIZE);
|
|
633
|
+
target.setTextBaseline("top");
|
|
634
|
+
target.setTextAlign("left");
|
|
635
|
+
const textWidth = target.measureText(name).width;
|
|
636
|
+
const chipX = tip.x + CURSOR_NAME_CHIP_OFFSET;
|
|
637
|
+
const chipY = tip.y + CURSOR_NAME_CHIP_OFFSET;
|
|
638
|
+
const chipW = textWidth + 2 * CURSOR_NAME_CHIP_PADDING_X;
|
|
639
|
+
const chipH = CURSOR_NAME_FONT_SIZE + 2 * CURSOR_NAME_CHIP_PADDING_Y;
|
|
640
|
+
target.setFill(color);
|
|
641
|
+
target.setStrokeWidth(0);
|
|
642
|
+
target.beginPath();
|
|
643
|
+
target.rect(chipX, chipY, chipW, chipH);
|
|
644
|
+
target.fill();
|
|
645
|
+
target.setFill("#fff");
|
|
646
|
+
target.fillText(name, chipX + CURSOR_NAME_CHIP_PADDING_X, chipY + CURSOR_NAME_CHIP_PADDING_Y);
|
|
647
|
+
};
|
|
648
|
+
const drawAnnotationPin = (target, center, annotation, selectedId) => {
|
|
649
|
+
const radius = ANNOTATION_PIN_RADIUS;
|
|
650
|
+
const fill = annotation.resolved ? ANNOTATION_PIN_RESOLVED_FILL : ANNOTATION_PIN_FILL;
|
|
651
|
+
const selected = annotation.id === selectedId;
|
|
652
|
+
// Circle body.
|
|
653
|
+
target.setFill(fill);
|
|
654
|
+
target.setStroke(selected ? "#1a73e8" : ANNOTATION_PIN_STROKE);
|
|
655
|
+
target.setStrokeWidth(selected ? 2 : 1.5);
|
|
656
|
+
target.setDashArray(null);
|
|
657
|
+
target.beginPath();
|
|
658
|
+
target.ellipse(center.x, center.y, radius, radius);
|
|
659
|
+
target.fill();
|
|
660
|
+
target.stroke();
|
|
661
|
+
// Comment-count badge (when thread length > 1; the first comment is the
|
|
662
|
+
// body of the pin itself).
|
|
663
|
+
if (annotation.thread.length > 0) {
|
|
664
|
+
const count = annotation.thread.length;
|
|
665
|
+
const label = count > 9 ? "9+" : String(count);
|
|
666
|
+
target.setFont("sans-serif", ANNOTATION_PIN_BADGE_FONT_SIZE);
|
|
667
|
+
target.setTextAlign("center");
|
|
668
|
+
target.setTextBaseline("middle");
|
|
669
|
+
target.setFill("#fff");
|
|
670
|
+
target.fillText(label, center.x, center.y);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
//# sourceMappingURL=overlay.js.map
|