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

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 (54) hide show
  1. package/README.md +226 -55
  2. package/dist/_dts/{CanvasStackContext-DtAn_3O-.d.ts → CanvasStackContext-LnCfqNBA.d.ts} +1 -1
  3. package/dist/_dts/{frac-Cp3NivlC.d.ts → frac-C-2c72Ij.d.ts} +9 -2
  4. package/dist/canvas/index.d.ts +73 -7
  5. package/dist/canvas/index.js +2 -2
  6. package/dist/chrome/index.d.ts +2 -2
  7. package/dist/chrome/index.js +5 -5
  8. package/dist/{chunk-LITE5YFB.js → chunk-6OEYTYML.js} +6 -6
  9. package/dist/{chunk-LITE5YFB.js.map → chunk-6OEYTYML.js.map} +1 -1
  10. package/dist/{chunk-JDDIZSLL.js → chunk-AE5CNVRU.js} +3 -3
  11. package/dist/{chunk-JDDIZSLL.js.map → chunk-AE5CNVRU.js.map} +1 -1
  12. package/dist/{chunk-EQIB2PGC.js → chunk-E2UQVZ44.js} +3 -3
  13. package/dist/{chunk-EQIB2PGC.js.map → chunk-E2UQVZ44.js.map} +1 -1
  14. package/dist/{chunk-4EY67BKJ.js → chunk-ESIQNQ6K.js} +3 -3
  15. package/dist/{chunk-4EY67BKJ.js.map → chunk-ESIQNQ6K.js.map} +1 -1
  16. package/dist/{chunk-6JMU56CU.js → chunk-HHGVISVZ.js} +4 -4
  17. package/dist/{chunk-6JMU56CU.js.map → chunk-HHGVISVZ.js.map} +1 -1
  18. package/dist/{chunk-3ZELRRBV.js → chunk-HMFODCOX.js} +86 -6
  19. package/dist/chunk-HMFODCOX.js.map +1 -0
  20. package/dist/{chunk-ROQDRXKW.js → chunk-L5QJOOLV.js} +126 -12
  21. package/dist/chunk-L5QJOOLV.js.map +1 -0
  22. package/dist/{chunk-5TCEVMPI.js → chunk-M75ZU6ZZ.js} +3 -3
  23. package/dist/{chunk-5TCEVMPI.js.map → chunk-M75ZU6ZZ.js.map} +1 -1
  24. package/dist/{chunk-3P5TTTA7.js → chunk-QB5SM4BM.js} +3 -3
  25. package/dist/{chunk-3P5TTTA7.js.map → chunk-QB5SM4BM.js.map} +1 -1
  26. package/dist/{chunk-HEORBLVT.js → chunk-RBGKL7NF.js} +3 -3
  27. package/dist/{chunk-HEORBLVT.js.map → chunk-RBGKL7NF.js.map} +1 -1
  28. package/dist/{chunk-YQJNT4XQ.js → chunk-TO2FUOKF.js} +4 -4
  29. package/dist/{chunk-YQJNT4XQ.js.map → chunk-TO2FUOKF.js.map} +1 -1
  30. package/dist/controls/index.js +3 -3
  31. package/dist/dragdrop/index.d.ts +1 -1
  32. package/dist/dragdrop/index.js +2 -2
  33. package/dist/index.d.ts +7 -52
  34. package/dist/index.js +49 -101
  35. package/dist/index.js.map +1 -1
  36. package/dist/layers/index.d.ts +2 -2
  37. package/dist/layers/index.js +3 -3
  38. package/dist/loupe/index.d.ts +3 -3
  39. package/dist/loupe/index.js +2 -2
  40. package/dist/passthrough/weasel-canvas.js +1 -1
  41. package/dist/passthrough/weasel-ui.js +2 -2
  42. package/dist/primitives/index.js +4 -4
  43. package/dist/surface/index.js +2 -2
  44. package/dist/ui/layers/index.js +2 -2
  45. package/dist/undo/index.d.ts +1 -1
  46. package/package.json +7 -7
  47. package/src/annotations/store.test.ts +72 -0
  48. package/src/annotations/store.ts +30 -0
  49. package/src/annotations/types.ts +9 -2
  50. package/src/canvas/index.ts +11 -0
  51. package/src/index.test.ts +1 -0
  52. package/src/index.ts +13 -9
  53. package/dist/chunk-3ZELRRBV.js.map +0 -1
  54. package/dist/chunk-ROQDRXKW.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { E as LayerDescriptor } from '../_dts/frac-Cp3NivlC.js';
2
- export { B as LayerCapability } from '../_dts/frac-Cp3NivlC.js';
1
+ import { E as LayerDescriptor } from '../_dts/frac-C-2c72Ij.js';
2
+ export { B as LayerCapability } from '../_dts/frac-C-2c72Ij.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import 'react';
5
5
  import '../_dts/types-D6s4b7if.js';
@@ -1,6 +1,6 @@
1
- export { LayerList } from '../chunk-JDDIZSLL.js';
1
+ export { LayerList } from '../chunk-AE5CNVRU.js';
2
2
  import '../chunk-SBXNQW4G.js';
3
- import '../chunk-HEORBLVT.js';
4
- import '../chunk-ROQDRXKW.js';
3
+ import '../chunk-RBGKL7NF.js';
4
+ import '../chunk-L5QJOOLV.js';
5
5
  //# sourceMappingURL=index.js.map
6
6
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ae as LoupeMode, V as ViewTransform, W as WorldFrame, c as WorldSpec, a2 as ViewportSize, K as LoupeRenderArgs, N as ResolvedLoupe } from '../_dts/frac-Cp3NivlC.js';
3
- export { L as LOUPE_DEFAULTS, G as LoupeCapability, J as LoupeDeclaration, ab as resolveLoupe } from '../_dts/frac-Cp3NivlC.js';
4
- import { c as CanvasStackSurface, C as CanvasLayerDescriptor } from '../_dts/CanvasStackContext-DtAn_3O-.js';
2
+ import { ae as LoupeMode, V as ViewTransform, W as WorldFrame, c as WorldSpec, a2 as ViewportSize, K as LoupeRenderArgs, N as ResolvedLoupe } from '../_dts/frac-C-2c72Ij.js';
3
+ export { L as LOUPE_DEFAULTS, G as LoupeCapability, J as LoupeDeclaration, ab as resolveLoupe } from '../_dts/frac-C-2c72Ij.js';
4
+ import { c as CanvasStackSurface, C as CanvasLayerDescriptor } from '../_dts/CanvasStackContext-LnCfqNBA.js';
5
5
  import { RefObject, ReactNode } from 'react';
6
6
  import '../_dts/types-D6s4b7if.js';
7
7
  import '../_dts/PrefsForm-BkUJZx0A.js';
@@ -1,7 +1,7 @@
1
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from '../chunk-EQIB2PGC.js';
1
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from '../chunk-E2UQVZ44.js';
2
2
  import '../chunk-CTRKTLYZ.js';
3
3
  import '../chunk-2AYGEN57.js';
4
4
  import '../chunk-CQLQPQ4P.js';
5
- import '../chunk-ROQDRXKW.js';
5
+ import '../chunk-L5QJOOLV.js';
6
6
  //# sourceMappingURL=index.js.map
7
7
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
- export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands, computeFitView, computeIndicatorCommand, renderSceneToCanvas } from '../chunk-ROQDRXKW.js';
1
+ export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands, computeFitView, computeIndicatorCommand, renderSceneToCanvas } from '../chunk-L5QJOOLV.js';
2
2
  //# sourceMappingURL=weasel-canvas.js.map
3
3
  //# sourceMappingURL=weasel-canvas.js.map
@@ -1,5 +1,5 @@
1
1
  import '../chunk-SBXNQW4G.js';
2
- export { m as ActionBar, r4 as ActionsBar, It as Badge, a3 as Button, h2 as Callout, c5 as CalloutTrigger, o as Checkbox, p3 as ComboBox, m3 as ComboBoxItem, F as CurveEditor, s as DataGrid, c as Dialog, n2 as DragHandleGlyph, Je as EDGE_PROFILES, n as Field, u as Input, s2 as KeyCap, c2 as KeySequence, c3 as LayerStack, e2 as MINUS_SIGN, f as NumberField, r3 as OptionsBar, f2 as Plot2D, n5 as PointPlotter, r5 as Powerline, u2 as Radio, l as RadioGroup, l2 as RangeSlider, p as Select, h as SelectItem, n3 as Sidebar, s3 as SidebarPanel, _2 as Slider, a2 as Switch, c4 as Tab, s4 as TabList, l3 as TabPanel, o4 as Tabs, r2 as ToggleBar, o3 as ToolButton, n4 as ToolGroup, _4 as ToolPalette, In as chromaAt, u3 as detectPlatform, a as dlog, r as fieldClasses, t as formatNumber, d2 as formatShortcut, u4 as formatShortcutParts, o2 as inferKeycapKind, i as isDebugEnabled, b2 as isPrefLeaf, e as keyGlyph, p2 as keySpecFromKey, d as keySpecsFromMods, Fn as oklchToHex, v2 as paintGradientTrack, x2 as prefValueAtPath, i2 as useReorderDragList, a4 as useRovingTabIndex, S as visiblePrefSubtree } from '../chunk-HEORBLVT.js';
3
- import '../chunk-ROQDRXKW.js';
2
+ export { m as ActionBar, r4 as ActionsBar, It as Badge, a3 as Button, h2 as Callout, c5 as CalloutTrigger, o as Checkbox, p3 as ComboBox, m3 as ComboBoxItem, F as CurveEditor, s as DataGrid, c as Dialog, n2 as DragHandleGlyph, Je as EDGE_PROFILES, n as Field, u as Input, s2 as KeyCap, c2 as KeySequence, c3 as LayerStack, e2 as MINUS_SIGN, f as NumberField, r3 as OptionsBar, f2 as Plot2D, n5 as PointPlotter, r5 as Powerline, u2 as Radio, l as RadioGroup, l2 as RangeSlider, p as Select, h as SelectItem, n3 as Sidebar, s3 as SidebarPanel, _2 as Slider, a2 as Switch, c4 as Tab, s4 as TabList, l3 as TabPanel, o4 as Tabs, r2 as ToggleBar, o3 as ToolButton, n4 as ToolGroup, _4 as ToolPalette, In as chromaAt, u3 as detectPlatform, a as dlog, r as fieldClasses, t as formatNumber, d2 as formatShortcut, u4 as formatShortcutParts, o2 as inferKeycapKind, i as isDebugEnabled, b2 as isPrefLeaf, e as keyGlyph, p2 as keySpecFromKey, d as keySpecsFromMods, Fn as oklchToHex, v2 as paintGradientTrack, x2 as prefValueAtPath, i2 as useReorderDragList, a4 as useRovingTabIndex, S as visiblePrefSubtree } from '../chunk-RBGKL7NF.js';
3
+ import '../chunk-L5QJOOLV.js';
4
4
  //# sourceMappingURL=weasel-ui.js.map
5
5
  //# sourceMappingURL=weasel-ui.js.map
@@ -1,8 +1,8 @@
1
- export { FloatingPanel, JobProgress, Legend, Sidebar } from '../chunk-3P5TTTA7.js';
2
- export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from '../chunk-6JMU56CU.js';
1
+ export { FloatingPanel, JobProgress, Legend, Sidebar } from '../chunk-QB5SM4BM.js';
2
+ export { FpsMeter, ScaleIndicator, StatusBar, Toolbar, ZoomControl } from '../chunk-HHGVISVZ.js';
3
3
  import '../chunk-SBXNQW4G.js';
4
- import '../chunk-HEORBLVT.js';
4
+ import '../chunk-RBGKL7NF.js';
5
5
  import '../chunk-CQLQPQ4P.js';
6
- import '../chunk-ROQDRXKW.js';
6
+ import '../chunk-L5QJOOLV.js';
7
7
  //# sourceMappingURL=index.js.map
8
8
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTiledSurface } from '../chunk-4EY67BKJ.js';
2
- import '../chunk-ROQDRXKW.js';
1
+ export { SurfaceCanvasContext, SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceCanvas, useSurfaceOptional, useSurfaceTile, useTiledSurface } from '../chunk-ESIQNQ6K.js';
2
+ import '../chunk-L5QJOOLV.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- export { c3 as LayerStack } from '../../chunk-HEORBLVT.js';
2
- import '../../chunk-ROQDRXKW.js';
1
+ export { c3 as LayerStack } from '../../chunk-RBGKL7NF.js';
2
+ import '../../chunk-L5QJOOLV.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- export { a0 as UndoCapability } from '../_dts/frac-Cp3NivlC.js';
1
+ export { a0 as UndoCapability } from '../_dts/frac-C-2c72Ij.js';
2
2
  import 'react';
3
3
  import '../_dts/types-D6s4b7if.js';
4
4
  import '../_dts/PrefsForm-BkUJZx0A.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weasel-js/labkit",
3
- "version": "1.4.0-pre.0",
3
+ "version": "1.4.0-pre.1",
4
4
  "description": "React widgets for building self-contained interactive lab pages",
5
5
  "license": "MIT",
6
6
  "author": "orochi235",
@@ -114,11 +114,11 @@
114
114
  "react-dom": "^19.0.0"
115
115
  },
116
116
  "dependencies": {
117
- "@weasel-js/core": "1.4.0-pre.0",
118
- "@weasel-js/loupe": "1.4.0-pre.0",
119
- "@weasel-js/svg": "1.4.0-pre.0",
120
- "@weasel-js/theme": "1.4.0-pre.0",
121
- "@weasel-js/ui": "1.4.0-pre.0",
117
+ "@weasel-js/core": "1.4.0-pre.1",
118
+ "@weasel-js/loupe": "1.4.0-pre.1",
119
+ "@weasel-js/svg": "1.4.0-pre.1",
120
+ "@weasel-js/theme": "1.4.0-pre.1",
121
+ "@weasel-js/ui": "1.4.0-pre.1",
122
122
  "earcut": "2.2.4",
123
123
  "polygon-clipping": "^0.15.7",
124
124
  "react-aria-components": "^1.5.0",
@@ -129,7 +129,7 @@
129
129
  "@biomejs/biome": "^2.4.12",
130
130
  "@rollup/plugin-alias": "^6.0.0",
131
131
  "@testing-library/jest-dom": "^6.6.0",
132
- "@weasel-js/modes": "1.4.0-pre.0",
132
+ "@weasel-js/modes": "1.4.0-pre.1",
133
133
  "less": "^4.2.0",
134
134
  "rollup-plugin-dts": "^6.4.1",
135
135
  "tsx": "^4.19.0",
@@ -155,6 +155,78 @@ describe('the annotation store', () => {
155
155
  expect(fn.mock.calls.length).toBe(before);
156
156
  });
157
157
 
158
+ // What the overlay's canvas does to a selection is `scene.setSelection` —
159
+ // weasel binds `useSelection` to the scene it is handed, so the selection is
160
+ // scene state, not React state, and these exercise the real write. What they
161
+ // do NOT reach is the pointer half: no click, marquee or handle runs here,
162
+ // because jsdom has no WebGL2 and the overlay's canvas never paints or
163
+ // hit-tests. That the canvas is bound to this scene is core's claim.
164
+ it('merges the selection across targets, and maps it back to annotation ids', () => {
165
+ const store = makeStore();
166
+ const onNaive = store.add(RING);
167
+ const onOcct = store.add({ ...RING, target: 'occt' });
168
+
169
+ store.setSelection([onOcct, onNaive]);
170
+ // Declaration order, not call order: one merged answer over several scenes.
171
+ expect(store.selection()).toEqual([onNaive, onOcct]);
172
+ expect(store.selection().map((id) => store.get(id)?.target)).toEqual(['naive', 'occt']);
173
+ });
174
+
175
+ it('reads back what the canvas would have written into the scene', () => {
176
+ const store = makeStore();
177
+ const id = store.add(RING);
178
+ const [node] = [...store.sceneFor('naive').renderOrder()];
179
+ if (node === undefined) throw new Error('unreachable');
180
+
181
+ store.sceneFor('naive').setSelection([node]);
182
+ expect(store.selection()).toEqual([id]);
183
+ });
184
+
185
+ it('replaces rather than adds, clearing a target the new selection omits', () => {
186
+ const store = makeStore();
187
+ const onNaive = store.add(RING);
188
+ const onOcct = store.add({ ...RING, target: 'occt' });
189
+
190
+ store.setSelection([onNaive]);
191
+ store.setSelection([onOcct]);
192
+ expect(store.selection()).toEqual([onOcct]);
193
+ expect(store.sceneFor('naive').getSelection()).toEqual([]);
194
+ });
195
+
196
+ it('drops an id it cannot resolve instead of throwing', () => {
197
+ const store = makeStore();
198
+ const id = store.add(RING);
199
+
200
+ expect(() => store.setSelection(['naive/nope', 'ghost/1', 'noslash', ''])).not.toThrow();
201
+ expect(store.selection()).toEqual([]);
202
+
203
+ store.setSelection([id, id]);
204
+ expect(store.selection()).toEqual([id]);
205
+ });
206
+
207
+ it('stops reporting a selected mark once it is removed', () => {
208
+ const store = makeStore();
209
+ const id = store.add(RING);
210
+ store.setSelection([id]);
211
+ store.remove(id);
212
+ expect(store.selection()).toEqual([]);
213
+ });
214
+
215
+ it('notifies subscribers when the selection changes', () => {
216
+ const store = makeStore();
217
+ const id = store.add(RING);
218
+ const fn = vi.fn();
219
+ store.subscribe(fn);
220
+
221
+ store.setSelection([id]);
222
+ expect(fn).toHaveBeenCalled();
223
+
224
+ // A write that changes nothing is not an event.
225
+ const before = fn.mock.calls.length;
226
+ store.setSelection([id]);
227
+ expect(fn.mock.calls.length).toBe(before);
228
+ });
229
+
158
230
  it('round-trips through JSON, and the snapshot is JSON-clean', () => {
159
231
  const store = makeStore();
160
232
  const id = store.add(RING, { angle: 'iso', shading: 'outline' });
@@ -221,6 +221,36 @@ export function createAnnotationStore(opts: AnnotationStoreOptions): Annotations
221
221
  return history.redo(historySceneAt);
222
222
  },
223
223
 
224
+ selection() {
225
+ const out: string[] = [];
226
+ for (const target of liveTargets()) {
227
+ const scene = scenes.get(target);
228
+ if (!scene) continue;
229
+ for (const node of scene.getSelection()) {
230
+ if (scene.get(node)) out.push(`${target}/${String(node)}`);
231
+ }
232
+ }
233
+ return out;
234
+ },
235
+
236
+ setSelection(ids) {
237
+ const wanted = new Map<string, string[]>();
238
+ for (const id of ids) {
239
+ const found = nodeOf(id);
240
+ if (!found) continue;
241
+ const node = String(found.node.id);
242
+ const on = wanted.get(found.target);
243
+ if (!on) wanted.set(found.target, [node]);
244
+ else if (!on.includes(node)) on.push(node);
245
+ }
246
+ for (const [target, scene] of scenes) {
247
+ const next = wanted.get(target) ?? [];
248
+ const now = scene.getSelection();
249
+ if (now.length === next.length && next.every((n, i) => String(now[i]) === n)) continue;
250
+ scene.setSelection(next.map(asNodeId));
251
+ }
252
+ },
253
+
224
254
  subscribe(fn) {
225
255
  subs.add(fn);
226
256
  return () => {
@@ -196,8 +196,15 @@ export interface AnnotationsApi {
196
196
  within(target: string, box: FracRect): Annotation[];
197
197
  /** Whether `a`'s position still describes the picture `config` produces. */
198
198
  isStale(a: Annotation, config: unknown): boolean;
199
- /** Fires after every mutation. No delta: the scene does not diff, so
200
- * re-query rather than expecting a change payload. */
199
+ /** The marks the user currently has selected, across every target, as
200
+ * annotation ids. Every one resolves through `get`. */
201
+ selection(): readonly string[];
202
+ /** Replace the selection. An id naming a target or a mark that is not
203
+ * there is dropped, the way `update` and `remove` ignore one. */
204
+ setSelection(ids: readonly string[]): void;
205
+ /** Fires after every mutation *and* after a selection change — weasel keeps
206
+ * a canvas's selection on the scene, so both already arrive on this one
207
+ * channel. No delta: re-query, and re-read `selection()`. */
201
208
  subscribe(fn: () => void): () => void;
202
209
 
203
210
  /** Whether the last mark change on any target can be taken back. Weasel
@@ -6,5 +6,16 @@ export type { ZoomAtOptions } from './camera';
6
6
  export { centerOn, zoomAt } from './camera';
7
7
  export { screenToWorld, worldToScreen } from './canvasCoords';
8
8
  export type { CanvasLayerDescriptor } from './useLayerScheduler';
9
+ export type { OrbitHandlers, OrbitView, UseOrbitOptions, Vec3 } from './useOrbit';
10
+ export {
11
+ clampPitch,
12
+ orbitAfterDrag,
13
+ orbitAfterWheel,
14
+ PITCH_LIMIT,
15
+ useOrbit,
16
+ wrapYaw,
17
+ } from './useOrbit';
18
+ export type { PanZoomHandlers, UsePanZoomOptions } from './usePanZoom';
19
+ export { usePanZoom } from './usePanZoom';
9
20
  export type { ViewportSize, WorldFrame, WorldSpec } from './worldSpec';
10
21
  export { applyCamera, DEFAULT_FRAME, resolveFrame } from './worldSpec';
package/src/index.test.ts CHANGED
@@ -67,6 +67,7 @@ describe('surface, job and orbit entry points', () => {
67
67
  expect(typeof kit.fracToWorld).toBe('function');
68
68
  expect(typeof kit.useOrbit).toBe('function');
69
69
  expect(typeof kit.orbitAfterDrag).toBe('function');
70
+ expect(typeof kit.usePanZoom).toBe('function');
70
71
  expect(typeof kit.useJob).toBe('function');
71
72
  expect(typeof kit.as2DView).toBe('function');
72
73
  expect(kit.SurfaceContext).toBeDefined();
package/src/index.ts CHANGED
@@ -115,6 +115,12 @@ export type {
115
115
  CanvasStackContextValue,
116
116
  CanvasStackProps,
117
117
  CanvasStackSurface,
118
+ OrbitHandlers,
119
+ OrbitView,
120
+ PanZoomHandlers,
121
+ UseOrbitOptions,
122
+ UsePanZoomOptions,
123
+ Vec3,
118
124
  ViewportSize,
119
125
  WorldFrame,
120
126
  WorldSpec,
@@ -125,21 +131,19 @@ export {
125
131
  CanvasStack,
126
132
  CanvasStackContext,
127
133
  centerOn,
128
- DEFAULT_FRAME,
129
- resolveFrame,
130
- screenToWorld,
131
- worldToScreen,
132
- zoomAt,
133
- } from './canvas';
134
- export type { OrbitHandlers, OrbitView, UseOrbitOptions, Vec3 } from './canvas/useOrbit';
135
- export {
136
134
  clampPitch,
135
+ DEFAULT_FRAME,
137
136
  orbitAfterDrag,
138
137
  orbitAfterWheel,
139
138
  PITCH_LIMIT,
139
+ resolveFrame,
140
+ screenToWorld,
140
141
  useOrbit,
142
+ usePanZoom,
143
+ worldToScreen,
141
144
  wrapYaw,
142
- } from './canvas/useOrbit';
145
+ zoomAt,
146
+ } from './canvas';
143
147
  export * from './chrome';
144
148
  export { f } from './config/builder';
145
149
  export { fromConfigFields } from './config/fromConfigField';
@@ -1 +0,0 @@
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"]}