@weasel-js/labkit 1.3.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 (210) 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-DCGEcMO_.d.ts → types-D6s4b7if.d.ts} +1 -1
  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 -4
  14. package/dist/chrome/index.d.ts +9 -6
  15. package/dist/chrome/index.js +6 -6
  16. package/dist/chunk-2AYGEN57.js +32 -0
  17. package/dist/chunk-2AYGEN57.js.map +1 -0
  18. package/dist/{chunk-QRYPXSGP.js → chunk-3P5TTTA7.js} +16 -8
  19. package/dist/chunk-3P5TTTA7.js.map +1 -0
  20. package/dist/{chunk-BDJXIBRZ.js → chunk-3ZELRRBV.js} +44 -27
  21. package/dist/chunk-3ZELRRBV.js.map +1 -0
  22. package/dist/{chunk-LGKRIUSW.js → chunk-4EY67BKJ.js} +32 -7
  23. package/dist/chunk-4EY67BKJ.js.map +1 -0
  24. package/dist/{chunk-3XXPU73K.js → chunk-5TCEVMPI.js} +9 -7
  25. package/dist/chunk-5TCEVMPI.js.map +1 -0
  26. package/dist/{chunk-JDROYQM3.js → chunk-6JMU56CU.js} +130 -131
  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-BMW4TDP5.js → chunk-JDDIZSLL.js} +3 -3
  37. package/dist/{chunk-BMW4TDP5.js.map → chunk-JDDIZSLL.js.map} +1 -1
  38. package/dist/chunk-LITE5YFB.js +669 -0
  39. package/dist/chunk-LITE5YFB.js.map +1 -0
  40. package/dist/{chunk-QCICIHEO.js → chunk-ROQDRXKW.js} +716 -311
  41. package/dist/chunk-ROQDRXKW.js.map +1 -0
  42. package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
  43. package/dist/chunk-XJ6N32QP.js.map +1 -0
  44. package/dist/{chunk-WTI26YTM.js → chunk-YQJNT4XQ.js} +40 -13
  45. package/dist/chunk-YQJNT4XQ.js.map +1 -0
  46. package/dist/controls/index.d.ts +4 -28
  47. package/dist/controls/index.js +3 -3
  48. package/dist/dragdrop/index.d.ts +9 -5
  49. package/dist/dragdrop/index.js +3 -3
  50. package/dist/index.d.ts +518 -190
  51. package/dist/index.js +1753 -75
  52. package/dist/index.js.map +1 -1
  53. package/dist/layers/index.d.ts +6 -5
  54. package/dist/layers/index.js +3 -3
  55. package/dist/loupe/index.d.ts +187 -0
  56. package/dist/loupe/index.js +7 -0
  57. package/dist/loupe/index.js.map +1 -0
  58. package/dist/passthrough/weasel-canvas.d.ts +3 -1
  59. package/dist/passthrough/weasel-canvas.js +1 -1
  60. package/dist/passthrough/weasel-ui.d.ts +138 -91
  61. package/dist/passthrough/weasel-ui.js +2 -2
  62. package/dist/primitives/index.d.ts +3 -1
  63. package/dist/primitives/index.js +5 -5
  64. package/dist/state/index.d.ts +3 -3
  65. package/dist/state/index.js +2 -2
  66. package/dist/styles.css +229 -1
  67. package/dist/surface/index.d.ts +18 -2
  68. package/dist/surface/index.js +2 -2
  69. package/dist/ui/layers/index.js +2 -2
  70. package/dist/undo/index.d.ts +5 -4
  71. package/package.json +14 -7
  72. package/src/annotations/AnnotationOverlay.tsx +205 -0
  73. package/src/annotations/AnnotationTargets.tsx +37 -0
  74. package/src/annotations/Annotations.less +129 -0
  75. package/src/annotations/Annotations.meaning.test.tsx +111 -0
  76. package/src/annotations/Annotations.overlay.test.tsx +142 -0
  77. package/src/annotations/AnnotationsContext.ts +49 -0
  78. package/src/annotations/ExportMenu.test.tsx +86 -0
  79. package/src/annotations/ExportMenu.tsx +182 -0
  80. package/src/annotations/MarkList.tsx +75 -0
  81. package/src/annotations/capture.test.ts +140 -0
  82. package/src/annotations/capture.ts +237 -0
  83. package/src/annotations/drawOne.test.ts +56 -0
  84. package/src/annotations/drawOne.ts +47 -0
  85. package/src/annotations/frac.test.ts +59 -0
  86. package/src/annotations/frac.ts +66 -0
  87. package/src/annotations/history.test.ts +82 -0
  88. package/src/annotations/history.ts +118 -0
  89. package/src/annotations/index.ts +37 -0
  90. package/src/annotations/paint.test.ts +125 -0
  91. package/src/annotations/paint.ts +130 -0
  92. package/src/annotations/staleness.test.ts +56 -0
  93. package/src/annotations/staleness.ts +41 -0
  94. package/src/annotations/store.test.ts +175 -0
  95. package/src/annotations/store.ts +317 -0
  96. package/src/annotations/svgNodes.test.ts +77 -0
  97. package/src/annotations/svgNodes.ts +74 -0
  98. package/src/annotations/toolMap.test.ts +36 -0
  99. package/src/annotations/toolMap.ts +54 -0
  100. package/src/annotations/types.ts +222 -0
  101. package/src/annotations/view.test.ts +37 -0
  102. package/src/annotations/view.ts +40 -0
  103. package/src/canvas/AGENTS.md +55 -5
  104. package/src/canvas/CanvasStack.test.tsx +27 -1
  105. package/src/canvas/CanvasStack.tsx +29 -4
  106. package/src/canvas/CanvasStackContext.ts +23 -4
  107. package/src/canvas/camera.test.ts +78 -0
  108. package/src/canvas/camera.ts +62 -0
  109. package/src/canvas/canvasCoords.test.ts +26 -0
  110. package/src/canvas/canvasCoords.ts +20 -7
  111. package/src/canvas/index.ts +5 -1
  112. package/src/canvas/useLayerScheduler.ts +7 -3
  113. package/src/canvas/usePanZoom.test.ts +36 -5
  114. package/src/canvas/usePanZoom.ts +20 -15
  115. package/src/canvas/worldSpec.test.ts +63 -0
  116. package/src/canvas/worldSpec.ts +48 -0
  117. package/src/chrome/builtins.test.ts +31 -1
  118. package/src/chrome/builtins.tsx +63 -19
  119. package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
  120. package/src/chrome/regions/PaletteRegion.tsx +4 -0
  121. package/src/chrome/regions/SidebarRegion.tsx +37 -13
  122. package/src/chrome/regions/ToolbarRegion.tsx +2 -1
  123. package/src/chrome/regions/ViewportRegion.tsx +9 -1
  124. package/src/chrome/regions/regions.test.tsx +6 -1
  125. package/src/chrome/types.ts +20 -0
  126. package/src/controls/ControlPanel.test.tsx +38 -0
  127. package/src/controls/ControlPanel.tsx +50 -3
  128. package/src/dragdrop/DragDropRuntime.tsx +8 -2
  129. package/src/dragdrop/dragDrop.test.tsx +29 -2
  130. package/src/index.test.ts +22 -0
  131. package/src/index.ts +87 -2
  132. package/src/instrument/SineWave.smoke.test.tsx +1 -0
  133. package/src/instrument/types.ts +24 -1
  134. package/src/lab/Lab.surface.test.tsx +162 -0
  135. package/src/lab/Lab.tsx +102 -17
  136. package/src/lab/Lab.undock.test.tsx +85 -0
  137. package/src/lab/LabFullChrome.stories.tsx +20 -0
  138. package/src/lab/LabShell.less +11 -0
  139. package/src/lab/Workspace.less +73 -0
  140. package/src/lab/Workspace.surface.test.tsx +2 -0
  141. package/src/lab/Workspace.tsx +97 -8
  142. package/src/lab/index.ts +1 -1
  143. package/src/lab/panelHost.ts +37 -0
  144. package/src/loupe/AGENTS.md +49 -0
  145. package/src/loupe/CanvasLoupe.tsx +83 -0
  146. package/src/loupe/DomLoupe.tsx +62 -0
  147. package/src/loupe/Loupe.less +38 -0
  148. package/src/loupe/LoupeBubble.tsx +32 -0
  149. package/src/loupe/TrialLoupe.tsx +93 -0
  150. package/src/loupe/canvasLens.test.ts +205 -0
  151. package/src/loupe/canvasLens.ts +141 -0
  152. package/src/loupe/index.ts +19 -0
  153. package/src/loupe/types.test.ts +29 -0
  154. package/src/loupe/types.ts +90 -0
  155. package/src/loupe/useHostSize.ts +30 -0
  156. package/src/loupe/useLoupe.test.tsx +197 -0
  157. package/src/loupe/useLoupe.ts +189 -0
  158. package/src/passthrough/weasel-ui.ts +3 -0
  159. package/src/primitives/FloatingPanel.test.tsx +76 -1
  160. package/src/primitives/FloatingPanel.tsx +19 -3
  161. package/src/primitives/ScaleIndicator.test.tsx +7 -2
  162. package/src/primitives/Sidebar.less +18 -0
  163. package/src/primitives/Toolbar.less +8 -0
  164. package/src/primitives/Toolbar.tsx +5 -0
  165. package/src/primitives/useRovingTabIndex.test.ts +21 -0
  166. package/src/primitives/useRovingTabIndex.ts +26 -17
  167. package/src/state/document.test.ts +23 -0
  168. package/src/state/document.ts +14 -2
  169. package/src/state/index.ts +2 -0
  170. package/src/state/store.test.ts +24 -0
  171. package/src/state/store.ts +35 -1
  172. package/src/state/types.ts +11 -0
  173. package/src/state/undock.test.ts +40 -0
  174. package/src/state/undock.ts +39 -0
  175. package/src/styles.less +2 -0
  176. package/src/surface/SurfaceContext.ts +4 -0
  177. package/src/surface/index.ts +8 -3
  178. package/src/surface/useSurfaceTile.test.tsx +2 -0
  179. package/src/surface/useSurfaceTile.ts +7 -1
  180. package/src/surface/useTiledSurface.test.tsx +58 -0
  181. package/src/surface/useTiledSurface.ts +38 -3
  182. package/src/trial/Trial.annotations.persist.test.tsx +166 -0
  183. package/src/trial/Trial.annotations.test.tsx +121 -0
  184. package/src/trial/Trial.canvas.test.tsx +76 -1
  185. package/src/trial/Trial.less +19 -0
  186. package/src/trial/Trial.loupe.test.tsx +128 -0
  187. package/src/trial/Trial.stories.tsx +1 -0
  188. package/src/trial/Trial.test.tsx +12 -0
  189. package/src/trial/Trial.trialId.test.tsx +36 -0
  190. package/src/trial/Trial.tsx +215 -40
  191. package/src/trial/TrialChrome.tsx +35 -0
  192. package/src/trial/UndockedSections.tsx +65 -0
  193. package/src/trial/trialOps.test.ts +21 -0
  194. package/src/trial/trialOps.ts +5 -3
  195. package/dist/_dts/types-BttTIent.d.ts +0 -314
  196. package/dist/chunk-3XXPU73K.js.map +0 -1
  197. package/dist/chunk-4TMMVIDM.js.map +0 -1
  198. package/dist/chunk-6X5RBC6G.js +0 -366
  199. package/dist/chunk-6X5RBC6G.js.map +0 -1
  200. package/dist/chunk-BDJXIBRZ.js.map +0 -1
  201. package/dist/chunk-CPUJ3QXL.js.map +0 -1
  202. package/dist/chunk-JDROYQM3.js.map +0 -1
  203. package/dist/chunk-LGKRIUSW.js.map +0 -1
  204. package/dist/chunk-QCICIHEO.js.map +0 -1
  205. package/dist/chunk-QRYPXSGP.js.map +0 -1
  206. package/dist/chunk-RGT4EJIB.js +0 -6961
  207. package/dist/chunk-RGT4EJIB.js.map +0 -1
  208. package/dist/chunk-T66HU3HX.js +0 -17
  209. package/dist/chunk-T66HU3HX.js.map +0 -1
  210. package/dist/chunk-WTI26YTM.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,35 +1,1401 @@
1
- import { useDragDrop, DragOverlay, Palette } from './chunk-3XXPU73K.js';
2
- import { useSurfaceOptional } from './chunk-LGKRIUSW.js';
3
- export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface } from './chunk-LGKRIUSW.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';
4
6
  import { useJob } from './chunk-LN6JDUGB.js';
5
7
  export { useJob } from './chunk-LN6JDUGB.js';
6
- import { suppressContributions, mergeContributions, builtinContributions, TitleBarRegion, ToolbarRegion, PaletteRegion, SidebarRegion, ViewportRegion, StatusRegion } from './chunk-6X5RBC6G.js';
7
- export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from './chunk-6X5RBC6G.js';
8
- import { JobProgress } from './chunk-QRYPXSGP.js';
9
- export { FloatingPanel, JobProgress, Legend, Sidebar } from './chunk-QRYPXSGP.js';
10
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from './chunk-JDROYQM3.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-WTI26YTM.js';
14
- export { ControlPanel, fromConfigFields, isLeafVisible } from './chunk-WTI26YTM.js';
15
- import { CanvasStack } from './chunk-BDJXIBRZ.js';
16
- export { CanvasStack } from './chunk-BDJXIBRZ.js';
17
- export { screenToWorld, worldToScreen } from './chunk-T66HU3HX.js';
18
- export { CanvasStackContext } from './chunk-CPUJ3QXL.js';
19
- import { LayerList } from './chunk-BMW4TDP5.js';
20
- export { LayerList } from './chunk-BMW4TDP5.js';
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
21
  import './chunk-SBXNQW4G.js';
22
- import { Rt, p, r } from './chunk-RGT4EJIB.js';
23
- export { k as CheckboxRow, O as ColorRow, m2 as CurveField, _2 as EffectCard, y as EffectCardList, c3 as LayerStack, j as NumberRow, b2 as PropertyGroup, S2 as PropertyList, x2 as PropertyPanel, w as PropertyRow, C as PropertySpan, M as SelectRow, E as SliderRow, g as Subpanel, A as TextRow, N as ToggleRow } from './chunk-RGT4EJIB.js';
24
- import './chunk-QCICIHEO.js';
25
- import { createEventBus, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
26
- export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './chunk-RL2LOLNI.js';
27
- import { createContext, useRef, useCallback, useContext, useMemo, useState, Children, useLayoutEffect, useEffect } from 'react';
28
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
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';
32
+ import { createPortal } from 'react-dom';
33
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
29
34
  import { useStore } from 'zustand/react';
30
35
  import { Provider, StrategyRegistryProvider, DragProvider, Container, useDragHandle } from 'windease/react';
31
- import { asNodeId, Store, createNode, gridStrategy } from 'windease';
36
+ import { asNodeId, Store, createNode, floatingStrategy, gridStrategy } from 'windease';
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
+ }
32
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
+ }
33
1399
  var PITCH_LIMIT = Math.PI / 2 - 0.01;
34
1400
  var YAW_PER_PX = 8e-3;
35
1401
  var PITCH_PER_PX = 8e-3;
@@ -1242,6 +2608,69 @@ function TrialTitleBar({ title, children }) {
1242
2608
  children
1243
2609
  ] });
1244
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
+ }
1245
2674
  var NO_OP = () => {
1246
2675
  };
1247
2676
  function TrialChrome({
@@ -1251,6 +2680,7 @@ function TrialChrome({
1251
2680
  isLastTrial,
1252
2681
  undoBindings,
1253
2682
  job,
2683
+ loupe,
1254
2684
  trialChrome,
1255
2685
  chrome,
1256
2686
  suppress,
@@ -1264,6 +2694,13 @@ function TrialChrome({
1264
2694
  const updateTrialView = useStore(storeCtx.store, (s) => s.updateTrialView);
1265
2695
  const updateTrialConfig = useStore(storeCtx.store, (s) => s.updateTrialConfig);
1266
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
+ );
1267
2704
  const view2d = as2DView(record.view);
1268
2705
  const configSchema = useConfigSchema(instrument);
1269
2706
  const ctx = useMemo(() => {
@@ -1281,6 +2718,8 @@ function TrialChrome({
1281
2718
  canRedo: undoBindings?.canRedo ?? false,
1282
2719
  undo: undoBindings?.undo ?? NO_OP,
1283
2720
  redo: undoBindings?.redo ?? NO_OP,
2721
+ loupeOn: loupe?.on ?? false,
2722
+ toggleLoupe: loupe?.toggle ?? NO_OP,
1284
2723
  configSchema,
1285
2724
  configFields: instrument.config ? [] : instrument.configSchema?.() ?? [],
1286
2725
  config: record.config,
@@ -1298,6 +2737,9 @@ function TrialChrome({
1298
2737
  updateTrialState(trialId, nextState);
1299
2738
  }
1300
2739
  },
2740
+ undockedPanels: undockedIds,
2741
+ undockPanel: (sectionId, as) => undockPanelAction(trialId, sectionId, as),
2742
+ dockPanel: (sectionId) => dockPanelAction(trialId, sectionId),
1301
2743
  savedSnapshots: lab.savedSnapshots.filter((s) => s.trialId === trialId),
1302
2744
  saveSnapshot: (name) => lab.saveSnapshot(trialId, name),
1303
2745
  loadSnapshot: (snapshotId) => lab.loadSnapshot(trialId, snapshotId),
@@ -1317,10 +2759,14 @@ function TrialChrome({
1317
2759
  updateTrialConfig,
1318
2760
  updateTrialState,
1319
2761
  undoBindings,
2762
+ loupe,
1320
2763
  view2d,
1321
2764
  activeToolId,
1322
2765
  setActiveTool,
1323
- configSchema
2766
+ configSchema,
2767
+ undockedIds,
2768
+ undockPanelAction,
2769
+ dockPanelAction
1324
2770
  ]);
1325
2771
  const contributions = useMemo(
1326
2772
  () => suppressContributions(
@@ -1359,7 +2805,17 @@ function TrialChrome({
1359
2805
  /* @__PURE__ */ jsx("div", { className: "lk-trial__toolbar", children: /* @__PURE__ */ jsx(ToolbarRegion, { contributions: inRegion("toolbar"), ctx }) }),
1360
2806
  /* @__PURE__ */ jsxs("div", { className: "lk-trial__body", children: [
1361
2807
  /* @__PURE__ */ jsx(PaletteRegion, { contributions: inRegion("palette"), ctx }),
1362
- /* @__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
+ ] }),
1363
2819
  /* @__PURE__ */ jsxs("div", { className: `lk-trial__content${instrument.canvas ? " lk-trial__content--flush" : ""}`, children: [
1364
2820
  children,
1365
2821
  /* @__PURE__ */ jsx(ViewportRegion, { contributions: inRegion("viewport"), ctx })
@@ -1405,10 +2861,12 @@ function Trial({ id, chrome, suppress }) {
1405
2861
  }
1406
2862
  function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1407
2863
  const canvasContainerRef = useRef(null);
2864
+ const loupeHostRef = useRef(null);
1408
2865
  const updateTrialState = useStore(store, (s) => s.updateTrialState);
1409
2866
  const updateTrialConfig = useStore(store, (s) => s.updateTrialConfig);
1410
2867
  const updateTrialView = useStore(store, (s) => s.updateTrialView);
1411
2868
  const updateTrialUndoStack = useStore(store, (s) => s.updateTrialUndoStack);
2869
+ const updateTrialAnnotations = useStore(store, (s) => s.updateTrialAnnotations);
1412
2870
  const labToolId = useStore(store, (s) => s.activeToolId);
1413
2871
  const setLabTool = useStore(store, (s) => s.setLabTool);
1414
2872
  const setTrialTool = useStore(store, (s) => s.setTrialTool);
@@ -1417,6 +2875,11 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1417
2875
  const bus = busRef.current;
1418
2876
  const [layerVisibility, setLayerVisibility] = useState({});
1419
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
+ );
1420
2883
  const undoCap = instrument.undo;
1421
2884
  const undoEvents = useMemo(() => new Set(undoCap?.snapshotOn ?? ["state.change"]), [undoCap]);
1422
2885
  const maxDepth = undoCap?.maxDepth ?? 50;
@@ -1428,8 +2891,69 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1428
2891
  updateTrialUndoStack(record.id, (prev) => pushSnapshot(prev, snap, maxDepth));
1429
2892
  };
1430
2893
  const setView = (v) => updateTrialView(record.id, v);
1431
- const declaresTools = instrument.tools != null;
1432
- 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;
1433
2957
  const setActiveTool = (id) => {
1434
2958
  if (declaresTools) setTrialTool(record.id, id);
1435
2959
  else setLabTool(id);
@@ -1468,7 +2992,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1468
2992
  if (!view2d) return;
1469
2993
  updateTrialView(record.id, { ...view2d, zoom: z });
1470
2994
  },
1471
- activeToolId: resolvedToolId
2995
+ activeToolId: resolvedToolId,
2996
+ visibleLayers
1472
2997
  },
1473
2998
  emit: (event) => {
1474
2999
  snapshotIfNeeded(event);
@@ -1476,22 +3001,36 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1476
3001
  },
1477
3002
  job: jobCap ? job : void 0
1478
3003
  };
1479
- const undoBindings = undoCap ? {
1480
- canUndo: record.undoStack.past.length > 0,
1481
- 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),
1482
3019
  undo: () => {
1483
- const result = undo(record.undoStack, structuredClone(record.state));
1484
- if (!result) return;
1485
- updateTrialState(record.id, result.snapshot);
1486
- updateTrialUndoStack(record.id, result.stack);
3020
+ if (annotationsCap && annotations.undo()) return;
3021
+ if (undoCap) undoState();
1487
3022
  },
1488
3023
  redo: () => {
1489
- const result = redo(record.undoStack, structuredClone(record.state));
1490
- if (!result) return;
1491
- updateTrialState(record.id, result.snapshot);
1492
- updateTrialUndoStack(record.id, result.stack);
3024
+ if (annotationsCap && annotations.redo()) return;
3025
+ if (undoCap) redoState();
1493
3026
  }
1494
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;
1495
3034
  const canvasLayers = useMemo(() => {
1496
3035
  if (!instrument.canvas) return [];
1497
3036
  const baseLayers = instrument.canvas.layers;
@@ -1499,9 +3038,8 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1499
3038
  return ordered.map((layer) => ({
1500
3039
  id: layer.id,
1501
3040
  visible: layerVisibility[layer.id] !== false,
1502
- render: (ctx, view) => {
1503
- ctx.translate(view.pan.x, view.pan.y);
1504
- ctx.scale(view.zoom, view.zoom);
3041
+ render: (ctx, view, frame) => {
3042
+ applyCamera(ctx, view, frame);
1505
3043
  layer.draw(ctx, { state: record.state, config: record.config, zoom: view.zoom });
1506
3044
  }
1507
3045
  }));
@@ -1514,6 +3052,7 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1514
3052
  capability: instrument.dragDrop ?? { palette: [], onDrop: (_p, _i, s) => s },
1515
3053
  canvasContainerRef,
1516
3054
  view: view2d ?? DEFAULT_VIEW,
3055
+ worldSpec: instrument.canvas?.worldSpec,
1517
3056
  state: record.state,
1518
3057
  config: record.config,
1519
3058
  setState: (next) => {
@@ -1554,22 +3093,44 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1554
3093
  }
1555
3094
  ];
1556
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;
1557
3108
  let body;
1558
3109
  if (instrument.canvas) {
1559
3110
  body = /* @__PURE__ */ jsxs("div", { ref: canvasContainerRef, className: "lk-trial__canvas-host", children: [
1560
- /* @__PURE__ */ jsx(
3111
+ /* @__PURE__ */ jsxs(
1561
3112
  CanvasStack,
1562
3113
  {
1563
3114
  layers: layersWithFeedback,
1564
3115
  view: view2d ?? DEFAULT_VIEW,
1565
3116
  onViewChange: setView,
3117
+ worldSpec: instrument.canvas.worldSpec,
3118
+ onResize: placeView,
1566
3119
  minZoom: instrument.canvas.minZoom,
1567
3120
  maxZoom: instrument.canvas.maxZoom,
1568
- children: instrument.render(renderCtx)
3121
+ children: [
3122
+ instrument.render(renderCtx),
3123
+ lens
3124
+ ]
1569
3125
  }
1570
3126
  ),
1571
3127
  /* @__PURE__ */ jsx(DragOverlay, { drag: dragDropResult.drag })
1572
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
+ ] });
1573
3134
  } else {
1574
3135
  body = instrument.render(renderCtx);
1575
3136
  }
@@ -1614,10 +3175,21 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1614
3175
  }
1615
3176
  return out;
1616
3177
  }, [paletteNode, layerListNode]);
1617
- 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(
1618
3189
  TrialChrome,
1619
3190
  {
1620
3191
  job: jobCap ? job : void 0,
3192
+ loupe: loupeBindings,
1621
3193
  trialId: record.id,
1622
3194
  record,
1623
3195
  instrument,
@@ -1628,9 +3200,12 @@ function TrialRuntime({ record, instrument, store, isLast, chrome, suppress }) {
1628
3200
  suppress,
1629
3201
  activeToolId: resolvedToolId,
1630
3202
  setActiveTool,
1631
- children: body
3203
+ children: [
3204
+ body,
3205
+ annotationOverlays
3206
+ ]
1632
3207
  }
1633
- );
3208
+ ) }) });
1634
3209
  }
1635
3210
 
1636
3211
  // src/trial/trialOps.ts
@@ -1643,7 +3218,9 @@ function findInstrument(instruments, name) {
1643
3218
  return found;
1644
3219
  }
1645
3220
  function initialView(instrument) {
1646
- 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 } };
1647
3224
  }
1648
3225
  function addTrial(trials, instruments, instrumentName) {
1649
3226
  const instrument = findInstrument(instruments, instrumentName);
@@ -1716,7 +3293,7 @@ function LabHeader() {
1716
3293
  className: "lk-lab-header__add",
1717
3294
  onClick: () => lab.addTrial(only.name),
1718
3295
  children: [
1719
- /* @__PURE__ */ jsx(Rt, { size: 16 }),
3296
+ /* @__PURE__ */ jsx(C, { size: 16 }),
1720
3297
  /* @__PURE__ */ jsx("span", { children: "Add trial" })
1721
3298
  ]
1722
3299
  }
@@ -1738,7 +3315,7 @@ function LabHeader() {
1738
3315
  )
1739
3316
  ),
1740
3317
  /* @__PURE__ */ jsx(
1741
- r,
3318
+ r2,
1742
3319
  {
1743
3320
  className: "lk-lab-header__mode",
1744
3321
  ariaLabel: "Color mode",
@@ -1800,7 +3377,9 @@ function LabShell({ title, children, header, footer, mode = "auto" }) {
1800
3377
  return outer ? shell : /* @__PURE__ */ jsx(ThemeProvider, { theme: interstellarTheme, mode: resolved, children: shell });
1801
3378
  }
1802
3379
  var ZONE_ID = asNodeId("lk-workspace");
1803
- 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";
1804
3383
  var KIND = "trial";
1805
3384
  function extentOf(store, id) {
1806
3385
  const p2 = store.getNode(id)?.membership?.placement;
@@ -1810,9 +3389,14 @@ function extentOf(store, id) {
1810
3389
  if (p2.span) out.span = p2.span;
1811
3390
  return Object.keys(out).length > 0 ? out : null;
1812
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
+ }
1813
3396
  function Workspace({
1814
3397
  children,
1815
3398
  ids,
3399
+ panels,
1816
3400
  resizable = false,
1817
3401
  reorderable = false,
1818
3402
  onReorder,
@@ -1823,8 +3407,20 @@ function Workspace({
1823
3407
  viewport
1824
3408
  }) {
1825
3409
  const items = Children.toArray(children);
1826
- const idKey = ids ? ids.join(",") : `#${items.length}`;
1827
- const nodeIds = useMemo(() => items.map((_2, 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;
1828
3424
  const layoutRef = useRef(layout);
1829
3425
  layoutRef.current = layout;
1830
3426
  const onLayoutChangeRef = useRef(onLayoutChange);
@@ -1839,6 +3435,13 @@ function Workspace({
1839
3435
  container: { strategyId: "grid", config: { resizable, gap, padding } }
1840
3436
  })
1841
3437
  );
3438
+ store2.registerNode(
3439
+ createNode({
3440
+ kind: "zone",
3441
+ id: FLOAT_ZONE_ID,
3442
+ container: { strategyId: "floating", config: {} }
3443
+ })
3444
+ );
1842
3445
  storeRef.current = store2;
1843
3446
  }
1844
3447
  const store = storeRef.current;
@@ -1854,7 +3457,8 @@ function Workspace({
1854
3457
  }
1855
3458
  for (const id of nodeIds) {
1856
3459
  if (present.has(id)) continue;
1857
- 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 }));
1858
3462
  store.showNode(id);
1859
3463
  const saved = layoutRef.current?.[id];
1860
3464
  if (saved) store.patchPlacement(id, saved);
@@ -1883,7 +3487,15 @@ function Workspace({
1883
3487
  );
1884
3488
  const chrome = useMemo(() => {
1885
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
+ );
1886
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
+ },
1887
3499
  [KIND]: ({ node }) => {
1888
3500
  const content = byId.get(node.id) ?? null;
1889
3501
  if (!reorderable) return content;
@@ -1893,7 +3505,8 @@ function Workspace({
1893
3505
  );
1894
3506
  }
1895
3507
  };
1896
- }, [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)) });
1897
3510
  const grid = /* @__PURE__ */ jsx(
1898
3511
  Container,
1899
3512
  {
@@ -1905,7 +3518,15 @@ function Workspace({
1905
3518
  onChildOrderChange: reorderable ? commitOrder : void 0
1906
3519
  }
1907
3520
  );
1908
- 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 }) });
1909
3530
  }
1910
3531
  function buildStore(instruments, defaultInstrument, storage, storageKey, initialMode) {
1911
3532
  const store = createLabStore({ storageKey, storage, initialMode });
@@ -1968,6 +3589,40 @@ function Lab({
1968
3589
  const savedSnapshots = useStore(store, (s) => s.savedSnapshots);
1969
3590
  const modeValue = useStore(store, (s) => s.mode);
1970
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
+ );
1971
3626
  const resolvedMode = useResolvedMode(modeValue);
1972
3627
  useEffect(() => {
1973
3628
  if (mode && mode !== store.getState().mode) {
@@ -2047,27 +3702,50 @@ function Lab({
2047
3702
  /* @__PURE__ */ jsx(LabHeader, {}),
2048
3703
  children
2049
3704
  ] }),
2050
- children: /* @__PURE__ */ jsxs("div", { className: "lk-lab__body", children: [
2051
- tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
2052
- /* @__PURE__ */ jsx(
2053
- Workspace,
2054
- {
2055
- ids: trials.map((w2) => w2.id),
2056
- resizable: true,
2057
- reorderable: true,
2058
- onReorder: (ids) => contextValue.reorderTrials(ids),
2059
- layout,
2060
- onLayoutChange: (next) => store.getState().setLayout(next),
2061
- children: trials.map((w2) => /* @__PURE__ */ jsx(Trial, { id: w2.id, chrome, suppress }, w2.id))
2062
- }
2063
- )
2064
- ] })
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
+ )
3725
+ ),
3726
+ tools ? /* @__PURE__ */ jsx(LabPalette, { tools }) : null,
3727
+ /* @__PURE__ */ jsx(
3728
+ Workspace,
3729
+ {
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))
3738
+ }
3739
+ )
3740
+ ]
3741
+ }
3742
+ ) }) }) })
2065
3743
  }
2066
3744
  )
2067
3745
  }
2068
3746
  ) }) });
2069
3747
  }
2070
3748
 
2071
- export { DEFAULT_VIEW, Lab, LabContext, LabPalette, LabShell, PITCH_LIMIT, 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 };
2072
3750
  //# sourceMappingURL=index.js.map
2073
3751
  //# sourceMappingURL=index.js.map