@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.
Files changed (269) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +60 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/actions/actionClipboard.d.ts +6 -0
  6. package/dist/actions/actionClipboard.d.ts.map +1 -0
  7. package/dist/actions/actionClipboard.js +26 -0
  8. package/dist/actions/actionClipboard.js.map +1 -0
  9. package/dist/actions/actionGrouping.d.ts +5 -0
  10. package/dist/actions/actionGrouping.d.ts.map +1 -0
  11. package/dist/actions/actionGrouping.js +26 -0
  12. package/dist/actions/actionGrouping.js.map +1 -0
  13. package/dist/actions/actionHistory.d.ts +5 -0
  14. package/dist/actions/actionHistory.d.ts.map +1 -0
  15. package/dist/actions/actionHistory.js +25 -0
  16. package/dist/actions/actionHistory.js.map +1 -0
  17. package/dist/actions/actionKeyboard.d.ts +3 -0
  18. package/dist/actions/actionKeyboard.d.ts.map +1 -0
  19. package/dist/actions/actionKeyboard.js +130 -0
  20. package/dist/actions/actionKeyboard.js.map +1 -0
  21. package/dist/actions/actionLayout.d.ts +3 -0
  22. package/dist/actions/actionLayout.d.ts.map +1 -0
  23. package/dist/actions/actionLayout.js +65 -0
  24. package/dist/actions/actionLayout.js.map +1 -0
  25. package/dist/actions/actionMode.d.ts +13 -0
  26. package/dist/actions/actionMode.d.ts.map +1 -0
  27. package/dist/actions/actionMode.js +116 -0
  28. package/dist/actions/actionMode.js.map +1 -0
  29. package/dist/actions/actionSelection.d.ts +9 -0
  30. package/dist/actions/actionSelection.d.ts.map +1 -0
  31. package/dist/actions/actionSelection.js +57 -0
  32. package/dist/actions/actionSelection.js.map +1 -0
  33. package/dist/actions/actionView.d.ts +7 -0
  34. package/dist/actions/actionView.d.ts.map +1 -0
  35. package/dist/actions/actionView.js +14 -0
  36. package/dist/actions/actionView.js.map +1 -0
  37. package/dist/actions/actionZOrder.d.ts +7 -0
  38. package/dist/actions/actionZOrder.d.ts.map +1 -0
  39. package/dist/actions/actionZOrder.js +42 -0
  40. package/dist/actions/actionZOrder.js.map +1 -0
  41. package/dist/actions/actionZoom.d.ts +8 -0
  42. package/dist/actions/actionZoom.d.ts.map +1 -0
  43. package/dist/actions/actionZoom.js +61 -0
  44. package/dist/actions/actionZoom.js.map +1 -0
  45. package/dist/actions/index.d.ts +12 -0
  46. package/dist/actions/index.d.ts.map +1 -0
  47. package/dist/actions/index.js +11 -0
  48. package/dist/actions/index.js.map +1 -0
  49. package/dist/actions/registry.d.ts +68 -0
  50. package/dist/actions/registry.d.ts.map +1 -0
  51. package/dist/actions/registry.js +300 -0
  52. package/dist/actions/registry.js.map +1 -0
  53. package/dist/actions/types.d.ts +103 -0
  54. package/dist/actions/types.d.ts.map +1 -0
  55. package/dist/actions/types.js +10 -0
  56. package/dist/actions/types.js.map +1 -0
  57. package/dist/animation-tick.d.ts +28 -0
  58. package/dist/animation-tick.d.ts.map +1 -0
  59. package/dist/animation-tick.js +50 -0
  60. package/dist/animation-tick.js.map +1 -0
  61. package/dist/auto-compact.d.ts +27 -0
  62. package/dist/auto-compact.d.ts.map +1 -0
  63. package/dist/auto-compact.js +46 -0
  64. package/dist/auto-compact.js.map +1 -0
  65. package/dist/auto-layout-scheduler.d.ts +50 -0
  66. package/dist/auto-layout-scheduler.d.ts.map +1 -0
  67. package/dist/auto-layout-scheduler.js +94 -0
  68. package/dist/auto-layout-scheduler.js.map +1 -0
  69. package/dist/built-in-handlers.d.ts +24 -0
  70. package/dist/built-in-handlers.d.ts.map +1 -0
  71. package/dist/built-in-handlers.js +210 -0
  72. package/dist/built-in-handlers.js.map +1 -0
  73. package/dist/clipboard.d.ts +51 -0
  74. package/dist/clipboard.d.ts.map +1 -0
  75. package/dist/clipboard.js +105 -0
  76. package/dist/clipboard.js.map +1 -0
  77. package/dist/constants.d.ts +544 -0
  78. package/dist/constants.d.ts.map +1 -0
  79. package/dist/constants.js +537 -0
  80. package/dist/constants.js.map +1 -0
  81. package/dist/dom-events.d.ts +13 -0
  82. package/dist/dom-events.d.ts.map +1 -0
  83. package/dist/dom-events.js +70 -0
  84. package/dist/dom-events.js.map +1 -0
  85. package/dist/editor/anchor-points.d.ts +22 -0
  86. package/dist/editor/anchor-points.d.ts.map +1 -0
  87. package/dist/editor/anchor-points.js +28 -0
  88. package/dist/editor/anchor-points.js.map +1 -0
  89. package/dist/editor/applies/create.d.ts +60 -0
  90. package/dist/editor/applies/create.d.ts.map +1 -0
  91. package/dist/editor/applies/create.js +107 -0
  92. package/dist/editor/applies/create.js.map +1 -0
  93. package/dist/editor/applies/edge.d.ts +32 -0
  94. package/dist/editor/applies/edge.d.ts.map +1 -0
  95. package/dist/editor/applies/edge.js +44 -0
  96. package/dist/editor/applies/edge.js.map +1 -0
  97. package/dist/editor/applies/link-move.d.ts +53 -0
  98. package/dist/editor/applies/link-move.d.ts.map +1 -0
  99. package/dist/editor/applies/link-move.js +160 -0
  100. package/dist/editor/applies/link-move.js.map +1 -0
  101. package/dist/editor/applies/move.d.ts +38 -0
  102. package/dist/editor/applies/move.d.ts.map +1 -0
  103. package/dist/editor/applies/move.js +83 -0
  104. package/dist/editor/applies/move.js.map +1 -0
  105. package/dist/editor/applies/resize.d.ts +68 -0
  106. package/dist/editor/applies/resize.d.ts.map +1 -0
  107. package/dist/editor/applies/resize.js +199 -0
  108. package/dist/editor/applies/resize.js.map +1 -0
  109. package/dist/editor/applies/selection.d.ts +34 -0
  110. package/dist/editor/applies/selection.d.ts.map +1 -0
  111. package/dist/editor/applies/selection.js +65 -0
  112. package/dist/editor/applies/selection.js.map +1 -0
  113. package/dist/editor/applies/snap-grid.d.ts +45 -0
  114. package/dist/editor/applies/snap-grid.d.ts.map +1 -0
  115. package/dist/editor/applies/snap-grid.js +90 -0
  116. package/dist/editor/applies/snap-grid.js.map +1 -0
  117. package/dist/editor/container-ops.d.ts +74 -0
  118. package/dist/editor/container-ops.d.ts.map +1 -0
  119. package/dist/editor/container-ops.js +262 -0
  120. package/dist/editor/container-ops.js.map +1 -0
  121. package/dist/editor/event-fanout.d.ts +48 -0
  122. package/dist/editor/event-fanout.d.ts.map +1 -0
  123. package/dist/editor/event-fanout.js +53 -0
  124. package/dist/editor/event-fanout.js.map +1 -0
  125. package/dist/editor/gesture-tx.d.ts +77 -0
  126. package/dist/editor/gesture-tx.d.ts.map +1 -0
  127. package/dist/editor/gesture-tx.js +93 -0
  128. package/dist/editor/gesture-tx.js.map +1 -0
  129. package/dist/editor/hit-test.d.ts +178 -0
  130. package/dist/editor/hit-test.d.ts.map +1 -0
  131. package/dist/editor/hit-test.js +460 -0
  132. package/dist/editor/hit-test.js.map +1 -0
  133. package/dist/editor/long-press.d.ts +34 -0
  134. package/dist/editor/long-press.d.ts.map +1 -0
  135. package/dist/editor/long-press.js +58 -0
  136. package/dist/editor/long-press.js.map +1 -0
  137. package/dist/editor/pinch.d.ts +37 -0
  138. package/dist/editor/pinch.d.ts.map +1 -0
  139. package/dist/editor/pinch.js +90 -0
  140. package/dist/editor/pinch.js.map +1 -0
  141. package/dist/editor/pointer-binding.d.ts +18 -0
  142. package/dist/editor/pointer-binding.d.ts.map +1 -0
  143. package/dist/editor/pointer-binding.js +1087 -0
  144. package/dist/editor/pointer-binding.js.map +1 -0
  145. package/dist/editor/public/annotations.d.ts +54 -0
  146. package/dist/editor/public/annotations.d.ts.map +1 -0
  147. package/dist/editor/public/annotations.js +106 -0
  148. package/dist/editor/public/annotations.js.map +1 -0
  149. package/dist/editor/public/arrange-group.d.ts +81 -0
  150. package/dist/editor/public/arrange-group.d.ts.map +1 -0
  151. package/dist/editor/public/arrange-group.js +193 -0
  152. package/dist/editor/public/arrange-group.js.map +1 -0
  153. package/dist/editor/public/brush.d.ts +33 -0
  154. package/dist/editor/public/brush.d.ts.map +1 -0
  155. package/dist/editor/public/brush.js +54 -0
  156. package/dist/editor/public/brush.js.map +1 -0
  157. package/dist/editor/public/clipboard.d.ts +31 -0
  158. package/dist/editor/public/clipboard.d.ts.map +1 -0
  159. package/dist/editor/public/clipboard.js +35 -0
  160. package/dist/editor/public/clipboard.js.map +1 -0
  161. package/dist/editor/public/image-insert.d.ts +30 -0
  162. package/dist/editor/public/image-insert.d.ts.map +1 -0
  163. package/dist/editor/public/image-insert.js +57 -0
  164. package/dist/editor/public/image-insert.js.map +1 -0
  165. package/dist/editor/public/layers.d.ts +56 -0
  166. package/dist/editor/public/layers.d.ts.map +1 -0
  167. package/dist/editor/public/layers.js +104 -0
  168. package/dist/editor/public/layers.js.map +1 -0
  169. package/dist/editor/public/link.d.ts +25 -0
  170. package/dist/editor/public/link.d.ts.map +1 -0
  171. package/dist/editor/public/link.js +74 -0
  172. package/dist/editor/public/link.js.map +1 -0
  173. package/dist/editor/public/placement.d.ts +60 -0
  174. package/dist/editor/public/placement.d.ts.map +1 -0
  175. package/dist/editor/public/placement.js +111 -0
  176. package/dist/editor/public/placement.js.map +1 -0
  177. package/dist/editor/public/selection-ops.d.ts +78 -0
  178. package/dist/editor/public/selection-ops.d.ts.map +1 -0
  179. package/dist/editor/public/selection-ops.js +226 -0
  180. package/dist/editor/public/selection-ops.js.map +1 -0
  181. package/dist/editor/public/text-edit.d.ts +17 -0
  182. package/dist/editor/public/text-edit.d.ts.map +1 -0
  183. package/dist/editor/public/text-edit.js +28 -0
  184. package/dist/editor/public/text-edit.js.map +1 -0
  185. package/dist/editor/public/z-order.d.ts +47 -0
  186. package/dist/editor/public/z-order.d.ts.map +1 -0
  187. package/dist/editor/public/z-order.js +151 -0
  188. package/dist/editor/public/z-order.js.map +1 -0
  189. package/dist/editor/public/zoom-pan.d.ts +44 -0
  190. package/dist/editor/public/zoom-pan.d.ts.map +1 -0
  191. package/dist/editor/public/zoom-pan.js +116 -0
  192. package/dist/editor/public/zoom-pan.js.map +1 -0
  193. package/dist/editor/render-orchestrator.d.ts +11 -0
  194. package/dist/editor/render-orchestrator.d.ts.map +1 -0
  195. package/dist/editor/render-orchestrator.js +473 -0
  196. package/dist/editor/render-orchestrator.js.map +1 -0
  197. package/dist/editor/resize-helpers.d.ts +30 -0
  198. package/dist/editor/resize-helpers.d.ts.map +1 -0
  199. package/dist/editor/resize-helpers.js +121 -0
  200. package/dist/editor/resize-helpers.js.map +1 -0
  201. package/dist/editor/shape-filters.d.ts +11 -0
  202. package/dist/editor/shape-filters.d.ts.map +1 -0
  203. package/dist/editor/shape-filters.js +19 -0
  204. package/dist/editor/shape-filters.js.map +1 -0
  205. package/dist/editor/shape-traits.d.ts +16 -0
  206. package/dist/editor/shape-traits.d.ts.map +1 -0
  207. package/dist/editor/shape-traits.js +29 -0
  208. package/dist/editor/shape-traits.js.map +1 -0
  209. package/dist/editor/viewport-helpers.d.ts +24 -0
  210. package/dist/editor/viewport-helpers.d.ts.map +1 -0
  211. package/dist/editor/viewport-helpers.js +63 -0
  212. package/dist/editor/viewport-helpers.js.map +1 -0
  213. package/dist/editor-events.d.ts +33 -0
  214. package/dist/editor-events.d.ts.map +1 -0
  215. package/dist/editor-events.js +2 -0
  216. package/dist/editor-events.js.map +1 -0
  217. package/dist/editor.d.ts +1841 -0
  218. package/dist/editor.d.ts.map +1 -0
  219. package/dist/editor.js +4803 -0
  220. package/dist/editor.js.map +1 -0
  221. package/dist/file-drop.d.ts +99 -0
  222. package/dist/file-drop.d.ts.map +1 -0
  223. package/dist/file-drop.js +198 -0
  224. package/dist/file-drop.js.map +1 -0
  225. package/dist/frame-helpers.d.ts +30 -0
  226. package/dist/frame-helpers.d.ts.map +1 -0
  227. package/dist/frame-helpers.js +105 -0
  228. package/dist/frame-helpers.js.map +1 -0
  229. package/dist/group-helpers.d.ts +39 -0
  230. package/dist/group-helpers.d.ts.map +1 -0
  231. package/dist/group-helpers.js +116 -0
  232. package/dist/group-helpers.js.map +1 -0
  233. package/dist/handle.d.ts +73 -0
  234. package/dist/handle.d.ts.map +1 -0
  235. package/dist/handle.js +184 -0
  236. package/dist/handle.js.map +1 -0
  237. package/dist/index.d.ts +26 -0
  238. package/dist/index.d.ts.map +1 -0
  239. package/dist/index.js +16 -0
  240. package/dist/index.js.map +1 -0
  241. package/dist/interactive.d.ts +21 -0
  242. package/dist/interactive.d.ts.map +1 -0
  243. package/dist/interactive.js +11 -0
  244. package/dist/interactive.js.map +1 -0
  245. package/dist/link-selection.d.ts +21 -0
  246. package/dist/link-selection.d.ts.map +1 -0
  247. package/dist/link-selection.js +37 -0
  248. package/dist/link-selection.js.map +1 -0
  249. package/dist/machine.d.ts +322 -0
  250. package/dist/machine.d.ts.map +1 -0
  251. package/dist/machine.js +568 -0
  252. package/dist/machine.js.map +1 -0
  253. package/dist/modes.d.ts +24 -0
  254. package/dist/modes.d.ts.map +1 -0
  255. package/dist/modes.js +2 -0
  256. package/dist/modes.js.map +1 -0
  257. package/dist/overlay.d.ts +262 -0
  258. package/dist/overlay.d.ts.map +1 -0
  259. package/dist/overlay.js +673 -0
  260. package/dist/overlay.js.map +1 -0
  261. package/dist/platform.d.ts +70 -0
  262. package/dist/platform.d.ts.map +1 -0
  263. package/dist/platform.js +115 -0
  264. package/dist/platform.js.map +1 -0
  265. package/dist/selection.d.ts +18 -0
  266. package/dist/selection.d.ts.map +1 -0
  267. package/dist/selection.js +31 -0
  268. package/dist/selection.js.map +1 -0
  269. package/package.json +59 -0
@@ -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