@oh-just-another/state 0.58.0 → 0.60.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 +158 -0
- package/README.md +78 -27
- package/dist/.tsbuildinfo +1 -1
- package/dist/actions/actionArrange.d.ts +13 -0
- package/dist/actions/actionArrange.d.ts.map +1 -0
- package/dist/actions/actionArrange.js +130 -0
- package/dist/actions/actionArrange.js.map +1 -0
- package/dist/actions/actionClipboard.d.ts +2 -0
- package/dist/actions/actionClipboard.d.ts.map +1 -1
- package/dist/actions/actionClipboard.js +32 -1
- package/dist/actions/actionClipboard.js.map +1 -1
- package/dist/actions/actionKeyboard.d.ts.map +1 -1
- package/dist/actions/actionKeyboard.js +77 -9
- package/dist/actions/actionKeyboard.js.map +1 -1
- package/dist/actions/actionMode.d.ts +4 -0
- package/dist/actions/actionMode.d.ts.map +1 -1
- package/dist/actions/actionMode.js +54 -0
- 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/actionText.d.ts +5 -0
- package/dist/actions/actionText.d.ts.map +1 -0
- package/dist/actions/actionText.js +37 -0
- package/dist/actions/actionText.js.map +1 -0
- package/dist/actions/actionView.d.ts +6 -0
- package/dist/actions/actionView.d.ts.map +1 -1
- package/dist/actions/actionView.js +20 -2
- 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 +6 -4
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +6 -4
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/registry.d.ts.map +1 -1
- package/dist/actions/registry.js +17 -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/auto-layout-scheduler.d.ts +1 -3
- package/dist/auto-layout-scheduler.d.ts.map +1 -1
- package/dist/auto-layout-scheduler.js +1 -2
- package/dist/auto-layout-scheduler.js.map +1 -1
- package/dist/built-in-handlers.d.ts.map +1 -1
- package/dist/built-in-handlers.js +14 -1
- 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 +208 -41
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +210 -25
- package/dist/constants.js.map +1 -1
- package/dist/dom-events.d.ts +8 -2
- package/dist/dom-events.d.ts.map +1 -1
- package/dist/dom-events.js +12 -0
- package/dist/dom-events.js.map +1 -1
- package/dist/editor/animation-scene.d.ts +36 -0
- package/dist/editor/animation-scene.d.ts.map +1 -0
- package/dist/editor/animation-scene.js +194 -0
- package/dist/editor/animation-scene.js.map +1 -0
- package/dist/editor/animation.d.ts +43 -0
- package/dist/editor/animation.d.ts.map +1 -0
- package/dist/editor/animation.js +80 -0
- package/dist/editor/animation.js.map +1 -0
- package/dist/editor/applies/arrange.d.ts +46 -0
- package/dist/editor/applies/arrange.d.ts.map +1 -0
- package/dist/editor/applies/arrange.js +162 -0
- package/dist/editor/applies/arrange.js.map +1 -0
- 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/edge.d.ts +8 -1
- package/dist/editor/applies/edge.d.ts.map +1 -1
- package/dist/editor/applies/edge.js +31 -1
- package/dist/editor/applies/edge.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/applies/move.d.ts +6 -0
- package/dist/editor/applies/move.d.ts.map +1 -1
- package/dist/editor/applies/move.js +6 -0
- package/dist/editor/applies/move.js.map +1 -1
- package/dist/editor/applies/resize.d.ts +26 -5
- package/dist/editor/applies/resize.d.ts.map +1 -1
- package/dist/editor/applies/resize.js +91 -13
- package/dist/editor/applies/resize.js.map +1 -1
- package/dist/editor/caret-blink.d.ts +18 -0
- package/dist/editor/caret-blink.d.ts.map +1 -0
- package/dist/editor/caret-blink.js +42 -0
- package/dist/editor/caret-blink.js.map +1 -0
- package/dist/editor/container-ops.d.ts.map +1 -1
- package/dist/editor/container-ops.js +2 -2
- package/dist/editor/container-ops.js.map +1 -1
- package/dist/editor/dirty-rect.d.ts +23 -0
- package/dist/editor/dirty-rect.d.ts.map +1 -0
- package/dist/editor/dirty-rect.js +115 -0
- package/dist/editor/dirty-rect.js.map +1 -0
- package/dist/editor/gif-playback.d.ts +45 -0
- package/dist/editor/gif-playback.d.ts.map +1 -0
- package/dist/editor/gif-playback.js +125 -0
- package/dist/editor/gif-playback.js.map +1 -0
- package/dist/editor/hit-test.d.ts +3 -3
- package/dist/editor/hit-test.d.ts.map +1 -1
- package/dist/editor/hit-test.js +40 -15
- 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 +152 -0
- package/dist/editor/link-handle-drag.d.ts.map +1 -0
- package/dist/editor/link-handle-drag.js +300 -0
- package/dist/editor/link-handle-drag.js.map +1 -0
- package/dist/editor/long-press.d.ts.map +1 -1
- package/dist/editor/long-press.js +2 -2
- package/dist/editor/long-press.js.map +1 -1
- package/dist/editor/pinch.d.ts.map +1 -1
- package/dist/editor/pinch.js +6 -6
- package/dist/editor/pinch.js.map +1 -1
- 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 +1191 -890
- package/dist/editor/pointer-binding.js.map +1 -1
- package/dist/editor/public/arrange-group.d.ts +3 -9
- package/dist/editor/public/arrange-group.d.ts.map +1 -1
- package/dist/editor/public/arrange-group.js +4 -6
- package/dist/editor/public/arrange-group.js.map +1 -1
- package/dist/editor/public/brush.d.ts +31 -12
- package/dist/editor/public/brush.d.ts.map +1 -1
- package/dist/editor/public/brush.js +66 -17
- package/dist/editor/public/brush.js.map +1 -1
- package/dist/editor/public/clipboard.d.ts +2 -4
- package/dist/editor/public/clipboard.d.ts.map +1 -1
- package/dist/editor/public/clipboard.js +2 -4
- package/dist/editor/public/clipboard.js.map +1 -1
- package/dist/editor/public/cursor.d.ts +29 -0
- package/dist/editor/public/cursor.d.ts.map +1 -0
- package/dist/editor/public/cursor.js +146 -0
- package/dist/editor/public/cursor.js.map +1 -0
- 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/frame-name.d.ts +20 -0
- package/dist/editor/public/frame-name.d.ts.map +1 -0
- package/dist/editor/public/frame-name.js +54 -0
- package/dist/editor/public/frame-name.js.map +1 -0
- package/dist/editor/public/image-insert.d.ts +1 -1
- package/dist/editor/public/image-insert.d.ts.map +1 -1
- 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/layers.d.ts.map +1 -1
- package/dist/editor/public/layers.js +2 -2
- package/dist/editor/public/layers.js.map +1 -1
- package/dist/editor/public/link.d.ts +11 -2
- package/dist/editor/public/link.d.ts.map +1 -1
- package/dist/editor/public/link.js +70 -2
- package/dist/editor/public/link.js.map +1 -1
- package/dist/editor/public/placement.d.ts +60 -2
- package/dist/editor/public/placement.d.ts.map +1 -1
- package/dist/editor/public/placement.js +193 -2
- package/dist/editor/public/placement.js.map +1 -1
- package/dist/editor/public/selection-ops.d.ts +35 -1
- package/dist/editor/public/selection-ops.d.ts.map +1 -1
- package/dist/editor/public/selection-ops.js +97 -2
- 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/text-edit.d.ts +1 -10
- package/dist/editor/public/text-edit.d.ts.map +1 -1
- package/dist/editor/public/text-edit.js +2 -16
- package/dist/editor/public/text-edit.js.map +1 -1
- 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 +5 -5
- package/dist/editor/public/z-order.js.map +1 -1
- package/dist/editor/public/zoom-pan.d.ts +15 -4
- package/dist/editor/public/zoom-pan.d.ts.map +1 -1
- package/dist/editor/public/zoom-pan.js +39 -7
- package/dist/editor/public/zoom-pan.js.map +1 -1
- package/dist/editor/render-orchestrator.d.ts +177 -5
- package/dist/editor/render-orchestrator.d.ts.map +1 -1
- package/dist/editor/render-orchestrator.js +519 -358
- package/dist/editor/render-orchestrator.js.map +1 -1
- package/dist/editor/resize-helpers.d.ts +21 -5
- package/dist/editor/resize-helpers.d.ts.map +1 -1
- package/dist/editor/resize-helpers.js +59 -19
- package/dist/editor/resize-helpers.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/viewport-helpers.d.ts.map +1 -1
- package/dist/editor/viewport-helpers.js +3 -3
- package/dist/editor/viewport-helpers.js.map +1 -1
- package/dist/editor-events.d.ts +1 -1
- package/dist/editor-events.d.ts.map +1 -1
- package/dist/editor.d.ts +603 -346
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1843 -1630
- package/dist/editor.js.map +1 -1
- package/dist/file-drop.d.ts.map +1 -1
- package/dist/file-drop.js +1 -2
- package/dist/file-drop.js.map +1 -1
- package/dist/frame-helpers.d.ts.map +1 -1
- package/dist/frame-helpers.js +5 -5
- package/dist/frame-helpers.js.map +1 -1
- package/dist/group-helpers.d.ts.map +1 -1
- package/dist/group-helpers.js +4 -4
- package/dist/group-helpers.js.map +1 -1
- package/dist/handle.d.ts +59 -6
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +99 -7
- package/dist/handle.js.map +1 -1
- package/dist/hotkeys-binding.d.ts +26 -0
- package/dist/hotkeys-binding.d.ts.map +1 -0
- package/dist/hotkeys-binding.js +35 -0
- package/dist/hotkeys-binding.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/link-selection.d.ts +3 -5
- package/dist/link-selection.d.ts.map +1 -1
- package/dist/link-selection.js +3 -12
- package/dist/link-selection.js.map +1 -1
- package/dist/machine.d.ts +27 -1
- package/dist/machine.d.ts.map +1 -1
- package/dist/machine.js +38 -0
- package/dist/machine.js.map +1 -1
- package/dist/modes.d.ts +15 -1
- package/dist/modes.d.ts.map +1 -1
- package/dist/modes.js.map +1 -1
- package/dist/overlay.d.ts +92 -6
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +582 -125
- 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/dist/selection.d.ts.map +1 -1
- package/dist/selection.js +2 -10
- package/dist/selection.js.map +1 -1
- package/dist/util.d.ts +8 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +17 -0
- package/dist/util.js.map +1 -0
- package/package.json +7 -7
package/dist/overlay.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import { getAnnotationWorldPosition, getElementWorldBounds, getWorldToScreen, } from "@oh-just-another/scene";
|
|
1
|
+
import { getAnnotationWorldPosition, getElementWorldBounds, getWorldToScreen, isGroup, brushOutline, } from "@oh-just-another/scene";
|
|
2
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 {
|
|
3
|
+
import { getElementRenderer, resolveImageSource, 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, CROP_BRACKET_LEN, CROP_BRACKET_WIDTH, CROP_GHOST_OPACITY, DRAW_PREVIEW_OPACITY, FLOWCHART_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, GIF_BADGE_W, GIF_BADGE_H, GIF_BADGE_PAD, GIF_BADGE_RADIUS, GIF_BADGE_BG_COLOR, LOCK_BADGE_SIZE, LOCK_BADGE_COLOR, LOCK_BADGE_KEYHOLE_COLOR, ROTATE_ICON_RADIUS, LASER_COLOR, LASER_WIDTH, LASER_TRAIL_TTL_MS, ERASER_CURSOR_STROKE, ERASER_CURSOR_LINE_WIDTH, ERASER_TRAIL_TTL_MS, } from "./constants.js";
|
|
5
|
+
import { smoothLaserPoints } from "./editor/public/laser.js";
|
|
6
|
+
import { CORNER_HANDLES, HANDLE_SIZE, frameCorners, handlePosition, handleWorldOnFrame, rotateGripForBounds, rotateGripWorld, shapeSelectionFrame, } from "./handle.js";
|
|
6
7
|
import { isResizable } from "./editor/shape-traits.js";
|
|
7
8
|
import { drawHitZones, hitZoneVisibility, } from "./editor/hit-test.js";
|
|
8
|
-
|
|
9
|
-
const req = (v) => {
|
|
10
|
-
if (v === undefined)
|
|
11
|
-
throw new Error("packages/state: index out of range");
|
|
12
|
-
return v;
|
|
13
|
-
};
|
|
9
|
+
import { req } from "./util.js";
|
|
14
10
|
/**
|
|
15
11
|
* Union AABB of every direct child of `groupId` (recursive). Returns
|
|
16
12
|
* `null` for empty groups so callers can skip the outline pass. Used
|
|
@@ -21,7 +17,7 @@ const groupWorldBounds = (scene, groupId) => {
|
|
|
21
17
|
for (const shape of scene.elements.values()) {
|
|
22
18
|
if (shape.parentId !== groupId)
|
|
23
19
|
continue;
|
|
24
|
-
const inner = shape
|
|
20
|
+
const inner = isGroup(shape) ? groupWorldBounds(scene, shape.id) : getElementWorldBounds(shape);
|
|
25
21
|
if (!inner)
|
|
26
22
|
continue;
|
|
27
23
|
acc = acc ? B.union(acc, inner) : inner;
|
|
@@ -85,6 +81,12 @@ export const paintElementSelectionHalo = (target, w2s, halos, zoom, style = DEFA
|
|
|
85
81
|
target.setOpacity(1);
|
|
86
82
|
target.setTransform(matrix.IDENTITY);
|
|
87
83
|
};
|
|
84
|
+
/**
|
|
85
|
+
* Draws selection outlines, resize handles, previews, peer chrome and other
|
|
86
|
+
* affordances on the overlay layer. Pure draw — does not alter scene or state.
|
|
87
|
+
* A thin dispatcher: each numbered layer lives in its own `render*` helper,
|
|
88
|
+
* called here in strict back-to-front paint order.
|
|
89
|
+
*/
|
|
88
90
|
export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
89
91
|
const style = { ...DEFAULT_OVERLAY_STYLE, ...options.style };
|
|
90
92
|
target.clear();
|
|
@@ -93,8 +95,29 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
93
95
|
const zoom = scene.viewport.zoom;
|
|
94
96
|
target.save();
|
|
95
97
|
target.setTransform(matrix.IDENTITY);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
const ctx = { scene, selection, target, options, style, w2s, zoom };
|
|
99
|
+
renderDebugHitZones(ctx);
|
|
100
|
+
renderSelectionHandles(ctx);
|
|
101
|
+
renderPreviews(ctx);
|
|
102
|
+
renderLinkHandles(ctx);
|
|
103
|
+
renderPeerSelections(ctx);
|
|
104
|
+
renderBrushPreview(ctx);
|
|
105
|
+
renderLaserTrails(ctx);
|
|
106
|
+
renderEraserCursor(ctx);
|
|
107
|
+
renderContainerDropZone(ctx);
|
|
108
|
+
renderGroupBounds(ctx);
|
|
109
|
+
renderAnnotations(ctx);
|
|
110
|
+
renderPeerCursors(ctx);
|
|
111
|
+
renderGifBadges(ctx);
|
|
112
|
+
renderTextEditing(ctx);
|
|
113
|
+
target.restore();
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Section 0 — debug hit-zones. Drawn first so the real selection chrome sits
|
|
117
|
+
* on top. Visualises every element's mouse hit-targets.
|
|
118
|
+
*/
|
|
119
|
+
const renderDebugHitZones = (ctx) => {
|
|
120
|
+
const { scene, selection, target, options, w2s, zoom } = ctx;
|
|
98
121
|
if (options.debugHitZones) {
|
|
99
122
|
drawHitZones(target, {
|
|
100
123
|
scene,
|
|
@@ -112,41 +135,96 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
112
135
|
: {}),
|
|
113
136
|
});
|
|
114
137
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Section 1 — per-shape selection outlines (+ resize/rotate handles only when a
|
|
141
|
+
* single shape is selected). Multi-selection skips per-shape handles in favour
|
|
142
|
+
* of the combined group bbox handles (section 7) — otherwise the overlay would
|
|
143
|
+
* look like a forest of corner squares and the user could grab a child handle,
|
|
144
|
+
* which `hitTest` also blocks.
|
|
145
|
+
*/
|
|
146
|
+
const renderSelectionHandles = (ctx) => {
|
|
147
|
+
const { scene, selection, target, options, style, w2s, zoom } = ctx;
|
|
148
|
+
// Crop mode owns the chrome: the crop frame + handles replace the normal
|
|
149
|
+
// per-shape resize/rotate affordances (drawn by renderPreviews).
|
|
150
|
+
if (options.cropFrame)
|
|
151
|
+
return;
|
|
120
152
|
const multiSelect = selection.size > 1;
|
|
121
153
|
for (const id of selection) {
|
|
122
154
|
const shape = scene.elements.get(id);
|
|
123
155
|
if (!shape)
|
|
124
156
|
continue;
|
|
125
157
|
// Groups have no intrinsic geometry — outline the union of their
|
|
126
|
-
// descendants
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
if (
|
|
158
|
+
// descendants as an axis-aligned box. A single shape gets an oriented
|
|
159
|
+
// frame that turns with its rotation, so the selection box hugs the body
|
|
160
|
+
// instead of its (larger, axis-aligned) AABB.
|
|
161
|
+
if (isGroup(shape)) {
|
|
162
|
+
const worldBounds = groupWorldBounds(scene, id);
|
|
163
|
+
if (!worldBounds)
|
|
164
|
+
continue;
|
|
165
|
+
const screenBounds = projectBounds(worldBounds, w2s);
|
|
166
|
+
drawOutline(target, screenBounds, style);
|
|
167
|
+
if (shape.locked === true) {
|
|
168
|
+
drawLockBadge(target, screenBounds);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Read-only: outline only — no resize/rotate affordances to grab.
|
|
172
|
+
if (multiSelect || !isResizable(shape) || options.readOnly === true)
|
|
173
|
+
continue;
|
|
174
|
+
for (const handle of CORNER_HANDLES) {
|
|
175
|
+
const worldPoint = handlePosition(handle, worldBounds, zoom);
|
|
176
|
+
drawHandle(target, matrix.applyToPoint(w2s, worldPoint), style);
|
|
177
|
+
}
|
|
178
|
+
drawRotateGripForBounds(target, worldBounds, zoom, w2s, style);
|
|
130
179
|
continue;
|
|
131
|
-
|
|
132
|
-
|
|
180
|
+
}
|
|
181
|
+
const frame = shapeSelectionFrame(shape);
|
|
182
|
+
// Unrotated → the plain AABB rect (unchanged rendering); rotated → a quad
|
|
183
|
+
// through the frame's corners so the box turns with the shape.
|
|
184
|
+
if (frame.rotation === 0) {
|
|
185
|
+
drawOutline(target, projectBounds(frame.bounds, w2s), style);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
drawFrameOutline(target, frame, w2s, style);
|
|
189
|
+
}
|
|
133
190
|
// Locked element: no resize handles; show a small lock badge instead so
|
|
134
191
|
// the user sees why it won't move/resize (click still selects → unlock).
|
|
135
192
|
if (shape.locked === true) {
|
|
136
|
-
drawLockBadge(target,
|
|
193
|
+
drawLockBadge(target, frameScreenBounds(frame, w2s));
|
|
137
194
|
continue;
|
|
138
195
|
}
|
|
139
|
-
|
|
196
|
+
// Read-only: outline only — no resize/rotate affordances to grab.
|
|
197
|
+
if (multiSelect || !isResizable(shape) || options.readOnly === true)
|
|
140
198
|
continue;
|
|
141
|
-
// Draw only the four CORNER dots
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// (ALL_HANDLES) still drives hit-test capability.
|
|
199
|
+
// Draw only the four CORNER dots — at the rotated frame corners. Edge
|
|
200
|
+
// resize is done by dragging the selection-box side itself, so no midpoint
|
|
201
|
+
// dot is needed; the edge handles stay hit-testable via `hitHandleOnFrame`.
|
|
145
202
|
for (const handle of CORNER_HANDLES) {
|
|
146
|
-
const worldPoint =
|
|
147
|
-
|
|
148
|
-
drawHandle(target, screenPoint, style);
|
|
203
|
+
const worldPoint = handleWorldOnFrame(handle, frame, zoom);
|
|
204
|
+
drawHandle(target, matrix.applyToPoint(w2s, worldPoint), style);
|
|
149
205
|
}
|
|
206
|
+
// Rotate grip from the shape's template anchor (default bottom-left).
|
|
207
|
+
drawRotateIcon(target, matrix.applyToPoint(w2s, rotateGripWorld(shape, zoom)), style);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Sections 2–3.5 — the transient draw/create previews and their landing
|
|
212
|
+
* highlights: the rubber-band rect, the WYSIWYG shape preview, the link
|
|
213
|
+
* preview, the click-create ghost, and the float-attach target outline.
|
|
214
|
+
*/
|
|
215
|
+
const renderPreviews = (ctx) => {
|
|
216
|
+
const { target, options, style, w2s } = ctx;
|
|
217
|
+
// 0. Crop-mode ghost — the faint full bitmap behind the crop chrome, so the
|
|
218
|
+
// pixels hidden by the window stay visible. Drawn first (under the frame).
|
|
219
|
+
if (options.cropGhost) {
|
|
220
|
+
drawCropGhost(target, options.cropGhost, w2s);
|
|
221
|
+
}
|
|
222
|
+
// 1. Image-crop frame: dashed accent quad + L-shaped corner brackets (the
|
|
223
|
+
// edge midpoints stay grabbable but aren't drawn, Excalidraw-style).
|
|
224
|
+
if (options.cropFrame && options.cropFrame.length >= 4) {
|
|
225
|
+
const cornersScreen = options.cropFrame.map((p) => matrix.applyToPoint(w2s, p));
|
|
226
|
+
drawCropFrame(target, cornersScreen, style);
|
|
227
|
+
drawCropCornerBrackets(target, cornersScreen, style);
|
|
150
228
|
}
|
|
151
229
|
// 2. Rubber-band drawing preview (already in world coords if drawn before transform reset)
|
|
152
230
|
if (options.drawingPreview) {
|
|
@@ -174,6 +252,47 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
174
252
|
target.restore();
|
|
175
253
|
}
|
|
176
254
|
}
|
|
255
|
+
// 2.6 Flowchart-create node preview — the pending nodes of a Cmd/Ctrl+Arrow
|
|
256
|
+
// grow session, each through its real renderer (faded). The connecting
|
|
257
|
+
// links are drawn by the orchestrator at the same opacity.
|
|
258
|
+
if (options.flowchartPreviewElements && options.flowchartPreviewElements.length > 0) {
|
|
259
|
+
for (const el of options.flowchartPreviewElements) {
|
|
260
|
+
const renderer = getElementRenderer(el.type);
|
|
261
|
+
if (!renderer)
|
|
262
|
+
continue;
|
|
263
|
+
target.save();
|
|
264
|
+
target.setTransform(w2s);
|
|
265
|
+
target.setOpacity(FLOWCHART_PREVIEW_OPACITY);
|
|
266
|
+
target.setDashArray(null);
|
|
267
|
+
target.translate(el.position.x, el.position.y);
|
|
268
|
+
if (el.rotation !== 0)
|
|
269
|
+
target.rotate(el.rotation);
|
|
270
|
+
if (el.scale.x !== 1 || el.scale.y !== 1)
|
|
271
|
+
target.scale(el.scale.x, el.scale.y);
|
|
272
|
+
renderer(el, target);
|
|
273
|
+
target.restore();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Stroke-erase live fragments — full opacity (the originals are hidden), drawn
|
|
277
|
+
// per-element so the overlay's cursor / trail chrome survive underneath.
|
|
278
|
+
if (options.strokeErasePreviewElements && options.strokeErasePreviewElements.length > 0) {
|
|
279
|
+
for (const el of options.strokeErasePreviewElements) {
|
|
280
|
+
const renderer = getElementRenderer(el.type);
|
|
281
|
+
if (!renderer)
|
|
282
|
+
continue;
|
|
283
|
+
target.save();
|
|
284
|
+
target.setTransform(w2s);
|
|
285
|
+
target.setOpacity(1);
|
|
286
|
+
target.setDashArray(null);
|
|
287
|
+
target.translate(el.position.x, el.position.y);
|
|
288
|
+
if (el.rotation !== 0)
|
|
289
|
+
target.rotate(el.rotation);
|
|
290
|
+
if (el.scale.x !== 1 || el.scale.y !== 1)
|
|
291
|
+
target.scale(el.scale.x, el.scale.y);
|
|
292
|
+
renderer(el, target);
|
|
293
|
+
target.restore();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
177
296
|
// 3. Link-drawing preview — dashed line in screen space. An elbow preview
|
|
178
297
|
// carries the full orthogonal polyline (`points`); otherwise a straight
|
|
179
298
|
// from→to segment.
|
|
@@ -236,6 +355,14 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
236
355
|
target.rect(b.x - pad, b.y - pad, b.width + pad * 2, b.height + pad * 2);
|
|
237
356
|
target.stroke();
|
|
238
357
|
}
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Sections 4–5 — link chrome: port-dot attach affordances, the persistent
|
|
361
|
+
* selection halo around every selected link, and the selected-edge endpoint /
|
|
362
|
+
* waypoint / midpoint handles.
|
|
363
|
+
*/
|
|
364
|
+
const renderLinkHandles = (ctx) => {
|
|
365
|
+
const { target, options, style, w2s, zoom } = ctx;
|
|
239
366
|
// 4. Port dots — hover affordance in draw-edge mode. May be one set or
|
|
240
367
|
// several (source start-anchors + target attach-anchors at once).
|
|
241
368
|
if (options.ports) {
|
|
@@ -246,7 +373,7 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
246
373
|
for (let i = 0; i < set.worldPoints.length; i++) {
|
|
247
374
|
const screen = matrix.applyToPoint(w2s, req(set.worldPoints[i]));
|
|
248
375
|
const active = set.activeIndex === i;
|
|
249
|
-
drawPortDot(target, screen,
|
|
376
|
+
drawPortDot(target, screen, active, set.role, active ? set.activeRadius : undefined);
|
|
250
377
|
}
|
|
251
378
|
}
|
|
252
379
|
}
|
|
@@ -256,7 +383,7 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
256
383
|
if (options.selectedLinkPaths && options.selectedLinkPaths.length > 0) {
|
|
257
384
|
target.setTransform(w2s);
|
|
258
385
|
target.setStroke(style.selectionStroke);
|
|
259
|
-
target.setOpacity(
|
|
386
|
+
target.setOpacity(SELECTION_HALO_OPACITY);
|
|
260
387
|
target.setDashArray(null);
|
|
261
388
|
target.setLineJoin("round");
|
|
262
389
|
target.setLineCap("round");
|
|
@@ -279,7 +406,9 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
279
406
|
target.setTransform(matrix.IDENTITY);
|
|
280
407
|
}
|
|
281
408
|
// 5. Selected-edge endpoint handles + bend-point (waypoint) handles.
|
|
282
|
-
|
|
409
|
+
// Read-only: the halo above still marks the selected link, but the
|
|
410
|
+
// endpoint/bend grips are suppressed (nothing to re-bind or drag).
|
|
411
|
+
if (options.edgeSelection && options.readOnly !== true) {
|
|
283
412
|
const from = matrix.applyToPoint(w2s, options.edgeSelection.from);
|
|
284
413
|
const to = matrix.applyToPoint(w2s, options.edgeSelection.to);
|
|
285
414
|
// Segment-midpoint "add waypoint" handles (drawn first, under the rest).
|
|
@@ -292,9 +421,14 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
292
421
|
drawLinkEndpointHandle(target, matrix.applyToPoint(w2s, w), style);
|
|
293
422
|
}
|
|
294
423
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* Section 6 — peer selection halos. Dashed outline around shapes selected by
|
|
427
|
+
* remote users, painted in their colour. Drawn before own-selection outlines
|
|
428
|
+
* so own selection stays on top.
|
|
429
|
+
*/
|
|
430
|
+
const renderPeerSelections = (ctx) => {
|
|
431
|
+
const { target, options, w2s } = ctx;
|
|
298
432
|
if (options.peerSelections) {
|
|
299
433
|
for (const peer of options.peerSelections) {
|
|
300
434
|
for (const wb of peer.bounds) {
|
|
@@ -303,6 +437,14 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
303
437
|
}
|
|
304
438
|
}
|
|
305
439
|
}
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* Section 6.5 — live brush stroke preview. Quad-strip with interpolated
|
|
443
|
+
* widths, same render path as the committed BrushElement. Runs in world coords
|
|
444
|
+
* so the stroke stays anchored to the cursor as the user zooms / pans mid-stroke.
|
|
445
|
+
*/
|
|
446
|
+
const renderBrushPreview = (ctx) => {
|
|
447
|
+
const { target, options, w2s } = ctx;
|
|
306
448
|
// 6.5. Live brush stroke preview — quad-strip with interpolated
|
|
307
449
|
// widths, same render path as the committed BrushElement. Runs
|
|
308
450
|
// in world coords so the stroke stays anchored to the cursor
|
|
@@ -311,6 +453,7 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
311
453
|
const bp = options.brushPreview;
|
|
312
454
|
target.save();
|
|
313
455
|
target.setTransform(w2s);
|
|
456
|
+
target.setOpacity(bp.opacity);
|
|
314
457
|
target.setFill(bp.fill);
|
|
315
458
|
target.setStroke(null);
|
|
316
459
|
const pts = bp.points;
|
|
@@ -323,35 +466,153 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
323
466
|
target.fill();
|
|
324
467
|
}
|
|
325
468
|
else {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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;
|
|
469
|
+
// Same single-outline fill as the committed stroke (see `brushOutline` /
|
|
470
|
+
// `drawBrush`), so the preview matches the result and honours `opacity`
|
|
471
|
+
// without the per-segment double-blend at joins.
|
|
472
|
+
const outline = brushOutline(pts);
|
|
473
|
+
if (outline.length >= 3) {
|
|
338
474
|
target.beginPath();
|
|
339
|
-
|
|
340
|
-
target.
|
|
341
|
-
|
|
342
|
-
|
|
475
|
+
const first = req(outline[0]);
|
|
476
|
+
target.moveTo(ox + first.x, oy + first.y);
|
|
477
|
+
for (let i = 1; i < outline.length; i++) {
|
|
478
|
+
const p = req(outline[i]);
|
|
479
|
+
target.lineTo(ox + p.x, oy + p.y);
|
|
480
|
+
}
|
|
343
481
|
target.closePath();
|
|
344
482
|
target.fill();
|
|
345
|
-
target.beginPath();
|
|
346
|
-
target.ellipse(bx, by, b.width, b.width);
|
|
347
|
-
target.fill();
|
|
348
483
|
}
|
|
349
484
|
}
|
|
350
485
|
target.restore();
|
|
351
486
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Section 6.6 — laser-pointer trails. Each segment is stroked in screen space
|
|
490
|
+
* (constant on-screen width at any zoom) with an opacity that decays from the
|
|
491
|
+
* newer endpoint's age: fresh points are opaque, older ones fade out, so a
|
|
492
|
+
* released stroke melts away tail-first. Read-only — never mutates state.
|
|
493
|
+
*/
|
|
494
|
+
const renderLaserTrails = (ctx) => {
|
|
495
|
+
const { target, options, w2s } = ctx;
|
|
496
|
+
drawFadingTrail(target, w2s, options.laserStrokes, LASER_COLOR, LASER_WIDTH, LASER_TRAIL_TTL_MS);
|
|
497
|
+
// Eraser drag trail — identical fade mechanics, neutral grey (not laser red).
|
|
498
|
+
// Its width matches the cursor ring's DIAMETER (2 × the panel radius) so the
|
|
499
|
+
// wake is exactly as wide as the eraser; a much shorter TTL than the laser
|
|
500
|
+
// keeps it a tight wake, not a long comet.
|
|
501
|
+
const eraserWidth = (options.eraserCursor?.radius ?? LASER_WIDTH / 2) * 2;
|
|
502
|
+
drawFadingTrail(target, w2s, options.eraserTrail, ERASER_CURSOR_STROKE, eraserWidth, ERASER_TRAIL_TTL_MS);
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Paint a trail (shared by the laser pointer and the eraser wake) the way
|
|
506
|
+
* Excalidraw does: ONE filled outline per stroke, not a stack of alpha-blended
|
|
507
|
+
* segments. The smoothed centreline is offset by a half-width that tapers from
|
|
508
|
+
* `width/2` at the head to 0 at the tail, giving a single comet shape with a
|
|
509
|
+
* pointed tail — no overlapping round caps beading at the joints. The whole
|
|
510
|
+
* stroke fills at one opacity that ramps down by the NEWEST point's age over
|
|
511
|
+
* `ttl` ms, so after release the comet dissolves as its tail is pruned. Drawn in
|
|
512
|
+
* screen space (constant on-screen width at any zoom). Read-only.
|
|
513
|
+
*/
|
|
514
|
+
const drawFadingTrail = (target, w2s, strokes, color, width, ttl) => {
|
|
515
|
+
if (!strokes || strokes.length === 0)
|
|
516
|
+
return;
|
|
517
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
518
|
+
const maxHalf = width / 2;
|
|
519
|
+
target.setStroke(null);
|
|
520
|
+
target.setDashArray(null);
|
|
521
|
+
target.setFill(color);
|
|
522
|
+
for (const stroke of strokes) {
|
|
523
|
+
const pts = smoothLaserPoints(stroke.points);
|
|
524
|
+
if (pts.length === 0)
|
|
525
|
+
continue;
|
|
526
|
+
// One opacity for the whole shape (Excalidraw fills a single path once);
|
|
527
|
+
// ramps by the freshest point's age so a released stroke fades as a whole.
|
|
528
|
+
const newest = req(pts[pts.length - 1]);
|
|
529
|
+
const op = 1 - (now - newest.t) / ttl;
|
|
530
|
+
if (op <= 0)
|
|
531
|
+
continue;
|
|
532
|
+
target.setOpacity(op);
|
|
533
|
+
const screen = pts.map((p) => matrix.applyToPoint(w2s, { x: p.x, y: p.y }));
|
|
534
|
+
if (screen.length === 1) {
|
|
535
|
+
const s = req(screen[0]);
|
|
536
|
+
target.beginPath();
|
|
537
|
+
target.ellipse(s.x, s.y, maxHalf, maxHalf);
|
|
538
|
+
target.fill();
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const outline = taperedTrailOutline(screen, maxHalf);
|
|
542
|
+
const first = req(outline[0]);
|
|
543
|
+
target.beginPath();
|
|
544
|
+
target.moveTo(first.x, first.y);
|
|
545
|
+
for (let i = 1; i < outline.length; i++) {
|
|
546
|
+
const p = req(outline[i]);
|
|
547
|
+
target.lineTo(p.x, p.y);
|
|
548
|
+
}
|
|
549
|
+
target.closePath();
|
|
550
|
+
target.fill();
|
|
551
|
+
}
|
|
552
|
+
target.setOpacity(1);
|
|
553
|
+
};
|
|
554
|
+
/**
|
|
555
|
+
* Build the closed outline polygon of a variable-width ribbon around the screen
|
|
556
|
+
* polyline `pts` (tail → head order). Each vertex is offset by ±`hw` along the
|
|
557
|
+
* local normal, where the half-width tapers linearly from 0 at the tail (index
|
|
558
|
+
* 0) to `maxHalf` at the head (last index). Returns the left side forward then
|
|
559
|
+
* the right side backward — one closed loop to fill in a single pass.
|
|
560
|
+
*/
|
|
561
|
+
export const taperedTrailOutline = (pts, maxHalf) => {
|
|
562
|
+
const n = pts.length;
|
|
563
|
+
const left = [];
|
|
564
|
+
const right = [];
|
|
565
|
+
for (let i = 0; i < n; i++) {
|
|
566
|
+
const p = req(pts[i]);
|
|
567
|
+
const prev = req(pts[Math.max(0, i - 1)]);
|
|
568
|
+
const next = req(pts[Math.min(n - 1, i + 1)]);
|
|
569
|
+
let tx = next.x - prev.x;
|
|
570
|
+
let ty = next.y - prev.y;
|
|
571
|
+
const len = Math.hypot(tx, ty) || 1;
|
|
572
|
+
tx /= len;
|
|
573
|
+
ty /= len;
|
|
574
|
+
// Normal (perpendicular) × the tapered half-width.
|
|
575
|
+
const hw = maxHalf * (n > 1 ? i / (n - 1) : 1);
|
|
576
|
+
const nx = -ty * hw;
|
|
577
|
+
const ny = tx * hw;
|
|
578
|
+
left.push({ x: p.x + nx, y: p.y + ny });
|
|
579
|
+
right.push({ x: p.x - nx, y: p.y - ny });
|
|
580
|
+
}
|
|
581
|
+
right.reverse();
|
|
582
|
+
return [...left, ...right];
|
|
583
|
+
};
|
|
584
|
+
/**
|
|
585
|
+
* Section 6.7 — eraser cursor ring. A grey circle following the pointer while
|
|
586
|
+
* the erase tool is active, its radius the panel's eraser width in SCREEN px
|
|
587
|
+
* (so it matches the slider number, unscaled by zoom). Replaces the hidden OS
|
|
588
|
+
* cursor. Read-only — never mutates state.
|
|
589
|
+
*/
|
|
590
|
+
const renderEraserCursor = (ctx) => {
|
|
591
|
+
const { target, options, w2s } = ctx;
|
|
592
|
+
const cursor = options.eraserCursor;
|
|
593
|
+
if (!cursor || cursor.radius <= 0)
|
|
594
|
+
return;
|
|
595
|
+
const s = matrix.applyToPoint(w2s, cursor.center);
|
|
596
|
+
target.setDashArray(null);
|
|
597
|
+
target.setOpacity(1);
|
|
598
|
+
target.beginPath();
|
|
599
|
+
target.ellipse(s.x, s.y, cursor.radius, cursor.radius);
|
|
600
|
+
// Solid disc in the trail colour (fully opaque), with the ring on top for a
|
|
601
|
+
// crisp radius edge.
|
|
602
|
+
target.setFill(ERASER_CURSOR_STROKE);
|
|
603
|
+
target.fill();
|
|
604
|
+
target.setFill(null);
|
|
605
|
+
target.setStroke(ERASER_CURSOR_STROKE);
|
|
606
|
+
target.setStrokeWidth(ERASER_CURSOR_LINE_WIDTH);
|
|
607
|
+
target.stroke();
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* Section 7.0 — container drop-zone highlight. Drawn under selection chrome so
|
|
611
|
+
* handles stay legible. Dashed rect + soft fill — same visual language as the
|
|
612
|
+
* drawing preview.
|
|
613
|
+
*/
|
|
614
|
+
const renderContainerDropZone = (ctx) => {
|
|
615
|
+
const { target, options, w2s } = ctx;
|
|
355
616
|
if (options.containerDropZone) {
|
|
356
617
|
const zoneScreen = projectBounds(options.containerDropZone, w2s);
|
|
357
618
|
target.setFill("rgba(26, 115, 232, 0.10)");
|
|
@@ -364,23 +625,41 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
364
625
|
target.stroke();
|
|
365
626
|
target.setDashArray(null);
|
|
366
627
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
628
|
+
};
|
|
629
|
+
/**
|
|
630
|
+
* Section 7 — multi-selection / group-typed combined bounds: outline + handles.
|
|
631
|
+
* Only CORNER dots are drawn (edge resize = drag the box side); the edge
|
|
632
|
+
* handles stay hit-testable via `hitHandle` for non-aspect-locked groups.
|
|
633
|
+
*/
|
|
634
|
+
const renderGroupBounds = (ctx) => {
|
|
635
|
+
const { target, options, style, w2s, zoom } = ctx;
|
|
636
|
+
// Crop mode owns the chrome: an image is aspect-locked, so its selection
|
|
637
|
+
// reports `groupBounds` — but its resize/rotate handles must not show on top
|
|
638
|
+
// of the crop brackets. Suppress them exactly like the per-shape handles.
|
|
639
|
+
if (options.cropFrame)
|
|
640
|
+
return;
|
|
371
641
|
if (options.groupBounds) {
|
|
372
642
|
const groupScreen = projectBounds(options.groupBounds, w2s);
|
|
373
643
|
drawOutline(target, groupScreen, style);
|
|
644
|
+
// Read-only: combined-bounds outline only — no resize/rotate handles.
|
|
645
|
+
if (options.readOnly === true)
|
|
646
|
+
return;
|
|
374
647
|
const handleSet = CORNER_HANDLES;
|
|
375
648
|
for (const handle of handleSet) {
|
|
376
649
|
const worldPoint = handlePosition(handle, options.groupBounds, zoom);
|
|
377
650
|
const screenPoint = matrix.applyToPoint(w2s, worldPoint);
|
|
378
651
|
drawHandle(target, screenPoint, style);
|
|
379
652
|
}
|
|
653
|
+
drawRotateGripForBounds(target, options.groupBounds, zoom, w2s, style);
|
|
380
654
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
655
|
+
};
|
|
656
|
+
/**
|
|
657
|
+
* Section 7.5 — annotation pins. Drawn before peer cursors so cursors stay on
|
|
658
|
+
* top, but on top of selection handles. Each pin shows a comment-count badge
|
|
659
|
+
* when the thread has > 0 replies.
|
|
660
|
+
*/
|
|
661
|
+
const renderAnnotations = (ctx) => {
|
|
662
|
+
const { scene, target, options, w2s } = ctx;
|
|
384
663
|
if (options.annotations && options.annotations.length > 0) {
|
|
385
664
|
for (const ann of options.annotations) {
|
|
386
665
|
const world = getAnnotationWorldPosition(scene, ann);
|
|
@@ -388,25 +667,39 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
388
667
|
drawAnnotationPin(target, screen, ann, options.selectedAnnotation ?? null);
|
|
389
668
|
}
|
|
390
669
|
}
|
|
391
|
-
|
|
392
|
-
|
|
670
|
+
};
|
|
671
|
+
/**
|
|
672
|
+
* Section 8 — remote peer cursors. Drawn near-last so they sit on top of every
|
|
673
|
+
* other overlay element (including own selection handles).
|
|
674
|
+
*/
|
|
675
|
+
const renderPeerCursors = (ctx) => {
|
|
676
|
+
const { target, options, w2s } = ctx;
|
|
393
677
|
if (options.peerCursors) {
|
|
394
678
|
for (const cursor of options.peerCursors) {
|
|
395
679
|
const screen = matrix.applyToPoint(w2s, cursor.position);
|
|
396
680
|
drawPeerCursor(target, screen, cursor.color, cursor.name);
|
|
397
681
|
}
|
|
398
682
|
}
|
|
399
|
-
|
|
400
|
-
|
|
683
|
+
};
|
|
684
|
+
/**
|
|
685
|
+
* Section 9 — GIF "play" badges on paused animated shapes. Drawn late so they
|
|
686
|
+
* sit above selection chrome.
|
|
687
|
+
*/
|
|
688
|
+
const renderGifBadges = (ctx) => {
|
|
689
|
+
const { target, options, w2s } = ctx;
|
|
401
690
|
if (options.gifBadges) {
|
|
402
691
|
for (const b of options.gifBadges) {
|
|
403
692
|
drawGifBadge(target, projectBounds(b, w2s));
|
|
404
693
|
}
|
|
405
694
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
695
|
+
};
|
|
696
|
+
/**
|
|
697
|
+
* Section 10 — in-canvas text editing: translucent selection highlight, then
|
|
698
|
+
* the caret bar on top. Both backends draw this via the shared RenderTarget
|
|
699
|
+
* primitives (rect + fill), so it's identical on Canvas2D and WebGL2.
|
|
700
|
+
*/
|
|
701
|
+
const renderTextEditing = (ctx) => {
|
|
702
|
+
const { target, options, w2s, zoom } = ctx;
|
|
410
703
|
if (options.editingText) {
|
|
411
704
|
const et = options.editingText;
|
|
412
705
|
if (et.selectionRects.length > 0) {
|
|
@@ -429,21 +722,12 @@ export const renderOverlay = (scene, selection, target, options = {}) => {
|
|
|
429
722
|
target.fill();
|
|
430
723
|
}
|
|
431
724
|
}
|
|
432
|
-
target.restore();
|
|
433
725
|
};
|
|
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
726
|
const drawLockBadge = (target, b) => {
|
|
443
727
|
const s = LOCK_BADGE_SIZE;
|
|
444
728
|
const x = b.x + b.width - s - 2;
|
|
445
729
|
const y = b.y + 2;
|
|
446
|
-
const color =
|
|
730
|
+
const color = LOCK_BADGE_COLOR;
|
|
447
731
|
// Shackle — a ring above the body; its lower half is hidden by the body so
|
|
448
732
|
// the visible top arc reads as the lock's bow.
|
|
449
733
|
target.setStroke(color);
|
|
@@ -461,7 +745,7 @@ const drawLockBadge = (target, b) => {
|
|
|
461
745
|
target.rect(x + s * 0.2, y + s * 0.44, s * 0.6, s * 0.48);
|
|
462
746
|
target.fill();
|
|
463
747
|
// Keyhole.
|
|
464
|
-
target.setFill(
|
|
748
|
+
target.setFill(LOCK_BADGE_KEYHOLE_COLOR);
|
|
465
749
|
target.beginPath();
|
|
466
750
|
target.ellipse(x + s / 2, y + s * 0.64, s * 0.07, s * 0.07);
|
|
467
751
|
target.fill();
|
|
@@ -474,7 +758,7 @@ const drawGifBadge = (target, screen) => {
|
|
|
474
758
|
const r = GIF_BADGE_RADIUS;
|
|
475
759
|
// Rounded-rect background.
|
|
476
760
|
target.setStroke(null);
|
|
477
|
-
target.setFill(
|
|
761
|
+
target.setFill(GIF_BADGE_BG_COLOR);
|
|
478
762
|
target.beginPath();
|
|
479
763
|
target.moveTo(x + r, y);
|
|
480
764
|
target.lineTo(x + w - r, y);
|
|
@@ -509,19 +793,121 @@ const drawOutline = (target, b, style) => {
|
|
|
509
793
|
target.rect(b.x, b.y, b.width, b.height);
|
|
510
794
|
target.stroke();
|
|
511
795
|
};
|
|
796
|
+
/** Selection outline as a (possibly rotated) quad through the frame's corners. */
|
|
797
|
+
const drawFrameOutline = (target, frame, w2s, style) => {
|
|
798
|
+
const [wnw, wne, wse, wsw] = frameCorners(frame);
|
|
799
|
+
const nw = matrix.applyToPoint(w2s, wnw);
|
|
800
|
+
const ne = matrix.applyToPoint(w2s, wne);
|
|
801
|
+
const se = matrix.applyToPoint(w2s, wse);
|
|
802
|
+
const sw = matrix.applyToPoint(w2s, wsw);
|
|
803
|
+
target.setStroke(style.selectionStroke);
|
|
804
|
+
target.setStrokeWidth(style.selectionStrokeWidth);
|
|
805
|
+
target.setDashArray(null);
|
|
806
|
+
target.beginPath();
|
|
807
|
+
target.moveTo(nw.x, nw.y);
|
|
808
|
+
target.lineTo(ne.x, ne.y);
|
|
809
|
+
target.lineTo(se.x, se.y);
|
|
810
|
+
target.lineTo(sw.x, sw.y);
|
|
811
|
+
target.closePath();
|
|
812
|
+
target.stroke();
|
|
813
|
+
};
|
|
814
|
+
/** Screen-space AABB enclosing the frame's (rotated) corners — for badges. */
|
|
815
|
+
const frameScreenBounds = (frame, w2s) => {
|
|
816
|
+
const pts = frameCorners(frame).map((c) => matrix.applyToPoint(w2s, c));
|
|
817
|
+
let minX = Infinity;
|
|
818
|
+
let minY = Infinity;
|
|
819
|
+
let maxX = -Infinity;
|
|
820
|
+
let maxY = -Infinity;
|
|
821
|
+
for (const p of pts) {
|
|
822
|
+
if (p.x < minX)
|
|
823
|
+
minX = p.x;
|
|
824
|
+
if (p.y < minY)
|
|
825
|
+
minY = p.y;
|
|
826
|
+
if (p.x > maxX)
|
|
827
|
+
maxX = p.x;
|
|
828
|
+
if (p.y > maxY)
|
|
829
|
+
maxY = p.y;
|
|
830
|
+
}
|
|
831
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
832
|
+
};
|
|
833
|
+
/**
|
|
834
|
+
* Shared draw for every "filled circle with an outline" handle affordance —
|
|
835
|
+
* resize handles, port dots, link endpoint / waypoint / midpoint grabs. A
|
|
836
|
+
* circle reads as "draggable handle" more clearly than a sharp rectangle, and
|
|
837
|
+
* `ellipse` is supported by every RenderTarget (canvas, svg). State-setter
|
|
838
|
+
* order is irrelevant to output; only the state at `fill()`/`stroke()` matters.
|
|
839
|
+
*/
|
|
840
|
+
const drawHandleDot = (target, center, radius, s) => {
|
|
841
|
+
if (s.opacity !== undefined)
|
|
842
|
+
target.setOpacity(s.opacity);
|
|
843
|
+
target.setStroke(s.stroke);
|
|
844
|
+
target.setStrokeWidth(s.strokeWidth);
|
|
845
|
+
target.setDashArray(null);
|
|
846
|
+
target.setFill(s.fill);
|
|
847
|
+
target.beginPath();
|
|
848
|
+
target.ellipse(center.x, center.y, radius, radius);
|
|
849
|
+
target.fill();
|
|
850
|
+
target.stroke();
|
|
851
|
+
if (s.opacity !== undefined)
|
|
852
|
+
target.setOpacity(1);
|
|
853
|
+
};
|
|
512
854
|
const drawHandle = (target, center, style) => {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
855
|
+
drawHandleDot(target, center, HANDLE_SIZE, {
|
|
856
|
+
fill: style.handleFill,
|
|
857
|
+
stroke: style.handleStroke,
|
|
858
|
+
strokeWidth: 1,
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
/**
|
|
862
|
+
* Rotate grip: a clockwise circular-arrow glyph (the `rotate-cw` icon) centred
|
|
863
|
+
* at `grip` (screen px), with no connector line back to the shape. Authored in
|
|
864
|
+
* lucide's 24×24 space — a near-full circle (radius 9) opening on the right plus
|
|
865
|
+
* an `L`-shaped arrowhead at the opening — scaled so the circle radius is
|
|
866
|
+
* `ROTATE_ICON_RADIUS` screen px.
|
|
867
|
+
*/
|
|
868
|
+
const drawRotateIcon = (target, grip, style) => {
|
|
869
|
+
const s = ROTATE_ICON_RADIUS / 9; // icon radius 9 (in its 24×24 box) → screen px
|
|
870
|
+
// Map a point from the icon's 24×24 space (centre 12,12) to screen.
|
|
871
|
+
const tx = (px, py) => ({
|
|
872
|
+
x: grip.x + (px - 12) * s,
|
|
873
|
+
y: grip.y + (py - 12) * s,
|
|
874
|
+
});
|
|
518
875
|
target.setStroke(style.handleStroke);
|
|
519
|
-
target.
|
|
876
|
+
target.setFill(null);
|
|
877
|
+
target.setStrokeWidth(2 * s); // lucide strokeWidth 2
|
|
878
|
+
target.setLineCap("round");
|
|
879
|
+
target.setLineJoin("round");
|
|
520
880
|
target.setDashArray(null);
|
|
881
|
+
// Arc: centre (12,12), radius 9, from 0° (east tail) clockwise to (21,8)
|
|
882
|
+
// (≈ 336° swept) — a polyline is smooth enough at this size.
|
|
883
|
+
const END_DEG = 336;
|
|
884
|
+
const STEPS = 30;
|
|
521
885
|
target.beginPath();
|
|
522
|
-
|
|
523
|
-
|
|
886
|
+
for (let i = 0; i <= STEPS; i++) {
|
|
887
|
+
const a = ((END_DEG * i) / STEPS) * (Math.PI / 180);
|
|
888
|
+
const p = tx(12 + 9 * Math.cos(a), 12 + 9 * Math.sin(a));
|
|
889
|
+
if (i === 0)
|
|
890
|
+
target.moveTo(p.x, p.y);
|
|
891
|
+
else
|
|
892
|
+
target.lineTo(p.x, p.y);
|
|
893
|
+
}
|
|
524
894
|
target.stroke();
|
|
895
|
+
// Arrowhead at the arc's end (lucide `M21 3 v5 h-5`): a right-angle chevron.
|
|
896
|
+
const a1 = tx(21, 3);
|
|
897
|
+
const a2 = tx(21, 8);
|
|
898
|
+
const a3 = tx(16, 8);
|
|
899
|
+
target.beginPath();
|
|
900
|
+
target.moveTo(a1.x, a1.y);
|
|
901
|
+
target.lineTo(a2.x, a2.y);
|
|
902
|
+
target.lineTo(a3.x, a3.y);
|
|
903
|
+
target.stroke();
|
|
904
|
+
target.setLineCap("butt");
|
|
905
|
+
target.setLineJoin("miter");
|
|
906
|
+
};
|
|
907
|
+
/** Project the rotate grip to screen and draw the icon for AABB `b`. */
|
|
908
|
+
const drawRotateGripForBounds = (target, b, zoom, w2s, style) => {
|
|
909
|
+
const grip = rotateGripForBounds(b, zoom);
|
|
910
|
+
drawRotateIcon(target, matrix.applyToPoint(w2s, grip), style);
|
|
525
911
|
};
|
|
526
912
|
const drawDrawingPreview = (target, b, style) => {
|
|
527
913
|
target.setStroke(style.drawingStroke);
|
|
@@ -541,6 +927,93 @@ const drawLinkPreview = (target, from, to, style) => {
|
|
|
541
927
|
target.stroke();
|
|
542
928
|
};
|
|
543
929
|
/** Dashed polyline preview (elbow) in screen space. */
|
|
930
|
+
/**
|
|
931
|
+
* Dashed accent quad for the image-crop window. `pts` are already in screen
|
|
932
|
+
* space (4 corners, clockwise). The grab handles are drawn separately (all 8
|
|
933
|
+
* corner + edge nubs) by the caller.
|
|
934
|
+
*/
|
|
935
|
+
const drawCropFrame = (target, pts, style) => {
|
|
936
|
+
target.setStroke(style.selectionStroke);
|
|
937
|
+
target.setStrokeWidth(1.5);
|
|
938
|
+
target.setDashArray(style.drawingDash);
|
|
939
|
+
target.beginPath();
|
|
940
|
+
const first = req(pts[0]);
|
|
941
|
+
target.moveTo(first.x, first.y);
|
|
942
|
+
for (let i = 1; i < pts.length; i++) {
|
|
943
|
+
const p = req(pts[i]);
|
|
944
|
+
target.lineTo(p.x, p.y);
|
|
945
|
+
}
|
|
946
|
+
target.lineTo(first.x, first.y);
|
|
947
|
+
target.stroke();
|
|
948
|
+
target.setDashArray(null);
|
|
949
|
+
};
|
|
950
|
+
/**
|
|
951
|
+
* L-shaped corner brackets on the crop frame (Excalidraw-style), replacing the
|
|
952
|
+
* round resize-nubs. `corners` are the 4 window corners in SCREEN space,
|
|
953
|
+
* clockwise (nw, ne, se, sw); each bracket's two arms run along the adjacent
|
|
954
|
+
* edges, so the marks stay aligned when the image is rotated. Only the corners
|
|
955
|
+
* are drawn — the edge midpoints remain grabbable via hit-testing.
|
|
956
|
+
*/
|
|
957
|
+
const drawCropCornerBrackets = (target, corners, style) => {
|
|
958
|
+
if (corners.length < 4)
|
|
959
|
+
return;
|
|
960
|
+
target.setStroke(style.selectionStroke);
|
|
961
|
+
target.setStrokeWidth(CROP_BRACKET_WIDTH);
|
|
962
|
+
target.setDashArray(null);
|
|
963
|
+
target.setLineCap("round");
|
|
964
|
+
target.setLineJoin("round");
|
|
965
|
+
const armTo = (from, to) => {
|
|
966
|
+
const dx = to.x - from.x;
|
|
967
|
+
const dy = to.y - from.y;
|
|
968
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
969
|
+
const t = Math.min(CROP_BRACKET_LEN, len) / len;
|
|
970
|
+
return { x: from.x + dx * t, y: from.y + dy * t };
|
|
971
|
+
};
|
|
972
|
+
for (let i = 0; i < 4; i++) {
|
|
973
|
+
const c = req(corners[i]);
|
|
974
|
+
const prev = req(corners[(i + 3) % 4]);
|
|
975
|
+
const next = req(corners[(i + 1) % 4]);
|
|
976
|
+
const a = armTo(c, prev);
|
|
977
|
+
const b = armTo(c, next);
|
|
978
|
+
target.beginPath();
|
|
979
|
+
target.moveTo(a.x, a.y);
|
|
980
|
+
target.lineTo(c.x, c.y);
|
|
981
|
+
target.lineTo(b.x, b.y);
|
|
982
|
+
target.stroke();
|
|
983
|
+
}
|
|
984
|
+
target.setLineCap("butt");
|
|
985
|
+
target.setLineJoin("miter");
|
|
986
|
+
};
|
|
987
|
+
/**
|
|
988
|
+
* Paint the faint full-image ghost behind the crop chrome. Replicates the main
|
|
989
|
+
* image renderer's transform path: apply the element's local→world transform,
|
|
990
|
+
* then draw the whole bitmap over the virtual full-image LOCAL rect at
|
|
991
|
+
* {@link CROP_GHOST_OPACITY}. Resolves the live handle exactly as the built-in
|
|
992
|
+
* image renderer (`metadata.image`, else `resolveImageSource`); skips silently
|
|
993
|
+
* when no handle is available (async decode in flight / headless).
|
|
994
|
+
*/
|
|
995
|
+
const drawCropGhost = (target, ghost, w2s) => {
|
|
996
|
+
const el = ghost.element;
|
|
997
|
+
const handle = el.animationKind !== undefined
|
|
998
|
+
? resolveImageSource(el, undefined)
|
|
999
|
+
: (el.metadata?.image ?? resolveImageSource(el, undefined));
|
|
1000
|
+
if (handle === null || handle === undefined)
|
|
1001
|
+
return;
|
|
1002
|
+
const dynamic = el.metadata?.animated === true || el.animationKind !== undefined;
|
|
1003
|
+
const { fullRect } = ghost;
|
|
1004
|
+
target.save();
|
|
1005
|
+
target.setTransform(w2s);
|
|
1006
|
+
target.setDashArray(null);
|
|
1007
|
+
target.translate(el.position.x, el.position.y);
|
|
1008
|
+
if (el.rotation !== 0)
|
|
1009
|
+
target.rotate(el.rotation);
|
|
1010
|
+
if (el.scale.x !== 1 || el.scale.y !== 1)
|
|
1011
|
+
target.scale(el.scale.x, el.scale.y);
|
|
1012
|
+
target.setOpacity(CROP_GHOST_OPACITY);
|
|
1013
|
+
target.drawImage(handle, fullRect.x, fullRect.y, fullRect.width, fullRect.height, dynamic);
|
|
1014
|
+
target.setOpacity(1);
|
|
1015
|
+
target.restore();
|
|
1016
|
+
};
|
|
544
1017
|
const drawLinkPreviewPath = (target, pts, style) => {
|
|
545
1018
|
target.setStroke(style.drawingStroke);
|
|
546
1019
|
target.setStrokeWidth(1.5);
|
|
@@ -554,7 +1027,7 @@ const drawLinkPreviewPath = (target, pts, style) => {
|
|
|
554
1027
|
}
|
|
555
1028
|
target.stroke();
|
|
556
1029
|
};
|
|
557
|
-
const drawPortDot = (target, center,
|
|
1030
|
+
const drawPortDot = (target, center, active, role = "link-start", activeRadius) => {
|
|
558
1031
|
const radius = active ? (activeRadius ?? ANCHOR_DOT_ACTIVE_RADIUS) : ANCHOR_DOT_RADIUS;
|
|
559
1032
|
const isStart = role === "link-start";
|
|
560
1033
|
// When active (snapped), use the inverse fill of the resting state
|
|
@@ -567,41 +1040,25 @@ const drawPortDot = (target, center, style, active, role = "link-start", activeR
|
|
|
567
1040
|
? LINK_START_ANCHOR_FILL
|
|
568
1041
|
: LINK_ATTACH_ANCHOR_FILL;
|
|
569
1042
|
const stroke = isStart ? LINK_START_ANCHOR_STROKE : LINK_ATTACH_ANCHOR_STROKE;
|
|
570
|
-
target
|
|
571
|
-
target.setStrokeWidth(ANCHOR_DOT_STROKE_WIDTH);
|
|
572
|
-
target.setDashArray(null);
|
|
573
|
-
target.setFill(fill);
|
|
574
|
-
target.beginPath();
|
|
575
|
-
target.ellipse(center.x, center.y, radius, radius);
|
|
576
|
-
target.fill();
|
|
577
|
-
target.stroke();
|
|
1043
|
+
drawHandleDot(target, center, radius, { fill, stroke, strokeWidth: ANCHOR_DOT_STROKE_WIDTH });
|
|
578
1044
|
};
|
|
579
1045
|
const drawLinkEndpointHandle = (target, center, style) => {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
target.beginPath();
|
|
586
|
-
target.ellipse(center.x, center.y, radius, radius);
|
|
587
|
-
target.fill();
|
|
588
|
-
target.stroke();
|
|
1046
|
+
drawHandleDot(target, center, LINK_ENDPOINT_HANDLE_DRAW_RADIUS, {
|
|
1047
|
+
fill: style.handleFill,
|
|
1048
|
+
stroke: style.selectionStroke,
|
|
1049
|
+
strokeWidth: 2,
|
|
1050
|
+
});
|
|
589
1051
|
};
|
|
590
1052
|
// Smaller, semi-transparent dot on a segment midpoint — the "drag to add a
|
|
591
1053
|
// bend point" affordance. Lighter than a real waypoint handle so it reads
|
|
592
1054
|
// as secondary.
|
|
593
1055
|
const drawLinkMidpointHandle = (target, center, style) => {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
target.beginPath();
|
|
601
|
-
target.ellipse(center.x, center.y, radius, radius);
|
|
602
|
-
target.fill();
|
|
603
|
-
target.stroke();
|
|
604
|
-
target.setOpacity(1);
|
|
1056
|
+
drawHandleDot(target, center, LINK_MIDPOINT_HANDLE_DRAW_RADIUS, {
|
|
1057
|
+
fill: style.handleFill,
|
|
1058
|
+
stroke: style.selectionStroke,
|
|
1059
|
+
strokeWidth: 1.5,
|
|
1060
|
+
opacity: 0.55,
|
|
1061
|
+
});
|
|
605
1062
|
};
|
|
606
1063
|
const drawPeerSelection = (target, b, color) => {
|
|
607
1064
|
const pad = PEER_SELECTION_PADDING;
|