@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.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 (259) hide show
  1. package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
  2. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
  3. package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
  4. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
  5. package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
  6. package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
  7. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
  8. package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
  9. package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
  10. package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
  11. package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
  12. package/dist/canvas/index.d.ts +43 -25
  13. package/dist/canvas/index.js +5 -3
  14. package/dist/chrome/index.d.ts +9 -6
  15. package/dist/chrome/index.js +7 -6
  16. package/dist/chunk-2AYGEN57.js +32 -0
  17. package/dist/chunk-2AYGEN57.js.map +1 -0
  18. package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
  19. package/dist/chunk-3P5TTTA7.js.map +1 -0
  20. package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
  21. package/dist/chunk-3ZELRRBV.js.map +1 -0
  22. package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
  23. package/dist/chunk-4EY67BKJ.js.map +1 -0
  24. package/dist/chunk-5TCEVMPI.js +140 -0
  25. package/dist/chunk-5TCEVMPI.js.map +1 -0
  26. package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
  27. package/dist/chunk-6JMU56CU.js.map +1 -0
  28. package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
  29. package/dist/chunk-CQLQPQ4P.js.map +1 -0
  30. package/dist/chunk-CTRKTLYZ.js +33 -0
  31. package/dist/chunk-CTRKTLYZ.js.map +1 -0
  32. package/dist/chunk-EQIB2PGC.js +458 -0
  33. package/dist/chunk-EQIB2PGC.js.map +1 -0
  34. package/dist/chunk-HEORBLVT.js +7128 -0
  35. package/dist/chunk-HEORBLVT.js.map +1 -0
  36. package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
  37. package/dist/chunk-JDDIZSLL.js.map +1 -0
  38. package/dist/chunk-LITE5YFB.js +669 -0
  39. package/dist/chunk-LITE5YFB.js.map +1 -0
  40. package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
  41. package/dist/chunk-ROQDRXKW.js.map +1 -0
  42. package/dist/chunk-SBXNQW4G.js +3 -0
  43. package/dist/chunk-SBXNQW4G.js.map +1 -0
  44. package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
  45. package/dist/chunk-XJ6N32QP.js.map +1 -0
  46. package/dist/chunk-YQJNT4XQ.js +293 -0
  47. package/dist/chunk-YQJNT4XQ.js.map +1 -0
  48. package/dist/controls/index.d.ts +4 -28
  49. package/dist/controls/index.js +3 -3
  50. package/dist/dragdrop/index.d.ts +50 -5
  51. package/dist/dragdrop/index.js +3 -1
  52. package/dist/index.d.ts +660 -302
  53. package/dist/index.js +1793 -587
  54. package/dist/index.js.map +1 -1
  55. package/dist/layers/index.d.ts +6 -5
  56. package/dist/layers/index.js +4 -2
  57. package/dist/loupe/index.d.ts +187 -0
  58. package/dist/loupe/index.js +7 -0
  59. package/dist/loupe/index.js.map +1 -0
  60. package/dist/passthrough/weasel-canvas.d.ts +3 -1
  61. package/dist/passthrough/weasel-canvas.js +1 -1
  62. package/dist/passthrough/weasel-ui.d.ts +151 -91
  63. package/dist/passthrough/weasel-ui.js +3 -2
  64. package/dist/primitives/index.d.ts +3 -1
  65. package/dist/primitives/index.js +6 -5
  66. package/dist/state/index.d.ts +3 -3
  67. package/dist/state/index.js +2 -2
  68. package/dist/styles.css +229 -703
  69. package/dist/surface/index.d.ts +18 -2
  70. package/dist/surface/index.js +2 -2
  71. package/dist/ui/layers/index.d.ts +8 -2
  72. package/dist/ui/layers/index.js +2 -4
  73. package/dist/undo/index.d.ts +5 -4
  74. package/package.json +14 -7
  75. package/src/annotations/AnnotationOverlay.tsx +205 -0
  76. package/src/annotations/AnnotationTargets.tsx +37 -0
  77. package/src/annotations/Annotations.less +129 -0
  78. package/src/annotations/Annotations.meaning.test.tsx +111 -0
  79. package/src/annotations/Annotations.overlay.test.tsx +142 -0
  80. package/src/annotations/AnnotationsContext.ts +49 -0
  81. package/src/annotations/ExportMenu.test.tsx +86 -0
  82. package/src/annotations/ExportMenu.tsx +182 -0
  83. package/src/annotations/MarkList.tsx +75 -0
  84. package/src/annotations/capture.test.ts +140 -0
  85. package/src/annotations/capture.ts +237 -0
  86. package/src/annotations/drawOne.test.ts +56 -0
  87. package/src/annotations/drawOne.ts +47 -0
  88. package/src/annotations/frac.test.ts +59 -0
  89. package/src/annotations/frac.ts +66 -0
  90. package/src/annotations/history.test.ts +82 -0
  91. package/src/annotations/history.ts +118 -0
  92. package/src/annotations/index.ts +37 -0
  93. package/src/annotations/paint.test.ts +125 -0
  94. package/src/annotations/paint.ts +130 -0
  95. package/src/annotations/staleness.test.ts +56 -0
  96. package/src/annotations/staleness.ts +41 -0
  97. package/src/annotations/store.test.ts +175 -0
  98. package/src/annotations/store.ts +317 -0
  99. package/src/annotations/svgNodes.test.ts +77 -0
  100. package/src/annotations/svgNodes.ts +74 -0
  101. package/src/annotations/toolMap.test.ts +36 -0
  102. package/src/annotations/toolMap.ts +54 -0
  103. package/src/annotations/types.ts +222 -0
  104. package/src/annotations/view.test.ts +37 -0
  105. package/src/annotations/view.ts +40 -0
  106. package/src/canvas/AGENTS.md +55 -5
  107. package/src/canvas/CanvasStack.test.tsx +27 -1
  108. package/src/canvas/CanvasStack.tsx +29 -4
  109. package/src/canvas/CanvasStackContext.ts +23 -4
  110. package/src/canvas/camera.test.ts +78 -0
  111. package/src/canvas/camera.ts +62 -0
  112. package/src/canvas/canvasCoords.test.ts +26 -0
  113. package/src/canvas/canvasCoords.ts +20 -7
  114. package/src/canvas/index.ts +5 -1
  115. package/src/canvas/useLayerScheduler.ts +7 -3
  116. package/src/canvas/usePanZoom.test.ts +36 -5
  117. package/src/canvas/usePanZoom.ts +20 -15
  118. package/src/canvas/worldSpec.test.ts +63 -0
  119. package/src/canvas/worldSpec.ts +48 -0
  120. package/src/chrome/builtins.test.ts +31 -1
  121. package/src/chrome/builtins.tsx +63 -19
  122. package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
  123. package/src/chrome/regions/PaletteRegion.tsx +4 -0
  124. package/src/chrome/regions/SidebarRegion.tsx +37 -13
  125. package/src/chrome/regions/ToolbarRegion.tsx +2 -1
  126. package/src/chrome/regions/ViewportRegion.tsx +9 -1
  127. package/src/chrome/regions/regions.test.tsx +6 -1
  128. package/src/chrome/types.ts +20 -0
  129. package/src/config/builder.test.ts +7 -1
  130. package/src/config/builder.ts +1 -5
  131. package/src/config/resolve.test.ts +1 -2
  132. package/src/controls/ControlPanel.stories.tsx +41 -2
  133. package/src/controls/ControlPanel.test.tsx +69 -9
  134. package/src/controls/ControlPanel.tsx +106 -25
  135. package/src/dragdrop/DragDropRuntime.tsx +8 -2
  136. package/src/dragdrop/dragDrop.test.tsx +29 -2
  137. package/src/dragdrop/index.ts +11 -0
  138. package/src/index.test.ts +22 -0
  139. package/src/index.ts +136 -4
  140. package/src/instrument/SineWave.smoke.test.tsx +1 -0
  141. package/src/instrument/types.ts +24 -1
  142. package/src/lab/Lab.surface.test.tsx +162 -0
  143. package/src/lab/Lab.tsx +102 -17
  144. package/src/lab/Lab.undock.test.tsx +85 -0
  145. package/src/lab/LabFullChrome.stories.tsx +20 -0
  146. package/src/lab/LabShell.less +11 -0
  147. package/src/lab/Workspace.less +73 -0
  148. package/src/lab/Workspace.surface.test.tsx +2 -0
  149. package/src/lab/Workspace.tsx +97 -8
  150. package/src/lab/index.ts +1 -1
  151. package/src/lab/panelHost.ts +37 -0
  152. package/src/layers/AGENTS.md +1 -1
  153. package/src/layers/LayerList.tsx +1 -1
  154. package/src/loupe/AGENTS.md +49 -0
  155. package/src/loupe/CanvasLoupe.tsx +83 -0
  156. package/src/loupe/DomLoupe.tsx +62 -0
  157. package/src/loupe/Loupe.less +38 -0
  158. package/src/loupe/LoupeBubble.tsx +32 -0
  159. package/src/loupe/TrialLoupe.tsx +93 -0
  160. package/src/loupe/canvasLens.test.ts +205 -0
  161. package/src/loupe/canvasLens.ts +141 -0
  162. package/src/loupe/index.ts +19 -0
  163. package/src/loupe/types.test.ts +29 -0
  164. package/src/loupe/types.ts +90 -0
  165. package/src/loupe/useHostSize.ts +30 -0
  166. package/src/loupe/useLoupe.test.tsx +197 -0
  167. package/src/loupe/useLoupe.ts +189 -0
  168. package/src/passthrough/weasel-ui.ts +25 -17
  169. package/src/primitives/FloatingPanel.test.tsx +76 -1
  170. package/src/primitives/FloatingPanel.tsx +19 -3
  171. package/src/primitives/ScaleIndicator.test.tsx +7 -2
  172. package/src/primitives/Sidebar.less +18 -0
  173. package/src/primitives/Toolbar.less +8 -0
  174. package/src/primitives/Toolbar.tsx +5 -0
  175. package/src/primitives/useRovingTabIndex.test.ts +21 -0
  176. package/src/primitives/useRovingTabIndex.ts +26 -17
  177. package/src/state/document.test.ts +23 -0
  178. package/src/state/document.ts +14 -2
  179. package/src/state/index.ts +2 -0
  180. package/src/state/store.test.ts +24 -0
  181. package/src/state/store.ts +35 -1
  182. package/src/state/types.ts +11 -0
  183. package/src/state/undock.test.ts +40 -0
  184. package/src/state/undock.ts +39 -0
  185. package/src/styles.less +2 -4
  186. package/src/surface/SurfaceContext.ts +4 -0
  187. package/src/surface/index.ts +8 -3
  188. package/src/surface/useSurfaceTile.test.tsx +2 -0
  189. package/src/surface/useSurfaceTile.ts +7 -1
  190. package/src/surface/useTiledSurface.test.tsx +58 -0
  191. package/src/surface/useTiledSurface.ts +38 -3
  192. package/src/theme/Interstellar.stories.tsx +1 -1
  193. package/src/trial/Trial.annotations.persist.test.tsx +166 -0
  194. package/src/trial/Trial.annotations.test.tsx +121 -0
  195. package/src/trial/Trial.canvas.test.tsx +76 -1
  196. package/src/trial/Trial.config.test.tsx +3 -8
  197. package/src/trial/Trial.less +19 -0
  198. package/src/trial/Trial.loupe.test.tsx +128 -0
  199. package/src/trial/Trial.stories.tsx +1 -0
  200. package/src/trial/Trial.test.tsx +12 -0
  201. package/src/trial/Trial.trialId.test.tsx +36 -0
  202. package/src/trial/Trial.tsx +215 -40
  203. package/src/trial/TrialChrome.tsx +35 -0
  204. package/src/trial/UndockedSections.tsx +65 -0
  205. package/src/trial/trialOps.test.ts +21 -0
  206. package/src/trial/trialOps.ts +5 -3
  207. package/src/ui/layers/index.ts +5 -1
  208. package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
  209. package/dist/chunk-4TMMVIDM.js.map +0 -1
  210. package/dist/chunk-CPUJ3QXL.js.map +0 -1
  211. package/dist/chunk-HFDVGF4X.js.map +0 -1
  212. package/dist/chunk-NS54R52R.js.map +0 -1
  213. package/dist/chunk-O2BULFHX.js +0 -152
  214. package/dist/chunk-O2BULFHX.js.map +0 -1
  215. package/dist/chunk-O7NVSCLN.js.map +0 -1
  216. package/dist/chunk-PO6L3NSH.js +0 -366
  217. package/dist/chunk-PO6L3NSH.js.map +0 -1
  218. package/dist/chunk-PQJ5B2U2.js +0 -27
  219. package/dist/chunk-PQJ5B2U2.js.map +0 -1
  220. package/dist/chunk-RVNN4CHQ.js.map +0 -1
  221. package/dist/chunk-TLGRYALP.js +0 -6151
  222. package/dist/chunk-TLGRYALP.js.map +0 -1
  223. package/dist/chunk-V7PRSIRQ.js.map +0 -1
  224. package/dist/chunk-Z6HJ5FEM.js.map +0 -1
  225. package/dist/chunk-ZZAYVX4X.js +0 -505
  226. package/dist/chunk-ZZAYVX4X.js.map +0 -1
  227. package/src/primitives/DragHandleGlyph.tsx +0 -29
  228. package/src/ui/format.test.ts +0 -33
  229. package/src/ui/format.ts +0 -30
  230. package/src/ui/layers/LayerStack.less +0 -143
  231. package/src/ui/layers/LayerStack.stories.tsx +0 -45
  232. package/src/ui/layers/LayerStack.test.tsx +0 -170
  233. package/src/ui/layers/LayerStack.tsx +0 -208
  234. package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
  235. package/src/ui/properties/ColorRow.stories.tsx +0 -106
  236. package/src/ui/properties/CurveField.less +0 -48
  237. package/src/ui/properties/CurveField.stories.tsx +0 -20
  238. package/src/ui/properties/CurveField.test.tsx +0 -127
  239. package/src/ui/properties/CurveField.tsx +0 -184
  240. package/src/ui/properties/EffectCard.tsx +0 -324
  241. package/src/ui/properties/Gallery.stories.tsx +0 -178
  242. package/src/ui/properties/NumberRow.stories.tsx +0 -65
  243. package/src/ui/properties/PropertyGroup.less +0 -50
  244. package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
  245. package/src/ui/properties/PropertyGroup.test.tsx +0 -33
  246. package/src/ui/properties/PropertyGroup.tsx +0 -45
  247. package/src/ui/properties/PropertyList.stories.tsx +0 -83
  248. package/src/ui/properties/PropertyPanel.less +0 -606
  249. package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
  250. package/src/ui/properties/PropertyPanel.test.tsx +0 -193
  251. package/src/ui/properties/PropertyPanel.tsx +0 -469
  252. package/src/ui/properties/PropertyRow.stories.tsx +0 -53
  253. package/src/ui/properties/SelectRow.stories.tsx +0 -58
  254. package/src/ui/properties/SliderRow.stories.tsx +0 -74
  255. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
  256. package/src/ui/properties/TextRow.stories.tsx +0 -62
  257. package/src/ui/properties/ToggleRow.stories.tsx +0 -57
  258. package/src/ui/properties/index.ts +0 -39
  259. package/src/ui/properties/storyLayouts.tsx +0 -36
package/dist/index.js CHANGED
@@ -1,34 +1,1401 @@
1
- import { useSurfaceOptional } from './chunk-HFDVGF4X.js';
2
- export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-HFDVGF4X.js';
1
+ import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
2
+ export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
3
+ import { useDragDrop, DragOverlay, Palette } from './chunk-5TCEVMPI.js';
4
+ import { useSurfaceOptional, useSurfaceCanvas, useTiledSurface, SurfaceContext, SurfaceCanvasContext } from './chunk-4EY67BKJ.js';
5
+ export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-4EY67BKJ.js';
3
6
  import { useJob } from './chunk-LN6JDUGB.js';
4
7
  export { useJob } from './chunk-LN6JDUGB.js';
5
- export { LayerStack } from './chunk-O2BULFHX.js';
6
- import { suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion } from './chunk-PO6L3NSH.js';
7
- export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from './chunk-PO6L3NSH.js';
8
- import { JobProgress } from './chunk-RVNN4CHQ.js';
9
- export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-RVNN4CHQ.js';
10
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-O7NVSCLN.js';
11
- import { LabStoreContext, createLabStore, noneAdapter } from './chunk-4TMMVIDM.js';
12
- export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, labDocumentKey, localStorageAdapter, noneAdapter, quarantineKey, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-4TMMVIDM.js';
13
- import { fromConfigFields } from './chunk-ZZAYVX4X.js';
14
- export { CheckboxRow, ColorRow, ControlPanel, NumberRow, PropertyGroup, PropertyList, PropertyPanel, PropertyRow, SelectRow, SliderRow, TextRow, ToggleRow, fromConfigFields, isLeafVisible } from './chunk-ZZAYVX4X.js';
15
- import { p, F, vt, p2, r } from './chunk-TLGRYALP.js';
16
- import { CanvasStack, screenToWorld } from './chunk-NS54R52R.js';
17
- export { CanvasStack, screenToWorld, worldToScreen } from './chunk-NS54R52R.js';
18
- export { CanvasStackContext } from './chunk-CPUJ3QXL.js';
19
- import { useVisibleRaf } from './chunk-Z6HJ5FEM.js';
20
- import { LayerList } from './chunk-V7PRSIRQ.js';
21
- export { LayerList } from './chunk-V7PRSIRQ.js';
22
- import './chunk-PQJ5B2U2.js';
23
- import { createEventBus, redo, undo, pushSnapshot } from './chunk-RL2LOLNI.js';
24
- export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
25
- import { createContext, useRef, useCallback, useContext, useMemo, useState, Children, useLayoutEffect, useEffect } from 'react';
26
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
27
- import { useStore } from 'zustand/react';
8
+ import { ANNOTATION_WEASEL_TOOLS, annotationToolInfo, suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion, AnnotationsContext } from './chunk-LITE5YFB.js';
9
+ export { ANNOTATION_TOOLS, ANNOTATION_WEASEL_TOOLS, AnnotationsContext, ExportMenu, MarkList, PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, annotationToolInfo, builtinContributions, mergeContributions, suppressContributions, useAnnotations, useAnnotationsOptional } from './chunk-LITE5YFB.js';
10
+ import { JobProgress } from './chunk-3P5TTTA7.js';
11
+ export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-3P5TTTA7.js';
12
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-6JMU56CU.js';
13
+ import { LabStoreContext, TrialIdProvider, createLabStore, panelKey, noneAdapter } from './chunk-XJ6N32QP.js';
14
+ export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createMemoryAdapter, dockPanel, labDocumentKey, localStorageAdapter, noneAdapter, panelKey, quarantineKey, sessionStorageAdapter, undockPanel, urlHashAdapter, useLabStore, useTrialId, useTrialState } from './chunk-XJ6N32QP.js';
15
+ import { fromConfigFields } from './chunk-YQJNT4XQ.js';
16
+ export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-YQJNT4XQ.js';
17
+ import { CanvasStack } from './chunk-3ZELRRBV.js';
18
+ export { CanvasStack } from './chunk-3ZELRRBV.js';
19
+ import { LayerList } from './chunk-JDDIZSLL.js';
20
+ export { LayerList } from './chunk-JDDIZSLL.js';
21
+ import './chunk-SBXNQW4G.js';
22
+ import { C, p, r2 } from './chunk-HEORBLVT.js';
23
+ export { k2 as CheckboxRow, O2 as ColorRow, m2 as CurveField, _3 as EffectCard, y2 as EffectCardList, c3 as LayerStack, j2 as NumberRow, b3 as PropertyGroup, S2 as PropertyList, x3 as PropertyPanel, w as PropertyRow, C2 as PropertySpan, M as SelectRow, E as SliderRow, g as Subpanel, A2 as TextRow, N as ToggleRow } from './chunk-HEORBLVT.js';
24
+ import { resolveLoupe, TrialLoupe } from './chunk-EQIB2PGC.js';
25
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from './chunk-EQIB2PGC.js';
26
+ export { centerOn, zoomAt } from './chunk-CTRKTLYZ.js';
27
+ import { applyCamera } from './chunk-2AYGEN57.js';
28
+ export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from './chunk-2AYGEN57.js';
29
+ export { CanvasStackContext } from './chunk-CQLQPQ4P.js';
30
+ import { textCommand, linePath, markerDrawCommands, ellipsePath, rectPath, createScene, asNodeId as asNodeId$1, WeaselProvider, SceneCanvas, sceneFromJSON, PathBuilder, renderSceneToPixels, useActiveToolContext, resolveStrokeWidth, getMarker, getPaintKind, boundsOfPath, tileGeometry, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, SCRIPT_METRICS } from './chunk-ROQDRXKW.js';
31
+ import { createContext, useState, useRef, useEffect, useCallback, useContext, useMemo, Children, useLayoutEffect, useSyncExternalStore } from 'react';
28
32
  import { createPortal } from 'react-dom';
33
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
34
+ import { useStore } from 'zustand/react';
29
35
  import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
30
- import { asNodeId, Store, createNode, gridStrategy } from 'windease';
36
+ import { asNodeId, Store, createNode, floatingStrategy, gridStrategy } from 'windease';
31
37
 
38
+ // ../svg/dist/index.js
39
+ var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
40
+ function multiply(a, b2) {
41
+ return [
42
+ a[0] * b2[0] + a[2] * b2[1],
43
+ a[1] * b2[0] + a[3] * b2[1],
44
+ a[0] * b2[2] + a[2] * b2[3],
45
+ a[1] * b2[2] + a[3] * b2[3],
46
+ a[0] * b2[4] + a[2] * b2[5] + a[4],
47
+ a[1] * b2[4] + a[3] * b2[5] + a[5]
48
+ ];
49
+ }
50
+ function isIdentity(m2, eps = 1e-9) {
51
+ return Math.abs(m2[0] - 1) < eps && Math.abs(m2[1]) < eps && Math.abs(m2[2]) < eps && Math.abs(m2[3] - 1) < eps && Math.abs(m2[4]) < eps && Math.abs(m2[5]) < eps;
52
+ }
53
+ function formatMatrix(m2) {
54
+ if (isIdentity(m2)) return null;
55
+ return `matrix(${m2.map((n) => trimNumber(n)).join(" ")})`;
56
+ }
57
+ function trimNumber(n) {
58
+ if (!Number.isFinite(n)) return "0";
59
+ const r2 = Math.round(n * 1e6) / 1e6;
60
+ if (Number.isInteger(r2)) return String(r2);
61
+ return String(r2);
62
+ }
63
+ function patternSpecOf(paint) {
64
+ return "tile" in paint.pattern ? paint.pattern : null;
65
+ }
66
+ function patternXml(id, paint, onWarn) {
67
+ const spec = patternSpecOf(paint);
68
+ if (!spec) {
69
+ onWarn?.("pattern fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
70
+ return "";
71
+ }
72
+ const geometry = tileGeometry(spec);
73
+ const body = geometry.shapes.map(shapeXml).join("");
74
+ const origin = paint.origin ?? { x: 0, y: 0 };
75
+ const attrs = [
76
+ `id="${id}"`,
77
+ 'patternUnits="userSpaceOnUse"',
78
+ `width="${trimNumber(geometry.size)}"`,
79
+ `height="${trimNumber(geometry.size)}"`
80
+ ];
81
+ if (origin.x !== 0 || origin.y !== 0) {
82
+ attrs.push(`x="${trimNumber(origin.x)}"`, `y="${trimNumber(origin.y)}"`);
83
+ }
84
+ attrs.push(`data-weasel-tile="${escapeAttr(JSON.stringify(spec))}"`);
85
+ return `<pattern ${attrs.join(" ")}>${body}</pattern>`;
86
+ }
87
+ function shapeXml(shape) {
88
+ switch (shape.kind) {
89
+ case "line":
90
+ return `<line x1="${trimNumber(shape.x1)}" y1="${trimNumber(shape.y1)}" x2="${trimNumber(shape.x2)}" y2="${trimNumber(shape.y2)}" stroke="${shape.color}" stroke-width="${trimNumber(shape.width)}"/>`;
91
+ case "circle":
92
+ return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${shape.color}"/>`;
93
+ case "ellipse": {
94
+ const deg = shape.rotation * 180 / Math.PI;
95
+ const transform = deg === 0 ? "" : ` transform="rotate(${trimNumber(deg)} ${trimNumber(shape.cx)} ${trimNumber(shape.cy)})"`;
96
+ return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${shape.color}"${transform}/>`;
97
+ }
98
+ case "rect":
99
+ return `<rect x="${trimNumber(shape.x)}" y="${trimNumber(shape.y)}" width="${trimNumber(shape.width)}" height="${trimNumber(shape.height)}" fill="${shape.color}"/>`;
100
+ default:
101
+ return "";
102
+ }
103
+ }
104
+ function escapeAttr(s) {
105
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
106
+ }
107
+ function serializePathD(path) {
108
+ if (path.kind === "rect") {
109
+ const { x: x2, y: y2, width, height } = path;
110
+ return `M${trimNumber(x2)} ${trimNumber(y2)}h${trimNumber(width)}v${trimNumber(height)}h${trimNumber(-width)}Z`;
111
+ }
112
+ const cmds = path.commands;
113
+ const xs = path.coords;
114
+ const out = [];
115
+ let ci = 0;
116
+ for (let i = 0; i < cmds.length; i++) {
117
+ const c2 = cmds[i];
118
+ if (c2 === PATH_M) {
119
+ out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
120
+ ci += 2;
121
+ } else if (c2 === PATH_L) {
122
+ out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
123
+ ci += 2;
124
+ } else if (c2 === PATH_C) {
125
+ out.push(
126
+ `C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
127
+ );
128
+ ci += 6;
129
+ } else if (c2 === PATH_Q) {
130
+ out.push(
131
+ `Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
132
+ );
133
+ ci += 4;
134
+ } else if (c2 === PATH_Z) {
135
+ out.push("Z");
136
+ }
137
+ }
138
+ return out.join("");
139
+ }
140
+ var PaintServerRegistry = class {
141
+ byPaint = /* @__PURE__ */ new Map();
142
+ order = [];
143
+ counter = 0;
144
+ // Marker keys referenced by any stroke, in first-use order. The `<defs>`
145
+ // id is the marker key itself, not a minted counter id: `parseSvg` reads a
146
+ // `marker-end="url(#id)"` fragment back as the marker key directly (it
147
+ // never inspects the `<marker>` element), so a synthetic id would come
148
+ // back unresolvable and warn on round-trip.
149
+ markerKeys = [];
150
+ markerKeySet = /* @__PURE__ */ new Set();
151
+ register(paint) {
152
+ const existing = this.byPaint.get(paint);
153
+ if (existing) return existing;
154
+ const id = paint.fill === "pattern" ? `pat${this.counter++}` : `grad${this.counter++}`;
155
+ this.byPaint.set(paint, id);
156
+ this.order.push(paint);
157
+ return id;
158
+ }
159
+ /** The `<defs>` id for a marker key — the key itself, minting nothing.
160
+ * `undefined` when nothing is registered under `key`, so a caller emits no
161
+ * attribute at all rather than a `url(#…)` pointing at a def that will
162
+ * never be written. */
163
+ markerId(key) {
164
+ if (getMarker(key) === void 0) return void 0;
165
+ if (!this.markerKeySet.has(key)) {
166
+ this.markerKeySet.add(key);
167
+ this.markerKeys.push(key);
168
+ }
169
+ return key;
170
+ }
171
+ /** Emit `<defs>...</defs>` XML for every registered paint server and marker. */
172
+ toDefsXml(onWarn) {
173
+ if (this.order.length === 0 && this.markerKeys.length === 0) return "";
174
+ const parts = ["<defs>"];
175
+ for (const paint of this.order) {
176
+ parts.push(paintServerXml(this.byPaint.get(paint), paint, onWarn));
177
+ }
178
+ for (const key of this.markerKeys) {
179
+ const entry = getMarker(key);
180
+ if (!entry) continue;
181
+ parts.push(entry.toSvg ? entry.toSvg(key, entry) : defaultMarkerXml(key, entry));
182
+ }
183
+ parts.push("</defs>");
184
+ return parts.join("");
185
+ }
186
+ };
187
+ function defaultMarkerXml(id, entry) {
188
+ const path = entry.path({ size: 1, stroke: { paint: { fill: "solid", color: "#000" } } });
189
+ const d = serializePathD(path);
190
+ const fill = entry.fill === "none" ? "none" : "context-stroke";
191
+ const outline = entry.outline ? ` stroke="context-stroke" stroke-width="${entry.outline.width}" stroke-linecap="round" stroke-linejoin="round"` : "";
192
+ return `<marker id="${id}" markerUnits="strokeWidth" markerWidth="8" markerHeight="8" refX="0" refY="0" orient="auto" overflow="visible"><path d="${d}" fill="${fill}"${outline}/></marker>`;
193
+ }
194
+ function paintServerXml(id, paint, onWarn) {
195
+ if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
196
+ const builtin = gradientXml(id, paint);
197
+ if (builtin) return builtin;
198
+ const custom = getPaintKind(paint.fill)?.toSvg?.(id, paint);
199
+ if (custom) return custom;
200
+ onWarn?.(`${paint.fill} fill has no vector form \u2014 omitted from <defs>`);
201
+ return "";
202
+ }
203
+ function gradientUnitsAttr(units) {
204
+ return units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
205
+ }
206
+ function gradientXml(id, paint) {
207
+ if (paint.fill === "linear-gradient") {
208
+ const stops = paint.stops.map(stopXml).join("");
209
+ return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}">${stops}</linearGradient>`;
210
+ }
211
+ if (paint.fill === "radial-gradient") {
212
+ const stops = paint.stops.map(stopXml).join("");
213
+ return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}">${stops}</radialGradient>`;
214
+ }
215
+ return "";
216
+ }
217
+ function stopXml(s) {
218
+ const m2 = /^#([0-9a-f]{6})([0-9a-f]{2})$/i.exec(s.color);
219
+ if (m2) {
220
+ const alpha = parseInt(m2[2], 16) / 255;
221
+ return `<stop offset="${trimNumber(s.offset)}" stop-color="#${m2[1].toLowerCase()}" stop-opacity="${trimNumber(alpha)}"/>`;
222
+ }
223
+ return `<stop offset="${trimNumber(s.offset)}" stop-color="${s.color}"/>`;
224
+ }
225
+ function serializeSvg(nodes, opts = {}) {
226
+ const registry = new PaintServerRegistry();
227
+ registerPaintServers(nodes, registry);
228
+ const bounds = opts.viewBox ?? computeBounds(nodes);
229
+ const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
230
+ const namespaces = opts.namespaces ?? {};
231
+ const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
232
+ for (const [prefix, uri] of Object.entries(namespaces)) {
233
+ rootAttrs.push(`xmlns:${prefix}="${escapeAttr2(uri)}"`);
234
+ }
235
+ rootAttrs.push(`viewBox="${vb}"`);
236
+ if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
237
+ if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
238
+ if (opts.documentMeta) {
239
+ for (const prefix of Object.keys(namespaces)) {
240
+ const bucket = opts.documentMeta[prefix];
241
+ if (!bucket?.attrs) continue;
242
+ for (const [name, value] of Object.entries(bucket.attrs)) {
243
+ rootAttrs.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
244
+ }
245
+ }
246
+ }
247
+ let docMetaXml = "";
248
+ if (opts.documentMeta) {
249
+ for (const prefix of Object.keys(namespaces)) {
250
+ const bucket = opts.documentMeta[prefix];
251
+ if (!bucket?.elements) continue;
252
+ for (const [localName, list] of Object.entries(bucket.elements)) {
253
+ for (const el of list) docMetaXml += namespacedElementXml(prefix, localName, el);
254
+ }
255
+ }
256
+ }
257
+ const defsXml = registry.toDefsXml(opts.onWarn);
258
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces)).join("");
259
+ const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
260
+ return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
261
+ }
262
+ function namespacedElementXml(prefix, localName, el) {
263
+ const attrs = [];
264
+ for (const [name, value] of Object.entries(el.attrs)) {
265
+ attrs.push(`${name}="${escapeAttr2(value)}"`);
266
+ }
267
+ const head = attrs.length > 0 ? `<${prefix}:${localName} ${attrs.join(" ")}>` : `<${prefix}:${localName}>`;
268
+ let body = "";
269
+ if (el.children) {
270
+ for (const [childName, list] of Object.entries(el.children)) {
271
+ for (const child of list) body += namespacedElementXml(prefix, childName, child);
272
+ }
273
+ } else if (el.text != null) {
274
+ body = escapeText(el.text);
275
+ }
276
+ return `${head}${body}</${prefix}:${localName}>`;
277
+ }
278
+ function metaAttrsXml(meta, namespaces) {
279
+ if (!meta) return "";
280
+ const parts = [];
281
+ for (const prefix of Object.keys(namespaces)) {
282
+ const bucket = meta[prefix];
283
+ if (!bucket?.attrs) continue;
284
+ for (const [name, value] of Object.entries(bucket.attrs)) {
285
+ parts.push(`${prefix}:${name}="${escapeAttr2(value)}"`);
286
+ }
287
+ }
288
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
289
+ }
290
+ function metaElementsXml(meta, namespaces) {
291
+ if (!meta) return "";
292
+ let out = "";
293
+ for (const prefix of Object.keys(namespaces)) {
294
+ const bucket = meta[prefix];
295
+ if (!bucket?.elements) continue;
296
+ for (const [localName, list] of Object.entries(bucket.elements)) {
297
+ for (const el of list) out += namespacedElementXml(prefix, localName, el);
298
+ }
299
+ }
300
+ return out;
301
+ }
302
+ function registerPaintServers(nodes, registry) {
303
+ for (const n of nodes) {
304
+ if (n.kind === "group") {
305
+ registerPaintServers(n.children, registry);
306
+ } else if (n.kind === "path") {
307
+ if (n.fill.kind === "gradient") registry.register(n.fill.paint);
308
+ if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
309
+ registerMarkers(n.stroke, registry);
310
+ } else if (n.kind === "text") {
311
+ registerTextPaint(n.fill, registry);
312
+ registerTextPaint(n.stroke?.paint, registry);
313
+ registerMarkers(n.stroke, registry);
314
+ for (const run of n.runs ?? []) {
315
+ registerTextPaint(run.fill, registry);
316
+ registerTextPaint(run.stroke?.paint, registry);
317
+ }
318
+ }
319
+ }
320
+ }
321
+ function registerTextPaint(paint, registry) {
322
+ if (paint && !("color" in paint)) registry.register(paint);
323
+ }
324
+ function markerKeyOfRef(ref) {
325
+ if (typeof ref === "string") return ref;
326
+ if (ref && typeof ref === "object" && "key" in ref) return String(ref.key);
327
+ return void 0;
328
+ }
329
+ function registerMarkers(stroke, registry) {
330
+ if (!stroke) return;
331
+ for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
332
+ const key = markerKeyOfRef(ref);
333
+ if (key) registry.markerId(key);
334
+ }
335
+ }
336
+ function nodeXml(node, registry, namespaces) {
337
+ if (node.kind === "group") return groupXml(node, registry, namespaces);
338
+ if (node.kind === "text") return textXml(node, registry, namespaces);
339
+ if (node.kind === "image") return imageXml(node, namespaces);
340
+ return pathXml(node, registry, namespaces);
341
+ }
342
+ function imageXml(node, namespaces) {
343
+ const attrs = [
344
+ `href="${escapeAttr2(node.href)}"`,
345
+ `x="${trimNumber(node.x)}"`,
346
+ `y="${trimNumber(node.y)}"`,
347
+ `width="${trimNumber(node.width)}"`,
348
+ `height="${trimNumber(node.height)}"`,
349
+ `preserveAspectRatio="none"`
350
+ ];
351
+ if (node.opacity != null && node.opacity !== 1) {
352
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
353
+ }
354
+ if (node.rotation != null && node.rotation !== 0) {
355
+ const cx = node.x + node.width / 2;
356
+ const cy = node.y + node.height / 2;
357
+ const deg = node.rotation * 180 / Math.PI;
358
+ attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
359
+ }
360
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
361
+ const metaEls = metaElementsXml(node.meta, namespaces);
362
+ if (metaEls) return `<image ${attrs.join(" ")}${metaAttrs}>${metaEls}</image>`;
363
+ return `<image ${attrs.join(" ")}${metaAttrs}/>`;
364
+ }
365
+ function groupXml(node, registry, namespaces) {
366
+ const attrs = [];
367
+ if (node.transform) {
368
+ const m2 = formatMatrix(node.transform);
369
+ if (m2) attrs.push(`transform="${m2}"`);
370
+ }
371
+ if (node.opacity != null && node.opacity !== 1) {
372
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
373
+ }
374
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
375
+ const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
376
+ const body = node.children.map((c2) => nodeXml(c2, registry, namespaces)).join("");
377
+ return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
378
+ }
379
+ function pathXml(node, registry, namespaces) {
380
+ const attrs = [`d="${serializePathD(node.path)}"`];
381
+ const fillAttrs = paintAttrs(node.fill, "fill", registry);
382
+ for (const a of fillAttrs) attrs.push(a);
383
+ if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
384
+ attrs.push(`fill-rule="evenodd"`);
385
+ }
386
+ if (node.stroke) {
387
+ const strokeAttrs = strokeAttrsFor(node.stroke, registry);
388
+ for (const a of strokeAttrs) attrs.push(a);
389
+ } else {
390
+ attrs.push('stroke="none"');
391
+ }
392
+ if (node.opacity != null && node.opacity !== 1) {
393
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
394
+ }
395
+ if (node.rotation != null && node.rotation !== 0) {
396
+ const b2 = pathBounds(node.path);
397
+ const cx = b2.minX + (b2.maxX - b2.minX) / 2;
398
+ const cy = b2.minY + (b2.maxY - b2.minY) / 2;
399
+ const deg = node.rotation * 180 / Math.PI;
400
+ attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
401
+ }
402
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
403
+ const metaEls = metaElementsXml(node.meta, namespaces);
404
+ if (metaEls) {
405
+ return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
406
+ }
407
+ return `<path ${attrs.join(" ")}${metaAttrs}/>`;
408
+ }
409
+ function paintAttrs(paint, name, registry, includeOpacity = true) {
410
+ if (paint.kind === "none") return [`${name}="none"`];
411
+ if (paint.kind === "solid") {
412
+ const out = [`${name}="${paint.color}"`];
413
+ if (includeOpacity && paint.opacity != null && paint.opacity !== 1) {
414
+ out.push(`${name}-opacity="${trimNumber(paint.opacity)}"`);
415
+ }
416
+ return out;
417
+ }
418
+ const id = registry.register(paint.paint);
419
+ return [`${name}="url(#${id})"`];
420
+ }
421
+ function coreStrokeAttrs(stroke, registry) {
422
+ if (!stroke) return [];
423
+ const width = resolveStrokeWidth(stroke.width ?? 1, 1);
424
+ if (!(width > 0)) return [];
425
+ const attrs = [];
426
+ if ("color" in stroke.paint) {
427
+ attrs.push(`stroke="${stroke.paint.color}"`);
428
+ if (stroke.paint.opacity != null && stroke.paint.opacity !== 1) {
429
+ attrs.push(`stroke-opacity="${trimNumber(stroke.paint.opacity)}"`);
430
+ }
431
+ } else {
432
+ attrs.push(`stroke="url(#${registry.register(stroke.paint)})"`);
433
+ }
434
+ attrs.push(`stroke-width="${trimNumber(width)}"`);
435
+ if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
436
+ if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
437
+ if (stroke.dash && stroke.dash.length > 0) {
438
+ attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
439
+ }
440
+ if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
441
+ for (const [field, attr] of [
442
+ ["markerStart", "marker-start"],
443
+ ["markerMid", "marker-mid"],
444
+ ["markerEnd", "marker-end"]
445
+ ]) {
446
+ const ref = stroke[field];
447
+ if (ref === void 0) continue;
448
+ const key = typeof ref === "string" ? ref : ref.key;
449
+ const id = registry.markerId(key);
450
+ if (id === void 0) continue;
451
+ attrs.push(`${attr}="url(#${id})"`);
452
+ }
453
+ return attrs;
454
+ }
455
+ function strokeAttrsFor(stroke, registry) {
456
+ const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
457
+ attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
458
+ const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
459
+ if (opacity != null && opacity !== 1) {
460
+ attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
461
+ }
462
+ if (stroke.cap) {
463
+ attrs.push(`stroke-linecap="${stroke.cap}"`);
464
+ }
465
+ if (stroke.join) {
466
+ attrs.push(`stroke-linejoin="${stroke.join}"`);
467
+ }
468
+ if (stroke.dash && stroke.dash.length > 0) {
469
+ attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
470
+ }
471
+ if (stroke.miterLimit != null) {
472
+ attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
473
+ }
474
+ for (const [field, attr] of [
475
+ ["markerStart", "marker-start"],
476
+ ["markerMid", "marker-mid"],
477
+ ["markerEnd", "marker-end"]
478
+ ]) {
479
+ const ref = stroke[field];
480
+ if (ref === void 0) continue;
481
+ const id = registry.markerId(ref);
482
+ if (id === void 0) continue;
483
+ attrs.push(`${attr}="url(#${id})"`);
484
+ }
485
+ return attrs;
486
+ }
487
+ function computeBounds(nodes) {
488
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
489
+ const visit = (n, ctm) => {
490
+ if (n.kind === "group") {
491
+ const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
492
+ n.children.forEach((c2) => visit(c2, childCtm));
493
+ return;
494
+ }
495
+ const b2 = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
496
+ const local = n.rotation ? rotateAboutCenter(ctm, b2, n.rotation) : ctm;
497
+ for (const [x2, y2] of corners(b2)) {
498
+ const px = local[0] * x2 + local[2] * y2 + local[4];
499
+ const py = local[1] * x2 + local[3] * y2 + local[5];
500
+ if (px < minX) minX = px;
501
+ if (py < minY) minY = py;
502
+ if (px > maxX) maxX = px;
503
+ if (py > maxY) maxY = py;
504
+ }
505
+ };
506
+ nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
507
+ if (!Number.isFinite(minX)) {
508
+ return { x: 0, y: 0, width: 0, height: 0 };
509
+ }
510
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
511
+ }
512
+ function corners(b2) {
513
+ return [[b2.minX, b2.minY], [b2.maxX, b2.minY], [b2.maxX, b2.maxY], [b2.minX, b2.maxY]];
514
+ }
515
+ function rotateAboutCenter(ctm, b2, angle) {
516
+ const cx = (b2.minX + b2.maxX) / 2;
517
+ const cy = (b2.minY + b2.maxY) / 2;
518
+ const c2 = Math.cos(angle);
519
+ const s = Math.sin(angle);
520
+ return multiply(ctm, [c2, s, -s, c2, cx - cx * c2 + cy * s, cy - cx * s - cy * c2]);
521
+ }
522
+ function textXml(node, registry, namespaces) {
523
+ const attrs = [
524
+ `x="${trimNumber(node.x)}"`,
525
+ `y="${trimNumber(node.y)}"`,
526
+ `dominant-baseline="text-before-edge"`,
527
+ // The runs model stores real line breaks; SVG's default whitespace
528
+ // handling would collapse them (and any indentation) away on re-import.
529
+ `xml:space="preserve"`,
530
+ `data-weasel-width="${trimNumber(node.width)}"`,
531
+ `data-weasel-height="${trimNumber(node.height)}"`
532
+ ];
533
+ const style = node.style;
534
+ if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(style.fontSize)}"`);
535
+ if (style?.fontFamily) attrs.push(`font-family="${escapeAttr2(style.fontFamily)}"`);
536
+ if (style?.fontWeight != null) attrs.push(`font-weight="${String(style.fontWeight)}"`);
537
+ if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
538
+ const direction = style?.direction ?? "ltr";
539
+ if (direction === "rtl") attrs.push('direction="rtl"');
540
+ if (style?.align != null || direction === "rtl") {
541
+ const align = style?.align ?? "left";
542
+ const anchor = align === "center" ? "middle" : align === "start" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
543
+ if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
544
+ }
545
+ if (style?.letterSpacing != null && style.letterSpacing !== 0) {
546
+ attrs.push(`letter-spacing="${trimNumber(style.letterSpacing)}"`);
547
+ }
548
+ const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
549
+ if (decoration) attrs.push(`text-decoration="${decoration}"`);
550
+ if (node.fill) {
551
+ if ("color" in node.fill) {
552
+ attrs.push(`fill="${node.fill.color}"`);
553
+ if (node.fill.opacity != null && node.fill.opacity !== 1) {
554
+ attrs.push(`fill-opacity="${trimNumber(node.fill.opacity)}"`);
555
+ }
556
+ } else {
557
+ const id = registry.register(node.fill);
558
+ attrs.push(`fill="url(#${id})"`);
559
+ }
560
+ }
561
+ for (const a of coreStrokeAttrs(node.stroke, registry)) attrs.push(a);
562
+ if (node.opacity != null && node.opacity !== 1) {
563
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
564
+ }
565
+ if (node.rotation != null && node.rotation !== 0) {
566
+ const cx = node.x + node.width / 2;
567
+ const cy = node.y + node.height / 2;
568
+ const deg = node.rotation * 180 / Math.PI;
569
+ attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
570
+ }
571
+ const body = node.runs && node.runs.length > 0 ? node.runs.map((r2) => runXml(r2, registry)).join("") : escapeText(node.text);
572
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
573
+ const metaEls = metaElementsXml(node.meta, namespaces);
574
+ return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
575
+ }
576
+ function runXml(run, registry) {
577
+ const attrs = [];
578
+ if (run.bold) attrs.push(`font-weight="700"`);
579
+ if (run.italic) attrs.push(`font-style="italic"`);
580
+ if (run.fontFamily) attrs.push(`font-family="${escapeAttr2(run.fontFamily)}"`);
581
+ const presetScale = run.script != null && run.baselineShift != null ? SCRIPT_METRICS[run.script].size : void 0;
582
+ const scale = run.fontScale ?? presetScale;
583
+ if (run.fontSize != null) attrs.push(`font-size="${trimNumber(run.fontSize)}"`);
584
+ else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
585
+ if (run.baselineShift != null) {
586
+ attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
587
+ } else if (run.script) {
588
+ attrs.push(`baseline-shift="${run.script}"`);
589
+ }
590
+ if (run.letterSpacing != null) {
591
+ attrs.push(`letter-spacing="${trimNumber(run.letterSpacing)}"`);
592
+ }
593
+ const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
594
+ if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
595
+ if (run.fill) {
596
+ if ("color" in run.fill) {
597
+ attrs.push(`fill="${run.fill.color}"`);
598
+ } else {
599
+ const id = registry.register(run.fill);
600
+ attrs.push(`fill="url(#${id})"`);
601
+ }
602
+ }
603
+ for (const a of coreStrokeAttrs(run.stroke, registry)) attrs.push(a);
604
+ const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
605
+ return `${head}${escapeText(run.text)}</tspan>`;
606
+ }
607
+ function textDecorationValue(underline, strikethrough, overline) {
608
+ const tokens = [];
609
+ if (underline) tokens.push("underline");
610
+ if (strikethrough) tokens.push("line-through");
611
+ if (overline) tokens.push("overline");
612
+ return tokens.length > 0 ? tokens.join(" ") : null;
613
+ }
614
+ function escapeAttr2(s) {
615
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
616
+ }
617
+ function escapeText(s) {
618
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
619
+ }
620
+ function pathBounds(path) {
621
+ if (path.kind === "rect") {
622
+ return {
623
+ minX: path.x,
624
+ minY: path.y,
625
+ maxX: path.x + path.width,
626
+ maxY: path.y + path.height
627
+ };
628
+ }
629
+ const b2 = boundsOfPath(path);
630
+ return { minX: b2.x, minY: b2.y, maxX: b2.x + b2.width, maxY: b2.y + b2.height };
631
+ }
632
+
633
+ // src/annotations/paint.ts
634
+ var MARK_COLOR = "#e5484d";
635
+ var MARK_WIDTH = 2;
636
+ var TEXT_SIZE = 14;
637
+ var toWorld = (p2, content) => ({
638
+ x: p2.x * content.w,
639
+ y: p2.y * content.h
640
+ });
641
+ function vertices(m2, content) {
642
+ const stored = m2.data.points;
643
+ if (stored && stored.length >= 2) return stored.map((p2) => toWorld(p2, content));
644
+ const { x: x2, y: y2, width, height } = m2.pose;
645
+ return [
646
+ { x: x2, y: y2 },
647
+ { x: x2 + width, y: y2 + height }
648
+ ];
649
+ }
650
+ function polyline(points) {
651
+ const b2 = new PathBuilder();
652
+ const first = points[0];
653
+ if (!first) return rectPath(0, 0, 0, 0);
654
+ b2.moveTo(first.x, first.y);
655
+ for (const p2 of points.slice(1)) b2.lineTo(p2.x, p2.y);
656
+ return b2.build();
657
+ }
658
+ function markCommands(m2, content, style = {}) {
659
+ const color = style.color ?? MARK_COLOR;
660
+ const stroke = {
661
+ paint: { color },
662
+ width: MARK_WIDTH,
663
+ cap: "round",
664
+ join: "round",
665
+ ...style.stale ? { dash: [6, 4] } : {}
666
+ };
667
+ switch (m2.data.kind) {
668
+ case "rect":
669
+ return [
670
+ { kind: "path", path: rectPath(m2.pose.x, m2.pose.y, m2.pose.width, m2.pose.height), stroke }
671
+ ];
672
+ case "ellipse":
673
+ return [{ kind: "path", path: ellipsePath(m2.pose), stroke }];
674
+ case "line": {
675
+ const [a, b2] = vertices(m2, content);
676
+ return [{ kind: "path", path: linePath(a, b2), stroke }];
677
+ }
678
+ case "arrow": {
679
+ const [a, b2] = vertices(m2, content);
680
+ const marked = { ...stroke, markerEnd: "arrow" };
681
+ const path = linePath(a, b2);
682
+ return [
683
+ { kind: "path", path, stroke: marked },
684
+ ...markerDrawCommands(path, marked, MARK_WIDTH, void 0)
685
+ ];
686
+ }
687
+ case "stroke":
688
+ return [{ kind: "path", path: polyline(vertices(m2, content)), stroke }];
689
+ case "text": {
690
+ const text = m2.data.title;
691
+ if (!text) return [];
692
+ return [
693
+ textCommand(
694
+ m2.pose.x,
695
+ m2.pose.y,
696
+ text,
697
+ { fontSize: TEXT_SIZE },
698
+ void 0,
699
+ void 0,
700
+ void 0,
701
+ {
702
+ fill: { color }
703
+ }
704
+ )
705
+ ];
706
+ }
707
+ }
708
+ }
709
+
710
+ // src/annotations/staleness.ts
711
+ function seenFrom(config, keys) {
712
+ const out = {};
713
+ if (config === null || typeof config !== "object") return out;
714
+ const src = config;
715
+ for (const k2 of keys) {
716
+ if (k2 in src) out[k2] = src[k2];
717
+ }
718
+ return out;
719
+ }
720
+ function isStale(seen, config, keys) {
721
+ if (!seen || keys.length === 0) return false;
722
+ if (config === null || typeof config !== "object") return false;
723
+ const src = config;
724
+ for (const k2 of keys) {
725
+ if (!(k2 in seen)) continue;
726
+ if (!Object.is(seen[k2], src[k2])) return true;
727
+ }
728
+ return false;
729
+ }
730
+
731
+ // src/annotations/drawOne.ts
732
+ function createMarkDrawOne(opts) {
733
+ return (node, pose) => markCommands({ pose, data: node.data }, opts.content, resolveMarkStyle(node.data, opts));
734
+ }
735
+ function resolveMarkStyle(data, opts) {
736
+ return {
737
+ color: opts.meaning?.statuses?.find((s) => s.id === data.status)?.color,
738
+ // Read off the node rather than through the store's `isStale`, which
739
+ // wants a projected Annotation this path does not have.
740
+ stale: isStale(data.seen, opts.config, opts.positionDependsOn ?? [])
741
+ };
742
+ }
743
+
744
+ // src/annotations/svgNodes.ts
745
+ var NO_PAINT = { kind: "none" };
746
+ function toSvgPaint(paint) {
747
+ if (!paint) return NO_PAINT;
748
+ if ("color" in paint && typeof paint.color === "string") {
749
+ return {
750
+ kind: "solid",
751
+ color: paint.color,
752
+ ...paint.opacity != null ? { opacity: paint.opacity } : {}
753
+ };
754
+ }
755
+ return { kind: "gradient", paint };
756
+ }
757
+ function toSvgStroke(stroke) {
758
+ return {
759
+ paint: toSvgPaint(stroke.paint),
760
+ width: resolveStrokeWidth(stroke.width ?? 1, 1),
761
+ ...stroke.cap ? { cap: stroke.cap } : {},
762
+ ...stroke.join ? { join: stroke.join } : {},
763
+ ...stroke.dash ? { dash: [...stroke.dash] } : {},
764
+ ...stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {}
765
+ };
766
+ }
767
+ function toSvgNode(cmd, m2) {
768
+ if (cmd.kind === "path") {
769
+ return {
770
+ kind: "path",
771
+ path: cmd.path,
772
+ fill: toSvgPaint(cmd.fill),
773
+ ...cmd.stroke ? { stroke: toSvgStroke(cmd.stroke) } : {}
774
+ };
775
+ }
776
+ if (cmd.kind === "text") {
777
+ return {
778
+ kind: "text",
779
+ x: cmd.x,
780
+ y: cmd.y,
781
+ width: m2.pose.width,
782
+ height: m2.pose.height,
783
+ text: cmd.runs.map((run) => run.text).join(""),
784
+ style: cmd.style,
785
+ ...cmd.runs[0]?.fill ? { fill: cmd.runs[0].fill } : {}
786
+ };
787
+ }
788
+ throw new Error(`[labkit] a mark drew a '${cmd.kind}' command, which has no vector form`);
789
+ }
790
+ function markSvgNodes(m2, content, style = {}) {
791
+ return markCommands(m2, content, style).map((cmd) => toSvgNode(cmd, m2));
792
+ }
793
+
794
+ // src/annotations/capture.ts
795
+ var SVG_NS = "http://www.w3.org/2000/svg";
796
+ function capturePlan(base, format) {
797
+ if (format === "svg") return "svg-document";
798
+ return base?.kind === "svg" ? "svg-document" : "raster-stack";
799
+ }
800
+ function nestBaseSvg(markup, w2, h) {
801
+ const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
802
+ const root = doc.documentElement;
803
+ if (doc.getElementsByTagName("parsererror").length > 0 || root.localName !== "svg") {
804
+ throw new Error("[labkit] a target's `base()` returned markup that is not an <svg> document");
805
+ }
806
+ root.setAttribute("x", "0");
807
+ root.setAttribute("y", "0");
808
+ root.setAttribute("width", String(w2));
809
+ root.setAttribute("height", String(h));
810
+ return new XMLSerializer().serializeToString(root);
811
+ }
812
+ function embedRasterBase(base, w2, h, onWarn) {
813
+ const href = base.kind === "canvas" ? base.canvas.toDataURL() : base.kind === "image" ? base.src : "";
814
+ if (base.kind === "image" && !href.startsWith("data:")) {
815
+ onWarn?.(`base image '${href}' is a reference, so this SVG is not self-contained`);
816
+ }
817
+ return serializeSvg([{ kind: "image", href, x: 0, y: 0, width: w2, height: h }], {
818
+ viewBox: { x: 0, y: 0, width: w2, height: h }
819
+ });
820
+ }
821
+ function markNodesOf(scene, draw) {
822
+ return [...scene.renderOrder()].flatMap((id) => {
823
+ const node = scene.get(asNodeId$1(String(id)));
824
+ if (!node) return [];
825
+ return markSvgNodes(
826
+ { pose: node.pose, data: node.data },
827
+ draw.content,
828
+ resolveMarkStyle(node.data, draw)
829
+ );
830
+ });
831
+ }
832
+ function composeCaptureSvg(args) {
833
+ const { base, scene, draw, scale, onWarn } = args;
834
+ const { w: w2, h } = draw.content;
835
+ const baseXml = base === void 0 ? "" : base.kind === "svg" ? nestBaseSvg(base.markup, w2, h) : embedRasterBase(base, w2, h, onWarn);
836
+ const marksXml = serializeSvg(markNodesOf(scene, draw), {
837
+ viewBox: { x: 0, y: 0, width: w2, height: h },
838
+ onWarn
839
+ });
840
+ const outW = Math.max(1, Math.round(w2 * scale));
841
+ const outH = Math.max(1, Math.round(h * scale));
842
+ return `<svg xmlns="${SVG_NS}" width="${outW}" height="${outH}" viewBox="0 0 ${w2} ${h}">${baseXml}${marksXml}</svg>`;
843
+ }
844
+ function context2d(width, height) {
845
+ const canvas = document.createElement("canvas");
846
+ canvas.width = width;
847
+ canvas.height = height;
848
+ const ctx = canvas.getContext("2d");
849
+ if (!ctx) throw new Error("[labkit] capture needs a 2D canvas context, and this one has none");
850
+ return ctx;
851
+ }
852
+ function toPngBlob(canvas) {
853
+ return new Promise((resolve, reject) => {
854
+ let done;
855
+ try {
856
+ done = (blob) => blob ? resolve(blob) : reject(new Error("[labkit] the browser produced no PNG"));
857
+ canvas.toBlob(done, "image/png");
858
+ } catch (err) {
859
+ reject(
860
+ new Error(
861
+ "[labkit] the capture canvas is tainted, so it cannot be read back \u2014 a base referencing a cross-origin image does this. Embed it as a data: URI.",
862
+ { cause: err }
863
+ )
864
+ );
865
+ }
866
+ });
867
+ }
868
+ function loadImage(src) {
869
+ return new Promise((resolve, reject) => {
870
+ const img = new Image();
871
+ img.onload = () => resolve(img);
872
+ img.onerror = () => reject(new Error(`[labkit] a capture base failed to load: ${src}`));
873
+ img.src = src;
874
+ });
875
+ }
876
+ async function rasterizeSvg(svg, width, height) {
877
+ const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
878
+ try {
879
+ const img = await loadImage(url);
880
+ const ctx = context2d(width, height);
881
+ ctx.drawImage(img, 0, 0, width, height);
882
+ return await toPngBlob(ctx.canvas);
883
+ } finally {
884
+ URL.revokeObjectURL(url);
885
+ }
886
+ }
887
+ async function stackRaster(args) {
888
+ const { base, scene, draw, scale, width, height } = args;
889
+ const ctx = context2d(width, height);
890
+ if (base?.kind === "image") ctx.drawImage(await loadImage(base.src), 0, 0, width, height);
891
+ else if (base?.kind === "canvas") ctx.drawImage(base.canvas, 0, 0, width, height);
892
+ const raster = renderSceneToPixels({
893
+ scene,
894
+ sourceRect: { x: 0, y: 0, width: draw.content.w, height: draw.content.h },
895
+ scale: { x: scale, y: scale },
896
+ drawOne: createMarkDrawOne(draw)
897
+ });
898
+ const marks = context2d(raster.width, raster.height);
899
+ marks.putImageData(
900
+ new ImageData(Uint8ClampedArray.from(raster.data), raster.width, raster.height),
901
+ 0,
902
+ 0
903
+ );
904
+ ctx.drawImage(marks.canvas, 0, 0, width, height);
905
+ return toPngBlob(ctx.canvas);
906
+ }
907
+ async function captureTarget(args, opts = {}) {
908
+ const format = opts.format ?? "png";
909
+ const scale = opts.scale ?? 2;
910
+ const { content } = args.draw;
911
+ const width = Math.max(1, Math.round(content.w * scale));
912
+ const height = Math.max(1, Math.round(content.h * scale));
913
+ const base = await args.base?.();
914
+ const compose = {
915
+ base,
916
+ scene: args.scene,
917
+ draw: args.draw,
918
+ scale,
919
+ onWarn: args.onWarn
920
+ };
921
+ if (capturePlan(base, format) === "raster-stack") {
922
+ return {
923
+ target: args.target,
924
+ blob: await stackRaster({ ...compose, width, height }),
925
+ format,
926
+ width,
927
+ height
928
+ };
929
+ }
930
+ const svg = composeCaptureSvg(compose);
931
+ const blob = format === "svg" ? new Blob([svg], { type: "image/svg+xml;charset=utf-8" }) : await rasterizeSvg(svg, width, height);
932
+ return { target: args.target, blob, format, width, height };
933
+ }
934
+
935
+ // src/annotations/frac.ts
936
+ function fracToWorld(f2, content) {
937
+ return {
938
+ x: f2.x * content.w,
939
+ y: f2.y * content.h,
940
+ width: f2.w * content.w,
941
+ height: f2.h * content.h
942
+ };
943
+ }
944
+ function worldToFrac(r2, content) {
945
+ const sx = content.w === 0 ? 0 : 1 / content.w;
946
+ const sy = content.h === 0 ? 0 : 1 / content.h;
947
+ return { x: r2.x * sx, y: r2.y * sy, w: r2.width * sx, h: r2.height * sy };
948
+ }
949
+ var DP = 1e4;
950
+ function roundFrac(f2) {
951
+ return {
952
+ x: Math.round(f2.x * DP) / DP,
953
+ y: Math.round(f2.y * DP) / DP,
954
+ w: Math.round(f2.w * DP) / DP,
955
+ h: Math.round(f2.h * DP) / DP
956
+ };
957
+ }
958
+ function fracContains(box, pt, tol = 0) {
959
+ return pt.x >= box.x - tol && pt.x <= box.x + box.w + tol && pt.y >= box.y - tol && pt.y <= box.y + box.h + tol;
960
+ }
961
+ function fracIntersects(outer, inner) {
962
+ return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.w <= outer.x + outer.w && inner.y + inner.h <= outer.y + outer.h;
963
+ }
964
+
965
+ // src/annotations/history.ts
966
+ var MarkHistory = class {
967
+ depth = /* @__PURE__ */ new Map();
968
+ undoable = [];
969
+ redoable = [];
970
+ /** Set while this class is driving, so a redo is not read as a new change. */
971
+ replaying = null;
972
+ /** Start following `scene` under `target`. Idempotent per target. */
973
+ track(target, scene) {
974
+ if (this.depth.has(target)) return;
975
+ this.depth.set(target, scene.historyIndex());
976
+ }
977
+ /** Call on every notification from `target`'s scene. */
978
+ observe(target, scene) {
979
+ const now = scene.historyIndex();
980
+ const before = this.depth.get(target) ?? 0;
981
+ this.depth.set(target, now);
982
+ if (now > before) {
983
+ if (this.replaying === "redo") {
984
+ drop(this.redoable, target);
985
+ } else {
986
+ this.redoable = [];
987
+ }
988
+ this.undoable.push(target);
989
+ } else if (now < before) {
990
+ drop(this.undoable, target);
991
+ this.redoable.push(target);
992
+ }
993
+ }
994
+ /** The target `undo` would act on, skipping any whose scene has nothing
995
+ * left — a scene can drop entries this ordering still lists. */
996
+ next(stack, can, at) {
997
+ for (let i = stack.length - 1; i >= 0; i--) {
998
+ const target = stack[i];
999
+ if (target === void 0) continue;
1000
+ const scene = at(target);
1001
+ if (scene && can(scene)) return target;
1002
+ }
1003
+ return null;
1004
+ }
1005
+ canUndo(at) {
1006
+ return this.next(this.undoable, (s) => s.canUndo(), at) !== null;
1007
+ }
1008
+ canRedo(at) {
1009
+ return this.next(this.redoable, (s) => s.canRedo(), at) !== null;
1010
+ }
1011
+ undo(at) {
1012
+ const target = this.next(this.undoable, (s) => s.canUndo(), at);
1013
+ const scene = target === null ? void 0 : at(target);
1014
+ if (!scene) return false;
1015
+ this.replaying = "undo";
1016
+ try {
1017
+ return scene.undo();
1018
+ } finally {
1019
+ this.replaying = null;
1020
+ }
1021
+ }
1022
+ redo(at) {
1023
+ const target = this.next(this.redoable, (s) => s.canRedo(), at);
1024
+ const scene = target === null ? void 0 : at(target);
1025
+ if (!scene) return false;
1026
+ this.replaying = "redo";
1027
+ try {
1028
+ return scene.redo();
1029
+ } finally {
1030
+ this.replaying = null;
1031
+ }
1032
+ }
1033
+ };
1034
+ function drop(stack, value) {
1035
+ for (let i = stack.length - 1; i >= 0; i--) {
1036
+ if (stack[i] === value) {
1037
+ stack.splice(i, 1);
1038
+ return;
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ // src/annotations/store.ts
1044
+ function createAnnotationScene() {
1045
+ return createScene({ systemLayers: [{ id: "marks" }] });
1046
+ }
1047
+ var NO_CONTENT = { w: 0, h: 0 };
1048
+ function splitId(id) {
1049
+ const at = id.lastIndexOf("/");
1050
+ return at <= 0 ? void 0 : { target: id.slice(0, at), node: id.slice(at + 1) };
1051
+ }
1052
+ function createAnnotationStore(opts) {
1053
+ const { targets, restore } = opts;
1054
+ const scenes = /* @__PURE__ */ new Map();
1055
+ const subs = /* @__PURE__ */ new Set();
1056
+ const history = new MarkHistory();
1057
+ const notify = () => {
1058
+ for (const fn of subs) fn();
1059
+ };
1060
+ const sceneFor = (target) => {
1061
+ const known = scenes.get(target);
1062
+ if (known) return known;
1063
+ const raw = restore?.[target];
1064
+ const scene = raw ? sceneFromJSON(
1065
+ raw,
1066
+ {}
1067
+ ) : createAnnotationScene();
1068
+ scenes.set(target, scene);
1069
+ history.track(target, scene);
1070
+ scene.subscribe(() => {
1071
+ history.observe(target, scene);
1072
+ notify();
1073
+ });
1074
+ return scene;
1075
+ };
1076
+ const historySceneAt = (target) => scenes.get(target);
1077
+ for (const target of Object.keys(restore ?? {})) sceneFor(target);
1078
+ const targetOf = (id) => targets().find((t) => t.id === id);
1079
+ const contentOf = (id) => targetOf(id)?.content ?? NO_CONTENT;
1080
+ const project = (target, node) => {
1081
+ const found = scenes.get(target)?.get(asNodeId$1(node));
1082
+ if (!found) return void 0;
1083
+ return {
1084
+ ...found.data,
1085
+ target,
1086
+ id: `${target}/${node}`,
1087
+ frac: roundFrac(worldToFrac(found.pose, contentOf(target)))
1088
+ };
1089
+ };
1090
+ const liveTargets = () => {
1091
+ const declared = targets().map((t) => t.id);
1092
+ const rest = [...scenes.keys()].filter((id) => !declared.includes(id));
1093
+ return [...declared, ...rest];
1094
+ };
1095
+ const marksOn = (target) => {
1096
+ const scene = scenes.get(target);
1097
+ if (!scene) return [];
1098
+ const out = [];
1099
+ for (const id of scene.renderOrder()) {
1100
+ const a = project(target, String(id));
1101
+ if (a) out.push(a);
1102
+ }
1103
+ return out;
1104
+ };
1105
+ const all = () => liveTargets().flatMap(marksOn);
1106
+ const matches = (a, q) => {
1107
+ if (q.target !== void 0 && a.target !== q.target) return false;
1108
+ if (q.kind !== void 0 && a.kind !== q.kind) return false;
1109
+ if (q.status !== void 0 && a.status !== q.status) return false;
1110
+ if (q.tags !== void 0) {
1111
+ const have = a.tags ?? [];
1112
+ for (const t of q.tags) if (!have.includes(t)) return false;
1113
+ }
1114
+ if (q.where && !q.where(a)) return false;
1115
+ return true;
1116
+ };
1117
+ const nodeOf = (id) => {
1118
+ const parts = splitId(id);
1119
+ if (!parts) return void 0;
1120
+ const scene = scenes.get(parts.target);
1121
+ const node = scene?.get(asNodeId$1(parts.node));
1122
+ return node && scene ? { ...parts, scene, node } : void 0;
1123
+ };
1124
+ return {
1125
+ sceneFor,
1126
+ targets,
1127
+ get(id) {
1128
+ const parts = splitId(id);
1129
+ return parts ? project(parts.target, parts.node) : void 0;
1130
+ },
1131
+ query(q) {
1132
+ return q ? all().filter((a) => matches(a, q)) : all();
1133
+ },
1134
+ hitTest(target, pt, tol = 0) {
1135
+ return marksOn(target).filter((a) => fracContains(a.frac, pt, tol)).reverse();
1136
+ },
1137
+ within(target, box) {
1138
+ return marksOn(target).filter((a) => fracIntersects(box, a.frac));
1139
+ },
1140
+ isStale(a, config) {
1141
+ const keys = targetOf(a.target)?.positionDependsOn ?? [];
1142
+ return isStale(a.seen, config, keys);
1143
+ },
1144
+ canUndo() {
1145
+ return history.canUndo(historySceneAt);
1146
+ },
1147
+ canRedo() {
1148
+ return history.canRedo(historySceneAt);
1149
+ },
1150
+ undo() {
1151
+ return history.undo(historySceneAt);
1152
+ },
1153
+ redo() {
1154
+ return history.redo(historySceneAt);
1155
+ },
1156
+ subscribe(fn) {
1157
+ subs.add(fn);
1158
+ return () => {
1159
+ subs.delete(fn);
1160
+ };
1161
+ },
1162
+ add(init, config) {
1163
+ const target = targetOf(init.target);
1164
+ const data = {
1165
+ target: init.target,
1166
+ kind: init.kind,
1167
+ ...init.points ? { points: init.points } : {},
1168
+ ...init.title !== void 0 ? { title: init.title } : {},
1169
+ ...init.status !== void 0 ? { status: init.status } : {},
1170
+ ...init.tags !== void 0 ? { tags: init.tags } : {},
1171
+ ...init.meta !== void 0 ? { meta: init.meta } : {},
1172
+ seen: seenFrom(config, target?.positionDependsOn ?? [])
1173
+ };
1174
+ const node = sceneFor(init.target).add({
1175
+ kind: "leaf",
1176
+ layer: "marks",
1177
+ pose: fracToWorld(roundFrac(init.frac), target?.content ?? NO_CONTENT),
1178
+ data
1179
+ });
1180
+ return `${init.target}/${String(node)}`;
1181
+ },
1182
+ update(id, patch) {
1183
+ const found = nodeOf(id);
1184
+ if (!found) return;
1185
+ const { frac, ...meaning } = patch;
1186
+ const nid = asNodeId$1(found.node.id);
1187
+ if (Object.keys(meaning).length > 0) {
1188
+ found.scene.update(nid, { data: { ...found.node.data, ...meaning } });
1189
+ }
1190
+ if (frac) {
1191
+ found.scene.setPose(nid, fracToWorld(roundFrac(frac), contentOf(found.target)));
1192
+ }
1193
+ },
1194
+ setMeta(id, meta) {
1195
+ const found = nodeOf(id);
1196
+ if (!found) return;
1197
+ found.scene.update(asNodeId$1(found.node.id), { data: { ...found.node.data, meta } });
1198
+ },
1199
+ remove(id) {
1200
+ const found = nodeOf(id);
1201
+ if (found) found.scene.remove(asNodeId$1(found.node.id));
1202
+ },
1203
+ async capture(target, captureOpts) {
1204
+ const info = targetOf(target);
1205
+ if (!info) throw new Error(`[labkit] no annotation target called '${target}'`);
1206
+ const result = await captureTarget(
1207
+ {
1208
+ target,
1209
+ scene: sceneFor(target),
1210
+ draw: {
1211
+ content: info.content,
1212
+ positionDependsOn: info.positionDependsOn,
1213
+ config: opts.config?.(),
1214
+ meaning: opts.meaning
1215
+ },
1216
+ base: info.base,
1217
+ onWarn: (message) => console.warn(`[labkit] capture: ${message}`)
1218
+ },
1219
+ captureOpts
1220
+ );
1221
+ opts.onCapture?.(result);
1222
+ return result;
1223
+ },
1224
+ toJSON() {
1225
+ const out = {};
1226
+ for (const [target, scene] of scenes) out[target] = scene.toJSON();
1227
+ return { version: 1, scenes: out };
1228
+ }
1229
+ };
1230
+ }
1231
+ function annotationsFromJSON(raw, targets, rest = {}) {
1232
+ const doc = raw;
1233
+ if (doc?.version !== 1 || !doc.scenes) return createAnnotationStore({ targets, ...rest });
1234
+ return createAnnotationStore({ targets, restore: doc.scenes, ...rest });
1235
+ }
1236
+
1237
+ // src/annotations/view.ts
1238
+ function toWeaselView(v) {
1239
+ return { x: v.pan.x, y: v.pan.y, scale: { x: v.zoom, y: v.zoom } };
1240
+ }
1241
+ function fromWeaselView(v) {
1242
+ return { zoom: v.scale.x, pan: { x: v.x, y: v.y } };
1243
+ }
1244
+ function fitView(content, pane) {
1245
+ const zoom = content.w > 0 && content.h > 0 ? Math.min(pane.w / content.w, pane.h / content.h) : 1;
1246
+ return { zoom: Number.isFinite(zoom) && zoom > 0 ? zoom : 1, pan: { x: 0, y: 0 } };
1247
+ }
1248
+ var sameRect = (a, b2) => a !== null && a.x === b2.x && a.y === b2.y && a.w === b2.w && a.h === b2.h;
1249
+ function ToolBridge({ toolId }) {
1250
+ const tools = useActiveToolContext();
1251
+ const setActive = tools.setActive;
1252
+ useEffect(() => {
1253
+ setActive(toolId);
1254
+ }, [toolId, setActive]);
1255
+ return null;
1256
+ }
1257
+ function pointsOf(extras, content) {
1258
+ const toFrac = (p2) => ({
1259
+ x: content.w === 0 ? 0 : p2.x / content.w,
1260
+ y: content.h === 0 ? 0 : p2.y / content.h
1261
+ });
1262
+ if (extras.kind === "line") {
1263
+ return [toFrac(extras.a), toFrac(extras.b)];
1264
+ }
1265
+ if (extras.kind === "pencil") {
1266
+ return extras.samples.map(toFrac);
1267
+ }
1268
+ return void 0;
1269
+ }
1270
+ function AnnotationOverlay({
1271
+ target,
1272
+ scene,
1273
+ config,
1274
+ activeToolId,
1275
+ meaning
1276
+ }) {
1277
+ const surface = useSurfaceOptional();
1278
+ const canvas = useSurfaceCanvas();
1279
+ const [rect, setRect] = useState(null);
1280
+ const [input, setInput] = useState(null);
1281
+ const sceneCanvas = useRef(null);
1282
+ const toolRef = useRef(activeToolId);
1283
+ toolRef.current = activeToolId;
1284
+ const configRef = useRef(config);
1285
+ configRef.current = config;
1286
+ const id = target.id;
1287
+ const lastTile = useRef(null);
1288
+ useEffect(() => {
1289
+ const el = target.ref.current ?? null;
1290
+ if (el === lastTile.current) return;
1291
+ lastTile.current = el;
1292
+ surface?.registerTile(id, el);
1293
+ });
1294
+ useEffect(
1295
+ () => () => {
1296
+ lastTile.current = null;
1297
+ surface?.registerTile(id, null);
1298
+ },
1299
+ [surface, id]
1300
+ );
1301
+ useEffect(
1302
+ () => surface?.registerPainter(id, (next) => {
1303
+ setRect((prev) => sameRect(prev, next) ? prev : next);
1304
+ sceneCanvas.current?.requestRedraw();
1305
+ }),
1306
+ [surface, id]
1307
+ );
1308
+ const container = surface?.getContainer() ?? null;
1309
+ if (!container || !rect) return null;
1310
+ const view = toWeaselView(target.view ?? fitView(target.content, { w: rect.w, h: rect.h }));
1311
+ const drawOne = createMarkDrawOne({
1312
+ content: target.content,
1313
+ positionDependsOn: target.positionDependsOn,
1314
+ config,
1315
+ meaning
1316
+ });
1317
+ const factories = {};
1318
+ for (const weaselTool of ANNOTATION_WEASEL_TOOLS) {
1319
+ factories[weaselTool] = (_bounds, extras) => {
1320
+ const info = annotationToolInfo(toolRef.current);
1321
+ const kind = info?.kind;
1322
+ if (!kind) return null;
1323
+ const points = pointsOf(extras, target.content);
1324
+ const data = {
1325
+ target: id,
1326
+ kind,
1327
+ ...points ? { points } : {},
1328
+ seen: seenFrom(configRef.current, target.positionDependsOn ?? [])
1329
+ };
1330
+ return { data };
1331
+ };
1332
+ }
1333
+ const box = {
1334
+ ["--lk-anno-x"]: `${rect.x}px`,
1335
+ ["--lk-anno-y"]: `${rect.y}px`,
1336
+ ["--lk-anno-w"]: `${rect.w}px`,
1337
+ ["--lk-anno-h"]: `${rect.h}px`
1338
+ };
1339
+ return createPortal(
1340
+ /* @__PURE__ */ jsxs(Fragment, { children: [
1341
+ /* @__PURE__ */ jsx(
1342
+ "div",
1343
+ {
1344
+ ref: setInput,
1345
+ className: "lk-annotate__input",
1346
+ "data-annotation-target": id,
1347
+ role: "application",
1348
+ "aria-label": `Annotations on ${id}`,
1349
+ tabIndex: 0,
1350
+ style: box
1351
+ }
1352
+ ),
1353
+ canvas && input ? (
1354
+ // One scope per pane: a shared <ActionsProvider> lets only the newest
1355
+ // canvas under it respond to input, and the rest go silently dead.
1356
+ /* @__PURE__ */ jsxs(WeaselProvider, { isolate: true, children: [
1357
+ /* @__PURE__ */ jsx(ToolBridge, { toolId: annotationToolInfo(activeToolId)?.weaselTool ?? "select" }),
1358
+ /* @__PURE__ */ jsx(
1359
+ SceneCanvas,
1360
+ {
1361
+ ref: sceneCanvas,
1362
+ scene,
1363
+ width: rect.w,
1364
+ height: rect.h,
1365
+ view,
1366
+ paintInto: { canvas, x: rect.x, y: rect.y },
1367
+ inputElement: input,
1368
+ defaultTools: ANNOTATION_WEASEL_TOOLS,
1369
+ insertNodeFactories: factories,
1370
+ layers: { scene: { drawOne } }
1371
+ }
1372
+ )
1373
+ ] })
1374
+ ) : null
1375
+ ] }),
1376
+ container
1377
+ );
1378
+ }
1379
+ function AnnotationTargets({
1380
+ capability,
1381
+ state,
1382
+ config,
1383
+ annotations,
1384
+ activeToolId
1385
+ }) {
1386
+ const targets = capability.targets(state, config);
1387
+ return /* @__PURE__ */ jsx(Fragment, { children: targets.map((t) => /* @__PURE__ */ jsx(
1388
+ AnnotationOverlay,
1389
+ {
1390
+ target: t,
1391
+ scene: annotations.sceneFor(t.id),
1392
+ meaning: capability.meaning,
1393
+ config,
1394
+ activeToolId
1395
+ },
1396
+ t.id
1397
+ )) });
1398
+ }
32
1399
  var PITCH_LIMIT = Math.PI / 2 - 0.01;
33
1400
  var YAW_PER_PX = 8e-3;
34
1401
  var PITCH_PER_PX = 8e-3;
@@ -287,9 +1654,9 @@ function applyRules(seed, ctx, rules) {
287
1654
  for (const rule of rules) {
288
1655
  const patch = rule({ ...ctx, leaf: acc });
289
1656
  if (patch === null) continue;
290
- for (const [k, v] of Object.entries(patch)) {
291
- if (v !== void 0 && acc[k] === void 0) {
292
- acc[k] = v;
1657
+ for (const [k2, v] of Object.entries(patch)) {
1658
+ if (v !== void 0 && acc[k2] === void 0) {
1659
+ acc[k2] = v;
293
1660
  }
294
1661
  }
295
1662
  }
@@ -299,7 +1666,7 @@ function applyRules(seed, ctx, rules) {
299
1666
  // src/config/resolve.ts
300
1667
  function defined(annotations) {
301
1668
  const out = {};
302
- for (const [k, v] of Object.entries(annotations)) if (v !== void 0) out[k] = v;
1669
+ for (const [k2, v] of Object.entries(annotations)) if (v !== void 0) out[k2] = v;
303
1670
  return out;
304
1671
  }
305
1672
  function resolveConfigSchema(schema, rules = []) {
@@ -950,21 +2317,21 @@ var weaselTheme = {
950
2317
  modes: THEME_SOURCES.weasel.modes
951
2318
  };
952
2319
  function hexToRgba(hex, alpha) {
953
- const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
954
- if (!m) throw new Error(`Expected a hex color, got "${hex}"`);
955
- const body = m[1];
956
- const full = body.length === 3 ? body.replace(/./g, (c) => c + c) : body;
2320
+ const m2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
2321
+ if (!m2) throw new Error(`Expected a hex color, got "${hex}"`);
2322
+ const body = m2[1];
2323
+ const full = body.length === 3 ? body.replace(/./g, (c2) => c2 + c2) : body;
957
2324
  const r2 = Number.parseInt(full.slice(0, 2), 16);
958
- const g = Number.parseInt(full.slice(2, 4), 16);
959
- const b = Number.parseInt(full.slice(4, 6), 16);
960
- return `rgba(${r2}, ${g}, ${b}, ${alpha})`;
2325
+ const g2 = Number.parseInt(full.slice(2, 4), 16);
2326
+ const b2 = Number.parseInt(full.slice(4, 6), 16);
2327
+ return `rgba(${r2}, ${g2}, ${b2}, ${alpha})`;
961
2328
  }
962
2329
  var REF = /^\{([^}]+)\}$/;
963
2330
  function refTarget(value) {
964
2331
  if (typeof value !== "string") return null;
965
- const m = REF.exec(value.trim());
966
- if (!m) return null;
967
- const path = m[1];
2332
+ const m2 = REF.exec(value.trim());
2333
+ if (!m2) return null;
2334
+ const path = m2[1];
968
2335
  const dot = path.indexOf(".");
969
2336
  return dot === -1 ? path : path.slice(dot + 1);
970
2337
  }
@@ -1103,9 +2470,9 @@ function flattenTokens(doc) {
1103
2470
  for (const [groupName, group] of Object.entries(doc)) {
1104
2471
  if (groupName.startsWith("$")) continue;
1105
2472
  if (typeof group !== "object" || group === null) continue;
1106
- const g = group;
1107
- const groupType = g.$type;
1108
- for (const [leaf, node] of Object.entries(g)) {
2473
+ const g2 = group;
2474
+ const groupType = g2.$type;
2475
+ for (const [leaf, node] of Object.entries(g2)) {
1109
2476
  if (leaf.startsWith("$")) continue;
1110
2477
  if (!isToken(node)) continue;
1111
2478
  const type = node.$type ?? groupType;
@@ -1210,134 +2577,6 @@ var interstellar_tokens_default = {
1210
2577
 
1211
2578
  // src/theme/interstellar.ts
1212
2579
  var interstellarTheme = loadDTCG(interstellar_tokens_default);
1213
- function DragGhost({ item, screenPos }) {
1214
- const [mounted, setMounted] = useState(false);
1215
- useEffect(() => setMounted(true), []);
1216
- if (!mounted || typeof document === "undefined") return null;
1217
- const style = {
1218
- left: `${screenPos.x + 12}px`,
1219
- top: `${screenPos.y + 12}px`
1220
- };
1221
- return createPortal(
1222
- /* @__PURE__ */ jsx("div", { className: "lk-drag-ghost", style, children: /* @__PURE__ */ jsx("span", { className: "lk-drag-ghost__label", children: item.label }) }),
1223
- document.body
1224
- );
1225
- }
1226
- function useDragDrop({
1227
- capability,
1228
- canvasContainerRef,
1229
- view,
1230
- state,
1231
- config,
1232
- setState,
1233
- emit
1234
- }) {
1235
- const [drag, setDrag] = useState(null);
1236
- const dragRef = useRef(null);
1237
- dragRef.current = drag;
1238
- const pendingPos = useRef(null);
1239
- const isOverCanvas = useCallback(
1240
- (screenPos) => {
1241
- const el = canvasContainerRef.current;
1242
- if (!el) return false;
1243
- const r2 = el.getBoundingClientRect();
1244
- return screenPos.x >= r2.left && screenPos.x <= r2.right && screenPos.y >= r2.top && screenPos.y <= r2.bottom;
1245
- },
1246
- [canvasContainerRef]
1247
- );
1248
- const screenToWorldFromContainer = useCallback(
1249
- (screenPos) => {
1250
- const el = canvasContainerRef.current;
1251
- if (!el) return null;
1252
- const r2 = el.getBoundingClientRect();
1253
- return screenToWorld({ x: screenPos.x - r2.left, y: screenPos.y - r2.top }, view);
1254
- },
1255
- [canvasContainerRef, view]
1256
- );
1257
- const frameLoop = useVisibleRaf(() => {
1258
- const screenPos = pendingPos.current;
1259
- pendingPos.current = null;
1260
- const active = dragRef.current;
1261
- if (!active || !screenPos) return;
1262
- let feedback = null;
1263
- if (capability.onDragOver && isOverCanvas(screenPos)) {
1264
- const world = screenToWorldFromContainer(screenPos);
1265
- if (world) feedback = capability.onDragOver(world, active.item, state, config);
1266
- }
1267
- setDrag({ ...active, screenPos, feedback });
1268
- });
1269
- useEffect(() => {
1270
- if (!drag) return;
1271
- const handleMove = (e) => {
1272
- const screenPos = { x: e.clientX, y: e.clientY };
1273
- const current = dragRef.current;
1274
- if (!current) return;
1275
- if (pendingPos.current !== null) {
1276
- pendingPos.current = screenPos;
1277
- dragRef.current = { ...current, screenPos };
1278
- setDrag(dragRef.current);
1279
- return;
1280
- }
1281
- pendingPos.current = screenPos;
1282
- frameLoop.request();
1283
- };
1284
- const handleUp = (e) => {
1285
- const screenPos = { x: e.clientX, y: e.clientY };
1286
- const active = dragRef.current;
1287
- if (!active) return;
1288
- pendingPos.current = null;
1289
- frameLoop.cancel();
1290
- if (isOverCanvas(screenPos)) {
1291
- const world = screenToWorldFromContainer(screenPos);
1292
- if (world) {
1293
- const nextState = capability.onDrop(world, active.item, state, config);
1294
- setState(nextState);
1295
- emit("canvas.itemAdded");
1296
- }
1297
- }
1298
- setDrag(null);
1299
- };
1300
- window.addEventListener("pointermove", handleMove);
1301
- window.addEventListener("pointerup", handleUp);
1302
- return () => {
1303
- window.removeEventListener("pointermove", handleMove);
1304
- window.removeEventListener("pointerup", handleUp);
1305
- };
1306
- }, [
1307
- drag,
1308
- capability,
1309
- state,
1310
- config,
1311
- setState,
1312
- emit,
1313
- frameLoop,
1314
- isOverCanvas,
1315
- screenToWorldFromContainer
1316
- ]);
1317
- const startDrag = useCallback((item, originScreenPos) => {
1318
- setDrag({ item, screenPos: originScreenPos, feedback: null });
1319
- }, []);
1320
- return { drag, startDrag };
1321
- }
1322
- function DragOverlay({ drag }) {
1323
- if (!drag) return null;
1324
- return /* @__PURE__ */ jsx(DragGhost, { item: drag.item, screenPos: drag.screenPos });
1325
- }
1326
- function Palette({ items, onDragStart, className }) {
1327
- return /* @__PURE__ */ jsx("div", { className: className ? `lk-palette ${className}` : "lk-palette", children: items.map((item) => /* @__PURE__ */ jsx(
1328
- "button",
1329
- {
1330
- type: "button",
1331
- className: "lk-palette__item",
1332
- onPointerDown: (e) => {
1333
- e.preventDefault();
1334
- onDragStart(item, { x: e.clientX, y: e.clientY });
1335
- },
1336
- children: /* @__PURE__ */ jsx("span", { className: "lk-palette__label", children: item.label })
1337
- },
1338
- item.id
1339
- )) });
1340
- }
1341
2580
 
1342
2581
  // src/state/view.ts
1343
2582
  var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
@@ -1369,6 +2608,69 @@ function TrialTitleBar({ title, children }) {
1369
2608
  children
1370
2609
  ] });
1371
2610
  }
2611
+ function createPanelHostRegistry() {
2612
+ const hosts = /* @__PURE__ */ new Map();
2613
+ const listeners = /* @__PURE__ */ new Set();
2614
+ const notify = () => {
2615
+ for (const fn of listeners) fn();
2616
+ };
2617
+ return {
2618
+ set: (key, el) => {
2619
+ if (el) hosts.set(key, el);
2620
+ else hosts.delete(key);
2621
+ notify();
2622
+ },
2623
+ get: (key) => hosts.get(key) ?? null,
2624
+ subscribe: (fn) => {
2625
+ listeners.add(fn);
2626
+ return () => listeners.delete(fn);
2627
+ }
2628
+ };
2629
+ }
2630
+ var PanelHostContext = createContext(null);
2631
+ function usePanelHosts() {
2632
+ return useContext(PanelHostContext);
2633
+ }
2634
+ function Portalled({
2635
+ trialId,
2636
+ contribution,
2637
+ onDock
2638
+ }) {
2639
+ const hosts = usePanelHosts();
2640
+ const key = panelKey(trialId, contribution.id);
2641
+ const host = useSyncExternalStore(
2642
+ (fn) => hosts?.subscribe(fn) ?? (() => {
2643
+ }),
2644
+ () => hosts?.get(key) ?? null,
2645
+ () => null
2646
+ );
2647
+ if (!host || !contribution.item) return null;
2648
+ const item = contribution.item;
2649
+ return createPortal(
2650
+ /* @__PURE__ */ jsxs(Fragment, { children: [
2651
+ /* @__PURE__ */ jsxs("div", { className: "lk-panel-tile__titlebar", children: [
2652
+ /* @__PURE__ */ jsx("span", { className: "lk-panel-tile__title", children: item.title }),
2653
+ /* @__PURE__ */ jsx(
2654
+ "button",
2655
+ {
2656
+ type: "button",
2657
+ className: "lk-panel-tile__dock",
2658
+ "aria-label": `Dock ${item.title}`,
2659
+ title: `Dock ${item.title}`,
2660
+ onClick: () => onDock(contribution.id),
2661
+ children: "\u21E4"
2662
+ }
2663
+ )
2664
+ ] }),
2665
+ item.body
2666
+ ] }),
2667
+ host
2668
+ );
2669
+ }
2670
+ function UndockedSections({ trialId, sections, onDock }) {
2671
+ if (sections.length === 0) return null;
2672
+ return /* @__PURE__ */ jsx(Fragment, { children: sections.map((c2) => /* @__PURE__ */ jsx(Portalled, { trialId, contribution: c2, onDock }, c2.id)) });
2673
+ }
1372
2674
  var NO_OP = () => {
1373
2675
  };
1374
2676
  function TrialChrome({
@@ -1378,6 +2680,7 @@ function TrialChrome({
1378
2680
  isLastTrial,
1379
2681
  undoBindings,
1380
2682
  job,
2683
+ loupe,
1381
2684
  trialChrome,
1382
2685
  chrome,
1383
2686
  suppress,
@@ -1391,6 +2694,13 @@ function TrialChrome({
1391
2694
  const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
1392
2695
  const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1393
2696
  const updateTrialState = useStore(storeCtx.store, (s) => s.updateTrialState);
2697
+ const undockPanelAction = useStore(storeCtx.store, (s) => s.undockPanel);
2698
+ const dockPanelAction = useStore(storeCtx.store, (s) => s.dockPanel);
2699
+ const undockedPanels = useStore(storeCtx.store, (s) => s.undockedPanels);
2700
+ const undockedIds = useMemo(
2701
+ () => Object.values(undockedPanels).filter((p2) => p2.trialId === trialId).map((p2) => p2.sectionId),
2702
+ [undockedPanels, trialId]
2703
+ );
1394
2704
  const view2d = as2DView(record.view);
1395
2705
  const configSchema = useConfigSchema(instrument);
1396
2706
  const ctx = useMemo(() => {
@@ -1408,6 +2718,8 @@ function TrialChrome({
1408
2718
  canRedo: undoBindings?.canRedo ?? false,
1409
2719
  undo: undoBindings?.undo ?? NO_OP,
1410
2720
  redo: undoBindings?.redo ?? NO_OP,
2721
+ loupeOn: loupe?.on ?? false,
2722
+ toggleLoupe: loupe?.toggle ?? NO_OP,
1411
2723
  configSchema,
1412
2724
  configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
1413
2725
  config: record.config,
@@ -1425,6 +2737,9 @@ function TrialChrome({
1425
2737
  updateTrialState(trialId, nextState);
1426
2738
  }
1427
2739
  },
2740
+ undockedPanels: undockedIds,
2741
+ undockPanel: (sectionId, as) => undockPanelAction(trialId, sectionId, as),
2742
+ dockPanel: (sectionId) => dockPanelAction(trialId, sectionId),
1428
2743
  savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1429
2744
  saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1430
2745
  loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
@@ -1444,10 +2759,14 @@ function TrialChrome({
1444
2759
  updateTrialConfig,
1445
2760
  updateTrialState,
1446
2761
  undoBindings,
2762
+ loupe,
1447
2763
  view2d,
1448
2764
  activeToolId,
1449
2765
  setActiveTool,
1450
- configSchema
2766
+ configSchema,
2767
+ undockedIds,
2768
+ undockPanelAction,
2769
+ dockPanelAction
1451
2770
  ]);
1452
2771
  const contributions = useMemo(
1453
2772
  () => suppressContributions(
@@ -1461,7 +2780,7 @@ function TrialChrome({
1461
2780
  ),
1462
2781
  [instrument, ctx, trialChrome, chrome, suppress, lab.controls]
1463
2782
  );
1464
- const inRegion = (region) => contributions.filter((c) => c.region === region);
2783
+ const inRegion = (region) => contributions.filter((c2) => c2.region === region);
1465
2784
  const handleKeyDown = (e) => {
1466
2785
  const mod = e.metaKey || e.ctrlKey;
1467
2786
  if (!mod) return;
@@ -1486,7 +2805,17 @@ function TrialChrome({
1486
2805
  /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
1487
2806
  /* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
1488
2807
  /* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
1489
- /* @__PURE__ */ jsx("div", { className: "lk-trial__sidebar", children: /* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }) }),
2808
+ /* @__PURE__ */ jsxs("div", { className: "lk-trial__sidebar", children: [
2809
+ /* @__PURE__ */ jsx(SidebarRegion, { contributions: inRegion("sidebar"), ctx }),
2810
+ /* @__PURE__ */ jsx(
2811
+ UndockedSections,
2812
+ {
2813
+ trialId,
2814
+ sections: inRegion("sidebar").filter((c2) => undockedIds.includes(c2.id)),
2815
+ onDock: ctx.dockPanel
2816
+ }
2817
+ )
2818
+ ] }),
1490
2819
  /* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
1491
2820
  children,
1492
2821
  /* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
@@ -1504,7 +2833,7 @@ function Trial({ id, chrome, suppress }) {
1504
2833
  const lab = useLabContext();
1505
2834
  const storeCtx = useContext(LabStoreContext);
1506
2835
  if (!storeCtx) throw new Error("[labkit] <Trial> requires <LabStoreProvider>");
1507
- const record = useStore(storeCtx.store, (s) => s.trials.find((w) => w.id === id));
2836
+ const record = useStore(storeCtx.store, (s) => s.trials.find((w2) => w2.id === id));
1508
2837
  if (!record) {
1509
2838
  return /* @__PURE__ */ jsxs("div", { className: "lk-trial lk-trial--unknown", children: [
1510
2839
  "Trial not found: ",
@@ -1532,10 +2861,12 @@ function Trial({ id, chrome, suppress }) {
1532
2861
  }
1533
2862
  function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1534
2863
  const canvasContainerRef = useRef(null);
2864
+ const loupeHostRef = useRef(null);
1535
2865
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
1536
2866
  const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
1537
2867
  const updateTrialView = useStore(store, (s) => s.updateTrialView);
1538
2868
  const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
2869
+ const updateTrialAnnotations = useStore(store, (s) => s.updateTrialAnnotations);
1539
2870
  const labToolId = useStore(store, (s) => s.activeToolId);
1540
2871
  const setLabTool = useStore(store, (s) => s.setLabTool);
1541
2872
  const setTrialTool = useStore(store, (s) => s.setTrialTool);
@@ -1544,19 +2875,85 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1544
2875
  const bus = busRef.current;
1545
2876
  const [layerVisibility, setLayerVisibility] = useState({});
1546
2877
  const [layerOrder, setLayerOrder] = useState(null);
2878
+ const [loupeOn, setLoupeOn] = useState(false);
2879
+ const visibleLayers = useMemo(
2880
+ () => (instrument.canvas?.layers ?? []).filter((l) => layerVisibility[l.id] !== false).map((l) => l.id),
2881
+ [instrument.canvas, layerVisibility]
2882
+ );
1547
2883
  const undoCap = instrument.undo;
1548
2884
  const undoEvents = useMemo(() => new Set(undoCap?.snapshotOn ?? ["state.change"]), [undoCap]);
1549
2885
  const maxDepth = undoCap?.maxDepth ?? 50;
1550
2886
  const snapshotIfNeeded = (event) => {
1551
2887
  if (!undoCap || !undoEvents.has(event)) return;
1552
- const current = store.getState().trials.find((w) => w.id === record.id);
2888
+ const current = store.getState().trials.find((w2) => w2.id === record.id);
1553
2889
  if (!current) return;
1554
2890
  const snap = structuredClone(current.state);
1555
2891
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1556
2892
  };
1557
2893
  const setView = (v) => updateTrialView(record.id, v);
1558
- const declaresTools = instrument.tools != null;
1559
- const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? null : labToolId;
2894
+ const placeView = (size) => {
2895
+ const declared = instrument.canvas?.initialView;
2896
+ if (typeof declared !== "function") return;
2897
+ if (record.view != null) return;
2898
+ setView(declared(size));
2899
+ };
2900
+ const annotationsCap = instrument.annotations;
2901
+ const targetsRef = useRef(() => []);
2902
+ targetsRef.current = () => annotationsCap ? annotationsCap.targets(record.state, record.config) : [];
2903
+ const annotationsRef = useRef(null);
2904
+ const configRef = useRef(record.config);
2905
+ configRef.current = record.config;
2906
+ if (annotationsRef.current === null) {
2907
+ const kept = annotationsCap?.storage ? annotationsCap.storage.load() : record.annotations;
2908
+ annotationsRef.current = annotationsFromJSON(kept, () => targetsRef.current(), {
2909
+ meaning: annotationsCap?.meaning,
2910
+ config: () => configRef.current,
2911
+ onCapture: (result) => annotationsCap?.onCapture?.(result)
2912
+ });
2913
+ }
2914
+ const annotations = annotationsRef.current;
2915
+ const saveCap = annotationsCap?.storage;
2916
+ const saveRef = useRef(() => {
2917
+ });
2918
+ saveRef.current = (doc) => {
2919
+ if (saveCap) saveCap.save(doc);
2920
+ else updateTrialAnnotations(record.id, doc);
2921
+ };
2922
+ const [markMoves, setMarkMoves] = useState({ undo: false, redo: false });
2923
+ useEffect(() => {
2924
+ if (!annotationsCap) return;
2925
+ const read = () => {
2926
+ const next = { undo: annotations.canUndo(), redo: annotations.canRedo() };
2927
+ setMarkMoves((prev) => prev.undo === next.undo && prev.redo === next.redo ? prev : next);
2928
+ };
2929
+ read();
2930
+ return annotations.subscribe(read);
2931
+ }, [annotations, annotationsCap]);
2932
+ useEffect(() => {
2933
+ if (!annotationsCap) return;
2934
+ let timer = null;
2935
+ let dirty = false;
2936
+ const flush = () => {
2937
+ if (!dirty) return;
2938
+ dirty = false;
2939
+ saveRef.current(annotations.toJSON());
2940
+ };
2941
+ const off = annotations.subscribe(() => {
2942
+ dirty = true;
2943
+ if (timer) clearTimeout(timer);
2944
+ timer = setTimeout(() => {
2945
+ timer = null;
2946
+ flush();
2947
+ }, 250);
2948
+ });
2949
+ return () => {
2950
+ off();
2951
+ if (timer) clearTimeout(timer);
2952
+ flush();
2953
+ };
2954
+ }, [annotations, annotationsCap]);
2955
+ const declaresTools = instrument.tools != null || instrument.annotations != null;
2956
+ const resolvedToolId = declaresTools ? record.activeToolId ?? instrument.tools?.initial ?? instrument.tools?.tools[0]?.id ?? (instrument.annotations ? "select" : null) : labToolId;
1560
2957
  const setActiveTool = (id) => {
1561
2958
  if (declaresTools) setTrialTool(record.id, id);
1562
2959
  else setLabTool(id);
@@ -1595,7 +2992,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1595
2992
  if (!view2d) return;
1596
2993
  updateTrialView(record.id, { ...view2d, zoom: z });
1597
2994
  },
1598
- activeToolId: resolvedToolId
2995
+ activeToolId: resolvedToolId,
2996
+ visibleLayers
1599
2997
  },
1600
2998
  emit: (event) => {
1601
2999
  snapshotIfNeeded(event);
@@ -1603,32 +3001,45 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1603
3001
  },
1604
3002
  job: jobCap ? job : void 0
1605
3003
  };
1606
- const undoBindings = undoCap ? {
1607
- canUndo: record.undoStack.past.length > 0,
1608
- canRedo: record.undoStack.future.length > 0,
3004
+ const undoState = () => {
3005
+ const result = undo(record.undoStack, structuredClone(record.state));
3006
+ if (!result) return;
3007
+ updateTrialState(record.id, result.snapshot);
3008
+ updateTrialUndoStack(record.id, result.stack);
3009
+ };
3010
+ const redoState = () => {
3011
+ const result = redo(record.undoStack, structuredClone(record.state));
3012
+ if (!result) return;
3013
+ updateTrialState(record.id, result.snapshot);
3014
+ updateTrialUndoStack(record.id, result.stack);
3015
+ };
3016
+ const undoBindings = undoCap || annotationsCap ? {
3017
+ canUndo: markMoves.undo || (undoCap ? record.undoStack.past.length > 0 : false),
3018
+ canRedo: markMoves.redo || (undoCap ? record.undoStack.future.length > 0 : false),
1609
3019
  undo: () => {
1610
- const result = undo(record.undoStack, structuredClone(record.state));
1611
- if (!result) return;
1612
- updateTrialState(record.id, result.snapshot);
1613
- updateTrialUndoStack(record.id, result.stack);
3020
+ if (annotationsCap && annotations.undo()) return;
3021
+ if (undoCap) undoState();
1614
3022
  },
1615
3023
  redo: () => {
1616
- const result = redo(record.undoStack, structuredClone(record.state));
1617
- if (!result) return;
1618
- updateTrialState(record.id, result.snapshot);
1619
- updateTrialUndoStack(record.id, result.stack);
3024
+ if (annotationsCap && annotations.redo()) return;
3025
+ if (undoCap) redoState();
1620
3026
  }
1621
3027
  } : void 0;
3028
+ const loupeCap = useMemo(() => {
3029
+ const declared = instrument.loupe;
3030
+ if (declared == null) return null;
3031
+ return resolveLoupe(typeof declared === "function" ? declared(record.config) : declared);
3032
+ }, [instrument.loupe, record.config]);
3033
+ const loupeBindings = loupeCap ? { on: loupeOn, toggle: () => setLoupeOn((v) => !v) } : void 0;
1622
3034
  const canvasLayers = useMemo(() => {
1623
3035
  if (!instrument.canvas) return [];
1624
3036
  const baseLayers = instrument.canvas.layers;
1625
- const ordered = layerOrder ? [...baseLayers].sort((a, b) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b.id)) : baseLayers;
3037
+ const ordered = layerOrder ? [...baseLayers].sort((a, b2) => layerOrder.indexOf(a.id) - layerOrder.indexOf(b2.id)) : baseLayers;
1626
3038
  return ordered.map((layer) => ({
1627
3039
  id: layer.id,
1628
3040
  visible: layerVisibility[layer.id] !== false,
1629
- render: (ctx, view) => {
1630
- ctx.translate(view.pan.x, view.pan.y);
1631
- ctx.scale(view.zoom, view.zoom);
3041
+ render: (ctx, view, frame) => {
3042
+ applyCamera(ctx, view, frame);
1632
3043
  layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
1633
3044
  }
1634
3045
  }));
@@ -1641,6 +3052,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1641
3052
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
1642
3053
  canvasContainerRef,
1643
3054
  view: view2d ?? DEFAULT_VIEW,
3055
+ worldSpec: instrument.canvas?.worldSpec,
1644
3056
  state: record.state,
1645
3057
  config: record.config,
1646
3058
  setState: (next) => {
@@ -1654,8 +3066,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1654
3066
  });
1655
3067
  const paletteItems = useMemo(() => {
1656
3068
  if (!instrument.dragDrop) return [];
1657
- const p3 = instrument.dragDrop.palette;
1658
- return typeof p3 === "function" ? p3(record.state, record.config) : p3;
3069
+ const p2 = instrument.dragDrop.palette;
3070
+ return typeof p2 === "function" ? p2(record.state, record.config) : p2;
1659
3071
  }, [instrument.dragDrop, record.state, record.config]);
1660
3072
  const layersWithFeedback = useMemo(() => {
1661
3073
  if (!dragDropResult.drag?.feedback) return canvasLayers;
@@ -1681,22 +3093,44 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1681
3093
  }
1682
3094
  ];
1683
3095
  }, [canvasLayers, dragDropResult.drag]);
3096
+ const lens = loupeCap ? /* @__PURE__ */ jsx(
3097
+ TrialLoupe,
3098
+ {
3099
+ capability: loupeCap,
3100
+ enabled: loupeOn,
3101
+ state: record.state,
3102
+ config: record.config,
3103
+ view: view2d ?? DEFAULT_VIEW,
3104
+ worldSpec: instrument.canvas?.worldSpec,
3105
+ hostRef: loupeHostRef
3106
+ }
3107
+ ) : null;
1684
3108
  let body;
1685
3109
  if (instrument.canvas) {
1686
3110
  body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
1687
- /* @__PURE__ */ jsx(
3111
+ /* @__PURE__ */ jsxs(
1688
3112
  CanvasStack,
1689
3113
  {
1690
3114
  layers: layersWithFeedback,
1691
3115
  view: view2d ?? DEFAULT_VIEW,
1692
3116
  onViewChange: setView,
3117
+ worldSpec: instrument.canvas.worldSpec,
3118
+ onResize: placeView,
1693
3119
  minZoom: instrument.canvas.minZoom,
1694
3120
  maxZoom: instrument.canvas.maxZoom,
1695
- children: instrument.render(renderCtx)
3121
+ children: [
3122
+ instrument.render(renderCtx),
3123
+ lens
3124
+ ]
1696
3125
  }
1697
3126
  ),
1698
3127
  /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1699
3128
  ] });
3129
+ } else if (lens) {
3130
+ body = /* @__PURE__ */ jsxs("div", { ref: loupeHostRef, className: "lk-trial__loupe-host", children: [
3131
+ instrument.render(renderCtx),
3132
+ lens
3133
+ ] });
1700
3134
  } else {
1701
3135
  body = instrument.render(renderCtx);
1702
3136
  }
@@ -1741,10 +3175,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1741
3175
  }
1742
3176
  return out;
1743
3177
  }, [paletteNode, layerListNode]);
1744
- return /* @__PURE__ */ jsx(
3178
+ const annotationOverlays = annotationsCap ? /* @__PURE__ */ jsx(
3179
+ AnnotationTargets,
3180
+ {
3181
+ capability: annotationsCap,
3182
+ state: record.state,
3183
+ config: record.config,
3184
+ annotations,
3185
+ activeToolId: resolvedToolId
3186
+ }
3187
+ ) : null;
3188
+ return /* @__PURE__ */ jsx(TrialIdProvider, { trialId: record.id, children: /* @__PURE__ */ jsx(AnnotationsContext.Provider, { value: annotationsCap ? annotations : null, children: /* @__PURE__ */ jsxs(
1745
3189
  TrialChrome,
1746
3190
  {
1747
3191
  job: jobCap ? job : void 0,
3192
+ loupe: loupeBindings,
1748
3193
  trialId: record.id,
1749
3194
  record,
1750
3195
  instrument,
@@ -1755,9 +3200,12 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1755
3200
  suppress,
1756
3201
  activeToolId: resolvedToolId,
1757
3202
  setActiveTool,
1758
- children: body
3203
+ children: [
3204
+ body,
3205
+ annotationOverlays
3206
+ ]
1759
3207
  }
1760
- );
3208
+ ) }) });
1761
3209
  }
1762
3210
 
1763
3211
  // src/trial/trialOps.ts
@@ -1770,7 +3218,9 @@ function findInstrument(instruments, name) {
1770
3218
  return found;
1771
3219
  }
1772
3220
  function initialView(instrument) {
1773
- return instrument.canvas?.initialView ? structuredClone(instrument.canvas.initialView) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
3221
+ const declared = instrument.canvas?.initialView;
3222
+ if (typeof declared === "function") return null;
3223
+ return declared ? structuredClone(declared) : { ...DEFAULT_VIEW2, pan: { ...DEFAULT_VIEW2.pan } };
1774
3224
  }
1775
3225
  function addTrial(trials, instruments, instrumentName) {
1776
3226
  const instrument = findInstrument(instruments, instrumentName);
@@ -1787,7 +3237,7 @@ function addTrial(trials, instruments, instrumentName) {
1787
3237
  return [...trials, record];
1788
3238
  }
1789
3239
  function cloneTrial(trials, id) {
1790
- const sourceIdx = trials.findIndex((w) => w.id === id);
3240
+ const sourceIdx = trials.findIndex((w2) => w2.id === id);
1791
3241
  const source = trials[sourceIdx];
1792
3242
  if (!source) return trials;
1793
3243
  const clone = {
@@ -1802,11 +3252,11 @@ function cloneTrial(trials, id) {
1802
3252
  }
1803
3253
  function closeTrial(trials, id) {
1804
3254
  if (trials.length <= 1) return trials;
1805
- const next = trials.filter((w) => w.id !== id);
3255
+ const next = trials.filter((w2) => w2.id !== id);
1806
3256
  return next.length === trials.length ? trials : next;
1807
3257
  }
1808
3258
  function resetTrial(trials, id, instruments) {
1809
- const idx = trials.findIndex((w) => w.id === id);
3259
+ const idx = trials.findIndex((w2) => w2.id === id);
1810
3260
  const current = trials[idx];
1811
3261
  if (!current) return trials;
1812
3262
  const instrument = findInstrument(instruments, current.instrumentName);
@@ -1821,10 +3271,10 @@ function resetTrial(trials, id, instruments) {
1821
3271
  return [...trials.slice(0, idx), reset, ...trials.slice(idx + 1)];
1822
3272
  }
1823
3273
  function reorderTrials(trials, ids) {
1824
- const byId = new Map(trials.map((w) => [w.id, w]));
1825
- const named = ids.map((id) => byId.get(id)).filter((w) => w !== void 0);
1826
- const seen = new Set(named.map((w) => w.id));
1827
- const rest = trials.filter((w) => !seen.has(w.id));
3274
+ const byId = new Map(trials.map((w2) => [w2.id, w2]));
3275
+ const named = ids.map((id) => byId.get(id)).filter((w2) => w2 !== void 0);
3276
+ const seen = new Set(named.map((w2) => w2.id));
3277
+ const rest = trials.filter((w2) => !seen.has(w2.id));
1828
3278
  return [...named, ...rest];
1829
3279
  }
1830
3280
  var MODES = [
@@ -1843,7 +3293,7 @@ function LabHeader() {
1843
3293
  className: "lk-lab-header__add",
1844
3294
  onClick: () => lab.addTrial(only.name),
1845
3295
  children: [
1846
- /* @__PURE__ */ jsx(vt, { size: 16 }),
3296
+ /* @__PURE__ */ jsx(C, { size: 16 }),
1847
3297
  /* @__PURE__ */ jsx("span", { children: "Add trial" })
1848
3298
  ]
1849
3299
  }
@@ -1851,7 +3301,7 @@ function LabHeader() {
1851
3301
  // Held at null so the control stays an "add one" action rather than
1852
3302
  // reading as the current instrument.
1853
3303
  /* @__PURE__ */ jsx(
1854
- p2,
3304
+ p,
1855
3305
  {
1856
3306
  className: "lk-lab-header__add-select",
1857
3307
  "aria-label": "Add trial",
@@ -1865,7 +3315,7 @@ function LabHeader() {
1865
3315
  )
1866
3316
  ),
1867
3317
  /* @__PURE__ */ jsx(
1868
- r,
3318
+ r2,
1869
3319
  {
1870
3320
  className: "lk-lab-header__mode",
1871
3321
  ariaLabel: "Color mode",
@@ -1927,19 +3377,26 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
1927
3377
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
1928
3378
  }
1929
3379
  var ZONE_ID = asNodeId("lk-workspace");
1930
- var STRATEGIES = { grid: gridStrategy };
3380
+ var STRATEGIES = { grid: gridStrategy, floating: floatingStrategy };
3381
+ var FLOAT_ZONE_ID = asNodeId("lk-workspace-floating");
3382
+ var PANEL_KIND = "panel";
1931
3383
  var KIND = "trial";
1932
3384
  function extentOf(store, id) {
1933
- const p3 = store.getNode(id)?.membership?.placement;
1934
- if (!p3) return null;
3385
+ const p2 = store.getNode(id)?.membership?.placement;
3386
+ if (!p2) return null;
1935
3387
  const out = {};
1936
- if (p3.size) out.size = p3.size;
1937
- if (p3.span) out.span = p3.span;
3388
+ if (p2.size) out.size = p2.size;
3389
+ if (p2.span) out.span = p2.span;
1938
3390
  return Object.keys(out).length > 0 ? out : null;
1939
3391
  }
3392
+ function PanelFrame({ panel, floating = false }) {
3393
+ const hosts = usePanelHosts();
3394
+ return /* @__PURE__ */ jsx("div", { className: floating ? "lk-panel-tile lk-panel-tile--floating" : "lk-panel-tile", children: /* @__PURE__ */ jsx("div", { className: "lk-panel-tile__body", ref: (el) => hosts?.set(panel.key, el) }) });
3395
+ }
1940
3396
  function Workspace({
1941
3397
  children,
1942
3398
  ids,
3399
+ panels,
1943
3400
  resizable = false,
1944
3401
  reorderable = false,
1945
3402
  onReorder,
@@ -1950,8 +3407,20 @@ function Workspace({
1950
3407
  viewport
1951
3408
  }) {
1952
3409
  const items = Children.toArray(children);
1953
- const idKey = ids ? ids.join(",") : `#${items.length}`;
1954
- const nodeIds = useMemo(() => items.map((_, i) => asNodeId(ids?.[i] ?? `lk-ws-${i}`)), [idKey]);
3410
+ const tilePanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "tile"), [panels]);
3411
+ const floatPanels = useMemo(() => (panels ?? []).filter((p2) => p2.as === "floating"), [panels]);
3412
+ const wantedIds = [
3413
+ ...items.map((_2, i) => ids?.[i] ?? `lk-ws-${i}`),
3414
+ ...tilePanels.map((p2) => `lk-panel-${p2.key}`)
3415
+ ];
3416
+ const idKey = wantedIds.join(",");
3417
+ const idKeyRef = useRef(null);
3418
+ const nodeIdsRef = useRef([]);
3419
+ if (idKeyRef.current !== idKey) {
3420
+ idKeyRef.current = idKey;
3421
+ nodeIdsRef.current = wantedIds.map(asNodeId);
3422
+ }
3423
+ const nodeIds = nodeIdsRef.current;
1955
3424
  const layoutRef = useRef(layout);
1956
3425
  layoutRef.current = layout;
1957
3426
  const onLayoutChangeRef = useRef(onLayoutChange);
@@ -1966,6 +3435,13 @@ function Workspace({
1966
3435
  container: { strategyId: "grid", config: { resizable, gap, padding } }
1967
3436
  })
1968
3437
  );
3438
+ store2.registerNode(
3439
+ createNode({
3440
+ kind: "zone",
3441
+ id: FLOAT_ZONE_ID,
3442
+ container: { strategyId: "floating", config: {} }
3443
+ })
3444
+ );
1969
3445
  storeRef.current = store2;
1970
3446
  }
1971
3447
  const store = storeRef.current;
@@ -1981,7 +3457,8 @@ function Workspace({
1981
3457
  }
1982
3458
  for (const id of nodeIds) {
1983
3459
  if (present.has(id)) continue;
1984
- store.registerNode(createNode({ kind: KIND, id, parentId: ZONE_ID, focus: true }));
3460
+ const kind = String(id).startsWith("lk-panel-") ? PANEL_KIND : KIND;
3461
+ store.registerNode(createNode({ kind, id, parentId: ZONE_ID, focus: true }));
1985
3462
  store.showNode(id);
1986
3463
  const saved = layoutRef.current?.[id];
1987
3464
  if (saved) store.patchPlacement(id, saved);
@@ -2010,7 +3487,15 @@ function Workspace({
2010
3487
  );
2011
3488
  const chrome = useMemo(() => {
2012
3489
  const byId = new Map(nodeIds.map((id, i) => [id, items[i]]));
3490
+ const panelByNode = new Map(
3491
+ tilePanels.map((p2) => [`lk-panel-${p2.key}`, p2])
3492
+ );
2013
3493
  return {
3494
+ [PANEL_KIND]: ({ node }) => {
3495
+ const panel = panelByNode.get(String(node.id));
3496
+ if (!panel) return null;
3497
+ return /* @__PURE__ */ jsx(PanelFrame, { panel });
3498
+ },
2014
3499
  [KIND]: ({ node }) => {
2015
3500
  const content = byId.get(node.id) ?? null;
2016
3501
  if (!reorderable) return content;
@@ -2020,7 +3505,8 @@ function Workspace({
2020
3505
  );
2021
3506
  }
2022
3507
  };
2023
- }, [nodeIds, items, reorderable]);
3508
+ }, [nodeIds, items, reorderable, tilePanels]);
3509
+ const floatingLayer = floatPanels.length === 0 ? null : /* @__PURE__ */ jsx("div", { className: "lk-workspace__floating", children: floatPanels.map((p2) => /* @__PURE__ */ jsx(PanelFrame, { panel: p2, floating: true }, p2.key)) });
2024
3510
  const grid = /* @__PURE__ */ jsx(
2025
3511
  Container,
2026
3512
  {
@@ -2032,7 +3518,15 @@ function Workspace({
2032
3518
  onChildOrderChange: reorderable ? commitOrder : void 0
2033
3519
  }
2034
3520
  );
2035
- return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
3521
+ return /* @__PURE__ */ jsx(Provider, { store, children: /* @__PURE__ */ jsx(StrategyRegistryProvider, { strategies: STRATEGIES, children: floatingLayer ? (
3522
+ // The floating layer insets against the workspace, so it needs a
3523
+ // positioned box around both. Without one it insets against the page
3524
+ // and lands over the lab header.
3525
+ /* @__PURE__ */ jsxs("div", { className: "lk-workspace-host", children: [
3526
+ reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid,
3527
+ floatingLayer
3528
+ ] })
3529
+ ) : reorderable ? /* @__PURE__ */ jsx(DragProvider, { children: grid }) : grid }) });
2036
3530
  }
2037
3531
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
2038
3532
  const store = createLabStore({ storageKey, storage, initialMode });
@@ -2054,9 +3548,9 @@ var NEBULA_SLOTS = [
2054
3548
  { cx: "85%", cy: "18%", sx: "55%", sy: "80%", stop: "70%" }
2055
3549
  ];
2056
3550
  function buildNebula(colors) {
2057
- const blobs = colors.map((c, i) => {
2058
- const p3 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
2059
- return `radial-gradient(ellipse ${p3.sx} ${p3.sy} at ${p3.cx} ${p3.cy}, color-mix(in srgb, ${c} 22%, transparent), transparent ${p3.stop})`;
3551
+ const blobs = colors.map((c2, i) => {
3552
+ const p2 = NEBULA_SLOTS[i % NEBULA_SLOTS.length];
3553
+ return `radial-gradient(ellipse ${p2.sx} ${p2.sy} at ${p2.cx} ${p2.cy}, color-mix(in srgb, ${c2} 22%, transparent), transparent ${p2.stop})`;
2060
3554
  });
2061
3555
  blobs.push("radial-gradient(ellipse at center, #0a0a18 0%, #02020a 100%)");
2062
3556
  return blobs.join(", ");
@@ -2095,6 +3589,40 @@ function Lab({
2095
3589
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
2096
3590
  const modeValue = useStore(store, (s) => s.mode);
2097
3591
  const layout = useStore(store, (s) => s.layout);
3592
+ const undockedPanels = useStore(store, (s) => s.undockedPanels);
3593
+ const panelHostsRef = useRef(null);
3594
+ if (panelHostsRef.current === null) panelHostsRef.current = createPanelHostRegistry();
3595
+ const outerSurface = useSurfaceOptional();
3596
+ const outerCanvas = useSurfaceCanvas();
3597
+ const [ownCanvas, setOwnCanvas] = useState(null);
3598
+ const ownCanvasRef = useRef(null);
3599
+ const bufferRef = useRef({ w: 0, h: 0 });
3600
+ const surfaceRef = useRef(null);
3601
+ const onFrame = useCallback((frame) => {
3602
+ const c2 = ownCanvasRef.current;
3603
+ if (!c2) return;
3604
+ const w2 = Math.round(frame.size.width * frame.dpr);
3605
+ const h = Math.round(frame.size.height * frame.dpr);
3606
+ if (bufferRef.current.w === w2 && bufferRef.current.h === h) return;
3607
+ bufferRef.current = { w: w2, h };
3608
+ c2.width = w2;
3609
+ c2.height = h;
3610
+ c2.style.width = `${frame.size.width}px`;
3611
+ c2.style.height = `${frame.size.height}px`;
3612
+ surfaceRef.current?.invalidateAll();
3613
+ }, []);
3614
+ const ownSurface = useTiledSurface({ onFrame });
3615
+ surfaceRef.current = ownSurface;
3616
+ const surface = outerSurface ?? ownSurface;
3617
+ const surfaceCanvas = outerSurface ? outerCanvas : ownCanvas;
3618
+ const workspacePanels = useMemo(
3619
+ () => Object.entries(undockedPanels).map(([key, panel]) => ({
3620
+ key,
3621
+ title: panel.sectionId,
3622
+ as: panel.as
3623
+ })),
3624
+ [undockedPanels]
3625
+ );
2098
3626
  const resolvedMode = useResolvedMode(modeValue);
2099
3627
  useEffect(() => {
2100
3628
  if (mode && mode !== store.getState().mode) {
@@ -2103,8 +3631,8 @@ function Lab({
2103
3631
  }, [mode, store]);
2104
3632
  const contextValue = useMemo(() => {
2105
3633
  const replaceTrials = (next) => {
2106
- const currentById = new Map(store.getState().trials.map((w) => [w.id, w]));
2107
- const merged = next.map((w) => currentById.get(w.id) ?? w);
3634
+ const currentById = new Map(store.getState().trials.map((w2) => [w2.id, w2]));
3635
+ const merged = next.map((w2) => currentById.get(w2.id) ?? w2);
2108
3636
  store.setState({ trials: merged });
2109
3637
  store.getState().setMode(store.getState().mode);
2110
3638
  };
@@ -2128,11 +3656,11 @@ function Lab({
2128
3656
  },
2129
3657
  resetTrial: (id) => {
2130
3658
  const next = resetTrial(store.getState().trials, id, instruments);
2131
- const record = next.find((w) => w.id === id);
3659
+ const record = next.find((w2) => w2.id === id);
2132
3660
  if (!record) return;
2133
3661
  store.setState((s) => ({
2134
3662
  trials: s.trials.map(
2135
- (w) => w.id === id ? { ...w, config: record.config, state: record.state, view: record.view } : w
3663
+ (w2) => w2.id === id ? { ...w2, config: record.config, state: record.state, view: record.view } : w2
2136
3664
  )
2137
3665
  }));
2138
3666
  store.getState().updateTrialView(id, record.view);
@@ -2149,8 +3677,8 @@ function Lab({
2149
3677
  store.getState().deleteSnapshot(snapshotId);
2150
3678
  },
2151
3679
  mode: modeValue,
2152
- setMode: (m) => {
2153
- store.getState().setMode(m);
3680
+ setMode: (m2) => {
3681
+ store.getState().setMode(m2);
2154
3682
  },
2155
3683
  configRules,
2156
3684
  controls
@@ -2174,372 +3702,50 @@ function Lab({
2174
3702
  /* @__PURE__ */ jsx(LabHeader, {}),
2175
3703
  children
2176
3704
  ] }),
2177
- children: /* @__PURE__ */ jsxs("div", { className: "lk-lab__body", children: [
2178
- tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
2179
- /* @__PURE__ */ jsx(
2180
- Workspace,
2181
- {
2182
- ids: trials.map((w) => w.id),
2183
- resizable: true,
2184
- reorderable: true,
2185
- onReorder: (ids) => contextValue.reorderTrials(ids),
2186
- layout,
2187
- onLayoutChange: (next) => store.getState().setLayout(next),
2188
- children: trials.map((w) => /* @__PURE__ */ jsx(Trial, { id: w.id, chrome, suppress }, w.id))
2189
- }
2190
- )
2191
- ] })
2192
- }
2193
- )
2194
- }
2195
- ) }) });
2196
- }
2197
- function CurveField({
2198
- values,
2199
- min,
2200
- max,
2201
- step,
2202
- width,
2203
- height = 110,
2204
- defaults,
2205
- onChange,
2206
- marks
2207
- }) {
2208
- const points = useMemo(() => {
2209
- const out = [];
2210
- for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });
2211
- return out;
2212
- }, [values]);
2213
- const handleChange = useCallback(
2214
- (next) => {
2215
- const flat = new Array(next.length * 2);
2216
- for (let i = 0; i < next.length; i++) {
2217
- const ySnap = Math.round(next[i].y / step) * step;
2218
- const y = Math.max(min, Math.min(max, ySnap));
2219
- flat[i * 2] = next[i].x;
2220
- flat[i * 2 + 1] = y;
2221
- }
2222
- p("curve-field", "handleChange", {
2223
- nLen: next.length,
2224
- flatLen: flat.length,
2225
- sample: flat.slice(0, 4)
2226
- });
2227
- onChange(flat);
2228
- },
2229
- [onChange, min, max, step]
2230
- );
2231
- const handleFlip = useCallback(() => {
2232
- const flipped = [];
2233
- for (let i = 0; i + 1 < values.length; i += 2) {
2234
- flipped.push({ x: 1 - values[i], y: values[i + 1] });
2235
- }
2236
- flipped.sort((a, b) => a.x - b.x);
2237
- const out = [];
2238
- for (const p3 of flipped) out.push(p3.x, p3.y);
2239
- onChange(out);
2240
- }, [values, onChange]);
2241
- const handleFlipVertical = useCallback(() => {
2242
- const mid = (min + max) / 2;
2243
- const flipped = new Array(values.length);
2244
- for (let i = 0; i + 1 < values.length; i += 2) {
2245
- flipped[i] = values[i];
2246
- flipped[i + 1] = 2 * mid - values[i + 1];
2247
- }
2248
- onChange(flipped);
2249
- }, [values, min, max, onChange]);
2250
- return /* @__PURE__ */ jsxs("div", { className: "lk-curve-field", children: [
2251
- /* @__PURE__ */ jsxs("div", { className: "lk-curve-field__plot", children: [
2252
- /* @__PURE__ */ jsx(
2253
- F,
2254
- {
2255
- value: points,
2256
- onInput: handleChange,
2257
- domain: "1d",
2258
- constrain: "function",
2259
- xRange: [0, 1],
2260
- yRange: [min, max],
2261
- width,
2262
- height,
2263
- endpoints: "pinned-x",
2264
- addPointMode: "click-curve",
2265
- minPoints: 2,
2266
- grid: {},
2267
- history: false
2268
- }
2269
- ),
2270
- marks && marks.length > 0 && /* @__PURE__ */ jsx(
2271
- "svg",
2272
- {
2273
- className: "lk-curve-field__marks",
2274
- width,
2275
- height,
2276
- viewBox: `0 0 ${width} ${height}`,
2277
- "aria-hidden": "true",
2278
- children: marks.map((m) => {
2279
- const color = m.color ?? "#ffcc00";
2280
- if (m.kind === "band") {
2281
- const x0 = Math.max(0, Math.min(1, m.x[0])) * width;
2282
- const x1 = Math.max(0, Math.min(1, m.x[1])) * width;
2283
- const left = Math.min(x0, x1);
2284
- const w = Math.abs(x1 - x0);
2285
- return /* @__PURE__ */ jsxs("g", { children: [
2286
- /* @__PURE__ */ jsx("rect", { x: left, y: 0, width: w, height, fill: color, fillOpacity: 0.4 }),
2287
- /* @__PURE__ */ jsx(
2288
- "line",
2289
- {
2290
- x1: left,
2291
- y1: 0,
2292
- x2: left,
2293
- y2: height,
2294
- stroke: color,
2295
- strokeOpacity: 0.85,
2296
- strokeWidth: 1
2297
- }
3705
+ children: /* @__PURE__ */ jsx(PanelHostContext.Provider, { value: panelHostsRef.current, children: /* @__PURE__ */ jsx(SurfaceContext.Provider, { value: surface, children: /* @__PURE__ */ jsx(SurfaceCanvasContext.Provider, { value: surfaceCanvas, children: /* @__PURE__ */ jsxs(
3706
+ "div",
3707
+ {
3708
+ className: "lk-lab__body",
3709
+ ref: outerSurface ? void 0 : ownSurface.containerRef,
3710
+ children: [
3711
+ outerSurface ? null : (
3712
+ // Above the trials and inert: the marks a tile paints have
3713
+ // to sit over the instrument's own DOM, and nothing on this
3714
+ // buffer takes input — each tile has its own input box.
3715
+ /* @__PURE__ */ jsx(
3716
+ "canvas",
3717
+ {
3718
+ className: "lk-lab__surface",
3719
+ ref: (el) => {
3720
+ ownCanvasRef.current = el;
3721
+ setOwnCanvas(el);
3722
+ }
3723
+ }
3724
+ )
2298
3725
  ),
3726
+ tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
2299
3727
  /* @__PURE__ */ jsx(
2300
- "line",
3728
+ Workspace,
2301
3729
  {
2302
- x1: left + w,
2303
- y1: 0,
2304
- x2: left + w,
2305
- y2: height,
2306
- stroke: color,
2307
- strokeOpacity: 0.85,
2308
- strokeWidth: 1
3730
+ panels: workspacePanels,
3731
+ ids: trials.map((w2) => w2.id),
3732
+ resizable: true,
3733
+ reorderable: true,
3734
+ onReorder: (ids) => contextValue.reorderTrials(ids),
3735
+ layout,
3736
+ onLayoutChange: (next) => store.getState().setLayout(next),
3737
+ children: trials.map((w2) => /* @__PURE__ */ jsx(Trial, { id: w2.id, chrome, suppress }, w2.id))
2309
3738
  }
2310
3739
  )
2311
- ] }, `band-${m.x[0]}-${m.x[1]}-${color}`);
3740
+ ]
2312
3741
  }
2313
- const x = Math.max(0, Math.min(1, m.x)) * width;
2314
- return /* @__PURE__ */ jsx(
2315
- "line",
2316
- {
2317
- x1: x,
2318
- y1: 0,
2319
- x2: x,
2320
- y2: height,
2321
- stroke: color,
2322
- strokeWidth: 1
2323
- },
2324
- `mark-${m.x}-${color}`
2325
- );
2326
- })
2327
- }
2328
- )
2329
- ] }),
2330
- /* @__PURE__ */ jsxs("div", { className: "lk-curve-field__actions", children: [
2331
- /* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlip, children: "Flip horizontally" }),
2332
- /* @__PURE__ */ jsx("button", { type: "button", className: "lk-curve-field__action", onClick: handleFlipVertical, children: "Flip vertically" }),
2333
- defaults && /* @__PURE__ */ jsx(
2334
- "button",
2335
- {
2336
- type: "button",
2337
- className: "lk-curve-field__action",
2338
- onClick: () => onChange([...defaults]),
2339
- children: "Reset"
3742
+ ) }) }) })
2340
3743
  }
2341
3744
  )
2342
- ] })
2343
- ] });
2344
- }
2345
- function Subpanel({ title, children, className }) {
2346
- const cls = className ? `lk-subpanel ${className}` : "lk-subpanel";
2347
- return /* @__PURE__ */ jsxs("div", { className: cls, children: [
2348
- /* @__PURE__ */ jsxs("h4", { className: "lk-subpanel__title", children: [
2349
- /* @__PURE__ */ jsx("span", { children: title }),
2350
- /* @__PURE__ */ jsx("hr", {})
2351
- ] }),
2352
- children
2353
- ] });
2354
- }
2355
- function EffectCard({
2356
- title,
2357
- primary,
2358
- accent,
2359
- index,
2360
- expanded = true,
2361
- onToggleExpanded,
2362
- onRemove,
2363
- draggable,
2364
- onPressHandle,
2365
- onReleaseHandle,
2366
- onDragStart,
2367
- onDragEnd,
2368
- onDragOver,
2369
- onDragLeave,
2370
- onDrop,
2371
- dragging,
2372
- children,
2373
- className
2374
- }) {
2375
- const stateClass = `${expanded ? "is-expanded" : "is-collapsed"}${dragging ? " is-dragging" : ""}${accent ? " has-accent" : ""}`;
2376
- const cls = `lk-effect-card ${stateClass}${className ? ` ${className}` : ""}`;
2377
- const style = accent ? { "--lk-panel-accent": accent } : void 0;
2378
- const headProps = onToggleExpanded ? {
2379
- role: "button",
2380
- tabIndex: 0,
2381
- onClick: onToggleExpanded,
2382
- onKeyDown: (e) => {
2383
- if (e.key === "Enter" || e.key === " ") {
2384
- e.preventDefault();
2385
- onToggleExpanded();
2386
- }
2387
3745
  }
2388
- } : {};
2389
- return (
2390
- // biome-ignore lint/a11y/noStaticElementInteractions: native HTML5 drag-and-drop container — drag events are not click/keyboard interactions and have no semantic element; the in-card handle and header carry the a11y affordances.
2391
- /* @__PURE__ */ jsxs(
2392
- "div",
2393
- {
2394
- className: cls,
2395
- style,
2396
- draggable,
2397
- onDragStart,
2398
- onDragEnd,
2399
- onDragOver,
2400
- onDragLeave,
2401
- onDrop,
2402
- children: [
2403
- /* @__PURE__ */ jsxs("div", { className: "lk-effect-card__head", ...headProps, children: [
2404
- index != null ? /* @__PURE__ */ jsx(
2405
- "button",
2406
- {
2407
- type: "button",
2408
- className: "lk-effect-card__index-badge lk-effect-card__handle",
2409
- title: "Drag to reorder \xB7 click to toggle",
2410
- "aria-label": `Item ${index + 1} \u2014 drag to reorder, click to toggle`,
2411
- onMouseDown: onPressHandle,
2412
- onMouseUp: onReleaseHandle,
2413
- onClick: (e) => {
2414
- e.stopPropagation();
2415
- onToggleExpanded?.();
2416
- },
2417
- children: index + 1
2418
- }
2419
- ) : /* @__PURE__ */ jsx(
2420
- "span",
2421
- {
2422
- className: "lk-effect-card__handle",
2423
- "aria-hidden": "true",
2424
- title: "Drag to reorder",
2425
- onMouseDown: onPressHandle,
2426
- onMouseUp: onReleaseHandle,
2427
- children: /* @__PURE__ */ jsx(DragHandleIcon, {})
2428
- }
2429
- ),
2430
- /* @__PURE__ */ jsx("span", { className: "lk-effect-card__title", children: title }),
2431
- primary != null && /* @__PURE__ */ jsx("span", { className: "lk-effect-card__primary", children: primary }),
2432
- onRemove && /* @__PURE__ */ jsx(
2433
- "button",
2434
- {
2435
- type: "button",
2436
- className: "lk-effect-card__remove",
2437
- onClick: (e) => {
2438
- e.stopPropagation();
2439
- onRemove();
2440
- },
2441
- title: "Remove",
2442
- "aria-label": "Remove",
2443
- children: "\u2715"
2444
- }
2445
- )
2446
- ] }),
2447
- expanded && /* @__PURE__ */ jsx("div", { className: "lk-effect-card__body", children })
2448
- ]
2449
- }
2450
- )
2451
- );
2452
- }
2453
- function DragHandleIcon() {
2454
- return /* @__PURE__ */ jsxs("svg", { width: "10", height: "14", viewBox: "0 0 10 14", "aria-hidden": "true", children: [
2455
- /* @__PURE__ */ jsx("circle", { cx: "3", cy: "3", r: "1.3", fill: "currentColor" }),
2456
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "3", r: "1.3", fill: "currentColor" }),
2457
- /* @__PURE__ */ jsx("circle", { cx: "3", cy: "7", r: "1.3", fill: "currentColor" }),
2458
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "1.3", fill: "currentColor" }),
2459
- /* @__PURE__ */ jsx("circle", { cx: "3", cy: "11", r: "1.3", fill: "currentColor" }),
2460
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "11", r: "1.3", fill: "currentColor" })
2461
- ] });
2462
- }
2463
- function EffectCardList({
2464
- items,
2465
- renderItem,
2466
- onReorder,
2467
- empty,
2468
- defaultExpandedIds
2469
- }) {
2470
- const [expanded, setExpanded] = useState(() => new Set(defaultExpandedIds ?? []));
2471
- const [draggingId, setDraggingId] = useState(null);
2472
- const [pressedId, setPressedId] = useState(null);
2473
- const [dropHint, setDropHint] = useState(
2474
- null
2475
- );
2476
- if (items.length === 0) {
2477
- return /* @__PURE__ */ jsx(Fragment, { children: empty ?? /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__empty", children: "No items." }) });
2478
- }
2479
- return /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list", children: items.map((item) => {
2480
- const isExpanded = expanded.has(item.id);
2481
- const isDragging = draggingId === item.id;
2482
- const isPressed = pressedId === item.id;
2483
- const showHintBefore = dropHint && dropHint.id === item.id && dropHint.position === "before";
2484
- const showHintAfter = dropHint && dropHint.id === item.id && dropHint.position === "after";
2485
- const toggleExpanded = () => setExpanded((prev) => {
2486
- const next = new Set(prev);
2487
- if (next.has(item.id)) next.delete(item.id);
2488
- else next.add(item.id);
2489
- return next;
2490
- });
2491
- return /* @__PURE__ */ jsxs("div", { className: "lk-effect-card-list__wrap", children: [
2492
- showHintBefore && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" }),
2493
- renderItem(item, {
2494
- isExpanded,
2495
- isDragging,
2496
- toggleExpanded,
2497
- cardProps: {
2498
- expanded: isExpanded,
2499
- dragging: isDragging,
2500
- draggable: isPressed,
2501
- onToggleExpanded: toggleExpanded,
2502
- onPressHandle: () => setPressedId(item.id),
2503
- onReleaseHandle: () => setPressedId(null),
2504
- onDragStart: (e) => {
2505
- e.dataTransfer.effectAllowed = "move";
2506
- e.dataTransfer.setData("text/plain", String(item.id));
2507
- setDraggingId(item.id);
2508
- },
2509
- onDragEnd: () => {
2510
- setDraggingId(null);
2511
- setPressedId(null);
2512
- setDropHint(null);
2513
- },
2514
- onDragOver: (e) => {
2515
- if (draggingId === null || draggingId === item.id) return;
2516
- e.preventDefault();
2517
- e.dataTransfer.dropEffect = "move";
2518
- const rect = e.currentTarget.getBoundingClientRect();
2519
- const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
2520
- setDropHint({ id: item.id, position });
2521
- },
2522
- onDragLeave: () => {
2523
- setDropHint((h) => h && h.id === item.id ? null : h);
2524
- },
2525
- onDrop: (e) => {
2526
- e.preventDefault();
2527
- if (draggingId === null || draggingId === item.id) {
2528
- setDropHint(null);
2529
- return;
2530
- }
2531
- const rect = e.currentTarget.getBoundingClientRect();
2532
- const position = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
2533
- onReorder(draggingId, item.id, position);
2534
- setDropHint(null);
2535
- }
2536
- }
2537
- }),
2538
- showHintAfter && /* @__PURE__ */ jsx("div", { className: "lk-effect-card-list__drop-hint" })
2539
- ] }, String(item.id));
2540
- }) });
3746
+ ) }) });
2541
3747
  }
2542
3748
 
2543
- export { CurveField, DEFAULT_VIEW, EffectCard, EffectCardList, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Subpanel, Trial, TrialChrome, Workspace, addTrial, applyRules, as2DView, builtinRules, clampPitch, cloneTrial, closeTrial, defineInstrument, f, interstellarTheme, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, titleCase, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, wrapYaw };
3749
+ export { AnnotationOverlay, AnnotationTargets, DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, Trial, TrialChrome, Workspace, addTrial, annotationsFromJSON, applyRules, as2DView, builtinRules, capturePlan, captureTarget, clampPitch, cloneTrial, closeTrial, composeCaptureSvg, createAnnotationScene, createAnnotationStore, createMarkDrawOne, defineInstrument, f, fitView, fracContains, fracIntersects, fracToWorld, fromWeaselView, interstellarTheme, isStale, markCommands, markSvgNodes, orbitAfterDrag, orbitAfterWheel, reorderTrials, resetTrial, resolveConfigSchema, resolveMarkStyle, roundFrac, seenFrom, titleCase, toWeaselView, useConfigSchema, useLabContext, useOrbit, validateConfigSchema, worldToFrac, wrapYaw };
2544
3750
  //# sourceMappingURL=index.js.map
2545
3751
  //# sourceMappingURL=index.js.map