@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
@@ -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, a as Point } from '../_dts/types-BttTIent.js';
5
- import '../_dts/types-DCGEcMO_.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,6 +1,7 @@
1
- export { CanvasStack } from '../chunk-BDJXIBRZ.js';
2
- export { screenToWorld, worldToScreen } from '../chunk-T66HU3HX.js';
3
- export { CanvasStackContext } from '../chunk-CPUJ3QXL.js';
4
- import '../chunk-QCICIHEO.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';
5
6
  //# sourceMappingURL=index.js.map
6
7
  //# sourceMappingURL=index.js.map
@@ -1,11 +1,12 @@
1
- import { c as ControlRenderer } from '../_dts/types-DCGEcMO_.js';
2
- import { I as Instrument, m as TrialChromeContext, T as TrialContribution } from '../_dts/types-BttTIent.js';
3
- export { f as IconComponent, S as SidebarSection, h as StatusReadout, k as ToolItem, l as ToolbarItem, n as TrialRegion, o as ViewportControl } from '../_dts/types-BttTIent.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,9 +1,9 @@
1
- export { PaletteRegion, SidebarRegion, StatusRegion, TitleBarRegion, ToolbarRegion, ViewportRegion, builtinContributions, mergeContributions, suppressContributions } from '../chunk-6X5RBC6G.js';
2
- import '../chunk-JDROYQM3.js';
3
- import '../chunk-WTI26YTM.js';
4
- import '../chunk-CPUJ3QXL.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';
5
4
  import '../chunk-SBXNQW4G.js';
6
- import '../chunk-RGT4EJIB.js';
7
- import '../chunk-QCICIHEO.js';
5
+ import '../chunk-HEORBLVT.js';
6
+ import '../chunk-CQLQPQ4P.js';
7
+ import '../chunk-ROQDRXKW.js';
8
8
  //# sourceMappingURL=index.js.map
9
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 { vn, yn } from './chunk-RGT4EJIB.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(vn, { 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(yn, { 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-QRYPXSGP.js.map
212
- //# sourceMappingURL=chunk-QRYPXSGP.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,12 +1,14 @@
1
- import { screenToWorld } from './chunk-T66HU3HX.js';
2
- import { CanvasStackContext } from './chunk-CPUJ3QXL.js';
3
- import { useVisibleRaf } from './chunk-QCICIHEO.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';
4
5
  import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
5
6
  import { jsx, jsxs } from 'react/jsx-runtime';
6
7
 
7
8
  function useLayerScheduler({
8
9
  layers,
9
10
  view,
11
+ frame = DEFAULT_FRAME,
10
12
  canvasRefs,
11
13
  size,
12
14
  host
@@ -25,7 +27,7 @@ function useLayerScheduler({
25
27
  }, [layers]);
26
28
  useEffect(() => {
27
29
  for (const layer of layers) dirty.current.add(layer.id);
28
- }, [view, size, layers]);
30
+ }, [view, frame, size, layers]);
29
31
  const frameLoop = useVisibleRaf(
30
32
  () => {
31
33
  if (dirty.current.size === 0) return;
@@ -44,7 +46,7 @@ function useLayerScheduler({
44
46
  ctx.save();
45
47
  ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
46
48
  ctx.clearRect(0, 0, size.width, size.height);
47
- layer.render(ctx, view);
49
+ layer.render(ctx, view, frame);
48
50
  ctx.restore();
49
51
  }
50
52
  dirty.current.clear();
@@ -63,7 +65,8 @@ function usePanZoom({
63
65
  view,
64
66
  onViewChange,
65
67
  minZoom = 0.1,
66
- maxZoom = 32
68
+ maxZoom = 32,
69
+ frame = DEFAULT_FRAME
67
70
  }) {
68
71
  const dragRef = useRef(null);
69
72
  const viewRef = useRef(view);
@@ -73,23 +76,21 @@ function usePanZoom({
73
76
  (e) => {
74
77
  e.preventDefault();
75
78
  const rect = e.currentTarget.getBoundingClientRect();
76
- const cursorX = e.clientX - rect.left;
77
- const cursorY = e.clientY - rect.top;
78
- const v = viewRef.current;
79
79
  const initialZoom = initialZoomRef.current;
80
- const effectiveMin = initialZoom == null ? minZoom : Math.min(minZoom, initialZoom);
81
- const effectiveMax = initialZoom == null ? maxZoom : Math.max(maxZoom, initialZoom);
82
- const factor = Math.exp(-e.deltaY * 1e-3);
83
- const rawZoom = isPositiveFinite(v.zoom) ? v.zoom : 1;
84
- const nextZoom = Math.min(effectiveMax, Math.max(effectiveMin, rawZoom * factor));
85
- const ratio = nextZoom / rawZoom;
86
- const nextPan = {
87
- x: cursorX - (cursorX - v.pan.x) * ratio,
88
- y: cursorY - (cursorY - v.pan.y) * ratio
89
- };
90
- 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
+ );
91
92
  },
92
- [onViewChange, minZoom, maxZoom]
93
+ [onViewChange, minZoom, maxZoom, frame]
93
94
  );
94
95
  const onPointerDown = useCallback((e) => {
95
96
  if (e.button !== 0) return;
@@ -132,11 +133,13 @@ function CanvasStack({
132
133
  layers,
133
134
  view,
134
135
  onViewChange,
136
+ worldSpec,
135
137
  minZoom,
136
138
  maxZoom,
137
139
  width = "100%",
138
140
  height = "100%",
139
141
  className,
142
+ onResize,
140
143
  onHitTest,
141
144
  children
142
145
  }) {
@@ -166,9 +169,23 @@ function CanvasStack({
166
169
  ro.observe(el);
167
170
  return () => ro.disconnect();
168
171
  }, []);
169
- const handlers = usePanZoom({ view, onViewChange, minZoom, maxZoom });
170
- useLayerScheduler({ layers, view, canvasRefs: canvasMap, size, host: containerRef });
171
- 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
+ );
172
189
  const containerStyle = { width, height };
173
190
  const canvasPx = {
174
191
  width: Math.max(0, Math.round(size.width * size.dpr)),
@@ -184,7 +201,7 @@ function CanvasStack({
184
201
  if (!wasDragging && onHitTest && containerRef.current) {
185
202
  const rect = containerRef.current.getBoundingClientRect();
186
203
  const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
187
- onHitTest(screenToWorld(screen, view));
204
+ onHitTest(screenToWorld(screen, view, frame));
188
205
  }
189
206
  };
190
207
  return /* @__PURE__ */ jsx(CanvasStackContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsxs(
@@ -216,5 +233,5 @@ function CanvasStack({
216
233
  }
217
234
 
218
235
  export { CanvasStack };
219
- //# sourceMappingURL=chunk-BDJXIBRZ.js.map
220
- //# sourceMappingURL=chunk-BDJXIBRZ.js.map
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-QCICIHEO.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-LGKRIUSW.js.map
170
- //# sourceMappingURL=chunk-LGKRIUSW.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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/surface/composeRects.ts","../src/surface/deviceRect.ts","../src/surface/SurfaceContext.ts","../src/surface/useSurfaceTile.ts","../src/surface/useTiledSurface.ts"],"names":["useCallback"],"mappings":"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","file":"chunk-4EY67BKJ.js","sourcesContent":["import type { Box, Rect } from './rect';\n\n/** Every tile's box expressed against the container's origin. */\nexport function composeRects(container: Box, tiles: ReadonlyMap<string, Box>): Map<string, Rect> {\n const out = new Map<string, Rect>();\n for (const [id, tile] of tiles) {\n out.set(id, {\n x: tile.left - container.left,\n y: tile.top - container.top,\n w: tile.width,\n h: tile.height,\n });\n }\n return out;\n}\n\nexport function rectsEqual(a: Rect | undefined, b: Rect): boolean {\n return a !== undefined && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;\n}\n","import type { Rect } from './rect';\n\n/**\n * A DOM rect as a GL viewport rect: origin at the bottom-left, every edge snapped\n * to the device-pixel grid. Still CSS pixels, because three.js applies its own\n * pixel ratio — snapping here is what stops a tile and its neighbour rounding\n * apart and leaving a hairline column between them.\n */\nexport function toDeviceRect(rect: Rect, surfaceHeight: number, dpr: number): Rect {\n const snap = (v: number) => Math.round(v * dpr) / dpr;\n const x = snap(rect.x);\n const y = snap(surfaceHeight - rect.y - rect.h);\n return {\n x,\n y,\n w: snap(rect.x + rect.w) - x,\n h: snap(surfaceHeight - rect.y) - y,\n };\n}\n","import { createContext } from 'react';\nimport type { SurfaceHandle } from './useTiledSurface';\n\n/** Null when no surface owner is above — a lab with no shared surface at all. */\nexport const SurfaceContext = createContext<SurfaceHandle | null>(null);\n\n/** The buffer the surface's tiles paint into. Null until the owner's canvas is\n * mounted, and for an owner that keeps its surface unpainted. */\nexport const SurfaceCanvasContext = createContext<HTMLCanvasElement | null>(null);\n","import { useCallback, useContext } from 'react';\nimport { SurfaceCanvasContext, SurfaceContext } from './SurfaceContext';\nimport type { SurfaceHandle } from './useTiledSurface';\n\n/** The surface's shared buffer, or null where nothing paints one. What a tile\n * hands to `paintInto`. */\nexport function useSurfaceCanvas(): HTMLCanvasElement | null {\n return useContext(SurfaceCanvasContext);\n}\n\n/** The surface above, or null. Use this where a surface is genuinely optional. */\nexport function useSurfaceOptional(): SurfaceHandle | null {\n return useContext(SurfaceContext);\n}\n\n/** The surface above. Throws where a caller cannot work without one. */\nexport function useSurface(): SurfaceHandle {\n const surface = useContext(SurfaceContext);\n if (!surface) throw new Error('[labkit] useSurface requires a surface owner above it');\n return surface;\n}\n\n/**\n * A ref callback that publishes this element's rect to the surface under `id`.\n *\n * Attach it to whichever element the surface should draw into — that is not\n * necessarily the trial's own element, since a trial may hold a drawn pane beside\n * an undrawn one, or none at all.\n */\nexport function useSurfaceTile(id: string): (el: HTMLElement | null) => void {\n const surface = useSurfaceOptional();\n return useCallback(\n (el: HTMLElement | null) => {\n surface?.registerTile(id, el);\n },\n [surface, id],\n );\n}\n","import { useVisibleRaf } from '@weasel-js/core';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { composeRects, rectsEqual } from './composeRects';\nimport type { Box, Rect } from './rect';\n\n/** What a surface owner is handed once per animation frame. `rects` carries every\n * tile, not only the dirty ones: a scissored draw has to know where it is drawing\n * relative to a surface that may have resized under it. */\nexport interface SurfaceFrame {\n dirty: ReadonlySet<string>;\n rects: ReadonlyMap<string, Rect>;\n dpr: number;\n size: { width: number; height: number };\n}\n\n/** The invalidators and the two ref callbacks that publish geometry. */\nexport interface SurfaceHandle {\n /** Mark one tile for redraw. */\n invalidate: (id: string) => void;\n /** Mark every tile — what a resize or a tile-set change means. */\n invalidateAll: () => void;\n /** Re-measure before the next frame. The escape hatch for a host that knows it\n * moved something a ResizeObserver cannot see. */\n invalidateRects: () => void;\n registerTile: (id: string, el: HTMLElement | null) => void;\n /** Subscribe a tile to the frames it is dirty on. A tile paints on its own\n * loop, so this is how the surface wakes one: a resize of the shared buffer\n * clears every tile, not only the one that moved. */\n registerPainter: (id: string, paint: TilePainter) => () => void;\n containerRef: (el: HTMLElement | null) => void;\n /** The element tile rects are measured against, and so the one a tile's own\n * chrome positions itself inside. Null before the owner attaches it. */\n getContainer: () => HTMLElement | null;\n}\n\n/** What a tile's painter is handed: where it sits on the surface now, and the\n * frame that dirtied it. */\nexport type TilePainter = (rect: Rect, frame: SurfaceFrame) => void;\n\nexport interface UseTiledSurfaceOptions {\n onFrame: (frame: SurfaceFrame) => void;\n}\n\nexport function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHandle {\n const container = useRef<HTMLElement | null>(null);\n const tiles = useRef(new Map<string, HTMLElement>());\n const rects = useRef(new Map<string, Rect>());\n const dirty = useRef(new Set<string>());\n const painters = useRef(new Map<string, TilePainter>());\n const needsMeasure = useRef(true);\n const observer = useRef<ResizeObserver | null>(null);\n const lastDpr = useRef(0);\n\n // Held in a ref so a caller passing an inline closure does not re-create every\n // callback below on each render.\n const onFrameRef = useRef(onFrame);\n onFrameRef.current = onFrame;\n\n const measure = useCallback((): boolean => {\n const el = container.current;\n if (!el) return false;\n const boxes = new Map<string, Box>();\n for (const [id, tile] of tiles.current) boxes.set(id, tile.getBoundingClientRect());\n const next = composeRects(el.getBoundingClientRect(), boxes);\n let changed = next.size !== rects.current.size;\n for (const [id, rect] of next) {\n if (!rectsEqual(rects.current.get(id), rect)) changed = true;\n }\n rects.current = next;\n return changed;\n }, []);\n\n const frameLoop = useVisibleRaf(\n () => {\n const el = container.current;\n if (!el) {\n dirty.current.clear();\n return;\n }\n if (needsMeasure.current) {\n needsMeasure.current = false;\n if (measure()) for (const id of rects.current.keys()) dirty.current.add(id);\n }\n const dpr = globalThis.devicePixelRatio ?? 1;\n if (dpr !== lastDpr.current) {\n lastDpr.current = dpr;\n for (const id of rects.current.keys()) dirty.current.add(id);\n }\n if (dirty.current.size === 0) return;\n const box = el.getBoundingClientRect();\n const frame: SurfaceFrame = {\n dirty: new Set(dirty.current),\n rects: new Map(rects.current),\n dpr,\n size: { width: box.width, height: box.height },\n };\n onFrameRef.current(frame);\n // After the owner, which is what resized the buffer this frame.\n for (const id of frame.dirty) {\n const rect = frame.rects.get(id);\n if (rect) painters.current.get(id)?.(rect, frame);\n }\n dirty.current.clear();\n },\n // The host attaches its container through `containerRef`, which may land\n // well after this hook's first effect; the gate re-resolves it per request.\n { target: () => container.current },\n );\n\n const schedule = useCallback(() => {\n frameLoop.request();\n }, [frameLoop]);\n\n const invalidate = useCallback(\n (id: string) => {\n dirty.current.add(id);\n schedule();\n },\n [schedule],\n );\n\n const invalidateAll = useCallback(() => {\n for (const id of tiles.current.keys()) dirty.current.add(id);\n schedule();\n }, [schedule]);\n\n const invalidateRects = useCallback(() => {\n needsMeasure.current = true;\n schedule();\n }, [schedule]);\n\n const registerTile = useCallback(\n (id: string, el: HTMLElement | null) => {\n const known = tiles.current.get(id);\n if (known === el) return;\n if (known) observer.current?.unobserve(known);\n if (el) {\n tiles.current.set(id, el);\n observer.current?.observe(el);\n } else {\n tiles.current.delete(id);\n rects.current.delete(id);\n dirty.current.delete(id);\n }\n needsMeasure.current = true;\n schedule();\n },\n [schedule],\n );\n\n const registerPainter = useCallback((id: string, paint: TilePainter) => {\n painters.current.set(id, paint);\n return () => {\n if (painters.current.get(id) === paint) painters.current.delete(id);\n };\n }, []);\n\n const getContainer = useCallback(() => container.current, []);\n\n const containerRef = useCallback(\n (el: HTMLElement | null) => {\n if (container.current === el) return;\n if (container.current) observer.current?.unobserve(container.current);\n container.current = el;\n if (el) observer.current?.observe(el);\n needsMeasure.current = true;\n schedule();\n },\n [schedule],\n );\n\n useEffect(() => {\n const ro = new ResizeObserver(() => {\n needsMeasure.current = true;\n schedule();\n });\n observer.current = ro;\n if (container.current) ro.observe(container.current);\n for (const el of tiles.current.values()) ro.observe(el);\n return () => {\n ro.disconnect();\n observer.current = null;\n frameLoop.cancel();\n };\n }, [schedule, frameLoop]);\n\n return {\n invalidate,\n invalidateAll,\n invalidateRects,\n registerTile,\n registerPainter,\n containerRef,\n getContainer,\n };\n}\n"]}