@weasel-js/labkit 1.0.4 → 1.2.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 +33 -6
- package/dist/_dts/{DrawCommand-BkZztJsW.d.ts → DrawCommand-C_XboUpz.d.ts} +48 -6
- package/dist/_dts/{index-CFlDPeZh.d.ts → index-JFAYj5Tv.d.ts} +29 -7
- package/dist/_dts/types-DJ79Tg5J.d.ts +56 -0
- package/dist/_dts/useTrialState-BMNIx3Cy.d.ts +171 -0
- package/dist/canvas/index.d.ts +2 -1
- package/dist/{chunk-BAPZPDDA.js → chunk-73KA7WBO.js} +425 -394
- package/dist/chunk-73KA7WBO.js.map +1 -0
- package/dist/{chunk-DWV7SFKR.js → chunk-BOHF3PQO.js} +4397 -3726
- package/dist/chunk-BOHF3PQO.js.map +1 -0
- package/dist/{chunk-DEWXYFEU.js → chunk-G5TJVQQT.js} +3 -3
- package/dist/{chunk-DEWXYFEU.js.map → chunk-G5TJVQQT.js.map} +1 -1
- package/dist/chunk-LN6JDUGB.js +106 -0
- package/dist/chunk-LN6JDUGB.js.map +1 -0
- package/dist/chunk-THBG7FQZ.js +167 -0
- package/dist/chunk-THBG7FQZ.js.map +1 -0
- package/dist/chunk-VUU5UXHE.js +491 -0
- package/dist/chunk-VUU5UXHE.js.map +1 -0
- package/dist/dragdrop/index.d.ts +2 -1
- package/dist/index.d.ts +211 -151
- package/dist/index.js +317 -169
- package/dist/index.js.map +1 -1
- package/dist/job/index.d.ts +13 -0
- package/dist/job/index.js +3 -0
- package/dist/job/index.js.map +1 -0
- package/dist/layers/index.d.ts +3 -2
- package/dist/passthrough/weasel-canvas.d.ts +1 -1
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +29 -6
- package/dist/passthrough/weasel-ui.js +2 -2
- package/dist/state/index.d.ts +12 -12
- package/dist/state/index.js +5 -5
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +31 -13
- package/dist/surface/index.d.ts +77 -0
- package/dist/surface/index.js +3 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/ui/layers/index.js +3 -3
- package/dist/undo/index.d.ts +2 -1
- package/package.json +9 -1
- package/src/canvas/AGENTS.md +14 -2
- package/src/canvas/useOrbit.test.ts +71 -0
- package/src/canvas/useOrbit.ts +149 -0
- package/src/dragdrop/dragDrop.test.tsx +4 -7
- package/src/index.test.ts +70 -0
- package/src/index.ts +24 -7
- package/src/instrument/SineWave.smoke.test.tsx +2 -1
- package/src/instrument/capabilityDetector.ts +1 -1
- package/src/instrument/types.ts +30 -6
- package/src/job/index.ts +3 -0
- package/src/job/types.ts +47 -0
- package/src/job/useJob.test.tsx +210 -0
- package/src/job/useJob.ts +134 -0
- package/src/lab/Lab.less +16 -0
- package/src/lab/Lab.stories.tsx +6 -7
- package/src/lab/Lab.test.tsx +25 -25
- package/src/lab/Lab.tsx +50 -50
- package/src/lab/LabContext.ts +12 -12
- package/src/lab/LabShell.tsx +1 -1
- package/src/lab/{WorkspaceGrid.less → Workspace.less} +2 -2
- package/src/lab/{WorkspaceGrid.stories.tsx → Workspace.stories.tsx} +13 -13
- package/src/lab/Workspace.surface.test.tsx +49 -0
- package/src/lab/{WorkspaceGrid.test.tsx → Workspace.test.tsx} +24 -24
- package/src/lab/{WorkspaceGrid.tsx → Workspace.tsx} +32 -19
- package/src/lab/index.ts +2 -2
- package/src/layers/AGENTS.md +3 -3
- package/src/primitives/Toolbar.stories.tsx +2 -2
- package/src/state/SingletonExperiment.test.tsx +8 -8
- package/src/state/SingletonExperiment.tsx +9 -9
- package/src/state/context.tsx +12 -12
- package/src/state/document.test.ts +369 -0
- package/src/state/document.ts +194 -0
- package/src/state/helpers.test.ts +74 -46
- package/src/state/helpers.ts +21 -29
- package/src/state/index.ts +16 -8
- package/src/state/store.test.ts +440 -41
- package/src/state/store.ts +165 -102
- package/src/state/types.ts +32 -13
- package/src/state/{useExperimentState.test.tsx → useTrialState.test.tsx} +18 -18
- package/src/state/useTrialState.ts +29 -0
- package/src/state/view.test.ts +127 -0
- package/src/state/view.ts +18 -0
- package/src/styles.less +2 -2
- package/src/surface/AGENTS.md +64 -0
- package/src/surface/SurfaceContext.ts +5 -0
- package/src/surface/composeRects.test.ts +50 -0
- package/src/surface/composeRects.ts +19 -0
- package/src/surface/deviceRect.test.ts +40 -0
- package/src/surface/deviceRect.ts +19 -0
- package/src/surface/index.ts +7 -0
- package/src/surface/rect.ts +16 -0
- package/src/surface/useSurfaceTile.test.tsx +67 -0
- package/src/surface/useSurfaceTile.ts +32 -0
- package/src/surface/useTiledSurface.test.tsx +231 -0
- package/src/surface/useTiledSurface.ts +157 -0
- package/src/theme/base.less +1 -1
- package/src/{workspace → trial}/DefaultSidebar.tsx +3 -3
- package/src/trial/DefaultStatusBar.tsx +19 -0
- package/src/{workspace → trial}/DefaultToolbar.tsx +9 -8
- package/src/trial/Trial.canvas.test.tsx +80 -0
- package/src/trial/Trial.job.test.tsx +75 -0
- package/src/{workspace/Workspace.less → trial/Trial.less} +13 -1
- package/src/{workspace/Workspace.stories.tsx → trial/Trial.stories.tsx} +20 -18
- package/src/{workspace/Workspace.test.tsx → trial/Trial.test.tsx} +26 -26
- package/src/{workspace/Workspace.tsx → trial/Trial.tsx} +77 -46
- package/src/{workspace/WorkspaceChrome.tsx → trial/TrialChrome.tsx} +73 -46
- package/src/{workspace → trial}/index.ts +13 -13
- package/src/{workspace → trial}/slotTypes.ts +18 -17
- package/src/{workspace/workspaceOps.test.ts → trial/trialOps.test.ts} +38 -44
- package/src/trial/trialOps.ts +99 -0
- package/src/ui/format.test.ts +33 -0
- package/src/ui/format.ts +11 -0
- package/src/ui/properties/CurveField.tsx +6 -6
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +3 -3
- package/dist/_dts/useExperimentState-D7EQnnwJ.d.ts +0 -151
- package/dist/chunk-BAPZPDDA.js.map +0 -1
- package/dist/chunk-DWV7SFKR.js.map +0 -1
- package/dist/chunk-KSTEW2AF.js +0 -367
- package/dist/chunk-KSTEW2AF.js.map +0 -1
- package/src/state/useExperimentState.ts +0 -31
- package/src/workspace/DefaultStatusBar.tsx +0 -18
- package/src/workspace/workspaceOps.ts +0 -102
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { a as JobCapability, J as JobHandle } from '../_dts/types-DJ79Tg5J.js';
|
|
2
|
+
export { b as JobEvent, c as JobFailure, d as JobStatus } from '../_dts/types-DJ79Tg5J.js';
|
|
3
|
+
|
|
4
|
+
interface UseJobOptions<TS, TC, TItem> {
|
|
5
|
+
capability: JobCapability<TS, TC, TItem>;
|
|
6
|
+
config: TC;
|
|
7
|
+
state: TS;
|
|
8
|
+
setState: (next: TS | ((prev: TS) => TS)) => void;
|
|
9
|
+
}
|
|
10
|
+
declare function useJob<TS, TC, TItem>({ capability, config, state, setState, }: UseJobOptions<TS, TC, TItem>): JobHandle;
|
|
11
|
+
|
|
12
|
+
export { JobCapability, JobHandle, useJob };
|
|
13
|
+
export type { UseJobOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/dist/layers/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { L as LayerCapability } from '../_dts/index-
|
|
1
|
+
import { d as LayerDescriptor } from '../_dts/index-JFAYj5Tv.js';
|
|
2
|
+
export { L as LayerCapability } from '../_dts/index-JFAYj5Tv.js';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import 'react';
|
|
5
5
|
import '../_dts/types-x92Kfeme.js';
|
|
6
|
+
import '../_dts/types-DJ79Tg5J.js';
|
|
6
7
|
|
|
7
8
|
/** Props for `<LayerList>`. */
|
|
8
9
|
interface LayerListProps {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { Ref } from 'react';
|
|
3
|
-
import { N as Node, V as View, D as DrawCommand, S as Scene, B as Bounds, a as ViewportDims, P as PathDrawCommand } from '../_dts/DrawCommand-
|
|
3
|
+
import { N as Node, V as View, D as DrawCommand, S as Scene, B as Bounds, a as ViewportDims, P as PathDrawCommand } from '../_dts/DrawCommand-C_XboUpz.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* `renderSceneToCanvas` — substrate for detached, read-only scene views
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands,
|
|
1
|
+
export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands, computeFitView, computeIndicatorCommand, renderSceneToCanvas } from '../chunk-BOHF3PQO.js';
|
|
2
2
|
//# sourceMappingURL=weasel-canvas.js.map
|
|
3
3
|
//# sourceMappingURL=weasel-canvas.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode, CSSProperties, ButtonHTMLAttributes, ReactElement, MutableRefObject, KeyboardEvent, PointerEvent as PointerEvent$1, RefCallback, RefObject } from 'react';
|
|
4
|
-
import { V as View, D as DrawCommand, O as Op, b as NodeId, c as Path, S as Scene, H as History, B as Bounds } from '../_dts/DrawCommand-
|
|
4
|
+
import { V as View, D as DrawCommand, O as Op, b as NodeId, c as Path, S as Scene, H as History, B as Bounds } from '../_dts/DrawCommand-C_XboUpz.js';
|
|
5
5
|
import { TextFieldProps, ValidationResult, CheckboxProps as CheckboxProps$1, SwitchProps as SwitchProps$1, TabProps as TabProps$1, TabListProps as TabListProps$1, TabPanelProps as TabPanelProps$1, TabsProps as TabsProps$1, RadioProps as RadioProps$1, RadioGroupProps as RadioGroupProps$1, NumberFieldProps as NumberFieldProps$1, SelectProps as SelectProps$1, ListBoxItemProps, ComboBoxProps as ComboBoxProps$1, SliderProps as SliderProps$1, ModalOverlayProps, DialogProps as DialogProps$1 } from 'react-aria-components';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1082,6 +1082,10 @@ type TrackCtx = {
|
|
|
1082
1082
|
* `onInput` fires continuously through a drag; `onChange` fires once when it
|
|
1083
1083
|
* ends and is the one to write to history.
|
|
1084
1084
|
*
|
|
1085
|
+
* `stops` are attractors: a drag that passes within a few pixels of one lands
|
|
1086
|
+
* on it, and the arrow keys move stop to stop. `step` still quantizes the
|
|
1087
|
+
* values between them.
|
|
1088
|
+
*
|
|
1085
1089
|
* `constraint: 'ordered'` keeps thumbs from crossing each other. Supplying
|
|
1086
1090
|
* `onAddThumb` makes a click on empty track create a thumb, and supplying
|
|
1087
1091
|
* `onRemoveThumb` lets a right-click or a drag off the track remove one —
|
|
@@ -1095,6 +1099,7 @@ type SliderProps<T extends Thumb = Thumb> = {
|
|
|
1095
1099
|
min: number;
|
|
1096
1100
|
max: number;
|
|
1097
1101
|
step?: number;
|
|
1102
|
+
stops?: number[];
|
|
1098
1103
|
constraint?: 'free' | 'ordered';
|
|
1099
1104
|
onAddThumb?: (atValue: number) => T | null;
|
|
1100
1105
|
onRemoveThumb?: (index: number) => boolean;
|
|
@@ -1368,7 +1373,9 @@ interface Dims {
|
|
|
1368
1373
|
* @template TData - The data object passed to each draw call.
|
|
1369
1374
|
*/
|
|
1370
1375
|
interface RenderLayer<TData> {
|
|
1371
|
-
/** Unique identifier used in visibility maps and ordering arrays.
|
|
1376
|
+
/** Unique identifier used in visibility maps and ordering arrays. When a
|
|
1377
|
+
* cache is in use, an id must identify the same logical layer across
|
|
1378
|
+
* frames — reusing it for a different layer can serve cross-layer commands. */
|
|
1372
1379
|
id: string;
|
|
1373
1380
|
/** Human-readable name for UI toggles. */
|
|
1374
1381
|
label: string;
|
|
@@ -1386,6 +1393,21 @@ interface RenderLayer<TData> {
|
|
|
1386
1393
|
* subset manually with `viewToMat3(view)`.
|
|
1387
1394
|
*/
|
|
1388
1395
|
draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
|
|
1396
|
+
/**
|
|
1397
|
+
* Optional cache key. When present and a `LayerCommandCache` is supplied to
|
|
1398
|
+
* `drawLayers`, the layer's previous `DrawCommand[]` is reused as long as
|
|
1399
|
+
* every entry is `Object.is`-equal to the previous call's. A layer with no
|
|
1400
|
+
* `deps` rebuilds on every frame.
|
|
1401
|
+
*
|
|
1402
|
+
* **The returned commands must be treated as immutable.** A cached tree is
|
|
1403
|
+
* handed to the renderer again on later frames, so mutating a tree you
|
|
1404
|
+
* previously returned corrupts the cache silently rather than erroring.
|
|
1405
|
+
*
|
|
1406
|
+
* **Screen-space layers are not protected against a stale `view`/`dims`
|
|
1407
|
+
* the way world-space layers are** (see `space` below) — include them in
|
|
1408
|
+
* `deps` if `draw` reads them.
|
|
1409
|
+
*/
|
|
1410
|
+
deps?: (data: TData, view: View, dims: Dims) => readonly unknown[];
|
|
1389
1411
|
/**
|
|
1390
1412
|
* Whether the layer is shown when no explicit visibility entry exists.
|
|
1391
1413
|
* Defaults to `true` when absent.
|
|
@@ -1676,8 +1698,10 @@ interface InsertAdapter<TNode extends {
|
|
|
1676
1698
|
dx: number;
|
|
1677
1699
|
dy: number;
|
|
1678
1700
|
};
|
|
1679
|
-
/** Mutator wired by `insertNode`-using ops (kit-side InsertOp).
|
|
1680
|
-
|
|
1701
|
+
/** Mutator wired by `insertNode`-using ops (kit-side InsertOp). `index`
|
|
1702
|
+
* carries the z-position a delete captured, so undo restores paint order;
|
|
1703
|
+
* see `SceneAdapter.insertNode`. */
|
|
1704
|
+
insertNode(node: TNode, index?: number): void;
|
|
1681
1705
|
/** Mutator wired by `setSelection` ops batched alongside paste. */
|
|
1682
1706
|
setSelection(ids: string[]): void;
|
|
1683
1707
|
/** Optional: see SceneAdapter.applyOps. */
|
|
@@ -4701,8 +4725,7 @@ interface AnchorRenderProps {
|
|
|
4701
4725
|
* Per-instance curve-rendering overrides. Currently empty — passing `{}`
|
|
4702
4726
|
* means "draw the curve", and `false`/`null` means don't.
|
|
4703
4727
|
*/
|
|
4704
|
-
|
|
4705
|
-
}
|
|
4728
|
+
type CurveSettings = object;
|
|
4706
4729
|
/** Shades the region between the curve and one edge of the plot. */
|
|
4707
4730
|
interface FillSettings {
|
|
4708
4731
|
side: 'below' | 'above';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { m as ActionBar, r3 as ActionsBar, Ft as Badge, a as Button, o3 as Checkbox, p4 as ComboBox,
|
|
2
|
-
import '../chunk-
|
|
1
|
+
export { m as ActionBar, r3 as ActionsBar, Ft as Badge, a as Button, o3 as Checkbox, p4 as ComboBox, m2 as ComboBoxItem, F as CurveEditor, u as DataGrid, c3 as Dialog, qe as EDGE_PROFILES, n as Field, u3 as Input, s as KeyCap, c2 as KeySequence, o2 as MINUS_SIGN, f2 as NumberField, r2 as OptionsBar, _ as Plot2D, n4 as PointPlotter, r4 as Powerline, u4 as Radio, l as RadioGroup, l2 as RangeSlider, p3 as Select, h as SelectItem, n2 as Sidebar, o4 as SidebarPanel, v as Slider, a3 as Switch, c4 as Tab, s3 as TabList, l3 as TabPanel, o6 as Tabs, r as ToggleBar, o5 as ToolButton, n3 as ToolGroup, _2 as ToolPalette, He as chromaAt, u2 as detectPlatform, p as dlog, r5 as fieldClasses, s2 as formatNumber, d2 as formatShortcut, u5 as formatShortcutParts, o as inferKeycapKind, f as isDebugEnabled, e as keyGlyph, p2 as keySpecFromKey, d as keySpecsFromMods, Ve as oklchToHex, p5 as paintGradientTrack, c as useReorderDragList, a2 as useRovingTabIndex } from '../chunk-73KA7WBO.js';
|
|
2
|
+
import '../chunk-BOHF3PQO.js';
|
|
3
3
|
//# sourceMappingURL=weasel-ui.js.map
|
|
4
4
|
//# sourceMappingURL=weasel-ui.js.map
|
package/dist/state/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as InstrumentSerializers,
|
|
2
|
-
export { C as
|
|
1
|
+
import { S as SerializedTrial, I as InstrumentSerializers, T as TrialRecord, U as UndoStack, a as StorageAdapter } from '../_dts/useTrialState-BMNIx3Cy.js';
|
|
2
|
+
export { C as CURRENT_DOCUMENT_VERSION, b as CreateLabStoreOptions, L as LabDocument, c as LabStore, d as LabStoreActions, e as LabStoreContext, f as LabStoreProvider, g as LabStoreState, M as Migration, h as SavedSnapshot, i as TrialIdContext, j as TrialIdProvider, k as TrialStateHandle, l as createLabStore, m as createMemoryAdapter, n as labDocumentKey, o as localStorageAdapter, p as noneAdapter, q as quarantineKey, s as sessionStorageAdapter, u as urlHashAdapter, r as useLabStore, t as useTrialId, v as useTrialState } from '../_dts/useTrialState-BMNIx3Cy.js';
|
|
3
3
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
5
|
import 'zustand/vanilla';
|
|
@@ -14,19 +14,19 @@ declare function encodeUrlHash(value: string): string;
|
|
|
14
14
|
declare function decodeUrlHash(hash: string): string | null;
|
|
15
15
|
/** A fresh, empty undo history. */
|
|
16
16
|
declare function emptyUndoStack(): UndoStack;
|
|
17
|
-
/** Serialize
|
|
17
|
+
/** Serialize trials for storage, running each instrument's own serializer
|
|
18
18
|
* over its state. Undo history is deliberately dropped — it does not survive
|
|
19
19
|
* a reload. */
|
|
20
|
-
declare function
|
|
21
|
-
/** Rebuild
|
|
20
|
+
declare function serializeTrials(trials: TrialRecord[], serializers: InstrumentSerializers): SerializedTrial[];
|
|
21
|
+
/** Rebuild trials from storage, running each instrument's deserializer
|
|
22
22
|
* over its state and starting each with an empty undo history. Returns an
|
|
23
23
|
* empty list rather than throwing on malformed input. */
|
|
24
|
-
declare function
|
|
24
|
+
declare function deserializeTrials(records: SerializedTrial[], deserializers: InstrumentSerializers): TrialRecord[];
|
|
25
25
|
|
|
26
26
|
/** Props for `<SingletonExperimentProvider>`. */
|
|
27
27
|
interface SingletonExperimentProviderProps<TS, TC> {
|
|
28
|
-
/** Stable id for the synthetic
|
|
29
|
-
*
|
|
28
|
+
/** Stable id for the synthetic trial; also doubles as the
|
|
29
|
+
* TrialIdContext value. */
|
|
30
30
|
id: string;
|
|
31
31
|
initialConfig: TC;
|
|
32
32
|
initialState: TS;
|
|
@@ -35,12 +35,12 @@ interface SingletonExperimentProviderProps<TS, TC> {
|
|
|
35
35
|
children: ReactNode;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* One-
|
|
39
|
-
* a `LabStoreProvider` + `
|
|
40
|
-
*
|
|
38
|
+
* One-trial `<Lab>` substitute for single-screen experiments. Mounts
|
|
39
|
+
* a `LabStoreProvider` + `TrialIdProvider` with one synthetic
|
|
40
|
+
* trial, so `useTrialState` works without going through the
|
|
41
41
|
* full `<Lab instruments={...}>` runtime.
|
|
42
42
|
*/
|
|
43
43
|
declare function SingletonExperimentProvider<TS, TC>({ id, initialConfig, initialState, storage, storageKey, children, }: SingletonExperimentProviderProps<TS, TC>): react_jsx_runtime.JSX.Element;
|
|
44
44
|
|
|
45
|
-
export { InstrumentSerializers, SingletonExperimentProvider, StorageAdapter,
|
|
45
|
+
export { InstrumentSerializers, SerializedTrial, SingletonExperimentProvider, StorageAdapter, TrialRecord, UndoStack, decodeUrlHash, deserializeTrials, emptyUndoStack, encodeUrlHash, labStorageKey, serializeTrials };
|
|
46
46
|
export type { SingletonExperimentProviderProps };
|
package/dist/state/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createLabStore, LabStoreProvider,
|
|
2
|
-
export { LabStoreContext, LabStoreProvider,
|
|
1
|
+
import { createLabStore, LabStoreProvider, TrialIdProvider } from '../chunk-VUU5UXHE.js';
|
|
2
|
+
export { CURRENT_DOCUMENT_VERSION, LabStoreContext, LabStoreProvider, TrialIdContext, TrialIdProvider, createLabStore, createMemoryAdapter, decodeUrlHash, deserializeTrials, emptyUndoStack, encodeUrlHash, labDocumentKey, labStorageKey, localStorageAdapter, noneAdapter, quarantineKey, serializeTrials, sessionStorageAdapter, urlHashAdapter, useLabStore, useTrialId, useTrialState } from '../chunk-VUU5UXHE.js';
|
|
3
3
|
import { useRef } from 'react';
|
|
4
4
|
import { jsx } from 'react/jsx-runtime';
|
|
5
5
|
|
|
@@ -15,8 +15,8 @@ function SingletonExperimentProvider({
|
|
|
15
15
|
const storeRef = useRef(null);
|
|
16
16
|
if (storeRef.current === null) {
|
|
17
17
|
const store = createLabStore({ storageKey, storage });
|
|
18
|
-
if (!store.getState().
|
|
19
|
-
store.getState().
|
|
18
|
+
if (!store.getState().trials.some((w) => w.id === id)) {
|
|
19
|
+
store.getState().addTrial({
|
|
20
20
|
id,
|
|
21
21
|
instrumentName: SINGLETON_INSTRUMENT,
|
|
22
22
|
config: initialConfig,
|
|
@@ -26,7 +26,7 @@ function SingletonExperimentProvider({
|
|
|
26
26
|
}
|
|
27
27
|
storeRef.current = store;
|
|
28
28
|
}
|
|
29
|
-
return /* @__PURE__ */ jsx(LabStoreProvider, { store: storeRef.current, children: /* @__PURE__ */ jsx(
|
|
29
|
+
return /* @__PURE__ */ jsx(LabStoreProvider, { store: storeRef.current, children: /* @__PURE__ */ jsx(TrialIdProvider, { trialId: id, children }) });
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export { SingletonExperimentProvider };
|
package/dist/state/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/state/SingletonExperiment.tsx"],"names":[],"mappings":";;;;;AAKA,IAAM,oBAAA,GAAuB,eAAA;AAoBtB,SAAS,2BAAA,CAAoC;AAAA,EAClD,EAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAA6C;AAC3C,EAAA,MAAM,QAAA,GAAW,OAAwB,IAAI,CAAA;AAC7C,EAAA,IAAI,QAAA,CAAS,YAAY,IAAA,EAAM;AAC7B,IAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,EAAE,UAAA,EAAY,SAAS,CAAA;AACpD,IAAA,IAAI,CAAC,KAAA,CAAM,QAAA,EAAS,CAAE,
|
|
1
|
+
{"version":3,"sources":["../../src/state/SingletonExperiment.tsx"],"names":[],"mappings":";;;;;AAKA,IAAM,oBAAA,GAAuB,eAAA;AAoBtB,SAAS,2BAAA,CAAoC;AAAA,EAClD,EAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAA6C;AAC3C,EAAA,MAAM,QAAA,GAAW,OAAwB,IAAI,CAAA;AAC7C,EAAA,IAAI,QAAA,CAAS,YAAY,IAAA,EAAM;AAC7B,IAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,EAAE,UAAA,EAAY,SAAS,CAAA;AACpD,IAAA,IAAI,CAAC,KAAA,CAAM,QAAA,EAAS,CAAE,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAA,EAAG;AACrD,MAAA,KAAA,CAAM,QAAA,GAAW,QAAA,CAAS;AAAA,QACxB,EAAA;AAAA,QACA,cAAA,EAAgB,oBAAA;AAAA,QAChB,MAAA,EAAQ,aAAA;AAAA,QACR,KAAA,EAAO,YAAA;AAAA,QACP,IAAA,EAAM,EAAE,IAAA,EAAM,CAAA,EAAG,GAAA,EAAK,EAAE,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAE;AAAE,OACtC,CAAA;AAAA,IACH;AACA,IAAA,QAAA,CAAS,OAAA,GAAU,KAAA;AAAA,EACrB;AACA,EAAA,uBACE,GAAA,CAAC,gBAAA,EAAA,EAAiB,KAAA,EAAO,QAAA,CAAS,OAAA,EAChC,8BAAC,eAAA,EAAA,EAAgB,OAAA,EAAS,EAAA,EAAK,QAAA,EAAS,CAAA,EAC1C,CAAA;AAEJ","file":"index.js","sourcesContent":["import { type ReactNode, useRef } from 'react';\nimport { LabStoreProvider, TrialIdProvider } from './context';\nimport { createLabStore, type LabStore } from './store';\nimport type { StorageAdapter } from './types';\n\nconst SINGLETON_INSTRUMENT = '__singleton__';\n\n/** Props for `<SingletonExperimentProvider>`. */\nexport interface SingletonExperimentProviderProps<TS, TC> {\n /** Stable id for the synthetic trial; also doubles as the\n * TrialIdContext value. */\n id: string;\n initialConfig: TC;\n initialState: TS;\n storage: StorageAdapter;\n storageKey: string;\n children: ReactNode;\n}\n\n/**\n * One-trial `<Lab>` substitute for single-screen experiments. Mounts\n * a `LabStoreProvider` + `TrialIdProvider` with one synthetic\n * trial, so `useTrialState` works without going through the\n * full `<Lab instruments={...}>` runtime.\n */\nexport function SingletonExperimentProvider<TS, TC>({\n id,\n initialConfig,\n initialState,\n storage,\n storageKey,\n children,\n}: SingletonExperimentProviderProps<TS, TC>) {\n const storeRef = useRef<LabStore | null>(null);\n if (storeRef.current === null) {\n const store = createLabStore({ storageKey, storage });\n if (!store.getState().trials.some((w) => w.id === id)) {\n store.getState().addTrial({\n id,\n instrumentName: SINGLETON_INSTRUMENT,\n config: initialConfig,\n state: initialState,\n view: { zoom: 1, pan: { x: 0, y: 0 } },\n });\n }\n storeRef.current = store;\n }\n return (\n <LabStoreProvider store={storeRef.current}>\n <TrialIdProvider trialId={id}>{children}</TrialIdProvider>\n </LabStoreProvider>\n );\n}\n"]}
|
package/dist/styles.css
CHANGED
|
@@ -356,15 +356,15 @@
|
|
|
356
356
|
letter-spacing: 0.01em;
|
|
357
357
|
line-height: 1.2;
|
|
358
358
|
}
|
|
359
|
-
.lk-workspace
|
|
359
|
+
.lk-workspace {
|
|
360
360
|
min-height: 0;
|
|
361
361
|
}
|
|
362
|
-
.lk-
|
|
362
|
+
.lk-trial-tile {
|
|
363
363
|
display: flex;
|
|
364
364
|
flex-direction: column;
|
|
365
365
|
height: 100%;
|
|
366
366
|
}
|
|
367
|
-
.lk-
|
|
367
|
+
.lk-trial-tile__grip {
|
|
368
368
|
flex: 0 0 auto;
|
|
369
369
|
display: flex;
|
|
370
370
|
align-items: center;
|
|
@@ -374,17 +374,17 @@
|
|
|
374
374
|
border-radius: var(--wzl-radius-sm) var(--wzl-radius-sm) 0 0;
|
|
375
375
|
background: var(--wzl-surface-sunken);
|
|
376
376
|
}
|
|
377
|
-
.lk-
|
|
377
|
+
.lk-trial-tile__grip:active {
|
|
378
378
|
cursor: grabbing;
|
|
379
379
|
}
|
|
380
|
-
.lk-
|
|
380
|
+
.lk-trial-tile__grip-dots {
|
|
381
381
|
width: 28px;
|
|
382
382
|
height: 3px;
|
|
383
383
|
border-radius: 999px;
|
|
384
384
|
background: var(--wzl-fg-muted);
|
|
385
385
|
opacity: 0.5;
|
|
386
386
|
}
|
|
387
|
-
.lk-
|
|
387
|
+
.lk-trial-tile__body {
|
|
388
388
|
flex: 1;
|
|
389
389
|
min-height: 0;
|
|
390
390
|
}
|
|
@@ -567,10 +567,18 @@
|
|
|
567
567
|
.lk-scale-indicator-label {
|
|
568
568
|
white-space: nowrap;
|
|
569
569
|
}
|
|
570
|
+
html:has(.lk-lab),
|
|
571
|
+
body:has(.lk-lab) {
|
|
572
|
+
margin: 0;
|
|
573
|
+
height: 100%;
|
|
574
|
+
}
|
|
575
|
+
body:has(.lk-lab) > :has(> .lk-lab) {
|
|
576
|
+
height: 100%;
|
|
577
|
+
}
|
|
570
578
|
.lk-lab {
|
|
571
579
|
height: 100%;
|
|
572
580
|
}
|
|
573
|
-
.lk-
|
|
581
|
+
.lk-trial {
|
|
574
582
|
display: flex;
|
|
575
583
|
flex-direction: column;
|
|
576
584
|
height: 100%;
|
|
@@ -579,27 +587,27 @@
|
|
|
579
587
|
border-radius: var(--wzl-radius-md);
|
|
580
588
|
overflow: hidden;
|
|
581
589
|
}
|
|
582
|
-
.lk-
|
|
590
|
+
.lk-trial__body {
|
|
583
591
|
display: flex;
|
|
584
592
|
flex: 1;
|
|
585
593
|
overflow: hidden;
|
|
586
594
|
}
|
|
587
|
-
.lk-
|
|
595
|
+
.lk-trial__sidebar {
|
|
588
596
|
flex-shrink: 0;
|
|
589
597
|
}
|
|
590
|
-
.lk-
|
|
598
|
+
.lk-trial__content {
|
|
591
599
|
flex: 1;
|
|
592
600
|
overflow: auto;
|
|
593
601
|
}
|
|
594
|
-
.lk-
|
|
602
|
+
.lk-trial__toolbar {
|
|
595
603
|
flex-shrink: 0;
|
|
596
604
|
border-bottom: 1px solid var(--wzl-line-subtle);
|
|
597
605
|
}
|
|
598
|
-
.lk-
|
|
606
|
+
.lk-trial__status {
|
|
599
607
|
flex-shrink: 0;
|
|
600
608
|
border-top: 1px solid var(--wzl-line-subtle);
|
|
601
609
|
}
|
|
602
|
-
.lk-
|
|
610
|
+
.lk-trial--unknown {
|
|
603
611
|
display: flex;
|
|
604
612
|
align-items: center;
|
|
605
613
|
justify-content: center;
|
|
@@ -619,6 +627,16 @@
|
|
|
619
627
|
color: var(--wzl-fg-muted);
|
|
620
628
|
font-size: var(--wzl-font-size-sm);
|
|
621
629
|
}
|
|
630
|
+
.lk-trial__job {
|
|
631
|
+
display: flex;
|
|
632
|
+
gap: 8px;
|
|
633
|
+
align-items: center;
|
|
634
|
+
font-size: 0.75rem;
|
|
635
|
+
}
|
|
636
|
+
.lk-trial__job-failures,
|
|
637
|
+
.lk-trial__job-error {
|
|
638
|
+
color: var(--wsl-color-danger, #c0392b);
|
|
639
|
+
}
|
|
622
640
|
.lk-control-panel {
|
|
623
641
|
display: flex;
|
|
624
642
|
flex-direction: column;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
/** A box in CSS pixels, measured from the surface's own top-left corner. */
|
|
4
|
+
interface Rect {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
w: number;
|
|
8
|
+
h: number;
|
|
9
|
+
}
|
|
10
|
+
/** The subset of `DOMRect` this package reads. Accepting the subset rather than
|
|
11
|
+
* `DOMRect` is what lets the pure functions be tested without a DOM. */
|
|
12
|
+
interface Box {
|
|
13
|
+
left: number;
|
|
14
|
+
top: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Every tile's box expressed against the container's origin. */
|
|
20
|
+
declare function composeRects(container: Box, tiles: ReadonlyMap<string, Box>): Map<string, Rect>;
|
|
21
|
+
declare function rectsEqual(a: Rect | undefined, b: Rect): boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A DOM rect as a GL viewport rect: origin at the bottom-left, every edge snapped
|
|
25
|
+
* to the device-pixel grid. Still CSS pixels, because three.js applies its own
|
|
26
|
+
* pixel ratio — snapping here is what stops a tile and its neighbour rounding
|
|
27
|
+
* apart and leaving a hairline column between them.
|
|
28
|
+
*/
|
|
29
|
+
declare function toDeviceRect(rect: Rect, surfaceHeight: number, dpr: number): Rect;
|
|
30
|
+
|
|
31
|
+
/** What a surface owner is handed once per animation frame. `rects` carries every
|
|
32
|
+
* tile, not only the dirty ones: a scissored draw has to know where it is drawing
|
|
33
|
+
* relative to a surface that may have resized under it. */
|
|
34
|
+
interface SurfaceFrame {
|
|
35
|
+
dirty: ReadonlySet<string>;
|
|
36
|
+
rects: ReadonlyMap<string, Rect>;
|
|
37
|
+
dpr: number;
|
|
38
|
+
size: {
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** The invalidators and the two ref callbacks that publish geometry. */
|
|
44
|
+
interface SurfaceHandle {
|
|
45
|
+
/** Mark one tile for redraw. */
|
|
46
|
+
invalidate: (id: string) => void;
|
|
47
|
+
/** Mark every tile — what a resize or a tile-set change means. */
|
|
48
|
+
invalidateAll: () => void;
|
|
49
|
+
/** Re-measure before the next frame. The escape hatch for a host that knows it
|
|
50
|
+
* moved something a ResizeObserver cannot see. */
|
|
51
|
+
invalidateRects: () => void;
|
|
52
|
+
registerTile: (id: string, el: HTMLElement | null) => void;
|
|
53
|
+
containerRef: (el: HTMLElement | null) => void;
|
|
54
|
+
}
|
|
55
|
+
interface UseTiledSurfaceOptions {
|
|
56
|
+
onFrame: (frame: SurfaceFrame) => void;
|
|
57
|
+
}
|
|
58
|
+
declare function useTiledSurface({ onFrame }: UseTiledSurfaceOptions): SurfaceHandle;
|
|
59
|
+
|
|
60
|
+
/** Null when no surface owner is above — a lab with no shared surface at all. */
|
|
61
|
+
declare const SurfaceContext: react.Context<SurfaceHandle | null>;
|
|
62
|
+
|
|
63
|
+
/** The surface above, or null. Use this where a surface is genuinely optional. */
|
|
64
|
+
declare function useSurfaceOptional(): SurfaceHandle | null;
|
|
65
|
+
/** The surface above. Throws where a caller cannot work without one. */
|
|
66
|
+
declare function useSurface(): SurfaceHandle;
|
|
67
|
+
/**
|
|
68
|
+
* A ref callback that publishes this element's rect to the surface under `id`.
|
|
69
|
+
*
|
|
70
|
+
* Attach it to whichever element the surface should draw into — that is not
|
|
71
|
+
* necessarily the trial's own element, since a trial may hold a drawn pane beside
|
|
72
|
+
* an undrawn one, or none at all.
|
|
73
|
+
*/
|
|
74
|
+
declare function useSurfaceTile(id: string): (el: HTMLElement | null) => void;
|
|
75
|
+
|
|
76
|
+
export { SurfaceContext, composeRects, rectsEqual, toDeviceRect, useSurface, useSurfaceOptional, useSurfaceTile, useTiledSurface };
|
|
77
|
+
export type { Box, Rect, SurfaceFrame, SurfaceHandle, UseTiledSurfaceOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/dist/ui/layers/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { LayerStack } from '../../chunk-
|
|
2
|
-
import '../../chunk-
|
|
3
|
-
import '../../chunk-
|
|
1
|
+
export { LayerStack } from '../../chunk-G5TJVQQT.js';
|
|
2
|
+
import '../../chunk-73KA7WBO.js';
|
|
3
|
+
import '../../chunk-BOHF3PQO.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/undo/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { U as UndoCapability } from '../_dts/index-
|
|
1
|
+
export { U as UndoCapability } from '../_dts/index-JFAYj5Tv.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import '../_dts/types-x92Kfeme.js';
|
|
4
|
+
import '../_dts/types-DJ79Tg5J.js';
|
|
4
5
|
|
|
5
6
|
/** A subscriber to a named event. */
|
|
6
7
|
type EventListener = () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/labkit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "React widgets for building self-contained interactive lab pages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "orochi235",
|
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
"types": "./dist/state/index.d.ts",
|
|
56
56
|
"import": "./dist/state/index.js"
|
|
57
57
|
},
|
|
58
|
+
"./surface": {
|
|
59
|
+
"types": "./dist/surface/index.d.ts",
|
|
60
|
+
"import": "./dist/surface/index.js"
|
|
61
|
+
},
|
|
62
|
+
"./job": {
|
|
63
|
+
"types": "./dist/job/index.d.ts",
|
|
64
|
+
"import": "./dist/job/index.js"
|
|
65
|
+
},
|
|
58
66
|
"./weasel-ui": {
|
|
59
67
|
"types": "./dist/passthrough/weasel-ui.d.ts",
|
|
60
68
|
"import": "./dist/passthrough/weasel-ui.js"
|
package/src/canvas/AGENTS.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
The `src/canvas/` directory implements the layered canvas primitive used by the `canvas` instrument capability.
|
|
4
4
|
|
|
5
|
+
For a renderer labkit does not own — three.js, raw WebGL, anything with its own
|
|
6
|
+
render loop — see `src/surface/AGENTS.md` instead. `CanvasStack` is 2D and
|
|
7
|
+
schedules its own layers; a foreign renderer wants rects and dirtiness only.
|
|
8
|
+
|
|
9
|
+
`useOrbit` also lives here, as the 3D peer of `usePanZoom`. It produces a trial
|
|
10
|
+
view (`{ yaw, pitch, distance, target }`) rather than a matrix — turning that
|
|
11
|
+
into a camera is the host's job.
|
|
12
|
+
|
|
5
13
|
## Files
|
|
6
14
|
|
|
7
15
|
| File | Role |
|
|
@@ -32,12 +40,12 @@ interface CanvasStackProps {
|
|
|
32
40
|
|
|
33
41
|
1. The container measures itself with `ResizeObserver` and reports `{width, height, dpr}`.
|
|
34
42
|
2. `useLayerScheduler` watches `layers`, `view`, and `size`, and on any change schedules a single `requestAnimationFrame` redraw.
|
|
35
|
-
3. Each layer's `render(ctx, view)` is called with a fresh transform: the canvas is sized to `width * dpr × height * dpr` so coordinates are in CSS pixels.
|
|
43
|
+
3. Each layer's `render(ctx, view)` is called with a fresh transform: the canvas is sized to `width * dpr × height * dpr` so coordinates are in CSS pixels. A `CanvasLayerDescriptor` gets the whole `view` and places its own geometry; an instrument's `CanvasLayer.draw` is called with the camera already applied — see below.
|
|
36
44
|
4. Layers with `visible: false` are skipped and their canvas is `display: none` (DOM is preserved to avoid remounts).
|
|
37
45
|
|
|
38
46
|
## Adding a new layer type
|
|
39
47
|
|
|
40
|
-
A layer is defined by the instrument's `canvas.layers[]` (type `CanvasLayer`), then translated to a `CanvasLayerDescriptor` inside `
|
|
48
|
+
A layer is defined by the instrument's `canvas.layers[]` (type `CanvasLayer`), then translated to a `CanvasLayerDescriptor` inside `Trial.tsx` before being passed to `<CanvasStack>`. To add a layer, push it into `instrument.canvas.layers`:
|
|
41
49
|
|
|
42
50
|
```ts
|
|
43
51
|
canvas: {
|
|
@@ -50,6 +58,10 @@ canvas: {
|
|
|
50
58
|
}
|
|
51
59
|
```
|
|
52
60
|
|
|
61
|
+
`draw` receives the context with the camera already applied, so its coordinates are world coordinates — `Trial.tsx` translates by `view.pan` and scales by `view.zoom` before calling it. `zoom` is still passed so a layer can keep line widths and handle sizes from growing: divide by it (`ctx.lineWidth = 1 / zoom`).
|
|
62
|
+
|
|
63
|
+
That is the difference between the two layer types. The lower-level `CanvasLayerDescriptor.render(ctx, view)` gets the raw view and an untransformed context, which is what screen-space chrome wants.
|
|
64
|
+
|
|
53
65
|
Layer order in the array = paint order (first drawn = bottom).
|
|
54
66
|
|
|
55
67
|
## Overlay (children)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { clampPitch, orbitAfterDrag, orbitAfterWheel, PITCH_LIMIT, wrapYaw } from './useOrbit';
|
|
3
|
+
|
|
4
|
+
const view = { yaw: 0, pitch: 0, distance: 5, target: { x: 0, y: 0, z: 0 } };
|
|
5
|
+
|
|
6
|
+
describe('clampPitch', () => {
|
|
7
|
+
it('stops just short of the poles, where azimuth becomes undefined', () => {
|
|
8
|
+
expect(clampPitch(Math.PI)).toBeCloseTo(PITCH_LIMIT);
|
|
9
|
+
expect(clampPitch(-Math.PI)).toBeCloseTo(-PITCH_LIMIT);
|
|
10
|
+
expect(PITCH_LIMIT).toBeLessThan(Math.PI / 2);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('leaves an in-range pitch alone', () => {
|
|
14
|
+
expect(clampPitch(0.3)).toBe(0.3);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('wrapYaw', () => {
|
|
19
|
+
it('wraps into (-PI, PI] so a value cannot drift without bound', () => {
|
|
20
|
+
expect(wrapYaw(3 * Math.PI)).toBeCloseTo(Math.PI);
|
|
21
|
+
expect(wrapYaw(-3 * Math.PI)).toBeCloseTo(Math.PI);
|
|
22
|
+
expect(wrapYaw(0.5)).toBeCloseTo(0.5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('keeps a full turn equivalent to no turn', () => {
|
|
26
|
+
expect(wrapYaw(2 * Math.PI)).toBeCloseTo(0);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('orbitAfterDrag', () => {
|
|
31
|
+
it('turns horizontal movement into yaw and vertical into pitch', () => {
|
|
32
|
+
const next = orbitAfterDrag(view, 100, 50);
|
|
33
|
+
expect(next.yaw).not.toBe(view.yaw);
|
|
34
|
+
expect(next.pitch).not.toBe(view.pitch);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('is absolute against the drag start, so re-applying does not compound', () => {
|
|
38
|
+
const once = orbitAfterDrag(view, 100, 50);
|
|
39
|
+
const twice = orbitAfterDrag(view, 100, 50);
|
|
40
|
+
expect(twice).toEqual(once);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('clamps pitch rather than tumbling past the pole', () => {
|
|
44
|
+
const next = orbitAfterDrag(view, 0, 100_000);
|
|
45
|
+
expect(Math.abs(next.pitch)).toBeLessThanOrEqual(PITCH_LIMIT);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('leaves distance and target untouched', () => {
|
|
49
|
+
const next = orbitAfterDrag(view, 100, 50);
|
|
50
|
+
expect(next.distance).toBe(view.distance);
|
|
51
|
+
expect(next.target).toEqual(view.target);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('orbitAfterWheel', () => {
|
|
56
|
+
it('moves the camera in and out', () => {
|
|
57
|
+
expect(orbitAfterWheel(view, 100, 0.5, 50).distance).toBeGreaterThan(view.distance);
|
|
58
|
+
expect(orbitAfterWheel(view, -100, 0.5, 50).distance).toBeLessThan(view.distance);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('is multiplicative, so a step feels the same at every distance', () => {
|
|
62
|
+
const near = orbitAfterWheel({ ...view, distance: 2 }, 100, 0.5, 50);
|
|
63
|
+
const far = orbitAfterWheel({ ...view, distance: 20 }, 100, 0.5, 50);
|
|
64
|
+
expect(far.distance / 20).toBeCloseTo(near.distance / 2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('honours its bounds', () => {
|
|
68
|
+
expect(orbitAfterWheel(view, 100_000, 0.5, 50).distance).toBe(50);
|
|
69
|
+
expect(orbitAfterWheel(view, -100_000, 0.5, 50).distance).toBe(0.5);
|
|
70
|
+
});
|
|
71
|
+
});
|