@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.
- package/README.md +72 -0
- package/dist/_dts/fitViewToBounds-rWrBI4a6.d.ts +870 -0
- package/dist/_dts/index-C6Yze7sQ.d.ts +93 -0
- package/dist/_dts/types-Si4Fw-1F.d.ts +48 -0
- package/dist/_dts/useExperimentState-eSvpwV_P.d.ts +102 -0
- package/dist/canvas/index.d.ts +34 -0
- package/dist/canvas/index.js +4 -0
- package/dist/canvas/index.js.map +1 -0
- package/dist/chunk-2QNYYL3V.js +86 -0
- package/dist/chunk-2QNYYL3V.js.map +1 -0
- package/dist/chunk-54IQ2DX7.js +222 -0
- package/dist/chunk-54IQ2DX7.js.map +1 -0
- package/dist/chunk-6FO7G5BB.js +6095 -0
- package/dist/chunk-6FO7G5BB.js.map +1 -0
- package/dist/chunk-7BKDG73Z.js +54 -0
- package/dist/chunk-7BKDG73Z.js.map +1 -0
- package/dist/chunk-B5ZUYRKN.js +23330 -0
- package/dist/chunk-B5ZUYRKN.js.map +1 -0
- package/dist/chunk-HXZHVU4G.js +126 -0
- package/dist/chunk-HXZHVU4G.js.map +1 -0
- package/dist/chunk-PWC7AQZM.js +8 -0
- package/dist/chunk-PWC7AQZM.js.map +1 -0
- package/dist/chunk-SD3HDXR7.js +161 -0
- package/dist/chunk-SD3HDXR7.js.map +1 -0
- package/dist/chunk-SSMHPMDV.js +350 -0
- package/dist/chunk-SSMHPMDV.js.map +1 -0
- package/dist/chunk-VLAHRJOC.js +201 -0
- package/dist/chunk-VLAHRJOC.js.map +1 -0
- package/dist/controls/index.d.ts +13 -0
- package/dist/controls/index.js +3 -0
- package/dist/controls/index.js.map +1 -0
- package/dist/dragdrop/index.d.ts +3 -0
- package/dist/dragdrop/index.js +3 -0
- package/dist/dragdrop/index.js.map +1 -0
- package/dist/fonts/oswald-latin-variable.woff2 +0 -0
- package/dist/index.d.ts +410 -0
- package/dist/index.js +1425 -0
- package/dist/index.js.map +1 -0
- package/dist/layers/index.d.ts +17 -0
- package/dist/layers/index.js +3 -0
- package/dist/layers/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +258 -0
- package/dist/passthrough/weasel-canvas.js +3 -0
- package/dist/passthrough/weasel-canvas.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +2826 -0
- package/dist/passthrough/weasel-ui.js +4 -0
- package/dist/passthrough/weasel-ui.js.map +1 -0
- package/dist/primitives/index.d.ts +57 -0
- package/dist/primitives/index.js +4 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/state/index.d.ts +33 -0
- package/dist/state/index.js +34 -0
- package/dist/state/index.js.map +1 -0
- package/dist/styles.css +1224 -0
- package/dist/theme-interstellar.css +25 -0
- package/dist/theme-light.css +13 -0
- package/dist/ui/layers/index.d.ts +42 -0
- package/dist/ui/layers/index.js +5 -0
- package/dist/ui/layers/index.js.map +1 -0
- package/dist/undo/index.d.ts +30 -0
- package/dist/undo/index.js +3 -0
- package/dist/undo/index.js.map +1 -0
- package/package.json +99 -0
- package/src/canvas/AGENTS.md +72 -0
- package/src/canvas/CanvasStack.less +22 -0
- package/src/canvas/CanvasStack.stories.tsx +63 -0
- package/src/canvas/CanvasStack.test.tsx +82 -0
- package/src/canvas/CanvasStack.tsx +108 -0
- package/src/canvas/CanvasStackContext.ts +8 -0
- package/src/canvas/canvasCoords.test.ts +30 -0
- package/src/canvas/canvasCoords.ts +15 -0
- package/src/canvas/index.ts +6 -0
- package/src/canvas/useLayerScheduler.ts +72 -0
- package/src/canvas/usePanZoom.ts +98 -0
- package/src/controls/ControlPanel.less +61 -0
- package/src/controls/ControlPanel.stories.tsx +77 -0
- package/src/controls/ControlPanel.test.tsx +149 -0
- package/src/controls/ControlPanel.tsx +249 -0
- package/src/controls/index.ts +12 -0
- package/src/controls/types.ts +61 -0
- package/src/dragdrop/DragDropRuntime.tsx +135 -0
- package/src/dragdrop/DragGhost.less +17 -0
- package/src/dragdrop/DragGhost.tsx +24 -0
- package/src/dragdrop/Palette.less +34 -0
- package/src/dragdrop/Palette.tsx +27 -0
- package/src/dragdrop/dragDrop.test.tsx +145 -0
- package/src/dragdrop/index.ts +1 -0
- package/src/fonts/oswald-latin-variable.woff2 +0 -0
- package/src/index.ts +70 -0
- package/src/instrument/SineWave.smoke.test.tsx +94 -0
- package/src/instrument/capabilityDetector.test.ts +64 -0
- package/src/instrument/capabilityDetector.ts +17 -0
- package/src/instrument/defineInstrument.test.ts +27 -0
- package/src/instrument/defineInstrument.ts +5 -0
- package/src/instrument/index.ts +19 -0
- package/src/instrument/types.ts +65 -0
- package/src/instrument/validateConfigSchema.test.ts +125 -0
- package/src/instrument/validateConfigSchema.ts +65 -0
- package/src/lab/Lab.less +3 -0
- package/src/lab/Lab.stories.tsx +51 -0
- package/src/lab/Lab.test.tsx +126 -0
- package/src/lab/Lab.tsx +196 -0
- package/src/lab/LabContext.test.ts +11 -0
- package/src/lab/LabContext.ts +28 -0
- package/src/lab/LabShell.less +45 -0
- package/src/lab/LabShell.stories.tsx +39 -0
- package/src/lab/LabShell.test.tsx +59 -0
- package/src/lab/LabShell.tsx +33 -0
- package/src/lab/WorkspaceGrid.less +8 -0
- package/src/lab/WorkspaceGrid.stories.tsx +61 -0
- package/src/lab/WorkspaceGrid.test.tsx +40 -0
- package/src/lab/WorkspaceGrid.tsx +20 -0
- package/src/lab/gridDims.test.ts +35 -0
- package/src/lab/gridDims.ts +11 -0
- package/src/lab/index.ts +10 -0
- package/src/layers/AGENTS.md +65 -0
- package/src/layers/LayerList.less +53 -0
- package/src/layers/LayerList.test.tsx +49 -0
- package/src/layers/LayerList.tsx +105 -0
- package/src/layers/index.ts +3 -0
- package/src/passthrough/weasel-canvas.test.ts +30 -0
- package/src/passthrough/weasel-canvas.ts +23 -0
- package/src/passthrough/weasel-ui.test.ts +79 -0
- package/src/passthrough/weasel-ui.ts +150 -0
- package/src/primitives/FpsMeter.less +14 -0
- package/src/primitives/FpsMeter.stories.tsx +14 -0
- package/src/primitives/FpsMeter.test.tsx +38 -0
- package/src/primitives/FpsMeter.tsx +36 -0
- package/src/primitives/ScaleIndicator.less +25 -0
- package/src/primitives/ScaleIndicator.stories.tsx +16 -0
- package/src/primitives/ScaleIndicator.test.tsx +46 -0
- package/src/primitives/ScaleIndicator.tsx +48 -0
- package/src/primitives/Sidebar.less +57 -0
- package/src/primitives/Sidebar.stories.tsx +38 -0
- package/src/primitives/Sidebar.test.tsx +74 -0
- package/src/primitives/Sidebar.tsx +32 -0
- package/src/primitives/StatusBar.less +21 -0
- package/src/primitives/StatusBar.stories.tsx +24 -0
- package/src/primitives/StatusBar.test.tsx +25 -0
- package/src/primitives/StatusBar.tsx +18 -0
- package/src/primitives/Toolbar.less +43 -0
- package/src/primitives/Toolbar.stories.tsx +36 -0
- package/src/primitives/Toolbar.test.tsx +65 -0
- package/src/primitives/Toolbar.tsx +44 -0
- package/src/primitives/fpsAverage.test.ts +21 -0
- package/src/primitives/fpsAverage.ts +5 -0
- package/src/primitives/index.ts +9 -0
- package/src/state/SingletonExperiment.test.tsx +101 -0
- package/src/state/SingletonExperiment.tsx +52 -0
- package/src/state/adapters.test.ts +94 -0
- package/src/state/adapters.ts +102 -0
- package/src/state/context.tsx +42 -0
- package/src/state/helpers.test.ts +89 -0
- package/src/state/helpers.ts +61 -0
- package/src/state/index.ts +40 -0
- package/src/state/store.test.ts +243 -0
- package/src/state/store.ts +225 -0
- package/src/state/types.ts +53 -0
- package/src/state/useExperimentState.test.tsx +102 -0
- package/src/state/useExperimentState.ts +28 -0
- package/src/styles.less +20 -0
- package/src/test-setup.ts +17 -0
- package/src/theme/Interstellar.stories.less +92 -0
- package/src/theme/Interstellar.stories.tsx +243 -0
- package/src/theme/base.less +122 -0
- package/src/theme/interstellar.less +34 -0
- package/src/theme/light.less +14 -0
- package/src/theme/tokens.less +85 -0
- package/src/ui/format.ts +19 -0
- package/src/ui/layers/LayerStack.less +143 -0
- package/src/ui/layers/LayerStack.stories.tsx +45 -0
- package/src/ui/layers/LayerStack.test.tsx +170 -0
- package/src/ui/layers/LayerStack.tsx +215 -0
- package/src/ui/layers/index.ts +1 -0
- package/src/ui/properties/CheckboxRow.stories.tsx +27 -0
- package/src/ui/properties/ColorRow.stories.tsx +106 -0
- package/src/ui/properties/CurveField.less +48 -0
- package/src/ui/properties/CurveField.stories.tsx +20 -0
- package/src/ui/properties/CurveField.test.tsx +127 -0
- package/src/ui/properties/CurveField.tsx +181 -0
- package/src/ui/properties/EffectCard.tsx +312 -0
- package/src/ui/properties/Gallery.stories.tsx +178 -0
- package/src/ui/properties/NumberRow.stories.tsx +65 -0
- package/src/ui/properties/PropertyGroup.less +50 -0
- package/src/ui/properties/PropertyGroup.stories.tsx +34 -0
- package/src/ui/properties/PropertyGroup.test.tsx +33 -0
- package/src/ui/properties/PropertyGroup.tsx +44 -0
- package/src/ui/properties/PropertyList.stories.tsx +83 -0
- package/src/ui/properties/PropertyPanel.less +592 -0
- package/src/ui/properties/PropertyPanel.stories.tsx +191 -0
- package/src/ui/properties/PropertyPanel.test.tsx +193 -0
- package/src/ui/properties/PropertyPanel.tsx +441 -0
- package/src/ui/properties/PropertyRow.stories.tsx +53 -0
- package/src/ui/properties/SelectRow.stories.tsx +58 -0
- package/src/ui/properties/SliderRow.stories.tsx +74 -0
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +396 -0
- package/src/ui/properties/TextRow.stories.tsx +62 -0
- package/src/ui/properties/ToggleRow.stories.tsx +57 -0
- package/src/ui/properties/index.ts +39 -0
- package/src/ui/properties/storyLayouts.tsx +36 -0
- package/src/undo/eventBus.test.ts +49 -0
- package/src/undo/eventBus.ts +32 -0
- package/src/undo/index.ts +5 -0
- package/src/undo/undoStack.test.ts +70 -0
- package/src/undo/undoStack.ts +38 -0
- package/src/workspace/DefaultSidebar.tsx +26 -0
- package/src/workspace/DefaultStatusBar.tsx +15 -0
- package/src/workspace/DefaultToolbar.tsx +88 -0
- package/src/workspace/Workspace.less +58 -0
- package/src/workspace/Workspace.stories.tsx +78 -0
- package/src/workspace/Workspace.test.tsx +186 -0
- package/src/workspace/Workspace.tsx +255 -0
- package/src/workspace/WorkspaceChrome.tsx +150 -0
- package/src/workspace/index.ts +24 -0
- package/src/workspace/slotTypes.ts +44 -0
- package/src/workspace/workspaceOps.test.ts +142 -0
- 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 @@
|
|
|
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"]}
|