@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
  2. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
  3. package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
  4. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
  5. package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
  6. package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
  7. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
  8. package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
  9. package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
  10. package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
  11. package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
  12. package/dist/canvas/index.d.ts +43 -25
  13. package/dist/canvas/index.js +5 -3
  14. package/dist/chrome/index.d.ts +9 -6
  15. package/dist/chrome/index.js +7 -6
  16. package/dist/chunk-2AYGEN57.js +32 -0
  17. package/dist/chunk-2AYGEN57.js.map +1 -0
  18. package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
  19. package/dist/chunk-3P5TTTA7.js.map +1 -0
  20. package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
  21. package/dist/chunk-3ZELRRBV.js.map +1 -0
  22. package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
  23. package/dist/chunk-4EY67BKJ.js.map +1 -0
  24. package/dist/chunk-5TCEVMPI.js +140 -0
  25. package/dist/chunk-5TCEVMPI.js.map +1 -0
  26. package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
  27. package/dist/chunk-6JMU56CU.js.map +1 -0
  28. package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
  29. package/dist/chunk-CQLQPQ4P.js.map +1 -0
  30. package/dist/chunk-CTRKTLYZ.js +33 -0
  31. package/dist/chunk-CTRKTLYZ.js.map +1 -0
  32. package/dist/chunk-EQIB2PGC.js +458 -0
  33. package/dist/chunk-EQIB2PGC.js.map +1 -0
  34. package/dist/chunk-HEORBLVT.js +7128 -0
  35. package/dist/chunk-HEORBLVT.js.map +1 -0
  36. package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
  37. package/dist/chunk-JDDIZSLL.js.map +1 -0
  38. package/dist/chunk-LITE5YFB.js +669 -0
  39. package/dist/chunk-LITE5YFB.js.map +1 -0
  40. package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
  41. package/dist/chunk-ROQDRXKW.js.map +1 -0
  42. package/dist/chunk-SBXNQW4G.js +3 -0
  43. package/dist/chunk-SBXNQW4G.js.map +1 -0
  44. package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
  45. package/dist/chunk-XJ6N32QP.js.map +1 -0
  46. package/dist/chunk-YQJNT4XQ.js +293 -0
  47. package/dist/chunk-YQJNT4XQ.js.map +1 -0
  48. package/dist/controls/index.d.ts +4 -28
  49. package/dist/controls/index.js +3 -3
  50. package/dist/dragdrop/index.d.ts +50 -5
  51. package/dist/dragdrop/index.js +3 -1
  52. package/dist/index.d.ts +660 -302
  53. package/dist/index.js +1793 -587
  54. package/dist/index.js.map +1 -1
  55. package/dist/layers/index.d.ts +6 -5
  56. package/dist/layers/index.js +4 -2
  57. package/dist/loupe/index.d.ts +187 -0
  58. package/dist/loupe/index.js +7 -0
  59. package/dist/loupe/index.js.map +1 -0
  60. package/dist/passthrough/weasel-canvas.d.ts +3 -1
  61. package/dist/passthrough/weasel-canvas.js +1 -1
  62. package/dist/passthrough/weasel-ui.d.ts +151 -91
  63. package/dist/passthrough/weasel-ui.js +3 -2
  64. package/dist/primitives/index.d.ts +3 -1
  65. package/dist/primitives/index.js +6 -5
  66. package/dist/state/index.d.ts +3 -3
  67. package/dist/state/index.js +2 -2
  68. package/dist/styles.css +229 -703
  69. package/dist/surface/index.d.ts +18 -2
  70. package/dist/surface/index.js +2 -2
  71. package/dist/ui/layers/index.d.ts +8 -2
  72. package/dist/ui/layers/index.js +2 -4
  73. package/dist/undo/index.d.ts +5 -4
  74. package/package.json +14 -7
  75. package/src/annotations/AnnotationOverlay.tsx +205 -0
  76. package/src/annotations/AnnotationTargets.tsx +37 -0
  77. package/src/annotations/Annotations.less +129 -0
  78. package/src/annotations/Annotations.meaning.test.tsx +111 -0
  79. package/src/annotations/Annotations.overlay.test.tsx +142 -0
  80. package/src/annotations/AnnotationsContext.ts +49 -0
  81. package/src/annotations/ExportMenu.test.tsx +86 -0
  82. package/src/annotations/ExportMenu.tsx +182 -0
  83. package/src/annotations/MarkList.tsx +75 -0
  84. package/src/annotations/capture.test.ts +140 -0
  85. package/src/annotations/capture.ts +237 -0
  86. package/src/annotations/drawOne.test.ts +56 -0
  87. package/src/annotations/drawOne.ts +47 -0
  88. package/src/annotations/frac.test.ts +59 -0
  89. package/src/annotations/frac.ts +66 -0
  90. package/src/annotations/history.test.ts +82 -0
  91. package/src/annotations/history.ts +118 -0
  92. package/src/annotations/index.ts +37 -0
  93. package/src/annotations/paint.test.ts +125 -0
  94. package/src/annotations/paint.ts +130 -0
  95. package/src/annotations/staleness.test.ts +56 -0
  96. package/src/annotations/staleness.ts +41 -0
  97. package/src/annotations/store.test.ts +175 -0
  98. package/src/annotations/store.ts +317 -0
  99. package/src/annotations/svgNodes.test.ts +77 -0
  100. package/src/annotations/svgNodes.ts +74 -0
  101. package/src/annotations/toolMap.test.ts +36 -0
  102. package/src/annotations/toolMap.ts +54 -0
  103. package/src/annotations/types.ts +222 -0
  104. package/src/annotations/view.test.ts +37 -0
  105. package/src/annotations/view.ts +40 -0
  106. package/src/canvas/AGENTS.md +55 -5
  107. package/src/canvas/CanvasStack.test.tsx +27 -1
  108. package/src/canvas/CanvasStack.tsx +29 -4
  109. package/src/canvas/CanvasStackContext.ts +23 -4
  110. package/src/canvas/camera.test.ts +78 -0
  111. package/src/canvas/camera.ts +62 -0
  112. package/src/canvas/canvasCoords.test.ts +26 -0
  113. package/src/canvas/canvasCoords.ts +20 -7
  114. package/src/canvas/index.ts +5 -1
  115. package/src/canvas/useLayerScheduler.ts +7 -3
  116. package/src/canvas/usePanZoom.test.ts +36 -5
  117. package/src/canvas/usePanZoom.ts +20 -15
  118. package/src/canvas/worldSpec.test.ts +63 -0
  119. package/src/canvas/worldSpec.ts +48 -0
  120. package/src/chrome/builtins.test.ts +31 -1
  121. package/src/chrome/builtins.tsx +63 -19
  122. package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
  123. package/src/chrome/regions/PaletteRegion.tsx +4 -0
  124. package/src/chrome/regions/SidebarRegion.tsx +37 -13
  125. package/src/chrome/regions/ToolbarRegion.tsx +2 -1
  126. package/src/chrome/regions/ViewportRegion.tsx +9 -1
  127. package/src/chrome/regions/regions.test.tsx +6 -1
  128. package/src/chrome/types.ts +20 -0
  129. package/src/config/builder.test.ts +7 -1
  130. package/src/config/builder.ts +1 -5
  131. package/src/config/resolve.test.ts +1 -2
  132. package/src/controls/ControlPanel.stories.tsx +41 -2
  133. package/src/controls/ControlPanel.test.tsx +69 -9
  134. package/src/controls/ControlPanel.tsx +106 -25
  135. package/src/dragdrop/DragDropRuntime.tsx +8 -2
  136. package/src/dragdrop/dragDrop.test.tsx +29 -2
  137. package/src/dragdrop/index.ts +11 -0
  138. package/src/index.test.ts +22 -0
  139. package/src/index.ts +136 -4
  140. package/src/instrument/SineWave.smoke.test.tsx +1 -0
  141. package/src/instrument/types.ts +24 -1
  142. package/src/lab/Lab.surface.test.tsx +162 -0
  143. package/src/lab/Lab.tsx +102 -17
  144. package/src/lab/Lab.undock.test.tsx +85 -0
  145. package/src/lab/LabFullChrome.stories.tsx +20 -0
  146. package/src/lab/LabShell.less +11 -0
  147. package/src/lab/Workspace.less +73 -0
  148. package/src/lab/Workspace.surface.test.tsx +2 -0
  149. package/src/lab/Workspace.tsx +97 -8
  150. package/src/lab/index.ts +1 -1
  151. package/src/lab/panelHost.ts +37 -0
  152. package/src/layers/AGENTS.md +1 -1
  153. package/src/layers/LayerList.tsx +1 -1
  154. package/src/loupe/AGENTS.md +49 -0
  155. package/src/loupe/CanvasLoupe.tsx +83 -0
  156. package/src/loupe/DomLoupe.tsx +62 -0
  157. package/src/loupe/Loupe.less +38 -0
  158. package/src/loupe/LoupeBubble.tsx +32 -0
  159. package/src/loupe/TrialLoupe.tsx +93 -0
  160. package/src/loupe/canvasLens.test.ts +205 -0
  161. package/src/loupe/canvasLens.ts +141 -0
  162. package/src/loupe/index.ts +19 -0
  163. package/src/loupe/types.test.ts +29 -0
  164. package/src/loupe/types.ts +90 -0
  165. package/src/loupe/useHostSize.ts +30 -0
  166. package/src/loupe/useLoupe.test.tsx +197 -0
  167. package/src/loupe/useLoupe.ts +189 -0
  168. package/src/passthrough/weasel-ui.ts +25 -17
  169. package/src/primitives/FloatingPanel.test.tsx +76 -1
  170. package/src/primitives/FloatingPanel.tsx +19 -3
  171. package/src/primitives/ScaleIndicator.test.tsx +7 -2
  172. package/src/primitives/Sidebar.less +18 -0
  173. package/src/primitives/Toolbar.less +8 -0
  174. package/src/primitives/Toolbar.tsx +5 -0
  175. package/src/primitives/useRovingTabIndex.test.ts +21 -0
  176. package/src/primitives/useRovingTabIndex.ts +26 -17
  177. package/src/state/document.test.ts +23 -0
  178. package/src/state/document.ts +14 -2
  179. package/src/state/index.ts +2 -0
  180. package/src/state/store.test.ts +24 -0
  181. package/src/state/store.ts +35 -1
  182. package/src/state/types.ts +11 -0
  183. package/src/state/undock.test.ts +40 -0
  184. package/src/state/undock.ts +39 -0
  185. package/src/styles.less +2 -4
  186. package/src/surface/SurfaceContext.ts +4 -0
  187. package/src/surface/index.ts +8 -3
  188. package/src/surface/useSurfaceTile.test.tsx +2 -0
  189. package/src/surface/useSurfaceTile.ts +7 -1
  190. package/src/surface/useTiledSurface.test.tsx +58 -0
  191. package/src/surface/useTiledSurface.ts +38 -3
  192. package/src/theme/Interstellar.stories.tsx +1 -1
  193. package/src/trial/Trial.annotations.persist.test.tsx +166 -0
  194. package/src/trial/Trial.annotations.test.tsx +121 -0
  195. package/src/trial/Trial.canvas.test.tsx +76 -1
  196. package/src/trial/Trial.config.test.tsx +3 -8
  197. package/src/trial/Trial.less +19 -0
  198. package/src/trial/Trial.loupe.test.tsx +128 -0
  199. package/src/trial/Trial.stories.tsx +1 -0
  200. package/src/trial/Trial.test.tsx +12 -0
  201. package/src/trial/Trial.trialId.test.tsx +36 -0
  202. package/src/trial/Trial.tsx +215 -40
  203. package/src/trial/TrialChrome.tsx +35 -0
  204. package/src/trial/UndockedSections.tsx +65 -0
  205. package/src/trial/trialOps.test.ts +21 -0
  206. package/src/trial/trialOps.ts +5 -3
  207. package/src/ui/layers/index.ts +5 -1
  208. package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
  209. package/dist/chunk-4TMMVIDM.js.map +0 -1
  210. package/dist/chunk-CPUJ3QXL.js.map +0 -1
  211. package/dist/chunk-HFDVGF4X.js.map +0 -1
  212. package/dist/chunk-NS54R52R.js.map +0 -1
  213. package/dist/chunk-O2BULFHX.js +0 -152
  214. package/dist/chunk-O2BULFHX.js.map +0 -1
  215. package/dist/chunk-O7NVSCLN.js.map +0 -1
  216. package/dist/chunk-PO6L3NSH.js +0 -366
  217. package/dist/chunk-PO6L3NSH.js.map +0 -1
  218. package/dist/chunk-PQJ5B2U2.js +0 -27
  219. package/dist/chunk-PQJ5B2U2.js.map +0 -1
  220. package/dist/chunk-RVNN4CHQ.js.map +0 -1
  221. package/dist/chunk-TLGRYALP.js +0 -6151
  222. package/dist/chunk-TLGRYALP.js.map +0 -1
  223. package/dist/chunk-V7PRSIRQ.js.map +0 -1
  224. package/dist/chunk-Z6HJ5FEM.js.map +0 -1
  225. package/dist/chunk-ZZAYVX4X.js +0 -505
  226. package/dist/chunk-ZZAYVX4X.js.map +0 -1
  227. package/src/primitives/DragHandleGlyph.tsx +0 -29
  228. package/src/ui/format.test.ts +0 -33
  229. package/src/ui/format.ts +0 -30
  230. package/src/ui/layers/LayerStack.less +0 -143
  231. package/src/ui/layers/LayerStack.stories.tsx +0 -45
  232. package/src/ui/layers/LayerStack.test.tsx +0 -170
  233. package/src/ui/layers/LayerStack.tsx +0 -208
  234. package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
  235. package/src/ui/properties/ColorRow.stories.tsx +0 -106
  236. package/src/ui/properties/CurveField.less +0 -48
  237. package/src/ui/properties/CurveField.stories.tsx +0 -20
  238. package/src/ui/properties/CurveField.test.tsx +0 -127
  239. package/src/ui/properties/CurveField.tsx +0 -184
  240. package/src/ui/properties/EffectCard.tsx +0 -324
  241. package/src/ui/properties/Gallery.stories.tsx +0 -178
  242. package/src/ui/properties/NumberRow.stories.tsx +0 -65
  243. package/src/ui/properties/PropertyGroup.less +0 -50
  244. package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
  245. package/src/ui/properties/PropertyGroup.test.tsx +0 -33
  246. package/src/ui/properties/PropertyGroup.tsx +0 -45
  247. package/src/ui/properties/PropertyList.stories.tsx +0 -83
  248. package/src/ui/properties/PropertyPanel.less +0 -606
  249. package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
  250. package/src/ui/properties/PropertyPanel.test.tsx +0 -193
  251. package/src/ui/properties/PropertyPanel.tsx +0 -469
  252. package/src/ui/properties/PropertyRow.stories.tsx +0 -53
  253. package/src/ui/properties/SelectRow.stories.tsx +0 -58
  254. package/src/ui/properties/SliderRow.stories.tsx +0 -74
  255. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
  256. package/src/ui/properties/TextRow.stories.tsx +0 -62
  257. package/src/ui/properties/ToggleRow.stories.tsx +0 -57
  258. package/src/ui/properties/index.ts +0 -39
  259. package/src/ui/properties/storyLayouts.tsx +0 -36
@@ -1,50 +1,68 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import * as react from 'react';
3
2
  import { ReactNode } from 'react';
4
- import { V as ViewTransform, g as Point } from '../_dts/index-DgcNdEdh.js';
5
- import '../_dts/types-AHlQxBNN.js';
6
- import '../_dts/PrefsForm-BYa6cWnO.js';
7
- import '../_dts/types-1Sdxy_Pv.js';
3
+ import { V as ViewTransform, c as WorldSpec, a2 as ViewportSize, a as Point, W as WorldFrame } from '../_dts/frac-Cp3NivlC.js';
4
+ export { x as DEFAULT_FRAME, a4 as applyCamera, aa as resolveFrame } from '../_dts/frac-Cp3NivlC.js';
5
+ import { C as CanvasLayerDescriptor } from '../_dts/CanvasStackContext-DtAn_3O-.js';
6
+ export { a as CanvasStackContext, b as CanvasStackContextValue, c as CanvasStackSurface } from '../_dts/CanvasStackContext-DtAn_3O-.js';
7
+ import '../_dts/types-D6s4b7if.js';
8
+ import '../_dts/PrefsForm-BkUJZx0A.js';
9
+ import '../_dts/types-DYMaEvM5.js';
8
10
  import '../_dts/types-DJ79Tg5J.js';
9
-
10
- /** One layer of a canvas stack: its id, whether it is currently shown, and how
11
- * it paints itself. */
12
- interface CanvasLayerDescriptor {
13
- id: string;
14
- visible: boolean;
15
- render: (ctx: CanvasRenderingContext2D, view: ViewTransform) => void;
16
- }
11
+ import '../_dts/types-C-gh9Ap-.js';
17
12
 
18
13
  /** Props for `<CanvasStack>`. */
19
14
  interface CanvasStackProps {
20
15
  layers: CanvasLayerDescriptor[];
21
16
  view: ViewTransform;
22
17
  onViewChange: (v: ViewTransform) => void;
18
+ /** The instrument's coordinate system. Omitted, world (0,0) sits at the
19
+ * element's top-left with y running down. */
20
+ worldSpec?: WorldSpec;
23
21
  minZoom?: number;
24
22
  maxZoom?: number;
25
23
  width?: number | string;
26
24
  height?: number | string;
27
25
  className?: string;
26
+ /** Fired whenever the stack is measured, so a consumer can place a view that
27
+ * only makes sense in terms of the viewport. */
28
+ onResize?: (size: ViewportSize) => void;
28
29
  onHitTest?: (worldPos: Point) => void;
29
30
  children?: ReactNode;
30
31
  }
31
32
  /** Stacks one `<canvas>` per layer and drives them from a shared view, so a
32
33
  * layer that changes rarely is not redrawn with one that changes every frame.
33
34
  * Handles sizing, device pixel ratio, and pan/zoom. */
34
- declare function CanvasStack({ layers, view, onViewChange, minZoom, maxZoom, width, height, className, onHitTest, children, }: CanvasStackProps): react_jsx_runtime.JSX.Element;
35
+ declare function CanvasStack({ layers, view, onViewChange, worldSpec, minZoom, maxZoom, width, height, className, onResize, onHitTest, children, }: CanvasStackProps): react_jsx_runtime.JSX.Element;
35
36
 
36
- /** What a canvas stack publishes to its children — currently the view, so
37
- * DOM overlays can position themselves in the same coordinates. */
38
- interface CanvasStackContextValue {
39
- view: ViewTransform;
37
+ /** Bounds and coordinate system a {@link zoomAt} works in. */
38
+ interface ZoomAtOptions {
39
+ /** The instrument's coordinate system, resolved against the viewport.
40
+ * Omitted, `at` is measured from the element's top-left. */
41
+ frame?: WorldFrame;
42
+ min?: number;
43
+ max?: number;
40
44
  }
41
- /** Context carrying the surrounding canvas stack's view. */
42
- declare const CanvasStackContext: react.Context<CanvasStackContextValue | null>;
45
+ /**
46
+ * Zoom a view by `factor` about a fixed point, so whatever world coordinate
47
+ * `at` was over stays under it.
48
+ *
49
+ * `at` is in the element's own pixels — a cursor position, or the centre of a
50
+ * lens. The clamp is applied before the pan is solved, so a zoom that hits a
51
+ * bound still anchors on the zoom it actually reached.
52
+ */
53
+ declare function zoomAt(view: ViewTransform, factor: number, at: Point, { frame, min, max }?: ZoomAtOptions): ViewTransform;
54
+ /** The view that puts a world point at the centre of a `size` viewport, at
55
+ * `zoom`. What a lens showing a region around one point renders through. */
56
+ declare function centerOn(world: Point, zoom: number, size: ViewportSize, frame?: WorldFrame): ViewTransform;
43
57
 
44
- /** Project a world point into screen coordinates under a view. */
45
- declare function worldToScreen(world: Point, view: ViewTransform): Point;
58
+ /** Project a world point into screen coordinates under a view.
59
+ *
60
+ * `frame` is the instrument's declared coordinate system, resolved against the
61
+ * viewport; omitting it gives labkit's original one — origin at the top-left,
62
+ * y downward. */
63
+ declare function worldToScreen(world: Point, view: ViewTransform, frame?: WorldFrame): Point;
46
64
  /** Unproject a screen point back to world coordinates under a view. */
47
- declare function screenToWorld(screen: Point, view: ViewTransform): Point;
65
+ declare function screenToWorld(screen: Point, view: ViewTransform, frame?: WorldFrame): Point;
48
66
 
49
- export { CanvasStack, CanvasStackContext, screenToWorld, worldToScreen };
50
- export type { CanvasLayerDescriptor, CanvasStackContextValue, CanvasStackProps };
67
+ export { CanvasLayerDescriptor, CanvasStack, ViewportSize, WorldFrame, WorldSpec, centerOn, screenToWorld, worldToScreen, zoomAt };
68
+ export type { CanvasStackProps, ZoomAtOptions };
@@ -1,5 +1,7 @@
1
- export { CanvasStack, screenToWorld, worldToScreen } from '../chunk-NS54R52R.js';
2
- export { CanvasStackContext } from '../chunk-CPUJ3QXL.js';
3
- import '../chunk-Z6HJ5FEM.js';
1
+ export { CanvasStack } from '../chunk-3ZELRRBV.js';
2
+ export { centerOn, zoomAt } from '../chunk-CTRKTLYZ.js';
3
+ export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen } from '../chunk-2AYGEN57.js';
4
+ export { CanvasStackContext } from '../chunk-CQLQPQ4P.js';
5
+ import '../chunk-ROQDRXKW.js';
4
6
  //# sourceMappingURL=index.js.map
5
7
  //# sourceMappingURL=index.js.map
@@ -1,11 +1,12 @@
1
- import { a as ControlRenderer } from '../_dts/types-AHlQxBNN.js';
2
- import { I as Instrument, m as TrialChromeContext, T as TrialContribution } from '../_dts/index-DgcNdEdh.js';
3
- export { e as IconComponent, S as SidebarSection, h as StatusReadout, k as ToolItem, l as ToolbarItem, n as TrialRegion, o as ViewportControl } from '../_dts/index-DgcNdEdh.js';
1
+ import { c as ControlRenderer } from '../_dts/types-D6s4b7if.js';
2
+ import { I as Instrument, _ as TrialChromeContext, m as TrialContribution } from '../_dts/frac-Cp3NivlC.js';
3
+ export { z as IconComponent, O as SidebarSection, Q as StatusReadout, Y as ToolItem, Z as ToolbarItem, $ as TrialRegion, a1 as ViewportControl } from '../_dts/frac-Cp3NivlC.js';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
- import '../_dts/PrefsForm-BYa6cWnO.js';
5
+ import '../_dts/PrefsForm-BkUJZx0A.js';
6
6
  import 'react';
7
- import '../_dts/types-1Sdxy_Pv.js';
7
+ import '../_dts/types-DYMaEvM5.js';
8
8
  import '../_dts/types-DJ79Tg5J.js';
9
+ import '../_dts/types-C-gh9Ap-.js';
9
10
 
10
11
  /**
11
12
  * The contributions a trial gets from what its instrument declared. This is
@@ -44,7 +45,9 @@ interface SidebarRegionProps {
44
45
  contributions: readonly TrialContribution[];
45
46
  ctx: TrialChromeContext;
46
47
  }
47
- /** Lays a trial's `sidebar` contributions out as titled, collapsible sections. */
48
+ /** Lays a trial's `sidebar` contributions out as titled, collapsible sections.
49
+ * A section the trial has torn out is not rendered here — `Trial` portals its
50
+ * body into the workspace instead. */
48
51
  declare function SidebarRegion({ contributions, ctx }: SidebarRegionProps): react_jsx_runtime.JSX.Element | null;
49
52
 
50
53
  /** Props for `<StatusRegion>`. */
@@ -1,8 +1,9 @@
1
- export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from '../chunk-PO6L3NSH.js';
2
- import '../chunk-O7NVSCLN.js';
3
- import '../chunk-ZZAYVX4X.js';
4
- import '../chunk-TLGRYALP.js';
5
- import '../chunk-CPUJ3QXL.js';
6
- import '../chunk-Z6HJ5FEM.js';
1
+ export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from '../chunk-LITE5YFB.js';
2
+ import '../chunk-6JMU56CU.js';
3
+ import '../chunk-YQJNT4XQ.js';
4
+ import '../chunk-SBXNQW4G.js';
5
+ import '../chunk-HEORBLVT.js';
6
+ import '../chunk-CQLQPQ4P.js';
7
+ import '../chunk-ROQDRXKW.js';
7
8
  //# sourceMappingURL=index.js.map
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,32 @@
1
+ // src/canvas/worldSpec.ts
2
+ var DEFAULT_FRAME = { originPx: { x: 0, y: 0 }, yDir: 1 };
3
+ function resolveFrame(spec, size) {
4
+ if (!spec) return DEFAULT_FRAME;
5
+ const origin = spec.origin ?? { x: 0, y: 0 };
6
+ return {
7
+ originPx: { x: origin.x * size.width, y: origin.y * size.height },
8
+ yDir: spec.yAxis === "up" ? -1 : 1
9
+ };
10
+ }
11
+ function applyCamera(ctx, view, frame = DEFAULT_FRAME) {
12
+ ctx.translate(frame.originPx.x + view.pan.x, frame.originPx.y + view.pan.y);
13
+ ctx.scale(view.zoom, view.zoom * frame.yDir);
14
+ }
15
+
16
+ // src/canvas/canvasCoords.ts
17
+ function worldToScreen(world, view, frame = DEFAULT_FRAME) {
18
+ return {
19
+ x: frame.originPx.x + view.pan.x + world.x * view.zoom,
20
+ y: frame.originPx.y + view.pan.y + world.y * view.zoom * frame.yDir
21
+ };
22
+ }
23
+ function screenToWorld(screen, view, frame = DEFAULT_FRAME) {
24
+ return {
25
+ x: (screen.x - frame.originPx.x - view.pan.x) / view.zoom,
26
+ y: (screen.y - frame.originPx.y - view.pan.y) / (view.zoom * frame.yDir)
27
+ };
28
+ }
29
+
30
+ export { DEFAULT_FRAME, applyCamera, resolveFrame, screenToWorld, worldToScreen };
31
+ //# sourceMappingURL=chunk-2AYGEN57.js.map
32
+ //# sourceMappingURL=chunk-2AYGEN57.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/canvas/worldSpec.ts","../src/canvas/canvasCoords.ts"],"names":[],"mappings":";AA2BO,IAAM,aAAA,GAA4B,EAAE,QAAA,EAAU,EAAE,CAAA,EAAG,GAAG,CAAA,EAAG,CAAA,EAAE,EAAG,IAAA,EAAM,CAAA;AAEpE,SAAS,YAAA,CAAa,MAA6B,IAAA,EAAgC;AACxF,EAAA,IAAI,CAAC,MAAM,OAAO,aAAA;AAClB,EAAA,MAAM,SAAS,IAAA,CAAK,MAAA,IAAU,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAC3C,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,EAAE,CAAA,EAAG,MAAA,CAAO,CAAA,GAAI,IAAA,CAAK,KAAA,EAAO,CAAA,EAAG,MAAA,CAAO,CAAA,GAAI,IAAA,CAAK,MAAA,EAAO;AAAA,IAChE,IAAA,EAAM,IAAA,CAAK,KAAA,KAAU,IAAA,GAAO,EAAA,GAAK;AAAA,GACnC;AACF;AAIO,SAAS,WAAA,CACd,GAAA,EACA,IAAA,EACA,KAAA,GAAoB,aAAA,EACd;AACN,EAAA,GAAA,CAAI,SAAA,CAAU,KAAA,CAAM,QAAA,CAAS,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,QAAA,CAAS,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA;AAC1E,EAAA,GAAA,CAAI,MAAM,IAAA,CAAK,IAAA,EAAM,IAAA,CAAK,IAAA,GAAO,MAAM,IAAI,CAAA;AAC7C;;;ACvCO,SAAS,aAAA,CACd,KAAA,EACA,IAAA,EACA,KAAA,GAAoB,aAAA,EACb;AACP,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,MAAM,QAAA,CAAS,CAAA,GAAI,KAAK,GAAA,CAAI,CAAA,GAAI,KAAA,CAAM,CAAA,GAAI,IAAA,CAAK,IAAA;AAAA,IAClD,CAAA,EAAG,KAAA,CAAM,QAAA,CAAS,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,KAAA,CAAM,CAAA,GAAI,IAAA,CAAK,IAAA,GAAO,KAAA,CAAM;AAAA,GACjE;AACF;AAGO,SAAS,aAAA,CACd,MAAA,EACA,IAAA,EACA,KAAA,GAAoB,aAAA,EACb;AACP,EAAA,OAAO;AAAA,IACL,CAAA,EAAA,CAAI,OAAO,CAAA,GAAI,KAAA,CAAM,SAAS,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,IAAK,IAAA,CAAK,IAAA;AAAA,IACrD,CAAA,EAAA,CAAI,MAAA,CAAO,CAAA,GAAI,KAAA,CAAM,QAAA,CAAS,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAA,KAAM,IAAA,CAAK,IAAA,GAAO,KAAA,CAAM,IAAA;AAAA,GACrE;AACF","file":"chunk-2AYGEN57.js","sourcesContent":["import type { Point, ViewTransform } from '../instrument/types';\n\n/** How an instrument's world sits on the canvas, independent of the camera.\n * The defaults reproduce the convention labkit shipped before this existed:\n * world (0,0) at the element's top-left, y growing downward. */\nexport interface WorldSpec {\n /** Where world (0,0) sits at `pan` zero, as a fraction of the viewport.\n * `{x:0,y:0}` top-left (default); `{x:0.5,y:0.5}` centre. */\n origin?: Point;\n /** Which way the world y axis runs on screen. Default `'down'`. */\n yAxis?: 'down' | 'up';\n}\n\n/** A viewport's CSS-pixel size. */\nexport interface ViewportSize {\n width: number;\n height: number;\n}\n\n/** A `WorldSpec` resolved against a viewport: what the coordinate helpers,\n * the camera and the wheel all read. */\nexport interface WorldFrame {\n /** Screen position of world (0,0) at `pan` zero. */\n originPx: Point;\n yDir: 1 | -1;\n}\n\nexport const DEFAULT_FRAME: WorldFrame = { originPx: { x: 0, y: 0 }, yDir: 1 };\n\nexport function resolveFrame(spec: WorldSpec | undefined, size: ViewportSize): WorldFrame {\n if (!spec) return DEFAULT_FRAME;\n const origin = spec.origin ?? { x: 0, y: 0 };\n return {\n originPx: { x: origin.x * size.width, y: origin.y * size.height },\n yDir: spec.yAxis === 'up' ? -1 : 1,\n };\n}\n\n/** Put a 2D context into the instrument's world coordinates, so a layer's\n * `draw` works in them. Must stay the exact inverse of `screenToWorld`. */\nexport function applyCamera(\n ctx: CanvasRenderingContext2D,\n view: ViewTransform,\n frame: WorldFrame = DEFAULT_FRAME,\n): void {\n ctx.translate(frame.originPx.x + view.pan.x, frame.originPx.y + view.pan.y);\n ctx.scale(view.zoom, view.zoom * frame.yDir);\n}\n","import type { Point, ViewTransform } from '../instrument/types';\nimport { DEFAULT_FRAME, type WorldFrame } from './worldSpec';\n\n/** Project a world point into screen coordinates under a view.\n *\n * `frame` is the instrument's declared coordinate system, resolved against the\n * viewport; omitting it gives labkit's original one — origin at the top-left,\n * y downward. */\nexport function worldToScreen(\n world: Point,\n view: ViewTransform,\n frame: WorldFrame = DEFAULT_FRAME,\n): Point {\n return {\n x: frame.originPx.x + view.pan.x + world.x * view.zoom,\n y: frame.originPx.y + view.pan.y + world.y * view.zoom * frame.yDir,\n };\n}\n\n/** Unproject a screen point back to world coordinates under a view. */\nexport function screenToWorld(\n screen: Point,\n view: ViewTransform,\n frame: WorldFrame = DEFAULT_FRAME,\n): Point {\n return {\n x: (screen.x - frame.originPx.x - view.pan.x) / view.zoom,\n y: (screen.y - frame.originPx.y - view.pan.y) / (view.zoom * frame.yDir),\n };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { Zt, Qt } from './chunk-TLGRYALP.js';
1
+ import { ae, oe } from './chunk-HEORBLVT.js';
2
2
  import { useRef, useMemo, useState, useEffect } from 'react';
3
3
  import { floatingStrategy, DEFAULT_INSET, DEFAULT_ANCHOR, FLOATING_DRAG_PREFIX } from 'windease';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -9,6 +9,7 @@ function borderBoxOf(entry) {
9
9
  if (box) return { w: box.inlineSize, h: box.blockSize };
10
10
  return { w: entry.contentRect.width, h: entry.contentRect.height };
11
11
  }
12
+ var DRAG_THRESHOLD = 3;
12
13
  function isInteractive(target) {
13
14
  if (!(target instanceof Element)) return false;
14
15
  return target.closest("input, button, a, select, textarea, [data-no-drag]") !== null;
@@ -77,15 +78,21 @@ function FloatingPanel({
77
78
  options
78
79
  }).placements.get(ITEM_ID);
79
80
  const dragging = useRef(null);
81
+ const pending = useRef(null);
80
82
  const [isDragging, setDragging] = useState(false);
81
83
  const onPointerDown = (e) => {
82
84
  if (isInteractive(e.target)) return;
83
85
  e.stopPropagation();
84
- dragging.current = { x: e.clientX, y: e.clientY };
85
- setDragging(true);
86
- e.currentTarget.setPointerCapture?.(e.pointerId);
86
+ pending.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId };
87
87
  };
88
88
  const onPointerMove = (e) => {
89
+ const armed = pending.current;
90
+ if (armed && armed.pointerId === e.pointerId && !dragging.current) {
91
+ if (Math.hypot(e.clientX - armed.x, e.clientY - armed.y) < DRAG_THRESHOLD) return;
92
+ dragging.current = { x: armed.x, y: armed.y };
93
+ setDragging(true);
94
+ e.currentTarget.setPointerCapture?.(e.pointerId);
95
+ }
89
96
  const from = dragging.current;
90
97
  if (!from) return;
91
98
  const dx = e.clientX - from.x;
@@ -105,6 +112,7 @@ function FloatingPanel({
105
112
  );
106
113
  };
107
114
  const endDrag = (e) => {
115
+ pending.current = null;
108
116
  if (!dragging.current) return;
109
117
  dragging.current = null;
110
118
  setDragging(false);
@@ -160,12 +168,12 @@ function JobProgress({ job, className }) {
160
168
  determinate ? ` / ${total}` : ""
161
169
  ] }),
162
170
  failures.length > 0 && /* @__PURE__ */ jsxs("span", { className: "lk-job__failures", title: failures.map((f) => f.error).join("\n"), children: [
163
- /* @__PURE__ */ jsx(Zt, { size: 14 }),
171
+ /* @__PURE__ */ jsx(ae, { size: 14 }),
164
172
  failures.length,
165
173
  " failed"
166
174
  ] }),
167
175
  error && /* @__PURE__ */ jsxs("span", { className: "lk-job__error", title: error, children: [
168
- /* @__PURE__ */ jsx(Qt, { size: 14 }),
176
+ /* @__PURE__ */ jsx(oe, { size: 14 }),
169
177
  error
170
178
  ] }),
171
179
  status === "running" && /* @__PURE__ */ jsx("button", { type: "button", className: "lk-job__cancel", onClick: job.cancel, children: "Cancel" })
@@ -208,5 +216,5 @@ function Sidebar({ children, title, collapsed = false, onToggle }) {
208
216
  }
209
217
 
210
218
  export { FloatingPanel, JobProgress, Legend, Sidebar };
211
- //# sourceMappingURL=chunk-RVNN4CHQ.js.map
212
- //# sourceMappingURL=chunk-RVNN4CHQ.js.map
219
+ //# sourceMappingURL=chunk-3P5TTTA7.js.map
220
+ //# sourceMappingURL=chunk-3P5TTTA7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/primitives/FloatingPanel.tsx","../src/primitives/JobProgress.tsx","../src/primitives/Legend.tsx","../src/primitives/Sidebar.tsx"],"names":["jsx","jsxs"],"mappings":"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","file":"chunk-3P5TTTA7.js","sourcesContent":["import type React from 'react';\nimport { type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n type Corner,\n DEFAULT_ANCHOR,\n DEFAULT_INSET,\n FLOATING_DRAG_PREFIX,\n type FloatingPlacement,\n floatingStrategy,\n} from 'windease';\n\n/** The strategy holds a map keyed by item id; a panel is always a lone item. */\nconst ITEM_ID = 'panel';\n\n/** Props for `<FloatingPanel>`. */\nexport interface FloatingPanelProps {\n children: ReactNode;\n /** Corner it rests in before it has ever been dragged. Default `'bottom-left'`. */\n anchor?: Corner;\n /** Corners that may capture it. Default: all four. */\n snapCorners?: readonly Corner[];\n /** Pixels in from a corner when snapped. Default 12. */\n inset?: number;\n /** localStorage key to remember its position under. Omit to forget on reload. */\n storageKey?: string;\n className?: string;\n}\n\n/**\n * The panel's footprint is its border box — `contentRect` excludes padding and\n * border, which would place a panel that then overflows its corner by exactly\n * that much.\n */\nfunction borderBoxOf(entry: ResizeObserverEntry): { w: number; h: number } {\n const box = entry.borderBoxSize?.[0];\n if (box) return { w: box.inlineSize, h: box.blockSize };\n return { w: entry.contentRect.width, h: entry.contentRect.height };\n}\n\n/** How far the pointer must travel before a press becomes a drag. */\nconst DRAG_THRESHOLD = 3;\n\n/** A pointerdown on one of these is the child's, not a drag. */\nfunction isInteractive(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n return target.closest('input, button, a, select, textarea, [data-no-drag]') !== null;\n}\n\nfunction readStored(key: string | undefined): FloatingPlacement | null {\n if (!key) return null;\n try {\n const raw = localStorage.getItem(key);\n return raw ? (JSON.parse(raw) as FloatingPlacement) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * A draggable box that floats over its offset parent and snaps to its corners.\n *\n * Drives windease's `floatingStrategy` as a pure function rather than mounting a\n * windease container: a lab overlay has one item and no zone tree, so the node\n * model would be all cost. Pointer handling is therefore this component's, and\n * windease's own affordance rendering never participates.\n */\nexport function FloatingPanel({\n children,\n anchor = DEFAULT_ANCHOR,\n snapCorners,\n inset = DEFAULT_INSET,\n storageKey,\n className,\n}: FloatingPanelProps) {\n const ref = useRef<HTMLDivElement>(null);\n const strategy = useMemo(() => floatingStrategy(), []);\n const options = useMemo(() => ({ defaultAnchor: anchor, inset }), [anchor, inset]);\n\n const [container, setContainer] = useState({ w: 0, h: 0 });\n const [size, setSize] = useState({ w: 0, h: 0 });\n const [place, setPlace] = useState<FloatingPlacement>(\n () => readStored(storageKey) ?? { x: 0, y: 0, anchor },\n );\n\n useEffect(() => {\n if (!storageKey) return;\n try {\n localStorage.setItem(storageKey, JSON.stringify(place));\n } catch {\n // A full or disabled store is not worth failing a lab over.\n }\n }, [place, storageKey]);\n\n const item = useMemo(\n () => ({\n id: ITEM_ID,\n meta: { floating: true, ...(snapCorners ? { snapCorners: [...snapCorners] } : {}) },\n natural: size,\n }),\n [size, snapCorners],\n );\n\n useEffect(() => {\n const el = ref.current;\n const parent = el?.offsetParent ?? el?.parentElement;\n if (!el || !parent) return;\n // Sizes come off the entry, not the DOM: jsdom reports 0 for every\n // clientWidth/offsetWidth, and an unmeasured item is withheld from layout.\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n if (entry.target === parent) {\n setContainer({ w: entry.contentRect.width, h: entry.contentRect.height });\n } else {\n setSize(borderBoxOf(entry));\n }\n }\n });\n observer.observe(parent);\n observer.observe(el);\n return () => observer.disconnect();\n }, []);\n\n const rect = strategy\n .layout({\n items: [item],\n container,\n state: { at: { [ITEM_ID]: place }, inner: undefined },\n options,\n })\n .placements.get(ITEM_ID);\n\n const dragging = useRef<{ x: number; y: number } | null>(null);\n /** Pressed but not yet moved far enough to be a drag. */\n const pending = useRef<{ x: number; y: number; pointerId: number } | null>(null);\n const [isDragging, setDragging] = useState(false);\n\n const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n if (isInteractive(e.target)) return;\n // The canvas stack underneath owns pan/zoom on the same pointer events.\n e.stopPropagation();\n // Armed, not capturing. Capture retargets mouseup to this element, and the\n // browser then synthesizes no `click` on the child under the cursor — so\n // capturing on press makes every non-native control in the panel dead to a\n // real mouse, silently. `isInteractive` only exempts the names it lists; a\n // `role=\"button\"` span, a react-aria control or a canvas is not among them.\n pending.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId };\n };\n\n const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n const armed = pending.current;\n if (armed && armed.pointerId === e.pointerId && !dragging.current) {\n if (Math.hypot(e.clientX - armed.x, e.clientY - armed.y) < DRAG_THRESHOLD) return;\n dragging.current = { x: armed.x, y: armed.y };\n setDragging(true);\n e.currentTarget.setPointerCapture?.(e.pointerId);\n }\n const from = dragging.current;\n if (!from) return;\n const dx = e.clientX - from.x;\n const dy = e.clientY - from.y;\n if (dx === 0 && dy === 0) return;\n dragging.current = { x: e.clientX, y: e.clientY };\n setPlace(\n (prev) =>\n strategy.reduce?.(\n { at: { [ITEM_ID]: prev }, inner: undefined },\n {\n affordanceId: `${FLOATING_DRAG_PREFIX}${ITEM_ID}`,\n kind: 'drag',\n payload: { dx, dy },\n },\n { container, options, items: [item] },\n ).at[ITEM_ID] ?? prev,\n );\n };\n\n const endDrag = (e: React.PointerEvent<HTMLDivElement>) => {\n pending.current = null;\n if (!dragging.current) return;\n dragging.current = null;\n setDragging(false);\n e.currentTarget.releasePointerCapture?.(e.pointerId);\n };\n\n return (\n <div\n className={className ? `lk-floating-panel ${className}` : 'lk-floating-panel'}\n data-dragging={isDragging ? 'true' : undefined}\n data-placed={rect ? 'true' : undefined}\n onPointerCancel={endDrag}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n ref={ref}\n style={\n {\n position: 'absolute',\n left: `${rect?.x ?? 0}px`,\n top: `${rect?.y ?? 0}px`,\n } as CSSProperties\n }\n >\n {children}\n </div>\n );\n}\n","import { ErrorIcon, WarningIcon } from '@weasel-js/ui';\nimport type { JobHandle } from '../job/types';\n\n/** Props for `<JobProgress>`. */\nexport interface JobProgressProps {\n job: JobHandle;\n className?: string;\n}\n\n/** A running job's progress, failures and outcome. Determinate once the job\n * reports a total; a job that cannot count up front never sends one, so the\n * bar stays indeterminate rather than inventing a denominator. */\nexport function JobProgress({ job, className }: JobProgressProps) {\n const { status, done, total, failures, error } = job;\n if (status === 'idle') return null;\n\n const determinate = total !== null && total > 0;\n const fraction = determinate ? Math.min(1, done / total) : 0;\n const cls = ['lk-job', `lk-job--${status}`, className].filter(Boolean).join(' ');\n\n return (\n <div className={cls}>\n <div\n className={`lk-job__track${determinate ? '' : ' lk-job__track--indeterminate'}`}\n role=\"progressbar\"\n aria-label=\"Job progress\"\n aria-valuenow={determinate ? done : undefined}\n aria-valuemin={determinate ? 0 : undefined}\n aria-valuemax={determinate ? total : undefined}\n >\n {/* Scaling a full-width bar keeps the fill off the layout path, so\n progress updates never reflow the status bar. */}\n <div\n className=\"lk-job__fill\"\n style={{ transform: `scaleX(${determinate ? fraction : 1})` }}\n />\n </div>\n\n <span className=\"lk-job__count\">\n {done}\n {determinate ? ` / ${total}` : ''}\n </span>\n\n {failures.length > 0 && (\n <span className=\"lk-job__failures\" title={failures.map((f) => f.error).join('\\n')}>\n <WarningIcon size={14} />\n {failures.length} failed\n </span>\n )}\n\n {error && (\n <span className=\"lk-job__error\" title={error}>\n <ErrorIcon size={14} />\n {error}\n </span>\n )}\n\n {status === 'running' && (\n <button type=\"button\" className=\"lk-job__cancel\" onClick={job.cancel}>\n Cancel\n </button>\n )}\n </div>\n );\n}\n","import type { CSSProperties } from 'react';\n\n/** How a legend entry's swatch is drawn, matching how the thing looks on canvas. */\nexport type LegendMark = 'line' | 'dash' | 'dot' | 'band';\n\n/** One row of a legend: a swatch and what it means. */\nexport interface LegendEntry {\n key: string;\n label: string;\n color: string;\n /** Defaults to `'line'`. */\n mark?: LegendMark;\n}\n\n/** Props for `<Legend>`. */\nexport interface LegendProps {\n entries: readonly LegendEntry[];\n className?: string;\n}\n\n/** A color key. Presentational only — no handlers, no state, no hover behavior. */\nexport function Legend({ entries, className }: LegendProps) {\n return (\n <ul className={className ? `lk-legend ${className}` : 'lk-legend'}>\n {entries.map((entry) => {\n const mark = entry.mark ?? 'line';\n return (\n <li className=\"lk-legend__row\" key={entry.key}>\n <span\n aria-hidden=\"true\"\n className={`lk-legend__swatch lk-legend__swatch--${mark}`}\n style={{ '--lk-legend-ink': entry.color } as CSSProperties}\n />\n <span className=\"lk-legend__label\">{entry.label}</span>\n </li>\n );\n })}\n </ul>\n );\n}\n","import type { ReactNode } from 'react';\n\n/** Props for `<Sidebar>`. */\nexport interface SidebarProps {\n children: ReactNode;\n title?: string;\n collapsed?: boolean;\n onToggle?: () => void;\n}\n\n/** A collapsible side panel with an optional title. Collapse state is the\n * caller's to hold. */\nexport function Sidebar({ children, title, collapsed = false, onToggle }: SidebarProps) {\n const className = `lk-sidebar${collapsed ? ' lk-sidebar--collapsed' : ''}`;\n return (\n <aside className={className}>\n {(title || onToggle) && (\n <div className=\"lk-sidebar-header\">\n {title && <span className=\"lk-sidebar-title\">{title}</span>}\n {onToggle && (\n <button\n type=\"button\"\n className=\"lk-sidebar-toggle\"\n onClick={onToggle}\n aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}\n >\n {collapsed ? '›' : '‹'}\n </button>\n )}\n </div>\n )}\n <div className=\"lk-sidebar-body\">{children}</div>\n </aside>\n );\n}\n"]}
@@ -1,24 +1,14 @@
1
- import { CanvasStackContext } from './chunk-CPUJ3QXL.js';
2
- import { useVisibleRaf } from './chunk-Z6HJ5FEM.js';
1
+ import { zoomAt } from './chunk-CTRKTLYZ.js';
2
+ import { resolveFrame, screenToWorld, DEFAULT_FRAME } from './chunk-2AYGEN57.js';
3
+ import { CanvasStackContext } from './chunk-CQLQPQ4P.js';
4
+ import { useVisibleRaf } from './chunk-ROQDRXKW.js';
3
5
  import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
4
6
  import { jsx, jsxs } from 'react/jsx-runtime';
5
7
 
6
- // src/canvas/canvasCoords.ts
7
- function worldToScreen(world, view) {
8
- return {
9
- x: world.x * view.zoom + view.pan.x,
10
- y: world.y * view.zoom + view.pan.y
11
- };
12
- }
13
- function screenToWorld(screen, view) {
14
- return {
15
- x: (screen.x - view.pan.x) / view.zoom,
16
- y: (screen.y - view.pan.y) / view.zoom
17
- };
18
- }
19
8
  function useLayerScheduler({
20
9
  layers,
21
10
  view,
11
+ frame = DEFAULT_FRAME,
22
12
  canvasRefs,
23
13
  size,
24
14
  host
@@ -37,7 +27,7 @@ function useLayerScheduler({
37
27
  }, [layers]);
38
28
  useEffect(() => {
39
29
  for (const layer of layers) dirty.current.add(layer.id);
40
- }, [view, size, layers]);
30
+ }, [view, frame, size, layers]);
41
31
  const frameLoop = useVisibleRaf(
42
32
  () => {
43
33
  if (dirty.current.size === 0) return;
@@ -56,7 +46,7 @@ function useLayerScheduler({
56
46
  ctx.save();
57
47
  ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
58
48
  ctx.clearRect(0, 0, size.width, size.height);
59
- layer.render(ctx, view);
49
+ layer.render(ctx, view, frame);
60
50
  ctx.restore();
61
51
  }
62
52
  dirty.current.clear();
@@ -75,7 +65,8 @@ function usePanZoom({
75
65
  view,
76
66
  onViewChange,
77
67
  minZoom = 0.1,
78
- maxZoom = 32
68
+ maxZoom = 32,
69
+ frame = DEFAULT_FRAME
79
70
  }) {
80
71
  const dragRef = useRef(null);
81
72
  const viewRef = useRef(view);
@@ -85,23 +76,21 @@ function usePanZoom({
85
76
  (e) => {
86
77
  e.preventDefault();
87
78
  const rect = e.currentTarget.getBoundingClientRect();
88
- const cursorX = e.clientX - rect.left;
89
- const cursorY = e.clientY - rect.top;
90
- const v = viewRef.current;
91
79
  const initialZoom = initialZoomRef.current;
92
- const effectiveMin = initialZoom == null ? minZoom : Math.min(minZoom, initialZoom);
93
- const effectiveMax = initialZoom == null ? maxZoom : Math.max(maxZoom, initialZoom);
94
- const factor = Math.exp(-e.deltaY * 1e-3);
95
- const rawZoom = isPositiveFinite(v.zoom) ? v.zoom : 1;
96
- const nextZoom = Math.min(effectiveMax, Math.max(effectiveMin, rawZoom * factor));
97
- const ratio = nextZoom / rawZoom;
98
- const nextPan = {
99
- x: cursorX - (cursorX - v.pan.x) * ratio,
100
- y: cursorY - (cursorY - v.pan.y) * ratio
101
- };
102
- onViewChange({ zoom: nextZoom, pan: nextPan });
80
+ onViewChange(
81
+ zoomAt(
82
+ viewRef.current,
83
+ Math.exp(-e.deltaY * 1e-3),
84
+ { x: e.clientX - rect.left, y: e.clientY - rect.top },
85
+ {
86
+ frame,
87
+ min: initialZoom == null ? minZoom : Math.min(minZoom, initialZoom),
88
+ max: initialZoom == null ? maxZoom : Math.max(maxZoom, initialZoom)
89
+ }
90
+ )
91
+ );
103
92
  },
104
- [onViewChange, minZoom, maxZoom]
93
+ [onViewChange, minZoom, maxZoom, frame]
105
94
  );
106
95
  const onPointerDown = useCallback((e) => {
107
96
  if (e.button !== 0) return;
@@ -144,11 +133,13 @@ function CanvasStack({
144
133
  layers,
145
134
  view,
146
135
  onViewChange,
136
+ worldSpec,
147
137
  minZoom,
148
138
  maxZoom,
149
139
  width = "100%",
150
140
  height = "100%",
151
141
  className,
142
+ onResize,
152
143
  onHitTest,
153
144
  children
154
145
  }) {
@@ -178,9 +169,23 @@ function CanvasStack({
178
169
  ro.observe(el);
179
170
  return () => ro.disconnect();
180
171
  }, []);
181
- const handlers = usePanZoom({ view, onViewChange, minZoom, maxZoom });
182
- useLayerScheduler({ layers, view, canvasRefs: canvasMap, size, host: containerRef });
183
- const ctxValue = useMemo(() => ({ view }), [view]);
172
+ const onResizeRef = useRef(onResize);
173
+ onResizeRef.current = onResize;
174
+ useEffect(() => {
175
+ if (size.width === 0 && size.height === 0) return;
176
+ onResizeRef.current?.({ width: size.width, height: size.height });
177
+ }, [size.width, size.height]);
178
+ const frame = useMemo(() => resolveFrame(worldSpec, size), [worldSpec, size]);
179
+ const handlers = usePanZoom({ view, onViewChange, minZoom, maxZoom, frame });
180
+ useLayerScheduler({ layers, view, frame, canvasRefs: canvasMap, size, host: containerRef });
181
+ const ctxValue = useMemo(
182
+ () => ({
183
+ view,
184
+ frame,
185
+ surface: { element: containerRef, size, canvases: canvasMap, layers }
186
+ }),
187
+ [view, frame, size, layers]
188
+ );
184
189
  const containerStyle = { width, height };
185
190
  const canvasPx = {
186
191
  width: Math.max(0, Math.round(size.width * size.dpr)),
@@ -196,7 +201,7 @@ function CanvasStack({
196
201
  if (!wasDragging && onHitTest && containerRef.current) {
197
202
  const rect = containerRef.current.getBoundingClientRect();
198
203
  const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
199
- onHitTest(screenToWorld(screen, view));
204
+ onHitTest(screenToWorld(screen, view, frame));
200
205
  }
201
206
  };
202
207
  return /* @__PURE__ */ jsx(CanvasStackContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsxs(
@@ -227,6 +232,6 @@ function CanvasStack({
227
232
  ) });
228
233
  }
229
234
 
230
- export { CanvasStack, screenToWorld, worldToScreen };
231
- //# sourceMappingURL=chunk-NS54R52R.js.map
232
- //# sourceMappingURL=chunk-NS54R52R.js.map
235
+ export { CanvasStack };
236
+ //# sourceMappingURL=chunk-3ZELRRBV.js.map
237
+ //# sourceMappingURL=chunk-3ZELRRBV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/canvas/useLayerScheduler.ts","../src/canvas/usePanZoom.ts","../src/canvas/CanvasStack.tsx"],"names":["useRef","useEffect"],"mappings":"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","file":"chunk-3ZELRRBV.js","sourcesContent":["import { useVisibleRaf } from '@weasel-js/core';\nimport { type RefObject, useEffect, useRef } from 'react';\nimport type { ViewTransform } from '../instrument/types';\nimport { DEFAULT_FRAME, type WorldFrame } from './worldSpec';\n\n/** One layer of a canvas stack: its id, whether it is currently shown, and how\n * it paints itself. */\nexport interface CanvasLayerDescriptor {\n id: string;\n visible: boolean;\n render: (ctx: CanvasRenderingContext2D, view: ViewTransform, frame: WorldFrame) => void;\n}\n\ninterface SchedulerOptions {\n layers: CanvasLayerDescriptor[];\n view: ViewTransform;\n /** The instrument's coordinate system, resolved against `size`. */\n frame?: WorldFrame;\n canvasRefs: RefObject<Map<string, HTMLCanvasElement>>;\n size: { width: number; height: number; dpr: number };\n /** The element the stack occupies. Given one, the scheduler also stops while\n * the stack sits outside the viewport. */\n host?: RefObject<Element | null>;\n}\n\nexport function useLayerScheduler({\n layers,\n view,\n frame = DEFAULT_FRAME,\n canvasRefs,\n size,\n host,\n}: SchedulerOptions): void {\n const dirty = useRef<Set<string>>(new Set());\n const lastRenderRef = useRef<Map<string, CanvasLayerDescriptor['render']>>(new Map());\n\n useEffect(() => {\n for (const layer of layers) {\n const prev = lastRenderRef.current.get(layer.id);\n if (prev !== layer.render) dirty.current.add(layer.id);\n lastRenderRef.current.set(layer.id, layer.render);\n }\n for (const id of [...lastRenderRef.current.keys()]) {\n if (!layers.find((l) => l.id === id)) lastRenderRef.current.delete(id);\n }\n }, [layers]);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: re-mark dirty when view or size changes\n useEffect(() => {\n for (const layer of layers) dirty.current.add(layer.id);\n }, [view, frame, size, layers]);\n\n // Painting only what is dirty is not the same as doing nothing: a hidden tab\n // still commits React updates, and the effect above marks every layer dirty\n // on any view or size change. The gate is what actually stops the work.\n const frameLoop = useVisibleRaf(\n () => {\n if (dirty.current.size === 0) return;\n const map = canvasRefs.current;\n if (!map) {\n dirty.current.clear();\n return;\n }\n for (const layer of layers) {\n if (!dirty.current.has(layer.id)) continue;\n if (!layer.visible) continue;\n const canvas = map.get(layer.id);\n if (!canvas) continue;\n const ctx = canvas.getContext('2d');\n if (!ctx) continue;\n ctx.save();\n ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);\n ctx.clearRect(0, 0, size.width, size.height);\n layer.render(ctx, view, frame);\n ctx.restore();\n }\n dirty.current.clear();\n },\n { target: host },\n );\n\n useEffect(() => {\n if (dirty.current.size > 0) frameLoop.request();\n });\n}\n","import { type PointerEvent, useCallback, useRef, type WheelEvent } from 'react';\nimport type { ViewTransform } from '../instrument/types';\nimport { zoomAt } from './camera';\nimport { DEFAULT_FRAME, type WorldFrame } from './worldSpec';\n\nexport interface UsePanZoomOptions {\n view: ViewTransform;\n onViewChange: (v: ViewTransform) => void;\n /** The opening `view.zoom` always stays reachable, widening these past\n * whatever is passed here if it would otherwise exclude it. */\n minZoom?: number;\n maxZoom?: number;\n /** The instrument's coordinate system, resolved against the viewport. The\n * wheel anchors in this frame; omitting it anchors at the element's\n * top-left, which is what labkit did before a frame could be declared. */\n frame?: WorldFrame;\n}\n\nexport interface PanZoomHandlers {\n onWheel: (e: WheelEvent<HTMLElement>) => void;\n onPointerDown: (e: PointerEvent<HTMLElement>) => void;\n onPointerMove: (e: PointerEvent<HTMLElement>) => void;\n onPointerUp: (e: PointerEvent<HTMLElement>) => void;\n isDragging: () => boolean;\n}\n\ninterface DragState {\n pointerId: number;\n startScreenX: number;\n startScreenY: number;\n startPan: { x: number; y: number };\n moved: boolean;\n}\n\nconst DRAG_THRESHOLD = 3;\n\nfunction isPositiveFinite(n: number): boolean {\n return Number.isFinite(n) && n > 0;\n}\n\nexport function usePanZoom({\n view,\n onViewChange,\n minZoom = 0.1,\n maxZoom = 32,\n frame = DEFAULT_FRAME,\n}: UsePanZoomOptions): PanZoomHandlers {\n const dragRef = useRef<DragState | null>(null);\n const viewRef = useRef(view);\n viewRef.current = view;\n // Captured once, from the view the canvas opened at — not the current view,\n // which would let a prior zoom-out shrink the range and trap the opening\n // view unreachable behind it.\n const initialZoomRef = useRef(isPositiveFinite(view.zoom) ? view.zoom : null);\n\n const onWheel = useCallback(\n (e: WheelEvent<HTMLElement>) => {\n e.preventDefault();\n const rect = e.currentTarget.getBoundingClientRect();\n const initialZoom = initialZoomRef.current;\n onViewChange(\n zoomAt(\n viewRef.current,\n Math.exp(-e.deltaY * 0.001),\n { x: e.clientX - rect.left, y: e.clientY - rect.top },\n {\n frame,\n min: initialZoom == null ? minZoom : Math.min(minZoom, initialZoom),\n max: initialZoom == null ? maxZoom : Math.max(maxZoom, initialZoom),\n },\n ),\n );\n },\n [onViewChange, minZoom, maxZoom, frame],\n );\n\n const onPointerDown = useCallback((e: PointerEvent<HTMLElement>) => {\n if (e.button !== 0) return;\n e.currentTarget.setPointerCapture(e.pointerId);\n dragRef.current = {\n pointerId: e.pointerId,\n startScreenX: e.clientX,\n startScreenY: e.clientY,\n startPan: { ...viewRef.current.pan },\n moved: false,\n };\n }, []);\n\n const onPointerMove = useCallback(\n (e: PointerEvent<HTMLElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n const dx = e.clientX - drag.startScreenX;\n const dy = e.clientY - drag.startScreenY;\n if (!drag.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;\n drag.moved = true;\n onViewChange({\n zoom: viewRef.current.zoom,\n pan: { x: drag.startPan.x + dx, y: drag.startPan.y + dy },\n });\n },\n [onViewChange],\n );\n\n const onPointerUp = useCallback((e: PointerEvent<HTMLElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n if (e.currentTarget.hasPointerCapture(e.pointerId)) {\n e.currentTarget.releasePointerCapture(e.pointerId);\n }\n dragRef.current = null;\n }, []);\n\n const isDragging = useCallback(() => dragRef.current?.moved === true, []);\n\n return { onWheel, onPointerDown, onPointerMove, onPointerUp, isDragging };\n}\n","import { type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';\nimport type { Point, ViewTransform } from '../instrument/types';\nimport { CanvasStackContext } from './CanvasStackContext';\nimport { screenToWorld } from './canvasCoords';\nimport { type CanvasLayerDescriptor, useLayerScheduler } from './useLayerScheduler';\nimport { usePanZoom } from './usePanZoom';\nimport { resolveFrame, type ViewportSize, type WorldSpec } from './worldSpec';\n\n/** Props for `<CanvasStack>`. */\nexport interface CanvasStackProps {\n layers: CanvasLayerDescriptor[];\n view: ViewTransform;\n onViewChange: (v: ViewTransform) => void;\n /** The instrument's coordinate system. Omitted, world (0,0) sits at the\n * element's top-left with y running down. */\n worldSpec?: WorldSpec;\n minZoom?: number;\n maxZoom?: number;\n width?: number | string;\n height?: number | string;\n className?: string;\n /** Fired whenever the stack is measured, so a consumer can place a view that\n * only makes sense in terms of the viewport. */\n onResize?: (size: ViewportSize) => void;\n onHitTest?: (worldPos: Point) => void;\n children?: ReactNode;\n}\n\n/** Stacks one `<canvas>` per layer and drives them from a shared view, so a\n * layer that changes rarely is not redrawn with one that changes every frame.\n * Handles sizing, device pixel ratio, and pan/zoom. */\nexport function CanvasStack({\n layers,\n view,\n onViewChange,\n worldSpec,\n minZoom,\n maxZoom,\n width = '100%',\n height = '100%',\n className,\n onResize,\n onHitTest,\n children,\n}: CanvasStackProps) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n const canvasMap = useRef<Map<string, HTMLCanvasElement>>(new Map());\n const [size, setSize] = useState({ width: 0, height: 0, dpr: 1 });\n\n const setCanvasRef = (id: string, el: HTMLCanvasElement | null): void => {\n if (el) canvasMap.current.set(id, el);\n else canvasMap.current.delete(id);\n };\n\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n const update = () => {\n const rect = el.getBoundingClientRect();\n const dpr = window.devicePixelRatio ?? 1;\n setSize((prev) => {\n if (prev.width === rect.width && prev.height === rect.height && prev.dpr === dpr) {\n return prev;\n }\n return { width: rect.width, height: rect.height, dpr };\n });\n };\n update();\n if (typeof ResizeObserver === 'undefined') return;\n const ro = new ResizeObserver(update);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const onResizeRef = useRef(onResize);\n onResizeRef.current = onResize;\n useEffect(() => {\n if (size.width === 0 && size.height === 0) return;\n onResizeRef.current?.({ width: size.width, height: size.height });\n }, [size.width, size.height]);\n\n const frame = useMemo(() => resolveFrame(worldSpec, size), [worldSpec, size]);\n\n const handlers = usePanZoom({ view, onViewChange, minZoom, maxZoom, frame });\n useLayerScheduler({ layers, view, frame, canvasRefs: canvasMap, size, host: containerRef });\n\n const ctxValue = useMemo(\n () => ({\n view,\n frame,\n surface: { element: containerRef, size, canvases: canvasMap, layers },\n }),\n [view, frame, size, layers],\n );\n\n const containerStyle: CSSProperties = { width, height };\n const canvasPx = {\n width: Math.max(0, Math.round(size.width * size.dpr)),\n height: Math.max(0, Math.round(size.height * size.dpr)),\n };\n const canvasCss: CSSProperties = {\n width: size.width,\n height: size.height,\n };\n\n const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n const wasDragging = handlers.isDragging();\n handlers.onPointerUp(e);\n if (!wasDragging && onHitTest && containerRef.current) {\n const rect = containerRef.current.getBoundingClientRect();\n const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };\n onHitTest(screenToWorld(screen, view, frame));\n }\n };\n\n return (\n <CanvasStackContext.Provider value={ctxValue}>\n <div\n ref={containerRef}\n className={className ? `lk-canvas-stack ${className}` : 'lk-canvas-stack'}\n style={containerStyle}\n onWheel={handlers.onWheel}\n onPointerDown={handlers.onPointerDown}\n onPointerMove={handlers.onPointerMove}\n onPointerUp={handlePointerUp}\n >\n {layers.map((layer) => (\n <canvas\n key={layer.id}\n ref={(el) => setCanvasRef(layer.id, el)}\n className=\"lk-canvas-stack__canvas\"\n width={canvasPx.width}\n height={canvasPx.height}\n style={{ ...canvasCss, display: layer.visible ? 'block' : 'none' }}\n />\n ))}\n <div className=\"lk-canvas-stack__overlay\">{children}</div>\n </div>\n </CanvasStackContext.Provider>\n );\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { useVisibleRaf } from './chunk-Z6HJ5FEM.js';
1
+ import { useVisibleRaf } from './chunk-ROQDRXKW.js';
2
2
  import { createContext, useContext, useCallback, useRef, useEffect } from 'react';
3
3
 
4
4
  // src/surface/composeRects.ts
@@ -31,6 +31,10 @@ function toDeviceRect(rect, surfaceHeight, dpr) {
31
31
  };
32
32
  }
33
33
  var SurfaceContext = createContext(null);
34
+ var SurfaceCanvasContext = createContext(null);
35
+ function useSurfaceCanvas() {
36
+ return useContext(SurfaceCanvasContext);
37
+ }
34
38
  function useSurfaceOptional() {
35
39
  return useContext(SurfaceContext);
36
40
  }
@@ -53,6 +57,7 @@ function useTiledSurface({ onFrame }) {
53
57
  const tiles = useRef(/* @__PURE__ */ new Map());
54
58
  const rects = useRef(/* @__PURE__ */ new Map());
55
59
  const dirty = useRef(/* @__PURE__ */ new Set());
60
+ const painters = useRef(/* @__PURE__ */ new Map());
56
61
  const needsMeasure = useRef(true);
57
62
  const observer = useRef(null);
58
63
  const lastDpr = useRef(0);
@@ -89,12 +94,17 @@ function useTiledSurface({ onFrame }) {
89
94
  }
90
95
  if (dirty.current.size === 0) return;
91
96
  const box = el.getBoundingClientRect();
92
- onFrameRef.current({
97
+ const frame = {
93
98
  dirty: new Set(dirty.current),
94
99
  rects: new Map(rects.current),
95
100
  dpr,
96
101
  size: { width: box.width, height: box.height }
97
- });
102
+ };
103
+ onFrameRef.current(frame);
104
+ for (const id of frame.dirty) {
105
+ const rect = frame.rects.get(id);
106
+ if (rect) painters.current.get(id)?.(rect, frame);
107
+ }
98
108
  dirty.current.clear();
99
109
  },
100
110
  // The host attaches its container through `containerRef`, which may land
@@ -137,6 +147,13 @@ function useTiledSurface({ onFrame }) {
137
147
  },
138
148
  [schedule]
139
149
  );
150
+ const registerPainter = useCallback((id, paint) => {
151
+ painters.current.set(id, paint);
152
+ return () => {
153
+ if (painters.current.get(id) === paint) painters.current.delete(id);
154
+ };
155
+ }, []);
156
+ const getContainer = useCallback(() => container.current, []);
140
157
  const containerRef = useCallback(
141
158
  (el) => {
142
159
  if (container.current === el) return;
@@ -162,9 +179,17 @@ function useTiledSurface({ onFrame }) {
162
179
  frameLoop.cancel();
163
180
  };
164
181
  }, [schedule, frameLoop]);
165
- return { invalidate, invalidateAll, invalidateRects, registerTile, containerRef };
182
+ return {
183
+ invalidate,
184
+ invalidateAll,
185
+ invalidateRects,
186
+ registerTile,
187
+ registerPainter,
188
+ containerRef,
189
+ getContainer
190
+ };
166
191
  }
167
192
 
168
- export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface };
169
- //# sourceMappingURL=chunk-HFDVGF4X.js.map
170
- //# sourceMappingURL=chunk-HFDVGF4X.js.map
193
+ export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTiledSurface };
194
+ //# sourceMappingURL=chunk-4EY67BKJ.js.map
195
+ //# sourceMappingURL=chunk-4EY67BKJ.js.map