@weasel-js/labkit 1.2.0 → 1.3.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_dts/{DrawCommand-C_XboUpz.d.ts → DrawCommand-DBB45NfN.d.ts} +196 -52
- package/dist/_dts/PrefsForm-BYa6cWnO.d.ts +201 -0
- package/dist/_dts/index-DgcNdEdh.d.ts +314 -0
- package/dist/_dts/types-1Sdxy_Pv.d.ts +90 -0
- package/dist/_dts/types-AHlQxBNN.d.ts +162 -0
- package/dist/_dts/{useTrialState-BMNIx3Cy.d.ts → useTrialState-CsGMjhu9.d.ts} +5 -87
- package/dist/canvas/index.d.ts +7 -3
- package/dist/canvas/index.js +2 -1
- package/dist/chrome/index.d.ts +86 -0
- package/dist/chrome/index.js +8 -0
- package/dist/chrome/index.js.map +1 -0
- package/dist/{chunk-VUU5UXHE.js → chunk-4TMMVIDM.js} +8 -5
- package/dist/chunk-4TMMVIDM.js.map +1 -0
- package/dist/{chunk-THBG7FQZ.js → chunk-HFDVGF4X.js} +15 -12
- package/dist/chunk-HFDVGF4X.js.map +1 -0
- package/dist/{chunk-574LJAV4.js → chunk-NS54R52R.js} +31 -21
- package/dist/chunk-NS54R52R.js.map +1 -0
- package/dist/{chunk-G5TJVQQT.js → chunk-O2BULFHX.js} +5 -14
- package/dist/chunk-O2BULFHX.js.map +1 -0
- package/dist/chunk-O7NVSCLN.js +266 -0
- package/dist/chunk-O7NVSCLN.js.map +1 -0
- package/dist/chunk-PO6L3NSH.js +366 -0
- package/dist/chunk-PO6L3NSH.js.map +1 -0
- package/dist/chunk-PQJ5B2U2.js +27 -0
- package/dist/chunk-PQJ5B2U2.js.map +1 -0
- package/dist/chunk-RVNN4CHQ.js +212 -0
- package/dist/chunk-RVNN4CHQ.js.map +1 -0
- package/dist/chunk-TLGRYALP.js +6151 -0
- package/dist/chunk-TLGRYALP.js.map +1 -0
- package/dist/{chunk-SFL7NFKN.js → chunk-V7PRSIRQ.js} +15 -7
- package/dist/chunk-V7PRSIRQ.js.map +1 -0
- package/dist/{chunk-BOHF3PQO.js → chunk-Z6HJ5FEM.js} +8111 -5190
- package/dist/chunk-Z6HJ5FEM.js.map +1 -0
- package/dist/chunk-ZZAYVX4X.js +505 -0
- package/dist/chunk-ZZAYVX4X.js.map +1 -0
- package/dist/controls/index.d.ts +19 -5
- package/dist/controls/index.js +3 -1
- package/dist/dragdrop/index.d.ts +4 -2
- package/dist/index.d.ts +246 -106
- package/dist/index.js +673 -559
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +5 -3
- package/dist/layers/index.js +2 -1
- package/dist/passthrough/weasel-canvas.d.ts +69 -13
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +3799 -3672
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/primitives/index.d.ts +102 -8
- package/dist/primitives/index.js +4 -1
- package/dist/state/index.d.ts +3 -2
- package/dist/state/index.js +2 -2
- package/dist/styles.css +542 -153
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +4 -3
- package/dist/undo/index.d.ts +4 -2
- package/package.json +16 -7
- package/src/canvas/AGENTS.md +4 -0
- package/src/canvas/CanvasStack.tsx +6 -2
- package/src/canvas/useLayerScheduler.ts +22 -15
- package/src/canvas/usePanZoom.test.ts +80 -0
- package/src/canvas/usePanZoom.ts +16 -2
- package/src/chrome/ChromeRegions.stories.tsx +106 -0
- package/src/chrome/builtins.test.ts +153 -0
- package/src/chrome/builtins.tsx +205 -0
- package/src/chrome/index.ts +25 -0
- package/src/chrome/merge.test.ts +51 -0
- package/src/chrome/merge.ts +52 -0
- package/src/chrome/regions/PaletteRegion.less +14 -0
- package/src/chrome/regions/PaletteRegion.test.tsx +36 -0
- package/src/chrome/regions/PaletteRegion.tsx +41 -0
- package/src/chrome/regions/SidebarRegion.tsx +56 -0
- package/src/chrome/regions/StatusRegion.tsx +31 -0
- package/src/chrome/regions/TitleBarRegion.tsx +41 -0
- package/src/chrome/regions/ToolbarRegion.test.tsx +82 -0
- package/src/chrome/regions/ToolbarRegion.tsx +71 -0
- package/src/chrome/regions/ViewportRegion.less +42 -0
- package/src/chrome/regions/ViewportRegion.tsx +37 -0
- package/src/chrome/regions/regions.test.tsx +81 -0
- package/src/chrome/types.ts +127 -0
- package/src/config/builder.test.ts +87 -0
- package/src/config/builder.ts +224 -0
- package/src/config/fromConfigField.test.ts +93 -0
- package/src/config/fromConfigField.ts +74 -0
- package/src/config/index.ts +22 -0
- package/src/config/resolve.test.ts +120 -0
- package/src/config/resolve.ts +67 -0
- package/src/config/rules.ts +68 -0
- package/src/config/types.ts +108 -0
- package/src/config/useConfigSchema.ts +30 -0
- package/src/config/visible.ts +20 -0
- package/src/controls/ControlPanel.less +4 -58
- package/src/controls/ControlPanel.stories.tsx +86 -0
- package/src/controls/ControlPanel.test.tsx +223 -10
- package/src/controls/ControlPanel.tsx +202 -203
- package/src/dragdrop/DragDropRuntime.tsx +36 -18
- package/src/dragdrop/DragGhost.less +1 -1
- package/src/dragdrop/Palette.less +1 -1
- package/src/index.ts +25 -3
- package/src/instrument/SineWave.smoke.test.tsx +8 -1
- package/src/instrument/defineInstrument.test.ts +38 -1
- package/src/instrument/defineInstrument.ts +31 -4
- package/src/instrument/index.ts +1 -0
- package/src/instrument/types.ts +27 -4
- package/src/instrument/validateConfigSchema.test.ts +13 -3
- package/src/instrument/validateConfigSchema.ts +7 -10
- package/src/lab/Lab.chrome.test.tsx +65 -0
- package/src/lab/Lab.tsx +58 -15
- package/src/lab/LabContext.ts +6 -0
- package/src/lab/LabFullChrome.stories.tsx +308 -0
- package/src/lab/LabHeader.test.tsx +42 -0
- package/src/lab/LabHeader.tsx +57 -0
- package/src/lab/LabPalette.test.tsx +35 -0
- package/src/lab/LabPalette.tsx +34 -0
- package/src/lab/LabShell.less +50 -3
- package/src/lab/Workspace.less +1 -30
- package/src/lab/Workspace.test.tsx +20 -10
- package/src/lab/Workspace.tsx +5 -8
- package/src/lab/index.ts +2 -0
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.less +31 -7
- package/src/layers/LayerList.tsx +17 -4
- package/src/passthrough/weasel-ui.ts +23 -0
- package/src/primitives/DragHandleGlyph.tsx +29 -0
- package/src/primitives/FloatingPanel.less +28 -0
- package/src/primitives/FloatingPanel.stories.tsx +46 -0
- package/src/primitives/FloatingPanel.test.tsx +240 -0
- package/src/primitives/FloatingPanel.tsx +190 -0
- package/src/primitives/FpsMeter.test.tsx +44 -0
- package/src/primitives/FpsMeter.tsx +20 -9
- package/src/primitives/JobProgress.less +69 -0
- package/src/primitives/JobProgress.stories.tsx +48 -0
- package/src/primitives/JobProgress.tsx +65 -0
- package/src/primitives/Legend.less +47 -0
- package/src/primitives/Legend.stories.tsx +32 -0
- package/src/primitives/Legend.test.tsx +57 -0
- package/src/primitives/Legend.tsx +40 -0
- package/src/primitives/Sidebar.less +32 -2
- package/src/primitives/StatusBar.less +7 -0
- package/src/primitives/StatusBar.tsx +10 -3
- package/src/primitives/Toolbar.less +50 -3
- package/src/primitives/Toolbar.test.tsx +67 -1
- package/src/primitives/Toolbar.tsx +72 -6
- package/src/primitives/ZoomControl.less +31 -0
- package/src/primitives/ZoomControl.tsx +76 -0
- package/src/primitives/floating.entry.test.tsx +14 -0
- package/src/primitives/index.ts +10 -2
- package/src/primitives/useRovingTabIndex.test.ts +27 -0
- package/src/primitives/useRovingTabIndex.ts +79 -0
- package/src/state/store.ts +11 -3
- package/src/state/toolSlot.test.ts +36 -0
- package/src/state/types.ts +4 -0
- package/src/styles.less +6 -0
- package/src/surface/useTiledSurface.ts +14 -10
- package/src/test-setup.ts +26 -0
- package/src/theme/base.less +19 -2
- package/src/theme/interstellar.tokens.json +6 -2
- package/src/tools/types.ts +24 -0
- package/src/trial/Trial.config.test.tsx +113 -0
- package/src/trial/Trial.job.test.tsx +13 -15
- package/src/trial/Trial.less +115 -10
- package/src/trial/Trial.stories.tsx +1 -0
- package/src/trial/Trial.test.tsx +67 -96
- package/src/trial/Trial.tsx +80 -39
- package/src/trial/TrialChrome.tsx +99 -80
- package/src/trial/TrialDragContext.ts +10 -0
- package/src/trial/TrialTitleBar.tsx +41 -0
- package/src/trial/index.ts +1 -15
- package/src/trial/trialChrome.borders.test.ts +38 -0
- package/src/ui/layers/LayerStack.less +9 -9
- package/src/ui/layers/LayerStack.tsx +1 -13
- package/src/ui/properties/CurveField.less +2 -2
- package/src/ui/properties/CurveField.test.tsx +11 -11
- package/src/ui/properties/PropertyGroup.less +2 -2
- package/src/ui/properties/PropertyPanel.less +55 -41
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +4 -2
- package/src/ui/properties/storyLayouts.tsx +2 -2
- package/dist/_dts/index-JFAYj5Tv.d.ts +0 -144
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/chunk-574LJAV4.js.map +0 -1
- package/dist/chunk-5R2ATYPJ.js +0 -201
- package/dist/chunk-5R2ATYPJ.js.map +0 -1
- package/dist/chunk-73KA7WBO.js +0 -5992
- package/dist/chunk-73KA7WBO.js.map +0 -1
- package/dist/chunk-BOHF3PQO.js.map +0 -1
- package/dist/chunk-G5TJVQQT.js.map +0 -1
- package/dist/chunk-PMAU3SEE.js +0 -126
- package/dist/chunk-PMAU3SEE.js.map +0 -1
- package/dist/chunk-SFL7NFKN.js.map +0 -1
- package/dist/chunk-THBG7FQZ.js.map +0 -1
- package/dist/chunk-VUU5UXHE.js.map +0 -1
- package/src/instrument/capabilityDetector.test.ts +0 -64
- package/src/instrument/capabilityDetector.ts +0 -20
- package/src/trial/DefaultSidebar.tsx +0 -29
- package/src/trial/DefaultStatusBar.tsx +0 -19
- package/src/trial/DefaultToolbar.tsx +0 -92
- package/src/trial/slotTypes.ts +0 -54
|
@@ -1,251 +1,250 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { isBuiltinToolPref } from '@weasel-js/core';
|
|
2
|
+
import type { PrefLeaf } from '@weasel-js/ui';
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
4
|
+
import { fromConfigFields } from '../config/fromConfigField';
|
|
5
|
+
import type { ControlRenderer, ResolvedConfig } from '../config/types';
|
|
6
|
+
import { isLeafVisible } from '../config/visible';
|
|
7
|
+
import { PropertyGroup } from '../ui/properties/PropertyGroup';
|
|
8
|
+
import {
|
|
9
|
+
CheckboxRow,
|
|
10
|
+
ColorRow,
|
|
11
|
+
NumberRow,
|
|
12
|
+
PropertyList,
|
|
13
|
+
PropertyRow,
|
|
14
|
+
SelectRow,
|
|
15
|
+
TextRow,
|
|
16
|
+
ToggleRow,
|
|
17
|
+
SliderRow,
|
|
18
|
+
} from '../ui/properties/PropertyPanel';
|
|
19
|
+
import type { ConfigField } from './types';
|
|
11
20
|
|
|
12
21
|
export interface ControlPanelProps<TC extends Record<string, unknown>> {
|
|
13
|
-
|
|
22
|
+
/** The instrument's resolved config schema. */
|
|
23
|
+
schema?: ResolvedConfig;
|
|
24
|
+
/** @deprecated Pass `schema`. A field list is adapted into one internally. */
|
|
25
|
+
fields?: ConfigField[];
|
|
14
26
|
config: TC;
|
|
15
27
|
setConfig: (key: keyof TC, value: unknown) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Control overrides and app-defined kinds, PrefsForm-style. Keys are config
|
|
30
|
+
* paths (checked first) or leaf kinds. A renderer returning `null` collapses
|
|
31
|
+
* the row; entries override the built-in rows on collision.
|
|
32
|
+
*/
|
|
33
|
+
renderers?: Record<string, ControlRenderer>;
|
|
34
|
+
/** Draw leaves marked `hidden`. */
|
|
35
|
+
showHidden?: boolean;
|
|
16
36
|
className?: string;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
/** Render an instrument's config schema as a stack of controls, each writing
|
|
20
|
-
* back through `setConfig`.
|
|
40
|
+
* back through `setConfig`. Built on the property rows, so a lab's controls
|
|
41
|
+
* are the same aligned, themed rows the rest of the kit uses. */
|
|
21
42
|
export function ControlPanel<TC extends Record<string, unknown>>({
|
|
43
|
+
schema,
|
|
22
44
|
fields,
|
|
23
45
|
config,
|
|
24
46
|
setConfig,
|
|
47
|
+
renderers,
|
|
48
|
+
showHidden = false,
|
|
25
49
|
className,
|
|
26
50
|
}: ControlPanelProps<TC>) {
|
|
51
|
+
const resolved = useMemo(
|
|
52
|
+
() => schema ?? fromConfigFields(fields ?? []),
|
|
53
|
+
[schema, fields],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const paths = Object.keys(resolved.group.children);
|
|
57
|
+
const sectioned = new Set(resolved.sections.flatMap((s) => s.paths));
|
|
58
|
+
const loose = paths.filter((p) => !sectioned.has(p));
|
|
59
|
+
|
|
60
|
+
const row = (path: string): ReactNode => (
|
|
61
|
+
<ControlRow
|
|
62
|
+
key={path}
|
|
63
|
+
path={path}
|
|
64
|
+
resolved={resolved}
|
|
65
|
+
config={config}
|
|
66
|
+
setConfig={setConfig}
|
|
67
|
+
renderers={renderers}
|
|
68
|
+
showHidden={showHidden}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
|
|
27
72
|
return (
|
|
28
|
-
<
|
|
29
|
-
{
|
|
30
|
-
|
|
73
|
+
<PropertyList className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}>
|
|
74
|
+
{loose.map(row)}
|
|
75
|
+
{resolved.sections.map((section) => (
|
|
76
|
+
<PropertyGroup key={section.label} title={section.label}>
|
|
77
|
+
{section.paths.map(row)}
|
|
78
|
+
</PropertyGroup>
|
|
31
79
|
))}
|
|
32
|
-
</
|
|
80
|
+
</PropertyList>
|
|
33
81
|
);
|
|
34
82
|
}
|
|
35
83
|
|
|
36
84
|
interface ControlRowProps<TC extends Record<string, unknown>> {
|
|
37
|
-
|
|
85
|
+
path: string;
|
|
86
|
+
resolved: ResolvedConfig;
|
|
38
87
|
config: TC;
|
|
39
88
|
setConfig: (key: keyof TC, value: unknown) => void;
|
|
89
|
+
renderers?: Record<string, ControlRenderer>;
|
|
90
|
+
showHidden: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Reads a labkit-only extra off a leaf. `PrefLeaf` has no field for these,
|
|
94
|
+
* and extra keys survive the resolve pass at runtime. */
|
|
95
|
+
function extra<T>(leaf: PrefLeaf, key: string): T | undefined {
|
|
96
|
+
return (leaf as unknown as Record<string, T | undefined>)[key];
|
|
40
97
|
}
|
|
41
98
|
|
|
42
99
|
function ControlRow<TC extends Record<string, unknown>>({
|
|
43
|
-
|
|
100
|
+
path,
|
|
101
|
+
resolved,
|
|
44
102
|
config,
|
|
45
103
|
setConfig,
|
|
104
|
+
renderers,
|
|
105
|
+
showHidden,
|
|
46
106
|
}: ControlRowProps<TC>) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
case 'checkbox':
|
|
51
|
-
return <CheckboxRow field={field} config={config} setConfig={setConfig} />;
|
|
52
|
-
case 'select':
|
|
53
|
-
return <SelectRow field={field} config={config} setConfig={setConfig} />;
|
|
54
|
-
case 'number':
|
|
55
|
-
return <NumberRow field={field} config={config} setConfig={setConfig} />;
|
|
56
|
-
case 'text':
|
|
57
|
-
return <TextRow field={field} config={config} setConfig={setConfig} />;
|
|
58
|
-
case 'color':
|
|
59
|
-
return <ColorRow field={field} config={config} setConfig={setConfig} />;
|
|
60
|
-
default:
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
107
|
+
const leaf = resolved.group.children[path] as PrefLeaf | undefined;
|
|
108
|
+
if (!leaf || !('kind' in leaf)) return null;
|
|
109
|
+
if (!isLeafVisible(resolved, path, config as Record<string, unknown>, showHidden)) return null;
|
|
64
110
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
config
|
|
68
|
-
setConfig: (key: keyof TC, value: unknown) => void;
|
|
69
|
-
};
|
|
111
|
+
const write = (value: unknown): void => setConfig(path as keyof TC, value);
|
|
112
|
+
const fallback = extra<unknown>(leaf, 'default');
|
|
113
|
+
const value = config[path] ?? fallback;
|
|
70
114
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const value = (config[field.key] as number | undefined) ?? field.default;
|
|
77
|
-
return (
|
|
78
|
-
<div className="lk-control-row">
|
|
79
|
-
<label className="lk-control-label" htmlFor={`lk-ctrl-${field.key}`}>
|
|
80
|
-
{field.label}
|
|
81
|
-
</label>
|
|
82
|
-
<div className="lk-control-slider-row">
|
|
83
|
-
<input
|
|
84
|
-
id={`lk-ctrl-${field.key}`}
|
|
85
|
-
className="lk-control-input lk-control-input--range"
|
|
86
|
-
type="range"
|
|
87
|
-
min={field.min}
|
|
88
|
-
max={field.max}
|
|
89
|
-
step={field.step ?? 1}
|
|
90
|
-
value={value}
|
|
91
|
-
onChange={(e) => setConfig(field.key as keyof TC, Number(e.target.value))}
|
|
92
|
-
/>
|
|
93
|
-
<span className="lk-control-slider-value">{value}</span>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
115
|
+
// Most specific wins, and within a tier the lab's entry beats the
|
|
116
|
+
// instrument's: controls[path] -> node .render -> controls[kind] -> built-in.
|
|
117
|
+
const custom =
|
|
118
|
+
renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];
|
|
119
|
+
if (custom) return custom({ path, pref: leaf, value, setValue: write });
|
|
98
120
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
config,
|
|
102
|
-
setConfig,
|
|
103
|
-
}: RowProps<CheckboxField, TC>) {
|
|
104
|
-
const value = (config[field.key] as boolean | undefined) ?? field.default;
|
|
105
|
-
return (
|
|
106
|
-
<div className="lk-control-row lk-control-row--inline">
|
|
107
|
-
<input
|
|
108
|
-
id={`lk-ctrl-${field.key}`}
|
|
109
|
-
className="lk-control-input lk-control-input--checkbox"
|
|
110
|
-
type="checkbox"
|
|
111
|
-
checked={value}
|
|
112
|
-
onChange={(e) => setConfig(field.key as keyof TC, e.target.checked)}
|
|
113
|
-
/>
|
|
114
|
-
<label className="lk-control-label" htmlFor={`lk-ctrl-${field.key}`}>
|
|
115
|
-
{field.label}
|
|
116
|
-
</label>
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
}
|
|
121
|
+
const label = leaf.name;
|
|
122
|
+
const read = <T,>(): T => value as T;
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
124
|
+
if (!isBuiltinToolPref(leaf)) return <UnwiredRow label={label} kind={leaf.kind} />;
|
|
125
|
+
|
|
126
|
+
switch (leaf.kind) {
|
|
127
|
+
case 'number': {
|
|
128
|
+
const min = extra<number>(leaf, 'min');
|
|
129
|
+
const max = extra<number>(leaf, 'max');
|
|
130
|
+
const step = extra<number>(leaf, 'step');
|
|
131
|
+
if (extra<string>(leaf, 'control') === 'slider' && min !== undefined && max !== undefined) {
|
|
132
|
+
return (
|
|
133
|
+
<SliderRow
|
|
134
|
+
label={label}
|
|
135
|
+
value={read<number>()}
|
|
136
|
+
min={min}
|
|
137
|
+
max={max}
|
|
138
|
+
step={step}
|
|
139
|
+
onChange={write}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
// NumberRow commits every keystroke and does not clamp, so the bounds a
|
|
144
|
+
// schema declares are enforced here — an instrument should never be
|
|
145
|
+
// handed a config value outside the range it asked for.
|
|
146
|
+
const lo = min ?? Number.NEGATIVE_INFINITY;
|
|
147
|
+
const hi = max ?? Number.POSITIVE_INFINITY;
|
|
148
|
+
return (
|
|
149
|
+
<NumberRow
|
|
150
|
+
label={label}
|
|
151
|
+
value={read<number>()}
|
|
152
|
+
min={min}
|
|
153
|
+
max={max}
|
|
154
|
+
step={step}
|
|
155
|
+
onChange={(n) => write(Math.min(hi, Math.max(lo, n)))}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
case 'boolean':
|
|
160
|
+
return <CheckboxRow label={label} value={read<boolean>()} onChange={write} />;
|
|
161
|
+
case 'enum': {
|
|
162
|
+
const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
|
|
163
|
+
const Row = extra<string>(leaf, 'control') === 'radio' ? ToggleRow : SelectRow;
|
|
164
|
+
return (
|
|
165
|
+
<Row label={label} value={read<string>()} options={options} onChange={write} />
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
case 'string':
|
|
169
|
+
return <DebouncedTextRow leaf={leaf} label={label} value={read<string>()} write={write} />;
|
|
170
|
+
case 'color':
|
|
171
|
+
return <ColorRow label={label} value={read<string>()} onChange={write} />;
|
|
172
|
+
case 'paint':
|
|
173
|
+
case 'object':
|
|
174
|
+
// Declined: a hex swatch would write a solid over a gradient, and a flat
|
|
175
|
+
// row would write one field into a half-built object. Override with
|
|
176
|
+
// `render` to edit either.
|
|
177
|
+
return <UnwiredRow label={label} kind={leaf.kind} />;
|
|
178
|
+
default: {
|
|
179
|
+
// Not reachable while every built-in kind has an arm — and a new kind
|
|
180
|
+
// that lacks one is a compile error here, never a blank row.
|
|
181
|
+
const _exhaustive: never = leaf;
|
|
182
|
+
throw new Error(
|
|
183
|
+
`ControlPanel: no control for built-in pref kind "${(_exhaustive as { kind: string }).kind}"`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
146
187
|
}
|
|
147
188
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
setConfig,
|
|
152
|
-
}: RowProps<NumberField, TC>) {
|
|
153
|
-
const value = (config[field.key] as number | undefined) ?? field.default;
|
|
189
|
+
/** A leaf this panel has no control for is named rather than dropped: a silent
|
|
190
|
+
* gap reads as "this control does not exist". */
|
|
191
|
+
function UnwiredRow({ label, kind }: { label: string; kind: string }) {
|
|
154
192
|
return (
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
</label>
|
|
159
|
-
<input
|
|
160
|
-
id={`lk-ctrl-${field.key}`}
|
|
161
|
-
className="lk-control-input"
|
|
162
|
-
type="number"
|
|
163
|
-
value={value}
|
|
164
|
-
min={field.min}
|
|
165
|
-
max={field.max}
|
|
166
|
-
step={field.step}
|
|
167
|
-
onChange={(e) => setConfig(field.key as keyof TC, Number(e.target.value))}
|
|
168
|
-
onBlur={(e) => {
|
|
169
|
-
const n = Number(e.target.value);
|
|
170
|
-
if (!Number.isFinite(n)) return;
|
|
171
|
-
let clamped = n;
|
|
172
|
-
if (field.min !== undefined && clamped < field.min) clamped = field.min;
|
|
173
|
-
if (field.max !== undefined && clamped > field.max) clamped = field.max;
|
|
174
|
-
if (clamped !== n) setConfig(field.key as keyof TC, clamped);
|
|
175
|
-
}}
|
|
176
|
-
/>
|
|
177
|
-
</div>
|
|
193
|
+
<PropertyRow label={label}>
|
|
194
|
+
<span className="lk-control-panel__unknown">no control for “{kind}”</span>
|
|
195
|
+
</PropertyRow>
|
|
178
196
|
);
|
|
179
197
|
}
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
config
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
/** Text writes are debounced so typing does not re-run the instrument on every
|
|
200
|
+
* keystroke, which means the row is locally controlled between commits and has
|
|
201
|
+
* to notice when the config changes underneath it. */
|
|
202
|
+
function DebouncedTextRow({
|
|
203
|
+
leaf,
|
|
204
|
+
label,
|
|
205
|
+
value,
|
|
206
|
+
write,
|
|
207
|
+
}: {
|
|
208
|
+
leaf: PrefLeaf;
|
|
209
|
+
label: string;
|
|
210
|
+
value: string;
|
|
211
|
+
write: (value: unknown) => void;
|
|
212
|
+
}) {
|
|
213
|
+
const debounceMs = extra<number>(leaf, 'debounceMs') ?? 150;
|
|
214
|
+
const [local, setLocal] = useState(value);
|
|
189
215
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
190
|
-
const lastExternal = useRef(
|
|
216
|
+
const lastExternal = useRef(value);
|
|
191
217
|
|
|
192
218
|
useEffect(() => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
setLocal(ext);
|
|
219
|
+
if (value !== lastExternal.current) {
|
|
220
|
+
lastExternal.current = value;
|
|
221
|
+
setLocal(value);
|
|
197
222
|
}
|
|
198
|
-
}, [
|
|
223
|
+
}, [value]);
|
|
199
224
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<input
|
|
206
|
-
id={`lk-ctrl-${field.key}`}
|
|
207
|
-
className="lk-control-input"
|
|
208
|
-
type="text"
|
|
209
|
-
value={local}
|
|
210
|
-
placeholder={field.placeholder}
|
|
211
|
-
maxLength={field.maxLength}
|
|
212
|
-
onChange={(e) => {
|
|
213
|
-
const next = e.target.value;
|
|
214
|
-
setLocal(next);
|
|
215
|
-
if (timer.current) clearTimeout(timer.current);
|
|
216
|
-
if (debounceMs === 0) {
|
|
217
|
-
lastExternal.current = next;
|
|
218
|
-
setConfig(field.key as keyof TC, next);
|
|
219
|
-
} else {
|
|
220
|
-
timer.current = setTimeout(() => {
|
|
221
|
-
lastExternal.current = next;
|
|
222
|
-
setConfig(field.key as keyof TC, next);
|
|
223
|
-
}, debounceMs);
|
|
224
|
-
}
|
|
225
|
-
}}
|
|
226
|
-
/>
|
|
227
|
-
</div>
|
|
225
|
+
useEffect(
|
|
226
|
+
() => () => {
|
|
227
|
+
if (timer.current) clearTimeout(timer.current);
|
|
228
|
+
},
|
|
229
|
+
[],
|
|
228
230
|
);
|
|
229
|
-
}
|
|
230
231
|
|
|
231
|
-
function ColorRow<TC extends Record<string, unknown>>({
|
|
232
|
-
field,
|
|
233
|
-
config,
|
|
234
|
-
setConfig,
|
|
235
|
-
}: RowProps<ColorField, TC>) {
|
|
236
|
-
const value = (config[field.key] as string | undefined) ?? field.default;
|
|
237
232
|
return (
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
233
|
+
<TextRow
|
|
234
|
+
label={label}
|
|
235
|
+
value={local}
|
|
236
|
+
placeholder={extra<string>(leaf, 'placeholder')}
|
|
237
|
+
maxLength={extra<number>(leaf, 'maxLength')}
|
|
238
|
+
onChange={(next) => {
|
|
239
|
+
setLocal(next);
|
|
240
|
+
if (timer.current) clearTimeout(timer.current);
|
|
241
|
+
const commit = () => {
|
|
242
|
+
lastExternal.current = next;
|
|
243
|
+
write(next);
|
|
244
|
+
};
|
|
245
|
+
if (debounceMs === 0) commit();
|
|
246
|
+
else timer.current = setTimeout(commit, debounceMs);
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
250
249
|
);
|
|
251
250
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useVisibleRaf } from '@weasel-js/core';
|
|
1
2
|
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
3
|
import { screenToWorld } from '../canvas/canvasCoords';
|
|
3
4
|
import type {
|
|
@@ -42,7 +43,9 @@ export function useDragDrop<TS, TC>({
|
|
|
42
43
|
const [drag, setDrag] = useState<DragState | null>(null);
|
|
43
44
|
const dragRef = useRef<DragState | null>(null);
|
|
44
45
|
dragRef.current = drag;
|
|
45
|
-
|
|
46
|
+
/** The move the next frame will resolve. Doubles as the throttle's flag:
|
|
47
|
+
* non-null means a frame is already queued. */
|
|
48
|
+
const pendingPos = useRef<Point | null>(null);
|
|
46
49
|
|
|
47
50
|
const isOverCanvas = useCallback(
|
|
48
51
|
(screenPos: Point): boolean => {
|
|
@@ -69,37 +72,42 @@ export function useDragDrop<TS, TC>({
|
|
|
69
72
|
[canvasContainerRef, view],
|
|
70
73
|
);
|
|
71
74
|
|
|
75
|
+
const frameLoop = useVisibleRaf(() => {
|
|
76
|
+
const screenPos = pendingPos.current;
|
|
77
|
+
pendingPos.current = null;
|
|
78
|
+
const active = dragRef.current;
|
|
79
|
+
if (!active || !screenPos) return;
|
|
80
|
+
let feedback: DragFeedback | null = null;
|
|
81
|
+
if (capability.onDragOver && isOverCanvas(screenPos)) {
|
|
82
|
+
const world = screenToWorldFromContainer(screenPos);
|
|
83
|
+
if (world) feedback = capability.onDragOver(world, active.item, state, config);
|
|
84
|
+
}
|
|
85
|
+
setDrag({ ...active, screenPos, feedback });
|
|
86
|
+
});
|
|
87
|
+
|
|
72
88
|
useEffect(() => {
|
|
73
89
|
if (!drag) return;
|
|
74
90
|
const handleMove = (e: PointerEvent) => {
|
|
75
91
|
const screenPos = { x: e.clientX, y: e.clientY };
|
|
76
92
|
const current = dragRef.current;
|
|
77
93
|
if (!current) return;
|
|
78
|
-
if (
|
|
94
|
+
if (pendingPos.current !== null) {
|
|
95
|
+
// A frame is already queued: keep the ghost under the cursor and let
|
|
96
|
+
// that frame probe the drop target from the newest position.
|
|
97
|
+
pendingPos.current = screenPos;
|
|
79
98
|
dragRef.current = { ...current, screenPos };
|
|
80
99
|
setDrag(dragRef.current);
|
|
81
100
|
return;
|
|
82
101
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const active = dragRef.current;
|
|
86
|
-
if (!active) return;
|
|
87
|
-
let feedback: DragFeedback | null = null;
|
|
88
|
-
if (capability.onDragOver && isOverCanvas(screenPos)) {
|
|
89
|
-
const world = screenToWorldFromContainer(screenPos);
|
|
90
|
-
if (world) feedback = capability.onDragOver(world, active.item, state, config);
|
|
91
|
-
}
|
|
92
|
-
setDrag({ ...active, screenPos, feedback });
|
|
93
|
-
});
|
|
102
|
+
pendingPos.current = screenPos;
|
|
103
|
+
frameLoop.request();
|
|
94
104
|
};
|
|
95
105
|
const handleUp = (e: PointerEvent) => {
|
|
96
106
|
const screenPos = { x: e.clientX, y: e.clientY };
|
|
97
107
|
const active = dragRef.current;
|
|
98
108
|
if (!active) return;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
rafThrottle.current = null;
|
|
102
|
-
}
|
|
109
|
+
pendingPos.current = null;
|
|
110
|
+
frameLoop.cancel();
|
|
103
111
|
if (isOverCanvas(screenPos)) {
|
|
104
112
|
const world = screenToWorldFromContainer(screenPos);
|
|
105
113
|
if (world) {
|
|
@@ -116,7 +124,17 @@ export function useDragDrop<TS, TC>({
|
|
|
116
124
|
window.removeEventListener('pointermove', handleMove);
|
|
117
125
|
window.removeEventListener('pointerup', handleUp);
|
|
118
126
|
};
|
|
119
|
-
}, [
|
|
127
|
+
}, [
|
|
128
|
+
drag,
|
|
129
|
+
capability,
|
|
130
|
+
state,
|
|
131
|
+
config,
|
|
132
|
+
setState,
|
|
133
|
+
emit,
|
|
134
|
+
frameLoop,
|
|
135
|
+
isOverCanvas,
|
|
136
|
+
screenToWorldFromContainer,
|
|
137
|
+
]);
|
|
120
138
|
|
|
121
139
|
const startDrag = useCallback((item: PaletteItem, originScreenPos: Point) => {
|
|
122
140
|
setDrag({ item, screenPos: originScreenPos, feedback: null });
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,29 @@ export {
|
|
|
9
9
|
useOrbit,
|
|
10
10
|
wrapYaw,
|
|
11
11
|
} from './canvas/useOrbit';
|
|
12
|
+
export * from './chrome';
|
|
13
|
+
export { f } from './config/builder';
|
|
14
|
+
export { fromConfigFields } from './config/fromConfigField';
|
|
15
|
+
export { resolveConfigSchema } from './config/resolve';
|
|
16
|
+
export { applyRules, builtinRules, titleCase } from './config/rules';
|
|
17
|
+
export type {
|
|
18
|
+
Annotations,
|
|
19
|
+
ConfigNode,
|
|
20
|
+
ConfigOf,
|
|
21
|
+
ConfigOption,
|
|
22
|
+
ConfigRule,
|
|
23
|
+
ConfigRuleContext,
|
|
24
|
+
ConfigSchema,
|
|
25
|
+
ControlRenderer,
|
|
26
|
+
InferConfig,
|
|
27
|
+
LeafPatch,
|
|
28
|
+
NodeOptions,
|
|
29
|
+
NodeValue,
|
|
30
|
+
ResolvedConfig,
|
|
31
|
+
SectionSpec,
|
|
32
|
+
} from './config/types';
|
|
33
|
+
export { useConfigSchema } from './config/useConfigSchema';
|
|
34
|
+
export { isLeafVisible } from './config/visible';
|
|
12
35
|
export { ControlPanel } from './controls/ControlPanel';
|
|
13
36
|
export type {
|
|
14
37
|
CheckboxField,
|
|
@@ -22,9 +45,7 @@ export type {
|
|
|
22
45
|
SliderField,
|
|
23
46
|
TextField,
|
|
24
47
|
} from './controls/types';
|
|
25
|
-
export type
|
|
26
|
-
export { detectCapabilities } from './instrument/capabilityDetector';
|
|
27
|
-
export { defineInstrument } from './instrument/defineInstrument';
|
|
48
|
+
export { defineInstrument, type InstrumentSpec } from './instrument/defineInstrument';
|
|
28
49
|
export type {
|
|
29
50
|
CanvasCapability,
|
|
30
51
|
CanvasLayer,
|
|
@@ -82,6 +103,7 @@ export type { ViewTransform2D } from './state/view';
|
|
|
82
103
|
export { as2DView, DEFAULT_VIEW } from './state/view';
|
|
83
104
|
export * from './surface';
|
|
84
105
|
export { interstellarTheme } from './theme/interstellar';
|
|
106
|
+
export type { ToolCapability, TrialTool } from './tools/types';
|
|
85
107
|
export * from './trial';
|
|
86
108
|
export * from './ui/layers';
|
|
87
109
|
export * from './ui/properties';
|
|
@@ -86,7 +86,14 @@ describe('SineWave instrument smoke test', () => {
|
|
|
86
86
|
config: SineWaveInstrument.defaultConfig(),
|
|
87
87
|
setState: vi.fn(),
|
|
88
88
|
setConfig: vi.fn(),
|
|
89
|
-
trial: {
|
|
89
|
+
trial: {
|
|
90
|
+
id: 'w',
|
|
91
|
+
view: DEFAULT_VIEW,
|
|
92
|
+
setView: vi.fn(),
|
|
93
|
+
zoom: 1,
|
|
94
|
+
setZoom: vi.fn(),
|
|
95
|
+
activeToolId: null,
|
|
96
|
+
},
|
|
90
97
|
emit: vi.fn(),
|
|
91
98
|
};
|
|
92
99
|
const { container } = render(SineWaveInstrument.render(ctx) as ReactElement);
|