@weasel-js/labkit 0.1.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 (217) hide show
  1. package/README.md +72 -0
  2. package/dist/_dts/fitViewToBounds-rWrBI4a6.d.ts +870 -0
  3. package/dist/_dts/index-C6Yze7sQ.d.ts +93 -0
  4. package/dist/_dts/types-Si4Fw-1F.d.ts +48 -0
  5. package/dist/_dts/useExperimentState-eSvpwV_P.d.ts +102 -0
  6. package/dist/canvas/index.d.ts +34 -0
  7. package/dist/canvas/index.js +4 -0
  8. package/dist/canvas/index.js.map +1 -0
  9. package/dist/chunk-2QNYYL3V.js +86 -0
  10. package/dist/chunk-2QNYYL3V.js.map +1 -0
  11. package/dist/chunk-54IQ2DX7.js +222 -0
  12. package/dist/chunk-54IQ2DX7.js.map +1 -0
  13. package/dist/chunk-6FO7G5BB.js +6095 -0
  14. package/dist/chunk-6FO7G5BB.js.map +1 -0
  15. package/dist/chunk-7BKDG73Z.js +54 -0
  16. package/dist/chunk-7BKDG73Z.js.map +1 -0
  17. package/dist/chunk-B5ZUYRKN.js +23330 -0
  18. package/dist/chunk-B5ZUYRKN.js.map +1 -0
  19. package/dist/chunk-HXZHVU4G.js +126 -0
  20. package/dist/chunk-HXZHVU4G.js.map +1 -0
  21. package/dist/chunk-PWC7AQZM.js +8 -0
  22. package/dist/chunk-PWC7AQZM.js.map +1 -0
  23. package/dist/chunk-SD3HDXR7.js +161 -0
  24. package/dist/chunk-SD3HDXR7.js.map +1 -0
  25. package/dist/chunk-SSMHPMDV.js +350 -0
  26. package/dist/chunk-SSMHPMDV.js.map +1 -0
  27. package/dist/chunk-VLAHRJOC.js +201 -0
  28. package/dist/chunk-VLAHRJOC.js.map +1 -0
  29. package/dist/controls/index.d.ts +13 -0
  30. package/dist/controls/index.js +3 -0
  31. package/dist/controls/index.js.map +1 -0
  32. package/dist/dragdrop/index.d.ts +3 -0
  33. package/dist/dragdrop/index.js +3 -0
  34. package/dist/dragdrop/index.js.map +1 -0
  35. package/dist/fonts/oswald-latin-variable.woff2 +0 -0
  36. package/dist/index.d.ts +410 -0
  37. package/dist/index.js +1425 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/layers/index.d.ts +17 -0
  40. package/dist/layers/index.js +3 -0
  41. package/dist/layers/index.js.map +1 -0
  42. package/dist/passthrough/weasel-canvas.d.ts +258 -0
  43. package/dist/passthrough/weasel-canvas.js +3 -0
  44. package/dist/passthrough/weasel-canvas.js.map +1 -0
  45. package/dist/passthrough/weasel-ui.d.ts +2826 -0
  46. package/dist/passthrough/weasel-ui.js +4 -0
  47. package/dist/passthrough/weasel-ui.js.map +1 -0
  48. package/dist/primitives/index.d.ts +57 -0
  49. package/dist/primitives/index.js +4 -0
  50. package/dist/primitives/index.js.map +1 -0
  51. package/dist/state/index.d.ts +33 -0
  52. package/dist/state/index.js +34 -0
  53. package/dist/state/index.js.map +1 -0
  54. package/dist/styles.css +1224 -0
  55. package/dist/theme-interstellar.css +25 -0
  56. package/dist/theme-light.css +13 -0
  57. package/dist/ui/layers/index.d.ts +42 -0
  58. package/dist/ui/layers/index.js +5 -0
  59. package/dist/ui/layers/index.js.map +1 -0
  60. package/dist/undo/index.d.ts +30 -0
  61. package/dist/undo/index.js +3 -0
  62. package/dist/undo/index.js.map +1 -0
  63. package/package.json +99 -0
  64. package/src/canvas/AGENTS.md +72 -0
  65. package/src/canvas/CanvasStack.less +22 -0
  66. package/src/canvas/CanvasStack.stories.tsx +63 -0
  67. package/src/canvas/CanvasStack.test.tsx +82 -0
  68. package/src/canvas/CanvasStack.tsx +108 -0
  69. package/src/canvas/CanvasStackContext.ts +8 -0
  70. package/src/canvas/canvasCoords.test.ts +30 -0
  71. package/src/canvas/canvasCoords.ts +15 -0
  72. package/src/canvas/index.ts +6 -0
  73. package/src/canvas/useLayerScheduler.ts +72 -0
  74. package/src/canvas/usePanZoom.ts +98 -0
  75. package/src/controls/ControlPanel.less +61 -0
  76. package/src/controls/ControlPanel.stories.tsx +77 -0
  77. package/src/controls/ControlPanel.test.tsx +149 -0
  78. package/src/controls/ControlPanel.tsx +249 -0
  79. package/src/controls/index.ts +12 -0
  80. package/src/controls/types.ts +61 -0
  81. package/src/dragdrop/DragDropRuntime.tsx +135 -0
  82. package/src/dragdrop/DragGhost.less +17 -0
  83. package/src/dragdrop/DragGhost.tsx +24 -0
  84. package/src/dragdrop/Palette.less +34 -0
  85. package/src/dragdrop/Palette.tsx +27 -0
  86. package/src/dragdrop/dragDrop.test.tsx +145 -0
  87. package/src/dragdrop/index.ts +1 -0
  88. package/src/fonts/oswald-latin-variable.woff2 +0 -0
  89. package/src/index.ts +70 -0
  90. package/src/instrument/SineWave.smoke.test.tsx +94 -0
  91. package/src/instrument/capabilityDetector.test.ts +64 -0
  92. package/src/instrument/capabilityDetector.ts +17 -0
  93. package/src/instrument/defineInstrument.test.ts +27 -0
  94. package/src/instrument/defineInstrument.ts +5 -0
  95. package/src/instrument/index.ts +19 -0
  96. package/src/instrument/types.ts +65 -0
  97. package/src/instrument/validateConfigSchema.test.ts +125 -0
  98. package/src/instrument/validateConfigSchema.ts +65 -0
  99. package/src/lab/Lab.less +3 -0
  100. package/src/lab/Lab.stories.tsx +51 -0
  101. package/src/lab/Lab.test.tsx +126 -0
  102. package/src/lab/Lab.tsx +196 -0
  103. package/src/lab/LabContext.test.ts +11 -0
  104. package/src/lab/LabContext.ts +28 -0
  105. package/src/lab/LabShell.less +45 -0
  106. package/src/lab/LabShell.stories.tsx +39 -0
  107. package/src/lab/LabShell.test.tsx +59 -0
  108. package/src/lab/LabShell.tsx +33 -0
  109. package/src/lab/WorkspaceGrid.less +8 -0
  110. package/src/lab/WorkspaceGrid.stories.tsx +61 -0
  111. package/src/lab/WorkspaceGrid.test.tsx +40 -0
  112. package/src/lab/WorkspaceGrid.tsx +20 -0
  113. package/src/lab/gridDims.test.ts +35 -0
  114. package/src/lab/gridDims.ts +11 -0
  115. package/src/lab/index.ts +10 -0
  116. package/src/layers/AGENTS.md +65 -0
  117. package/src/layers/LayerList.less +53 -0
  118. package/src/layers/LayerList.test.tsx +49 -0
  119. package/src/layers/LayerList.tsx +105 -0
  120. package/src/layers/index.ts +3 -0
  121. package/src/passthrough/weasel-canvas.test.ts +30 -0
  122. package/src/passthrough/weasel-canvas.ts +23 -0
  123. package/src/passthrough/weasel-ui.test.ts +79 -0
  124. package/src/passthrough/weasel-ui.ts +150 -0
  125. package/src/primitives/FpsMeter.less +14 -0
  126. package/src/primitives/FpsMeter.stories.tsx +14 -0
  127. package/src/primitives/FpsMeter.test.tsx +38 -0
  128. package/src/primitives/FpsMeter.tsx +36 -0
  129. package/src/primitives/ScaleIndicator.less +25 -0
  130. package/src/primitives/ScaleIndicator.stories.tsx +16 -0
  131. package/src/primitives/ScaleIndicator.test.tsx +46 -0
  132. package/src/primitives/ScaleIndicator.tsx +48 -0
  133. package/src/primitives/Sidebar.less +57 -0
  134. package/src/primitives/Sidebar.stories.tsx +38 -0
  135. package/src/primitives/Sidebar.test.tsx +74 -0
  136. package/src/primitives/Sidebar.tsx +32 -0
  137. package/src/primitives/StatusBar.less +21 -0
  138. package/src/primitives/StatusBar.stories.tsx +24 -0
  139. package/src/primitives/StatusBar.test.tsx +25 -0
  140. package/src/primitives/StatusBar.tsx +18 -0
  141. package/src/primitives/Toolbar.less +43 -0
  142. package/src/primitives/Toolbar.stories.tsx +36 -0
  143. package/src/primitives/Toolbar.test.tsx +65 -0
  144. package/src/primitives/Toolbar.tsx +44 -0
  145. package/src/primitives/fpsAverage.test.ts +21 -0
  146. package/src/primitives/fpsAverage.ts +5 -0
  147. package/src/primitives/index.ts +9 -0
  148. package/src/state/SingletonExperiment.test.tsx +101 -0
  149. package/src/state/SingletonExperiment.tsx +52 -0
  150. package/src/state/adapters.test.ts +94 -0
  151. package/src/state/adapters.ts +102 -0
  152. package/src/state/context.tsx +42 -0
  153. package/src/state/helpers.test.ts +89 -0
  154. package/src/state/helpers.ts +61 -0
  155. package/src/state/index.ts +40 -0
  156. package/src/state/store.test.ts +243 -0
  157. package/src/state/store.ts +225 -0
  158. package/src/state/types.ts +53 -0
  159. package/src/state/useExperimentState.test.tsx +102 -0
  160. package/src/state/useExperimentState.ts +28 -0
  161. package/src/styles.less +20 -0
  162. package/src/test-setup.ts +17 -0
  163. package/src/theme/Interstellar.stories.less +92 -0
  164. package/src/theme/Interstellar.stories.tsx +243 -0
  165. package/src/theme/base.less +122 -0
  166. package/src/theme/interstellar.less +34 -0
  167. package/src/theme/light.less +14 -0
  168. package/src/theme/tokens.less +85 -0
  169. package/src/ui/format.ts +19 -0
  170. package/src/ui/layers/LayerStack.less +143 -0
  171. package/src/ui/layers/LayerStack.stories.tsx +45 -0
  172. package/src/ui/layers/LayerStack.test.tsx +170 -0
  173. package/src/ui/layers/LayerStack.tsx +215 -0
  174. package/src/ui/layers/index.ts +1 -0
  175. package/src/ui/properties/CheckboxRow.stories.tsx +27 -0
  176. package/src/ui/properties/ColorRow.stories.tsx +106 -0
  177. package/src/ui/properties/CurveField.less +48 -0
  178. package/src/ui/properties/CurveField.stories.tsx +20 -0
  179. package/src/ui/properties/CurveField.test.tsx +127 -0
  180. package/src/ui/properties/CurveField.tsx +181 -0
  181. package/src/ui/properties/EffectCard.tsx +312 -0
  182. package/src/ui/properties/Gallery.stories.tsx +178 -0
  183. package/src/ui/properties/NumberRow.stories.tsx +65 -0
  184. package/src/ui/properties/PropertyGroup.less +50 -0
  185. package/src/ui/properties/PropertyGroup.stories.tsx +34 -0
  186. package/src/ui/properties/PropertyGroup.test.tsx +33 -0
  187. package/src/ui/properties/PropertyGroup.tsx +44 -0
  188. package/src/ui/properties/PropertyList.stories.tsx +83 -0
  189. package/src/ui/properties/PropertyPanel.less +592 -0
  190. package/src/ui/properties/PropertyPanel.stories.tsx +191 -0
  191. package/src/ui/properties/PropertyPanel.test.tsx +193 -0
  192. package/src/ui/properties/PropertyPanel.tsx +441 -0
  193. package/src/ui/properties/PropertyRow.stories.tsx +53 -0
  194. package/src/ui/properties/SelectRow.stories.tsx +58 -0
  195. package/src/ui/properties/SliderRow.stories.tsx +74 -0
  196. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +396 -0
  197. package/src/ui/properties/TextRow.stories.tsx +62 -0
  198. package/src/ui/properties/ToggleRow.stories.tsx +57 -0
  199. package/src/ui/properties/index.ts +39 -0
  200. package/src/ui/properties/storyLayouts.tsx +36 -0
  201. package/src/undo/eventBus.test.ts +49 -0
  202. package/src/undo/eventBus.ts +32 -0
  203. package/src/undo/index.ts +5 -0
  204. package/src/undo/undoStack.test.ts +70 -0
  205. package/src/undo/undoStack.ts +38 -0
  206. package/src/workspace/DefaultSidebar.tsx +26 -0
  207. package/src/workspace/DefaultStatusBar.tsx +15 -0
  208. package/src/workspace/DefaultToolbar.tsx +88 -0
  209. package/src/workspace/Workspace.less +58 -0
  210. package/src/workspace/Workspace.stories.tsx +78 -0
  211. package/src/workspace/Workspace.test.tsx +186 -0
  212. package/src/workspace/Workspace.tsx +255 -0
  213. package/src/workspace/WorkspaceChrome.tsx +150 -0
  214. package/src/workspace/index.ts +24 -0
  215. package/src/workspace/slotTypes.ts +44 -0
  216. package/src/workspace/workspaceOps.test.ts +142 -0
  217. package/src/workspace/workspaceOps.ts +78 -0
@@ -0,0 +1,93 @@
1
+ import { ReactNode } from 'react';
2
+ import { C as ConfigField } from './types-Si4Fw-1F.js';
3
+
4
+ interface RenderContext<TS = unknown, TC = unknown> {
5
+ state: TS;
6
+ config: TC;
7
+ setState: (next: TS | ((prev: TS) => TS)) => void;
8
+ setConfig: (key: keyof TC, value: unknown) => void;
9
+ workspace: {
10
+ id: string;
11
+ zoom: number;
12
+ setZoom: (z: number) => void;
13
+ };
14
+ emit: (event: string) => void;
15
+ }
16
+ interface CanvasLayer<TS = unknown, TC = unknown> {
17
+ id: string;
18
+ draw: (ctx: CanvasRenderingContext2D, args: {
19
+ state: TS;
20
+ config: TC;
21
+ zoom: number;
22
+ }) => void;
23
+ }
24
+ interface CanvasCapability<TS = unknown, TC = unknown> {
25
+ layers: CanvasLayer<TS, TC>[];
26
+ initialView?: {
27
+ zoom: number;
28
+ pan: {
29
+ x: number;
30
+ y: number;
31
+ };
32
+ };
33
+ }
34
+ interface LayerCapability {
35
+ ids: string[];
36
+ }
37
+ interface DragDropCapability<TS = unknown, TC = unknown> {
38
+ palette: PaletteItem[] | ((state: TS, config: TC) => PaletteItem[]);
39
+ onDrop: (worldPos: Point, item: PaletteItem, state: TS, config: TC) => TS;
40
+ onDragOver?: (worldPos: Point, item: PaletteItem, state: TS, config: TC) => DragFeedback | null;
41
+ pickUp?: (hit: HitResult, state: TS, config: TC) => {
42
+ item: PaletteItem;
43
+ state: TS;
44
+ } | null;
45
+ }
46
+ interface UndoCapability {
47
+ snapshotOn?: string[];
48
+ maxDepth?: number;
49
+ }
50
+ type SystemEvent = string;
51
+ type Point = {
52
+ x: number;
53
+ y: number;
54
+ };
55
+ type HitResult = {
56
+ hit: boolean;
57
+ layerId?: string;
58
+ pointId?: string;
59
+ };
60
+ type ViewTransform = {
61
+ zoom: number;
62
+ pan: Point;
63
+ };
64
+ type LayerDescriptor = {
65
+ id: string;
66
+ label: string;
67
+ alwaysOn?: boolean;
68
+ };
69
+ type PaletteItem = {
70
+ id: string;
71
+ label: string;
72
+ data?: unknown;
73
+ };
74
+ type DragFeedback = {
75
+ ok: boolean;
76
+ reason?: string;
77
+ };
78
+ interface Instrument<TS = unknown, TC = unknown> {
79
+ name: string;
80
+ defaultConfig: () => TC;
81
+ initialState: (config: TC) => TS;
82
+ configSchema?: () => ConfigField[];
83
+ render: (ctx: RenderContext<TS, TC>) => ReactNode;
84
+ onConfigChange?: (config: TC, prev: TC, state: TS) => TS;
85
+ serialize?: (state: TS) => unknown;
86
+ deserialize?: (data: unknown, config: TC) => TS;
87
+ canvas?: CanvasCapability<TS, TC>;
88
+ layers?: LayerCapability;
89
+ dragDrop?: DragDropCapability<TS, TC>;
90
+ undo?: UndoCapability;
91
+ }
92
+
93
+ export type { CanvasCapability as C, DragDropCapability as D, HitResult as H, Instrument as I, LayerCapability as L, PaletteItem as P, RenderContext as R, SystemEvent as S, UndoCapability as U, ViewTransform as V, CanvasLayer as a, DragFeedback as b, LayerDescriptor as c, Point as d };
@@ -0,0 +1,48 @@
1
+ type ConfigFieldType = 'slider' | 'checkbox' | 'select' | 'number' | 'text' | 'color';
2
+ interface ConfigFieldBase {
3
+ key: string;
4
+ label: string;
5
+ type: ConfigFieldType;
6
+ }
7
+ interface SliderField extends ConfigFieldBase {
8
+ type: 'slider';
9
+ default: number;
10
+ min: number;
11
+ max: number;
12
+ step?: number;
13
+ }
14
+ interface CheckboxField extends ConfigFieldBase {
15
+ type: 'checkbox';
16
+ default: boolean;
17
+ }
18
+ interface SelectOption {
19
+ value: string;
20
+ label: string;
21
+ }
22
+ interface SelectField extends ConfigFieldBase {
23
+ type: 'select';
24
+ default: string;
25
+ options: SelectOption[];
26
+ }
27
+ interface NumberField extends ConfigFieldBase {
28
+ type: 'number';
29
+ default: number;
30
+ min?: number;
31
+ max?: number;
32
+ step?: number;
33
+ }
34
+ interface TextField extends ConfigFieldBase {
35
+ type: 'text';
36
+ default: string;
37
+ placeholder?: string;
38
+ maxLength?: number;
39
+ /** Milliseconds to debounce live setConfig calls. Default 150 ms. Set to 0 to disable. */
40
+ debounceMs?: number;
41
+ }
42
+ interface ColorField extends ConfigFieldBase {
43
+ type: 'color';
44
+ default: string;
45
+ }
46
+ type ConfigField = SliderField | CheckboxField | SelectField | NumberField | TextField | ColorField;
47
+
48
+ export type { ConfigField as C, NumberField as N, SelectField as S, TextField as T, CheckboxField as a, ColorField as b, ConfigFieldBase as c, ConfigFieldType as d, SelectOption as e, SliderField as f };
@@ -0,0 +1,102 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ReactElement } from 'react';
3
+ import { StoreApi } from 'zustand/vanilla';
4
+
5
+ interface UndoStack {
6
+ past: unknown[];
7
+ future: unknown[];
8
+ }
9
+ interface WorkspaceRecord<TS = unknown, TC = unknown> {
10
+ id: string;
11
+ instrumentName: string;
12
+ config: TC;
13
+ state: TS;
14
+ view: {
15
+ zoom: number;
16
+ pan: {
17
+ x: number;
18
+ y: number;
19
+ };
20
+ };
21
+ undoStack: UndoStack;
22
+ }
23
+ interface SavedSnapshot {
24
+ id: string;
25
+ name: string;
26
+ workspaceId: string;
27
+ instrumentName: string;
28
+ config: unknown;
29
+ state: unknown;
30
+ savedAt: number;
31
+ }
32
+ interface LabStoreState {
33
+ workspaces: WorkspaceRecord[];
34
+ savedSnapshots: SavedSnapshot[];
35
+ theme: 'light' | 'interstellar' | 'auto';
36
+ }
37
+ interface StorageAdapter {
38
+ read(key: string): string | null;
39
+ write(key: string, value: string): void;
40
+ delete?(key: string): void;
41
+ }
42
+ interface ExperimentStateHandle<TS, TC> {
43
+ state: TS;
44
+ setState: (next: TS | ((prev: TS) => TS)) => void;
45
+ config: TC;
46
+ setConfig: (key: keyof TC, value: TC[keyof TC]) => void;
47
+ }
48
+ interface CreateLabStoreOptions {
49
+ storageKey: string;
50
+ storage: StorageAdapter;
51
+ initialTheme?: 'light' | 'interstellar' | 'auto';
52
+ }
53
+ type InstrumentSerializers = Record<string, {
54
+ serialize?: (state: unknown) => unknown;
55
+ deserialize?: (data: unknown) => unknown;
56
+ } | undefined>;
57
+
58
+ declare const localStorageAdapter: StorageAdapter;
59
+ declare const sessionStorageAdapter: StorageAdapter;
60
+ declare const urlHashAdapter: StorageAdapter;
61
+ declare function createMemoryAdapter(): StorageAdapter;
62
+ declare const noneAdapter: StorageAdapter;
63
+
64
+ interface LabStoreActions {
65
+ addWorkspace: (record: Omit<WorkspaceRecord, 'undoStack'>) => void;
66
+ removeWorkspace: (id: string) => void;
67
+ updateWorkspaceState: <TS>(id: string, next: TS | ((prev: TS) => TS)) => void;
68
+ updateWorkspaceConfig: <TC>(id: string, key: keyof TC, value: TC[keyof TC]) => void;
69
+ updateWorkspaceView: (id: string, view: WorkspaceRecord['view']) => void;
70
+ updateWorkspaceUndoStack: (id: string, next: WorkspaceRecord['undoStack'] | ((prev: WorkspaceRecord['undoStack']) => WorkspaceRecord['undoStack'])) => void;
71
+ setWorkspaceInstrument: (id: string, instrumentName: string) => void;
72
+ saveSnapshot: (workspaceId: string, name: string) => void;
73
+ loadSnapshot: (snapshotId: string, workspaceId: string) => void;
74
+ deleteSnapshot: (snapshotId: string) => void;
75
+ listSnapshots: (workspaceId?: string) => SavedSnapshot[];
76
+ setTheme: (theme: 'light' | 'interstellar' | 'auto') => void;
77
+ }
78
+ type LabStore = StoreApi<LabStoreState & LabStoreActions> & {
79
+ registerSerializers: (s: InstrumentSerializers) => void;
80
+ };
81
+ declare function createLabStore(options: CreateLabStoreOptions): LabStore;
82
+
83
+ type LabStoreCtx = {
84
+ store: LabStore;
85
+ } | null;
86
+ declare const LabStoreContext: react.Context<LabStoreCtx>;
87
+ declare function LabStoreProvider({ store, children, }: {
88
+ store: LabStore;
89
+ children: ReactNode;
90
+ }): ReactElement;
91
+ declare function useLabStore(): LabStoreState & ReturnType<LabStore['getState']>;
92
+ declare const WorkspaceIdContext: react.Context<string | null>;
93
+ declare function WorkspaceIdProvider({ workspaceId, children, }: {
94
+ workspaceId: string;
95
+ children: ReactNode;
96
+ }): ReactElement;
97
+ declare function useWorkspaceId(): string;
98
+
99
+ declare function useExperimentState<TS = unknown, TC = unknown>(): ExperimentStateHandle<TS, TC>;
100
+
101
+ export { LabStoreContext as b, LabStoreProvider as c, WorkspaceIdContext as f, WorkspaceIdProvider as g, createLabStore as h, createMemoryAdapter as i, useExperimentState as j, useLabStore as k, localStorageAdapter as l, useWorkspaceId as m, noneAdapter as n, sessionStorageAdapter as s, urlHashAdapter as u };
102
+ export type { CreateLabStoreOptions as C, ExperimentStateHandle as E, InstrumentSerializers as I, LabStore as L, StorageAdapter as S, UndoStack as U, WorkspaceRecord as W, LabStoreActions as a, LabStoreState as d, SavedSnapshot as e };
@@ -0,0 +1,34 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { ReactNode } from 'react';
4
+ import { V as ViewTransform, d as Point } from '../_dts/index-C6Yze7sQ.js';
5
+ import '../_dts/types-Si4Fw-1F.js';
6
+
7
+ interface CanvasLayerDescriptor {
8
+ id: string;
9
+ visible: boolean;
10
+ render: (ctx: CanvasRenderingContext2D, view: ViewTransform) => void;
11
+ }
12
+
13
+ interface CanvasStackProps {
14
+ layers: CanvasLayerDescriptor[];
15
+ view: ViewTransform;
16
+ onViewChange: (v: ViewTransform) => void;
17
+ width?: number | string;
18
+ height?: number | string;
19
+ className?: string;
20
+ onHitTest?: (worldPos: Point) => void;
21
+ children?: ReactNode;
22
+ }
23
+ declare function CanvasStack({ layers, view, onViewChange, width, height, className, onHitTest, children, }: CanvasStackProps): react_jsx_runtime.JSX.Element;
24
+
25
+ interface CanvasStackContextValue {
26
+ view: ViewTransform;
27
+ }
28
+ declare const CanvasStackContext: react.Context<CanvasStackContextValue | null>;
29
+
30
+ declare function worldToScreen(world: Point, view: ViewTransform): Point;
31
+ declare function screenToWorld(screen: Point, view: ViewTransform): Point;
32
+
33
+ export { CanvasStack, CanvasStackContext, screenToWorld, worldToScreen };
34
+ export type { CanvasLayerDescriptor, CanvasStackContextValue, CanvasStackProps };
@@ -0,0 +1,4 @@
1
+ export { CanvasStack, screenToWorld, worldToScreen } from '../chunk-54IQ2DX7.js';
2
+ export { CanvasStackContext } from '../chunk-PWC7AQZM.js';
3
+ //# sourceMappingURL=index.js.map
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,86 @@
1
+ import { useState, useRef } from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/layers/LayerList.tsx
5
+ function LayerList({ layers, visibility, onReorder, onToggle, className }) {
6
+ const reorderable = layers.filter((l) => !l.alwaysOn);
7
+ const pinned = layers.filter((l) => l.alwaysOn);
8
+ const [dragIndex, setDragIndex] = useState(null);
9
+ const dragRef = useRef(null);
10
+ if (layers.length === 0) {
11
+ return /* @__PURE__ */ jsx("div", { className: className ? `lk-layer-list ${className}` : "lk-layer-list", children: /* @__PURE__ */ jsx("div", { className: "lk-layer-list__empty", children: "No layers" }) });
12
+ }
13
+ const startDrag = (e, index) => {
14
+ e.currentTarget.setPointerCapture(e.pointerId);
15
+ dragRef.current = { pointerId: e.pointerId, fromIndex: index, startY: e.clientY };
16
+ setDragIndex(index);
17
+ };
18
+ const moveDrag = (e) => {
19
+ const drag = dragRef.current;
20
+ if (!drag || drag.pointerId !== e.pointerId) return;
21
+ const rowHeight = 28;
22
+ const delta = Math.round((e.clientY - drag.startY) / rowHeight);
23
+ const target = Math.min(reorderable.length - 1, Math.max(0, drag.fromIndex + delta));
24
+ setDragIndex(target);
25
+ };
26
+ const endDrag = (e) => {
27
+ const drag = dragRef.current;
28
+ if (!drag || drag.pointerId !== e.pointerId) return;
29
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
30
+ e.currentTarget.releasePointerCapture(e.pointerId);
31
+ }
32
+ if (dragIndex !== null && dragIndex !== drag.fromIndex) {
33
+ const next = [...reorderable];
34
+ const [moved] = next.splice(drag.fromIndex, 1);
35
+ if (moved) next.splice(dragIndex, 0, moved);
36
+ onReorder([...next, ...pinned]);
37
+ }
38
+ dragRef.current = null;
39
+ setDragIndex(null);
40
+ };
41
+ return /* @__PURE__ */ jsxs("div", { className: className ? `lk-layer-list ${className}` : "lk-layer-list", children: [
42
+ reorderable.map((layer, i) => {
43
+ const isDragging = dragIndex === i;
44
+ return /* @__PURE__ */ jsxs(
45
+ "div",
46
+ {
47
+ className: isDragging ? "lk-layer-list__row lk-layer-list__row--dragging" : "lk-layer-list__row",
48
+ children: [
49
+ /* @__PURE__ */ jsx(
50
+ "button",
51
+ {
52
+ type: "button",
53
+ className: "lk-layer-list__handle",
54
+ "aria-label": `Reorder ${layer.label}`,
55
+ onPointerDown: (e) => startDrag(e, i),
56
+ onPointerMove: moveDrag,
57
+ onPointerUp: endDrag,
58
+ children: "\u22EE\u22EE"
59
+ }
60
+ ),
61
+ /* @__PURE__ */ jsx(
62
+ "input",
63
+ {
64
+ className: "lk-layer-list__check",
65
+ type: "checkbox",
66
+ checked: visibility[layer.id] !== false,
67
+ onChange: (e) => onToggle(layer.id, e.target.checked),
68
+ "aria-label": `Toggle ${layer.label}`
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx("span", { className: "lk-layer-list__label", children: layer.label })
72
+ ]
73
+ },
74
+ layer.id
75
+ );
76
+ }),
77
+ pinned.map((layer) => /* @__PURE__ */ jsxs("div", { className: "lk-layer-list__row lk-layer-list__row--pinned", children: [
78
+ /* @__PURE__ */ jsx("span", { className: "lk-layer-list__lock", role: "img", "aria-label": "Always on", children: "\u{1F512}" }),
79
+ /* @__PURE__ */ jsx("span", { className: "lk-layer-list__label", children: layer.label })
80
+ ] }, layer.id))
81
+ ] });
82
+ }
83
+
84
+ export { LayerList };
85
+ //# sourceMappingURL=chunk-2QNYYL3V.js.map
86
+ //# sourceMappingURL=chunk-2QNYYL3V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/layers/LayerList.tsx"],"names":[],"mappings":"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","file":"chunk-2QNYYL3V.js","sourcesContent":["import { type PointerEvent, useRef, useState } from 'react';\nimport type { LayerDescriptor } from '../instrument/types';\n\nexport interface LayerListProps {\n layers: LayerDescriptor[];\n visibility: Record<string, boolean>;\n onReorder: (newOrder: LayerDescriptor[]) => void;\n onToggle: (id: string, visible: boolean) => void;\n className?: string;\n}\n\ninterface DragState {\n pointerId: number;\n fromIndex: number;\n startY: number;\n}\n\nexport function LayerList({ layers, visibility, onReorder, onToggle, className }: LayerListProps) {\n const reorderable = layers.filter((l) => !l.alwaysOn);\n const pinned = layers.filter((l) => l.alwaysOn);\n const [dragIndex, setDragIndex] = useState<number | null>(null);\n const dragRef = useRef<DragState | null>(null);\n\n if (layers.length === 0) {\n return (\n <div className={className ? `lk-layer-list ${className}` : 'lk-layer-list'}>\n <div className=\"lk-layer-list__empty\">No layers</div>\n </div>\n );\n }\n\n const startDrag = (e: PointerEvent<HTMLElement>, index: number) => {\n e.currentTarget.setPointerCapture(e.pointerId);\n dragRef.current = { pointerId: e.pointerId, fromIndex: index, startY: e.clientY };\n setDragIndex(index);\n };\n\n const moveDrag = (e: PointerEvent<HTMLElement>) => {\n const drag = dragRef.current;\n if (!drag || drag.pointerId !== e.pointerId) return;\n const rowHeight = 28;\n const delta = Math.round((e.clientY - drag.startY) / rowHeight);\n const target = Math.min(reorderable.length - 1, Math.max(0, drag.fromIndex + delta));\n setDragIndex(target);\n };\n\n const endDrag = (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 if (dragIndex !== null && dragIndex !== drag.fromIndex) {\n const next = [...reorderable];\n const [moved] = next.splice(drag.fromIndex, 1);\n if (moved) next.splice(dragIndex, 0, moved);\n onReorder([...next, ...pinned]);\n }\n dragRef.current = null;\n setDragIndex(null);\n };\n\n return (\n <div className={className ? `lk-layer-list ${className}` : 'lk-layer-list'}>\n {reorderable.map((layer, i) => {\n const isDragging = dragIndex === i;\n return (\n <div\n key={layer.id}\n className={\n isDragging ? 'lk-layer-list__row lk-layer-list__row--dragging' : 'lk-layer-list__row'\n }\n >\n <button\n type=\"button\"\n className=\"lk-layer-list__handle\"\n aria-label={`Reorder ${layer.label}`}\n onPointerDown={(e) => startDrag(e, i)}\n onPointerMove={moveDrag}\n onPointerUp={endDrag}\n >\n ⋮⋮\n </button>\n <input\n className=\"lk-layer-list__check\"\n type=\"checkbox\"\n checked={visibility[layer.id] !== false}\n onChange={(e) => onToggle(layer.id, e.target.checked)}\n aria-label={`Toggle ${layer.label}`}\n />\n <span className=\"lk-layer-list__label\">{layer.label}</span>\n </div>\n );\n })}\n {pinned.map((layer) => (\n <div key={layer.id} className=\"lk-layer-list__row lk-layer-list__row--pinned\">\n <span className=\"lk-layer-list__lock\" role=\"img\" aria-label=\"Always on\">\n 🔒\n </span>\n <span className=\"lk-layer-list__label\">{layer.label}</span>\n </div>\n ))}\n </div>\n );\n}\n"]}
@@ -0,0 +1,222 @@
1
+ import { CanvasStackContext } from './chunk-PWC7AQZM.js';
2
+ import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ // src/canvas/canvasCoords.ts
6
+ function worldToScreen(world, view) {
7
+ return {
8
+ x: world.x * view.zoom + view.pan.x,
9
+ y: world.y * view.zoom + view.pan.y
10
+ };
11
+ }
12
+ function screenToWorld(screen, view) {
13
+ return {
14
+ x: (screen.x - view.pan.x) / view.zoom,
15
+ y: (screen.y - view.pan.y) / view.zoom
16
+ };
17
+ }
18
+ function useLayerScheduler({ layers, view, canvasRefs, size }) {
19
+ const dirty = useRef(/* @__PURE__ */ new Set());
20
+ const rafId = useRef(null);
21
+ const lastRenderRef = useRef(/* @__PURE__ */ new Map());
22
+ useEffect(() => {
23
+ for (const layer of layers) {
24
+ const prev = lastRenderRef.current.get(layer.id);
25
+ if (prev !== layer.render) dirty.current.add(layer.id);
26
+ lastRenderRef.current.set(layer.id, layer.render);
27
+ }
28
+ for (const id of [...lastRenderRef.current.keys()]) {
29
+ if (!layers.find((l) => l.id === id)) lastRenderRef.current.delete(id);
30
+ }
31
+ }, [layers]);
32
+ useEffect(() => {
33
+ for (const layer of layers) dirty.current.add(layer.id);
34
+ }, [view, size, layers]);
35
+ useEffect(() => {
36
+ const tick = () => {
37
+ rafId.current = null;
38
+ if (dirty.current.size === 0) return;
39
+ const map = canvasRefs.current;
40
+ if (!map) {
41
+ dirty.current.clear();
42
+ return;
43
+ }
44
+ for (const layer of layers) {
45
+ if (!dirty.current.has(layer.id)) continue;
46
+ if (!layer.visible) continue;
47
+ const canvas = map.get(layer.id);
48
+ if (!canvas) continue;
49
+ const ctx = canvas.getContext("2d");
50
+ if (!ctx) continue;
51
+ ctx.save();
52
+ ctx.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
53
+ ctx.clearRect(0, 0, size.width, size.height);
54
+ layer.render(ctx, view);
55
+ ctx.restore();
56
+ }
57
+ dirty.current.clear();
58
+ };
59
+ if (dirty.current.size > 0 && rafId.current === null) {
60
+ rafId.current = requestAnimationFrame(tick);
61
+ }
62
+ return () => {
63
+ if (rafId.current !== null) {
64
+ cancelAnimationFrame(rafId.current);
65
+ rafId.current = null;
66
+ }
67
+ };
68
+ });
69
+ }
70
+ var DRAG_THRESHOLD = 3;
71
+ function usePanZoom({
72
+ view,
73
+ onViewChange,
74
+ minZoom = 0.1,
75
+ maxZoom = 32
76
+ }) {
77
+ const dragRef = useRef(null);
78
+ const viewRef = useRef(view);
79
+ viewRef.current = view;
80
+ const onWheel = useCallback(
81
+ (e) => {
82
+ e.preventDefault();
83
+ const rect = e.currentTarget.getBoundingClientRect();
84
+ const cursorX = e.clientX - rect.left;
85
+ const cursorY = e.clientY - rect.top;
86
+ const v = viewRef.current;
87
+ const factor = Math.exp(-e.deltaY * 1e-3);
88
+ const nextZoom = Math.min(maxZoom, Math.max(minZoom, v.zoom * factor));
89
+ const ratio = nextZoom / v.zoom;
90
+ const nextPan = {
91
+ x: cursorX - (cursorX - v.pan.x) * ratio,
92
+ y: cursorY - (cursorY - v.pan.y) * ratio
93
+ };
94
+ onViewChange({ zoom: nextZoom, pan: nextPan });
95
+ },
96
+ [onViewChange, minZoom, maxZoom]
97
+ );
98
+ const onPointerDown = useCallback((e) => {
99
+ if (e.button !== 0) return;
100
+ e.currentTarget.setPointerCapture(e.pointerId);
101
+ dragRef.current = {
102
+ pointerId: e.pointerId,
103
+ startScreenX: e.clientX,
104
+ startScreenY: e.clientY,
105
+ startPan: { ...viewRef.current.pan },
106
+ moved: false
107
+ };
108
+ }, []);
109
+ const onPointerMove = useCallback(
110
+ (e) => {
111
+ const drag = dragRef.current;
112
+ if (!drag || drag.pointerId !== e.pointerId) return;
113
+ const dx = e.clientX - drag.startScreenX;
114
+ const dy = e.clientY - drag.startScreenY;
115
+ if (!drag.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
116
+ drag.moved = true;
117
+ onViewChange({
118
+ zoom: viewRef.current.zoom,
119
+ pan: { x: drag.startPan.x + dx, y: drag.startPan.y + dy }
120
+ });
121
+ },
122
+ [onViewChange]
123
+ );
124
+ const onPointerUp = useCallback((e) => {
125
+ const drag = dragRef.current;
126
+ if (!drag || drag.pointerId !== e.pointerId) return;
127
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
128
+ e.currentTarget.releasePointerCapture(e.pointerId);
129
+ }
130
+ dragRef.current = null;
131
+ }, []);
132
+ const isDragging = useCallback(() => dragRef.current?.moved === true, []);
133
+ return { onWheel, onPointerDown, onPointerMove, onPointerUp, isDragging };
134
+ }
135
+ function CanvasStack({
136
+ layers,
137
+ view,
138
+ onViewChange,
139
+ width = "100%",
140
+ height = "100%",
141
+ className,
142
+ onHitTest,
143
+ children
144
+ }) {
145
+ const containerRef = useRef(null);
146
+ const canvasMap = useRef(/* @__PURE__ */ new Map());
147
+ const [size, setSize] = useState({ width: 0, height: 0, dpr: 1 });
148
+ const setCanvasRef = (id, el) => {
149
+ if (el) canvasMap.current.set(id, el);
150
+ else canvasMap.current.delete(id);
151
+ };
152
+ useEffect(() => {
153
+ const el = containerRef.current;
154
+ if (!el) return;
155
+ const update = () => {
156
+ const rect = el.getBoundingClientRect();
157
+ const dpr = window.devicePixelRatio ?? 1;
158
+ setSize((prev) => {
159
+ if (prev.width === rect.width && prev.height === rect.height && prev.dpr === dpr) {
160
+ return prev;
161
+ }
162
+ return { width: rect.width, height: rect.height, dpr };
163
+ });
164
+ };
165
+ update();
166
+ if (typeof ResizeObserver === "undefined") return;
167
+ const ro = new ResizeObserver(update);
168
+ ro.observe(el);
169
+ return () => ro.disconnect();
170
+ }, []);
171
+ const handlers = usePanZoom({ view, onViewChange });
172
+ useLayerScheduler({ layers, view, canvasRefs: canvasMap, size });
173
+ const ctxValue = useMemo(() => ({ view }), [view]);
174
+ const containerStyle = { width, height };
175
+ const canvasPx = {
176
+ width: Math.max(0, Math.round(size.width * size.dpr)),
177
+ height: Math.max(0, Math.round(size.height * size.dpr))
178
+ };
179
+ const canvasCss = {
180
+ width: size.width,
181
+ height: size.height
182
+ };
183
+ const handlePointerUp = (e) => {
184
+ const wasDragging = handlers.isDragging();
185
+ handlers.onPointerUp(e);
186
+ if (!wasDragging && onHitTest && containerRef.current) {
187
+ const rect = containerRef.current.getBoundingClientRect();
188
+ const screen = { x: e.clientX - rect.left, y: e.clientY - rect.top };
189
+ onHitTest(screenToWorld(screen, view));
190
+ }
191
+ };
192
+ return /* @__PURE__ */ jsx(CanvasStackContext.Provider, { value: ctxValue, children: /* @__PURE__ */ jsxs(
193
+ "div",
194
+ {
195
+ ref: containerRef,
196
+ className: className ? `lk-canvas-stack ${className}` : "lk-canvas-stack",
197
+ style: containerStyle,
198
+ onWheel: handlers.onWheel,
199
+ onPointerDown: handlers.onPointerDown,
200
+ onPointerMove: handlers.onPointerMove,
201
+ onPointerUp: handlePointerUp,
202
+ children: [
203
+ layers.map((layer) => /* @__PURE__ */ jsx(
204
+ "canvas",
205
+ {
206
+ ref: (el) => setCanvasRef(layer.id, el),
207
+ className: "lk-canvas-stack__canvas",
208
+ width: canvasPx.width,
209
+ height: canvasPx.height,
210
+ style: { ...canvasCss, display: layer.visible ? "block" : "none" }
211
+ },
212
+ layer.id
213
+ )),
214
+ /* @__PURE__ */ jsx("div", { className: "lk-canvas-stack__overlay", children })
215
+ ]
216
+ }
217
+ ) });
218
+ }
219
+
220
+ export { CanvasStack, screenToWorld, worldToScreen };
221
+ //# sourceMappingURL=chunk-54IQ2DX7.js.map
222
+ //# sourceMappingURL=chunk-54IQ2DX7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/canvas/canvasCoords.ts","../src/canvas/useLayerScheduler.ts","../src/canvas/usePanZoom.ts","../src/canvas/CanvasStack.tsx"],"names":["useRef","useEffect"],"mappings":"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","file":"chunk-54IQ2DX7.js","sourcesContent":["import type { Point, ViewTransform } from '../instrument/types';\n\nexport function worldToScreen(world: Point, view: ViewTransform): Point {\n return {\n x: world.x * view.zoom + view.pan.x,\n y: world.y * view.zoom + view.pan.y,\n };\n}\n\nexport function screenToWorld(screen: Point, view: ViewTransform): Point {\n return {\n x: (screen.x - view.pan.x) / view.zoom,\n y: (screen.y - view.pan.y) / view.zoom,\n };\n}\n","import { type RefObject, useEffect, useRef } from 'react';\nimport type { ViewTransform } from '../instrument/types';\n\nexport interface CanvasLayerDescriptor {\n id: string;\n visible: boolean;\n render: (ctx: CanvasRenderingContext2D, view: ViewTransform) => void;\n}\n\ninterface SchedulerOptions {\n layers: CanvasLayerDescriptor[];\n view: ViewTransform;\n canvasRefs: RefObject<Map<string, HTMLCanvasElement>>;\n size: { width: number; height: number; dpr: number };\n}\n\nexport function useLayerScheduler({ layers, view, canvasRefs, size }: SchedulerOptions): void {\n const dirty = useRef<Set<string>>(new Set());\n const rafId = useRef<number | null>(null);\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, size, layers]);\n\n useEffect(() => {\n const tick = () => {\n rafId.current = null;\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);\n ctx.restore();\n }\n dirty.current.clear();\n };\n if (dirty.current.size > 0 && rafId.current === null) {\n rafId.current = requestAnimationFrame(tick);\n }\n return () => {\n if (rafId.current !== null) {\n cancelAnimationFrame(rafId.current);\n rafId.current = null;\n }\n };\n });\n}\n","import { type PointerEvent, useCallback, useRef, type WheelEvent } from 'react';\nimport type { ViewTransform } from '../instrument/types';\n\nexport interface UsePanZoomOptions {\n view: ViewTransform;\n onViewChange: (v: ViewTransform) => void;\n minZoom?: number;\n maxZoom?: number;\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\nexport function usePanZoom({\n view,\n onViewChange,\n minZoom = 0.1,\n maxZoom = 32,\n}: UsePanZoomOptions): PanZoomHandlers {\n const dragRef = useRef<DragState | null>(null);\n const viewRef = useRef(view);\n viewRef.current = view;\n\n const onWheel = useCallback(\n (e: WheelEvent<HTMLElement>) => {\n e.preventDefault();\n const rect = e.currentTarget.getBoundingClientRect();\n const cursorX = e.clientX - rect.left;\n const cursorY = e.clientY - rect.top;\n const v = viewRef.current;\n const factor = Math.exp(-e.deltaY * 0.001);\n const nextZoom = Math.min(maxZoom, Math.max(minZoom, v.zoom * factor));\n const ratio = nextZoom / v.zoom;\n const nextPan = {\n x: cursorX - (cursorX - v.pan.x) * ratio,\n y: cursorY - (cursorY - v.pan.y) * ratio,\n };\n onViewChange({ zoom: nextZoom, pan: nextPan });\n },\n [onViewChange, minZoom, maxZoom],\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';\n\nexport interface CanvasStackProps {\n layers: CanvasLayerDescriptor[];\n view: ViewTransform;\n onViewChange: (v: ViewTransform) => void;\n width?: number | string;\n height?: number | string;\n className?: string;\n onHitTest?: (worldPos: Point) => void;\n children?: ReactNode;\n}\n\nexport function CanvasStack({\n layers,\n view,\n onViewChange,\n width = '100%',\n height = '100%',\n className,\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 handlers = usePanZoom({ view, onViewChange });\n useLayerScheduler({ layers, view, canvasRefs: canvasMap, size });\n\n const ctxValue = useMemo(() => ({ view }), [view]);\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));\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"]}