@weasel-js/labkit 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
  2. package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
  3. package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-DiuEArKq.d.ts} +4 -4
  4. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +79 -0
  5. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +141 -0
  6. package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Bensr_MT.d.ts} +15 -6
  7. package/dist/_dts/Stage-CCWfQQWM.d.ts +202 -0
  8. package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
  9. package/dist/_dts/{index-B0--8PEM.d.ts → index-CsYqUHD3.d.ts} +12 -13
  10. package/dist/_dts/{types-B7y8hX1D.d.ts → types-CRPSGFAp.d.ts} +33 -5
  11. package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-CSm9jgol.d.ts} +700 -650
  12. package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CTZPgeZ7.d.ts} +2 -3
  13. package/dist/canvas/index.d.ts +40 -8
  14. package/dist/canvas/index.js +5 -3
  15. package/dist/chrome/index.d.ts +5 -6
  16. package/dist/chrome/index.js +9 -7
  17. package/dist/chunk-24W5VEJW.js +18 -0
  18. package/dist/chunk-24W5VEJW.js.map +1 -0
  19. package/dist/chunk-275JRRVD.js +728 -0
  20. package/dist/chunk-275JRRVD.js.map +1 -0
  21. package/dist/chunk-3RY6WL5T.js +38 -0
  22. package/dist/chunk-3RY6WL5T.js.map +1 -0
  23. package/dist/chunk-4LJSC7MB.js +143 -0
  24. package/dist/chunk-4LJSC7MB.js.map +1 -0
  25. package/dist/{chunk-3FI7Y4R4.js → chunk-7BO7HNGF.js} +5 -5
  26. package/dist/{chunk-3FI7Y4R4.js.map → chunk-7BO7HNGF.js.map} +1 -1
  27. package/dist/chunk-AWQG7FCC.js +19 -0
  28. package/dist/chunk-AWQG7FCC.js.map +1 -0
  29. package/dist/chunk-CZZPXUOU.js +3 -0
  30. package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
  31. package/dist/{chunk-ZORVNOKX.js → chunk-FMK7UQ6V.js} +24 -145
  32. package/dist/chunk-FMK7UQ6V.js.map +1 -0
  33. package/dist/{chunk-BVL7Y574.js → chunk-FZ7YFQN7.js} +26 -10
  34. package/dist/chunk-FZ7YFQN7.js.map +1 -0
  35. package/dist/{chunk-USO6TERP.js → chunk-GOZLNM4L.js} +11 -9
  36. package/dist/chunk-GOZLNM4L.js.map +1 -0
  37. package/dist/{chunk-H5VIV7MQ.js → chunk-I5DUQ5HM.js} +88 -82
  38. package/dist/chunk-I5DUQ5HM.js.map +1 -0
  39. package/dist/chunk-J7OWWTCW.js +143 -0
  40. package/dist/chunk-J7OWWTCW.js.map +1 -0
  41. package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
  42. package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
  43. package/dist/{chunk-VQ47KWD5.js → chunk-KMJ7RWD3.js} +18 -7
  44. package/dist/chunk-KMJ7RWD3.js.map +1 -0
  45. package/dist/{chunk-ERFMC6WU.js → chunk-M7HP3FTK.js} +20 -11
  46. package/dist/chunk-M7HP3FTK.js.map +1 -0
  47. package/dist/chunk-MIM5R4ZT.js +14972 -0
  48. package/dist/chunk-MIM5R4ZT.js.map +1 -0
  49. package/dist/{chunk-HFRE4QQV.js → chunk-T5IBSPAM.js} +96 -32
  50. package/dist/chunk-T5IBSPAM.js.map +1 -0
  51. package/dist/chunk-UNAZSLWE.js +294 -0
  52. package/dist/chunk-UNAZSLWE.js.map +1 -0
  53. package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
  54. package/dist/chunk-WPLAANH4.js.map +1 -0
  55. package/dist/{chunk-AWH3SRPW.js → chunk-Y23G2VZL.js} +5 -37
  56. package/dist/chunk-Y23G2VZL.js.map +1 -0
  57. package/dist/{chunk-MOLIUNVH.js → chunk-YT75VYRD.js} +4 -4
  58. package/dist/{chunk-MOLIUNVH.js.map → chunk-YT75VYRD.js.map} +1 -1
  59. package/dist/chunk-ZIFPHIWZ.js +302 -0
  60. package/dist/chunk-ZIFPHIWZ.js.map +1 -0
  61. package/dist/config/index.d.ts +48 -40
  62. package/dist/config/index.js +4 -5
  63. package/dist/controls/index.d.ts +91 -7
  64. package/dist/controls/index.js +5 -4
  65. package/dist/dragdrop/index.d.ts +9 -7
  66. package/dist/dragdrop/index.js +3 -2
  67. package/dist/index.d.ts +294 -82
  68. package/dist/index.js +425 -402
  69. package/dist/index.js.map +1 -1
  70. package/dist/layers/index.d.ts +4 -5
  71. package/dist/layers/index.js +2 -1
  72. package/dist/loupe/index.d.ts +20 -17
  73. package/dist/loupe/index.js +5 -3
  74. package/dist/overview/index.d.ts +56 -0
  75. package/dist/overview/index.js +289 -0
  76. package/dist/overview/index.js.map +1 -0
  77. package/dist/passthrough/weasel-ui.d.ts +239 -69
  78. package/dist/passthrough/weasel-ui.js +3 -2
  79. package/dist/primitives/index.d.ts +5 -204
  80. package/dist/primitives/index.js +6 -4
  81. package/dist/state/index.d.ts +8 -8
  82. package/dist/state/index.js +4 -5
  83. package/dist/state/index.js.map +1 -1
  84. package/dist/styles.css +152 -82
  85. package/dist/undo/index.d.ts +4 -4
  86. package/package.json +17 -12
  87. package/src/annotations/AnnotationOverlay.tsx +2 -2
  88. package/src/annotations/Annotations.less +0 -10
  89. package/src/annotations/MarkList.tsx +4 -8
  90. package/src/annotations/index.ts +1 -0
  91. package/src/annotations/paint.test.ts +3 -14
  92. package/src/annotations/paint.ts +3 -19
  93. package/src/annotations/store.test.ts +26 -0
  94. package/src/annotations/store.ts +25 -6
  95. package/src/annotations/svgNodes.test.ts +6 -9
  96. package/src/annotations/svgNodes.ts +3 -3
  97. package/src/annotations/toolMap.test.ts +37 -2
  98. package/src/annotations/toolMap.ts +13 -0
  99. package/src/annotations/types.ts +27 -0
  100. package/src/canvas/AGENTS.md +9 -10
  101. package/src/canvas/CameraInput.test.tsx +136 -0
  102. package/src/canvas/CameraInput.tsx +322 -0
  103. package/src/canvas/CanvasStack.tsx +47 -58
  104. package/src/canvas/LinkedCursor.less +16 -0
  105. package/src/canvas/LinkedCursor.tsx +54 -0
  106. package/src/canvas/Stage.test.tsx +33 -4
  107. package/src/canvas/Stage.tsx +47 -37
  108. package/src/canvas/cameraRegistry.test.ts +55 -0
  109. package/src/canvas/cameraRegistry.ts +86 -0
  110. package/src/canvas/cameraView.test.ts +60 -0
  111. package/src/canvas/cameraView.ts +61 -0
  112. package/src/canvas/cameraZoom.test.ts +46 -0
  113. package/src/canvas/cameraZoom.ts +24 -0
  114. package/src/canvas/index.ts +23 -2
  115. package/src/canvas/sharedScope.test.tsx +94 -0
  116. package/src/chrome/ChromeRegions.stories.tsx +1 -0
  117. package/src/chrome/regions/PaletteRegion.less +0 -7
  118. package/src/chrome/regions/SidebarRegion.less +12 -2
  119. package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
  120. package/src/config/builder.test.ts +2 -1
  121. package/src/config/builder.ts +15 -8
  122. package/src/config/entry.test.ts +3 -1
  123. package/src/config/fromConfigField.test.ts +14 -0
  124. package/src/config/fromConfigField.ts +19 -4
  125. package/src/config/index.ts +4 -1
  126. package/src/config/path.ts +2 -5
  127. package/src/config/resolve.ts +15 -1
  128. package/src/config/sectionTree.test.ts +12 -6
  129. package/src/config/sectionTree.ts +15 -5
  130. package/src/config/types.ts +34 -9
  131. package/src/controls/ControlPanel.less +1 -1
  132. package/src/controls/ControlPanel.stories.tsx +3 -2
  133. package/src/controls/ControlPanel.test.tsx +76 -13
  134. package/src/controls/ControlPanel.tsx +80 -11
  135. package/src/controls/inDialog.test.tsx +18 -0
  136. package/src/controls/inDialog.tsx +2 -10
  137. package/src/index.test.ts +1 -1
  138. package/src/index.ts +23 -5
  139. package/src/instrument/SineWave.smoke.test.tsx +2 -0
  140. package/src/instrument/types.ts +10 -1
  141. package/src/lab/Lab.nebula.test.tsx +11 -2
  142. package/src/lab/Lab.test.tsx +3 -1
  143. package/src/lab/Lab.tsx +87 -76
  144. package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
  145. package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
  146. package/src/lab/LabFullChrome.stories.tsx +1 -0
  147. package/src/lab/LabHeader.test.tsx +24 -14
  148. package/src/lab/LabHeader.tsx +11 -12
  149. package/src/lab/LabShell.less +8 -17
  150. package/src/lab/LabSwitcher.less +6 -6
  151. package/src/lab/LabZoom.test.tsx +223 -0
  152. package/src/lab/LabZoom.tsx +170 -0
  153. package/src/lab/index.ts +1 -0
  154. package/src/loupe/AGENTS.md +7 -6
  155. package/src/loupe/LoupeGestures.tsx +15 -10
  156. package/src/loupe/TrialLoupe.tsx +5 -5
  157. package/src/loupe/index.ts +1 -0
  158. package/src/loupe/loupeActions.ts +6 -2
  159. package/src/overview/AGENTS.md +38 -0
  160. package/src/overview/OverviewMarks.test.tsx +100 -0
  161. package/src/overview/OverviewMarks.tsx +93 -0
  162. package/src/overview/TrialOverview.less +53 -0
  163. package/src/overview/TrialOverview.stories.tsx +50 -0
  164. package/src/overview/TrialOverview.test.tsx +116 -0
  165. package/src/overview/TrialOverview.tsx +376 -0
  166. package/src/overview/index.ts +2 -0
  167. package/src/passthrough/weasel-ui.test.ts +2 -0
  168. package/src/passthrough/weasel-ui.ts +23 -4
  169. package/src/primitives/FloatingPanel.test.tsx +11 -0
  170. package/src/primitives/FloatingPanel.tsx +2 -5
  171. package/src/primitives/JobProgress.less +1 -1
  172. package/src/primitives/Legend.stories.tsx +1 -0
  173. package/src/primitives/Readout.browser.test.tsx +137 -0
  174. package/src/primitives/Readout.less +12 -0
  175. package/src/primitives/Readout.stories.tsx +58 -0
  176. package/src/primitives/Readout.test.tsx +93 -0
  177. package/src/primitives/Readout.tsx +56 -0
  178. package/src/primitives/index.ts +2 -0
  179. package/src/specimen/Specimen.stories.tsx +1 -0
  180. package/src/specimen/Specimen.tsx +21 -4
  181. package/src/state/Persistence.browser.test.tsx +47 -0
  182. package/src/state/labRecords.test.ts +4 -1
  183. package/src/state/store.test.ts +9 -5
  184. package/src/state/usePersistedState.test.tsx +1 -1
  185. package/src/state/useTrialState.test.tsx +1 -1
  186. package/src/styles.less +3 -0
  187. package/src/theme/Interstellar.stories.tsx +4 -6
  188. package/src/theme/base.less +21 -47
  189. package/src/theme/interstellar.test.ts +1 -1
  190. package/src/theme/interstellar.theme.json +10 -3
  191. package/src/tools/labTool.ts +6 -2
  192. package/src/trial/Trial.annotations.test.tsx +16 -0
  193. package/src/trial/Trial.less +0 -13
  194. package/src/trial/Trial.loupe.test.tsx +73 -1
  195. package/src/trial/Trial.stories.tsx +2 -0
  196. package/src/trial/Trial.tsx +23 -1
  197. package/src/trial/TrialChrome.tsx +21 -3
  198. package/src/trial/trialOps.test.ts +4 -4
  199. package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
  200. package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
  201. package/dist/_dts/index-DINmCuLA.d.ts +0 -453
  202. package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
  203. package/dist/_dts/usePanZoom-Bq73pwFe.d.ts +0 -117
  204. package/dist/chunk-2AYGEN57.js +0 -32
  205. package/dist/chunk-2AYGEN57.js.map +0 -1
  206. package/dist/chunk-AWH3SRPW.js.map +0 -1
  207. package/dist/chunk-BVL7Y574.js.map +0 -1
  208. package/dist/chunk-ERFMC6WU.js.map +0 -1
  209. package/dist/chunk-H5VIV7MQ.js.map +0 -1
  210. package/dist/chunk-HFRE4QQV.js.map +0 -1
  211. package/dist/chunk-RS3HRQWU.js.map +0 -1
  212. package/dist/chunk-SBXNQW4G.js +0 -3
  213. package/dist/chunk-TT5C5S6R.js +0 -368
  214. package/dist/chunk-TT5C5S6R.js.map +0 -1
  215. package/dist/chunk-USO6TERP.js.map +0 -1
  216. package/dist/chunk-VQ47KWD5.js.map +0 -1
  217. package/dist/chunk-WUJXJC5O.js +0 -14568
  218. package/dist/chunk-WUJXJC5O.js.map +0 -1
  219. package/dist/chunk-ZORVNOKX.js.map +0 -1
  220. package/src/canvas/usePanZoom.test.ts +0 -244
  221. package/src/canvas/usePanZoom.ts +0 -147
  222. package/src/state/Persistence.stories.tsx +0 -75
@@ -0,0 +1,294 @@
1
+ import { DEFAULT_FRAME } from './chunk-24W5VEJW.js';
2
+ import { createContext, useContext, useEffect, useRef, useMemo } from 'react';
3
+ import { normalizeZoom, useActionsRegistry, usePointerContext, PointerContextProvider, useDepSource, makeViewportZoomAction, viewportDragPanAction, useGestureDispatcher, InputScope } from '@weasel-js/core';
4
+ import { jsx, Fragment } from 'react/jsx-runtime';
5
+
6
+ var CameraWheelContext = createContext(null);
7
+ var DEFAULT_VIEW = { zoom: 1, pan: { x: 0, y: 0 } };
8
+ function as2DView(view) {
9
+ if (typeof view !== "object" || view === null) return null;
10
+ const v = view;
11
+ if (typeof v.zoom !== "number") return null;
12
+ if (typeof v.pan !== "object" || v.pan === null) return null;
13
+ if (typeof v.pan.x !== "number" || typeof v.pan.y !== "number") return null;
14
+ return normalize2DView({ zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } });
15
+ }
16
+ function normalize2DView(view) {
17
+ const zoom = normalizeZoom(view.zoom);
18
+ const x = Number.isFinite(view.pan.x) ? view.pan.x : 0;
19
+ const y = Number.isFinite(view.pan.y) ? view.pan.y : 0;
20
+ if (zoom === view.zoom && x === view.pan.x && y === view.pan.y) return view;
21
+ return { zoom, pan: { x, y } };
22
+ }
23
+ function withZoom(view, zoom) {
24
+ return normalize2DView({ zoom, pan: view.pan });
25
+ }
26
+ function createCameraRegistry() {
27
+ const stacks = /* @__PURE__ */ new Map();
28
+ const listeners = /* @__PURE__ */ new Set();
29
+ const notify = () => {
30
+ for (const l of listeners) l();
31
+ };
32
+ return {
33
+ register(key, camera) {
34
+ const stack = stacks.get(key) ?? [];
35
+ stack.push(camera);
36
+ stacks.set(key, stack);
37
+ notify();
38
+ let released = false;
39
+ return () => {
40
+ if (released) return;
41
+ released = true;
42
+ const cur = stacks.get(key);
43
+ const i = cur?.lastIndexOf(camera) ?? -1;
44
+ if (!cur || i === -1) return;
45
+ cur.splice(i, 1);
46
+ if (cur.length === 0) stacks.delete(key);
47
+ notify();
48
+ };
49
+ },
50
+ get: (key) => stacks.get(key)?.at(-1) ?? null,
51
+ subscribe(listener) {
52
+ listeners.add(listener);
53
+ return () => {
54
+ listeners.delete(listener);
55
+ };
56
+ }
57
+ };
58
+ }
59
+ var CameraRegistryContext = createContext(null);
60
+ var CameraPublishContext = createContext(
61
+ null
62
+ );
63
+ function usePublishCamera(camera) {
64
+ const publish = useContext(CameraPublishContext);
65
+ const actions = useActionsRegistry();
66
+ useEffect(() => {
67
+ if (!publish) return;
68
+ let release = publish(camera);
69
+ if (!actions) return release;
70
+ let was = actions.isActive();
71
+ const off = actions.subscribe(() => {
72
+ const now = actions.isActive();
73
+ if (now && !was) {
74
+ release();
75
+ release = publish(camera);
76
+ }
77
+ was = now;
78
+ });
79
+ return () => {
80
+ off();
81
+ release();
82
+ };
83
+ }, [publish, camera, actions]);
84
+ }
85
+
86
+ // src/canvas/cameraView.ts
87
+ function toCameraView(view, frame = DEFAULT_FRAME) {
88
+ const v = normalize2DView(view);
89
+ return {
90
+ x: -(frame.originPx.x + v.pan.x) / v.zoom,
91
+ y: -(frame.originPx.y + v.pan.y) / v.zoom,
92
+ scale: { x: v.zoom, y: v.zoom }
93
+ };
94
+ }
95
+ function fromCameraView(view, frame = DEFAULT_FRAME) {
96
+ const zoom = view.scale.x;
97
+ return {
98
+ zoom,
99
+ pan: { x: -view.x * zoom - frame.originPx.x, y: -view.y * zoom - frame.originPx.y }
100
+ };
101
+ }
102
+ function frameLocalToWorld(p, frame = DEFAULT_FRAME) {
103
+ return { x: p.x, y: p.y * frame.yDir };
104
+ }
105
+ function clampZoomAbout(prev, next, min, max) {
106
+ const s0 = prev.scale.x;
107
+ const s1 = next.scale.x;
108
+ const clamped = Math.min(max, Math.max(min, s1));
109
+ if (clamped === s1) return next;
110
+ if (s0 === s1) return { ...next, scale: { x: clamped, y: clamped } };
111
+ const inv = 1 / s0 - 1 / s1;
112
+ const px = (next.x - prev.x) / inv;
113
+ const py = (next.y - prev.y) / inv;
114
+ return {
115
+ x: prev.x + px / s0 - px / clamped,
116
+ y: prev.y + py / s0 - py / clamped,
117
+ scale: { x: clamped, y: clamped }
118
+ };
119
+ }
120
+ var STAGE_VIEW_ID = "stage";
121
+ var CameraContext = createContext(null);
122
+ function CameraScope({ yoke, children }) {
123
+ const around = useContext(CameraContext);
124
+ const pointer = usePointerContext();
125
+ if (around) return /* @__PURE__ */ jsx(Fragment, { children });
126
+ const scoped = /* @__PURE__ */ jsx(InputScope, { yoke, children });
127
+ return pointer ? scoped : /* @__PURE__ */ jsx(PointerContextProvider, { children: scoped });
128
+ }
129
+ function isPositiveFinite(n) {
130
+ return Number.isFinite(n) && n > 0;
131
+ }
132
+ function useCameraView({
133
+ view,
134
+ onViewChange,
135
+ frame,
136
+ hostRef,
137
+ minZoom = 0.1,
138
+ maxZoom = 32
139
+ }) {
140
+ const live = useRef({ view: normalize2DView(view), onViewChange, frame });
141
+ live.current = { view: normalize2DView(view), onViewChange, frame };
142
+ const initialZoom = useRef(isPositiveFinite(view.zoom) ? view.zoom : null);
143
+ const bounds = useRef({ min: minZoom, max: maxZoom });
144
+ const opening = initialZoom.current;
145
+ bounds.current = {
146
+ min: opening == null ? minZoom : Math.min(minZoom, opening),
147
+ max: opening == null ? maxZoom : Math.max(maxZoom, opening)
148
+ };
149
+ return useMemo(() => {
150
+ const get = () => toCameraView(live.current.view, live.current.frame);
151
+ return {
152
+ get,
153
+ set: (next) => {
154
+ const { min, max } = bounds.current;
155
+ const clamped = clampZoomAbout(get(), next, min, max);
156
+ const vt = fromCameraView(clamped, live.current.frame);
157
+ live.current.view = vt;
158
+ live.current.onViewChange(vt);
159
+ },
160
+ hostSize: () => {
161
+ const r = hostRef.current?.getBoundingClientRect();
162
+ return r ? { width: r.width, height: r.height } : null;
163
+ },
164
+ zoomRange: () => ({ ...bounds.current })
165
+ };
166
+ }, [hostRef]);
167
+ }
168
+ var TAP_ID = "camera.tap";
169
+ var NO_TOOLS = /* @__PURE__ */ new Map();
170
+ var CHANNELS = { contextMenu: false, ingest: false };
171
+ function CameraInput(props) {
172
+ const registry = useActionsRegistry();
173
+ if (!registry) return null;
174
+ return /* @__PURE__ */ jsx(CameraDispatch, { ...props, registry });
175
+ }
176
+ function CameraDispatch({
177
+ hostRef,
178
+ camera,
179
+ frame,
180
+ onTap,
181
+ registry
182
+ }) {
183
+ const frameRef = useRef(frame);
184
+ frameRef.current = frame;
185
+ const onTapRef = useRef(onTap);
186
+ onTapRef.current = onTap;
187
+ useDepSource("view", () => camera);
188
+ useDepSource("rootView", () => camera);
189
+ const actions = useMemo(() => {
190
+ const zoom = makeViewportZoomAction({
191
+ wheel: "plain",
192
+ min: 1e-9,
193
+ max: Number.POSITIVE_INFINITY
194
+ });
195
+ const wheelOnly = zoom.defaultBinding.filter(
196
+ (b) => b.spec.kind === "wheel" || b.spec.kind === "pinch"
197
+ );
198
+ const tap = {
199
+ id: TAP_ID,
200
+ label: "Tap the canvas",
201
+ defaultBinding: { kind: "click" },
202
+ invoker: {
203
+ timing: "immediate",
204
+ run: (_deps, params) => {
205
+ const p = params;
206
+ if (p?.worldX === void 0 || p.worldY === void 0) return;
207
+ onTapRef.current?.(frameLocalToWorld({ x: p.worldX, y: p.worldY }, frameRef.current));
208
+ }
209
+ }
210
+ };
211
+ return [
212
+ // A trial pans on a plain drag; the kit action binds none of its own.
213
+ { ...viewportDragPanAction, defaultBinding: { kind: "drag" } },
214
+ { ...zoom, defaultBinding: wheelOnly },
215
+ tap
216
+ ];
217
+ }, []);
218
+ useEffect(() => {
219
+ const offs = actions.map((a) => registry.register(a));
220
+ return () => {
221
+ for (const off of offs) off();
222
+ };
223
+ }, [registry, actions]);
224
+ const clientToWorld = useMemo(
225
+ () => (cx, cy) => {
226
+ const r = hostRef.current?.getBoundingClientRect();
227
+ const v = camera.get();
228
+ return {
229
+ x: (cx - (r?.left ?? 0)) / v.scale.x + v.x,
230
+ y: (cy - (r?.top ?? 0)) / v.scale.y + v.y
231
+ };
232
+ },
233
+ [hostRef, camera]
234
+ );
235
+ useGestureDispatcher({
236
+ canvasRef: hostRef,
237
+ actions: registry,
238
+ entriesById: NO_TOOLS,
239
+ clientToWorld,
240
+ channels: CHANNELS
241
+ });
242
+ usePublishPointer(hostRef, clientToWorld, frame, STAGE_VIEW_ID);
243
+ usePublishCamera(camera);
244
+ const wheelSlot = useContext(CameraWheelContext);
245
+ useEffect(() => {
246
+ if (!wheelSlot) return;
247
+ const forward = (e) => {
248
+ hostRef.current?.dispatchEvent(new WheelEvent("wheel", e));
249
+ };
250
+ const claim = () => {
251
+ if (registry.isActive()) wheelSlot.current = forward;
252
+ };
253
+ claim();
254
+ const off = registry.subscribe(claim);
255
+ return () => {
256
+ off();
257
+ if (wheelSlot.current === forward) wheelSlot.current = null;
258
+ };
259
+ }, [wheelSlot, hostRef, registry]);
260
+ return null;
261
+ }
262
+ function usePublishPointer(hostRef, clientToLocal, frame, viewId) {
263
+ const store = usePointerContext();
264
+ const frameRef = useRef(frame);
265
+ frameRef.current = frame;
266
+ const toLocal = useRef(clientToLocal);
267
+ toLocal.current = clientToLocal;
268
+ useEffect(() => {
269
+ const el = hostRef.current;
270
+ if (!el || !store) return;
271
+ const onMove = (e) => {
272
+ const w = frameLocalToWorld(toLocal.current(e.clientX, e.clientY), frameRef.current);
273
+ store.set({ worldX: w.x, worldY: w.y, viewId });
274
+ };
275
+ const onLeave = (e) => {
276
+ if (e.buttons !== 0) return;
277
+ if (store.get()?.viewId === viewId) store.set(null);
278
+ };
279
+ el.addEventListener("pointermove", onMove);
280
+ el.addEventListener("pointerleave", onLeave);
281
+ return () => {
282
+ el.removeEventListener("pointermove", onMove);
283
+ el.removeEventListener("pointerleave", onLeave);
284
+ };
285
+ }, [hostRef, store, viewId]);
286
+ }
287
+ function pointerElsewhere(p, viewId, frame) {
288
+ if (!p || p.viewId === viewId) return null;
289
+ return frameLocalToWorld({ x: p.worldX, y: p.worldY }, frame);
290
+ }
291
+
292
+ export { CameraContext, CameraInput, CameraPublishContext, CameraRegistryContext, CameraScope, CameraWheelContext, DEFAULT_VIEW, STAGE_VIEW_ID, as2DView, clampZoomAbout, createCameraRegistry, frameLocalToWorld, fromCameraView, normalize2DView, pointerElsewhere, toCameraView, useCameraView, usePublishCamera, usePublishPointer, withZoom };
293
+ //# sourceMappingURL=chunk-UNAZSLWE.js.map
294
+ //# sourceMappingURL=chunk-UNAZSLWE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/canvas/CameraWheelContext.ts","../src/state/view.ts","../src/canvas/cameraRegistry.ts","../src/canvas/cameraView.ts","../src/canvas/CameraInput.tsx"],"names":["createContext","useContext","useActionsRegistry","useEffect"],"mappings":"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","file":"chunk-UNAZSLWE.js","sourcesContent":["import { createContext } from 'react';\n\n/** Where a trial's camera takes a wheel event from outside its own element. */\nexport type CameraWheelSlot = { current: ((e: WheelEvent) => void) | null };\n\n/**\n * The trial's camera, for input that lands outside it. An annotation target's\n * input box is portalled into the surface container, so a wheel over a mark\n * never bubbles through the stage or canvas stack it sits on; the box hands\n * the event here instead.\n */\nexport const CameraWheelContext = createContext<CameraWheelSlot | null>(null);\n","import { normalizeZoom } from '@weasel-js/core';\nimport type { ViewTransform } from '../instrument/types';\n\n/** The 2D view labkit has always shipped, and what a trial gets when it names no\n * other. Re-exported under its own name so a consumer can say which it means. */\nexport type ViewTransform2D = ViewTransform;\n\nexport const DEFAULT_VIEW: ViewTransform2D = { zoom: 1, pan: { x: 0, y: 0 } };\n\n/** A trial's view is opaque to labkit, so anything that needs the 2D shape — the\n * zoom chrome, `CanvasStack` — asks for it and handles not getting it. */\nexport function as2DView(view: unknown): ViewTransform2D | null {\n if (typeof view !== 'object' || view === null) return null;\n const v = view as Partial<ViewTransform2D>;\n if (typeof v.zoom !== 'number') return null;\n if (typeof v.pan !== 'object' || v.pan === null) return null;\n if (typeof v.pan.x !== 'number' || typeof v.pan.y !== 'number') return null;\n return normalize2DView({ zoom: v.zoom, pan: { x: v.pan.x, y: v.pan.y } });\n}\n\n/** `view` inside the invariant every view holds: a zoom that is not a finite\n * number of at least `ZOOM_FLOOR` becomes the floor, and a non-finite pan\n * becomes 0. Returns `view` itself when it already holds. */\nexport function normalize2DView(view: ViewTransform2D): ViewTransform2D {\n const zoom = normalizeZoom(view.zoom);\n const x = Number.isFinite(view.pan.x) ? view.pan.x : 0;\n const y = Number.isFinite(view.pan.y) ? view.pan.y : 0;\n if (zoom === view.zoom && x === view.pan.x && y === view.pan.y) return view;\n return { zoom, pan: { x, y } };\n}\n\n/** `view` at a new zoom, through the same rule. */\nexport function withZoom(view: ViewTransform2D, zoom: number): ViewTransform2D {\n return normalize2DView({ zoom, pan: view.pan });\n}\n","import { useActionsRegistry } from '@weasel-js/core';\nimport { createContext, useContext, useEffect } from 'react';\nimport type { CameraView } from './CameraInput';\n\n/**\n * Cameras by key, for chrome that acts on a camera it does not contain — the\n * lab's zoom controls, which drive whichever trial has the focus.\n */\nexport interface CameraRegistry {\n /** Publish `camera` under `key` and return its release. Registrants for one\n * key stack, newest live. */\n register(key: string, camera: CameraView): () => void;\n get(key: string): CameraView | null;\n /** Fires after any register or release. Shaped for `useSyncExternalStore`. */\n subscribe(listener: () => void): () => void;\n}\n\nexport function createCameraRegistry(): CameraRegistry {\n const stacks = new Map<string, CameraView[]>();\n const listeners = new Set<() => void>();\n const notify = (): void => {\n for (const l of listeners) l();\n };\n return {\n register(key, camera) {\n const stack = stacks.get(key) ?? [];\n stack.push(camera);\n stacks.set(key, stack);\n notify();\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const cur = stacks.get(key);\n const i = cur?.lastIndexOf(camera) ?? -1;\n if (!cur || i === -1) return;\n cur.splice(i, 1);\n if (cur.length === 0) stacks.delete(key);\n notify();\n };\n },\n get: (key) => stacks.get(key)?.at(-1) ?? null,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** The registry a lab keeps its trials' cameras in. */\nexport const CameraRegistryContext = createContext<CameraRegistry | null>(null);\n\n/** Where a camera publishes itself. The host binds the key — a trial provides\n * one that registers under its own id — so the camera needs no name. */\nexport const CameraPublishContext = createContext<((camera: CameraView) => () => void) | null>(\n null,\n);\n\n/** Publish `camera` to the host in scope for as long as the caller is mounted,\n * and again each time its input scope becomes the active one — a registry\n * key stacks newest-live, so the camera last used is the one chrome reaches.\n * Call it inside the camera's scope. */\nexport function usePublishCamera(camera: CameraView): void {\n const publish = useContext(CameraPublishContext);\n const actions = useActionsRegistry();\n useEffect(() => {\n if (!publish) return;\n let release = publish(camera);\n if (!actions) return release;\n let was = actions.isActive();\n const off = actions.subscribe(() => {\n const now = actions.isActive();\n if (now && !was) {\n release();\n release = publish(camera);\n }\n was = now;\n });\n return () => {\n off();\n release();\n };\n }, [publish, camera, actions]);\n}\n","/**\n * labkit's camera as a weasel `View`, so core's viewport actions drive it.\n *\n * labkit places a world point on screen at\n * `originPx + pan + zoom * (x, yDir * y)`. A weasel `View` places one at\n * `(p - (x, y)) * scale`. Over *frame-local* coordinates — the world with `y`\n * multiplied by `yDir` — the two are the same affine map, which is all a pan\n * or a zoom about a screen point needs.\n */\nimport type { View } from '@weasel-js/core';\nimport type { Point, ViewTransform } from '../instrument/types';\nimport { normalize2DView } from '../state/view';\nimport { DEFAULT_FRAME, type WorldFrame } from './worldSpec';\n\n/** The weasel `View` equal to `view` under `frame`. */\nexport function toCameraView(view: ViewTransform, frame: WorldFrame = DEFAULT_FRAME): View {\n const v = normalize2DView(view);\n return {\n x: -(frame.originPx.x + v.pan.x) / v.zoom,\n y: -(frame.originPx.y + v.pan.y) / v.zoom,\n scale: { x: v.zoom, y: v.zoom },\n };\n}\n\n/** The labkit camera equal to weasel `view` under `frame`. */\nexport function fromCameraView(view: View, frame: WorldFrame = DEFAULT_FRAME): ViewTransform {\n const zoom = view.scale.x;\n return {\n zoom,\n pan: { x: -view.x * zoom - frame.originPx.x, y: -view.y * zoom - frame.originPx.y },\n };\n}\n\n/** A frame-local point in the instrument's world, and back — the map is its\n * own inverse. */\nexport function frameLocalToWorld(p: Point, frame: WorldFrame = DEFAULT_FRAME): Point {\n return { x: p.x, y: p.y * frame.yDir };\n}\n\n/**\n * `next` with its zoom held to [`min`, `max`], about the screen point the step\n * from `prev` was anchored on, so a zoom that hits a bound stays anchored where\n * the pointer is. A pan (equal scales) passes through.\n */\nexport function clampZoomAbout(prev: View, next: View, min: number, max: number): View {\n const s0 = prev.scale.x;\n const s1 = next.scale.x;\n const clamped = Math.min(max, Math.max(min, s1));\n if (clamped === s1) return next;\n if (s0 === s1) return { ...next, scale: { x: clamped, y: clamped } };\n // The screen point both views agree on: p / s0 + x0 = p / s1 + x1.\n const inv = 1 / s0 - 1 / s1;\n const px = (next.x - prev.x) / inv;\n const py = (next.y - prev.y) / inv;\n // The same point under the clamped scale.\n return {\n x: prev.x + px / s0 - px / clamped,\n y: prev.y + py / s0 - py / clamped,\n scale: { x: clamped, y: clamped },\n };\n}\n","/**\n * A labkit camera driven by weasel's dispatcher: drag pans, the wheel zooms\n * about the pointer, a tap reports the world point, and the pointer is\n * published into the store in scope. What `usePanZoom` used to hand-roll.\n */\nimport {\n type Action,\n type ActionsRegistry,\n InputScope,\n makeViewportZoomAction,\n PointerContextProvider,\n type PointerWorldPos,\n type Tool,\n useActionsRegistry,\n useDepSource,\n useGestureDispatcher,\n usePointerContext,\n type View,\n type ViewApi,\n viewportDragPanAction,\n type Yoke,\n} from '@weasel-js/core';\nimport {\n createContext,\n type ReactNode,\n type RefObject,\n useContext,\n useEffect,\n useMemo,\n useRef,\n} from 'react';\nimport type { Point, ViewTransform } from '../instrument/types';\nimport { normalize2DView } from '../state/view';\nimport { CameraWheelContext } from './CameraWheelContext';\nimport { usePublishCamera } from './cameraRegistry';\nimport { clampZoomAbout, frameLocalToWorld, fromCameraView, toCameraView } from './cameraView';\nimport type { ViewportSize, WorldFrame } from './worldSpec';\n\n/** The view id the stage publishes its pointer under. */\nexport const STAGE_VIEW_ID = 'stage';\n\n/**\n * A camera, as the things that sit beside it read it: an overview, a loupe, a\n * linked cursor. Points are *frame-local* — the world with `y` multiplied by\n * `frame.yDir` — which is the space `view` works in.\n */\nexport interface CameraContextValue {\n /** The camera as a weasel `ViewApi`, clamped to its zoom range. */\n view: ViewApi;\n frame: WorldFrame;\n /** The element the camera's dispatcher listens on. */\n element: () => HTMLElement | null;\n /** The content's own rect, frame-local, when it has one — a `<Stage>`'s. */\n content?: { x: number; y: number; width: number; height: number };\n}\n\n/** The camera around the caller, or `null` outside one. */\nexport const CameraContext = createContext<CameraContextValue | null>(null);\n\n/**\n * The input scope a camera routes through. Inside another camera — a loupe or\n * an overlay laid over a canvas stack — it joins that camera's scope, so its\n * actions reach the dispatcher already listening there. Anywhere else it\n * mounts a scope of its own, which keeps its own tool unless it joins `yoke`:\n * a camera beside another never takes that one's gestures. The pointer store is the exception — one\n * already in scope is kept, since sharing it is the point.\n */\nexport function CameraScope({ yoke, children }: { yoke?: Yoke; children: ReactNode }) {\n const around = useContext(CameraContext);\n const pointer = usePointerContext();\n if (around) return <>{children}</>;\n const scoped = <InputScope yoke={yoke}>{children}</InputScope>;\n return pointer ? scoped : <PointerContextProvider>{scoped}</PointerContextProvider>;\n}\n\n/** Options for {@link useCameraView}. */\nexport interface CameraViewOptions {\n view: ViewTransform;\n onViewChange: (v: ViewTransform) => void;\n frame: WorldFrame;\n hostRef: RefObject<HTMLElement | null>;\n minZoom?: number;\n maxZoom?: number;\n}\n\n/** A camera's zoom limits, after widening to admit its opening zoom. */\nexport interface ZoomRange {\n min: number;\n max: number;\n}\n\n/** A labkit camera: a weasel `ViewApi` that also says how far it zooms. */\nexport interface CameraView extends ViewApi {\n zoomRange(): ZoomRange;\n}\n\nfunction isPositiveFinite(n: number): boolean {\n return Number.isFinite(n) && n > 0;\n}\n\n/**\n * A labkit camera as a stable `ViewApi`. Its zoom range is widened to keep the\n * opening zoom reachable — captured once, so a zoom-out cannot shrink the\n * range and strand the opening view behind it.\n */\nexport function useCameraView({\n view,\n onViewChange,\n frame,\n hostRef,\n minZoom = 0.1,\n maxZoom = 32,\n}: CameraViewOptions): CameraView {\n const live = useRef({ view: normalize2DView(view), onViewChange, frame });\n live.current = { view: normalize2DView(view), onViewChange, frame };\n const initialZoom = useRef(isPositiveFinite(view.zoom) ? view.zoom : null);\n const bounds = useRef({ min: minZoom, max: maxZoom });\n const opening = initialZoom.current;\n bounds.current = {\n min: opening == null ? minZoom : Math.min(minZoom, opening),\n max: opening == null ? maxZoom : Math.max(maxZoom, opening),\n };\n\n return useMemo<CameraView>(() => {\n const get = (): View => toCameraView(live.current.view, live.current.frame);\n return {\n get,\n set: (next: View) => {\n const { min, max } = bounds.current;\n const clamped = clampZoomAbout(get(), next, min, max);\n const vt = fromCameraView(clamped, live.current.frame);\n // Written ahead of the render, so a second event inside one commit\n // steps from this one.\n live.current.view = vt;\n live.current.onViewChange(vt);\n },\n hostSize: () => {\n const r = hostRef.current?.getBoundingClientRect();\n return r ? { width: r.width, height: r.height } : null;\n },\n zoomRange: () => ({ ...bounds.current }),\n };\n }, [hostRef]);\n}\n\nconst TAP_ID = 'camera.tap';\nconst NO_TOOLS: ReadonlyMap<string, Tool> = new Map();\nconst CHANNELS = { contextMenu: false, ingest: false } as const;\n\n/** Props for `<CameraInput>`. */\nexport interface CameraInputProps {\n hostRef: RefObject<HTMLElement | null>;\n /** Published to the host's camera registry while its scope is the active\n * one, so chrome acting on \"the camera\" acts on the one last used. */\n camera: CameraView;\n frame: WorldFrame;\n /** A primary-button press released without crossing the drag threshold, at\n * the world point it landed on. */\n onTap?: (world: Point) => void;\n}\n\n/**\n * Mounts the dispatcher on `hostRef` and routes the camera's gestures through\n * it. Render it inside a `<CameraScope>`; with no actions registry in scope it\n * renders nothing.\n */\nexport function CameraInput(props: CameraInputProps) {\n const registry = useActionsRegistry();\n if (!registry) return null;\n return <CameraDispatch {...props} registry={registry} />;\n}\n\nfunction CameraDispatch({\n hostRef,\n camera,\n frame,\n onTap,\n registry,\n}: CameraInputProps & { registry: ActionsRegistry }) {\n const frameRef = useRef(frame);\n frameRef.current = frame;\n const onTapRef = useRef(onTap);\n onTapRef.current = onTap;\n\n useDepSource('view', () => camera);\n useDepSource('rootView', () => camera);\n\n const actions = useMemo<Action[]>(() => {\n // Unclamped here: `camera.set` clamps, to a range widened around the\n // opening zoom, and anchors the clamp where the wheel was.\n const zoom = makeViewportZoomAction({\n wheel: 'plain',\n min: 1e-9,\n max: Number.POSITIVE_INFINITY,\n });\n // The wheel and the trackpad's two pinch forms only: the zoom keys listen\n // on the window, and every trial on a page would take them at once.\n const wheelOnly = (zoom.defaultBinding as { spec: { kind: string } }[]).filter(\n (b) => b.spec.kind === 'wheel' || b.spec.kind === 'pinch',\n );\n const tap: Action = {\n id: TAP_ID,\n label: 'Tap the canvas',\n defaultBinding: { kind: 'click' },\n invoker: {\n timing: 'immediate',\n run: (_deps, params) => {\n const p = params as { worldX?: number; worldY?: number } | undefined;\n if (p?.worldX === undefined || p.worldY === undefined) return;\n onTapRef.current?.(frameLocalToWorld({ x: p.worldX, y: p.worldY }, frameRef.current));\n },\n },\n };\n return [\n // A trial pans on a plain drag; the kit action binds none of its own.\n { ...viewportDragPanAction, defaultBinding: { kind: 'drag' } },\n { ...zoom, defaultBinding: wheelOnly as Action['defaultBinding'] },\n tap,\n ];\n }, []);\n\n useEffect(() => {\n const offs = actions.map((a) => registry.register(a));\n return () => {\n for (const off of offs) off();\n };\n }, [registry, actions]);\n\n const clientToWorld = useMemo(\n () => (cx: number, cy: number) => {\n const r = hostRef.current?.getBoundingClientRect();\n const v = camera.get();\n return {\n x: (cx - (r?.left ?? 0)) / v.scale.x + v.x,\n y: (cy - (r?.top ?? 0)) / v.scale.y + v.y,\n };\n },\n [hostRef, camera],\n );\n\n useGestureDispatcher({\n canvasRef: hostRef,\n actions: registry,\n entriesById: NO_TOOLS,\n clientToWorld,\n channels: CHANNELS,\n });\n\n usePublishPointer(hostRef, clientToWorld, frame, STAGE_VIEW_ID);\n usePublishCamera(camera);\n\n // Input that lands outside the camera's element — an annotation target's\n // box, portalled into the surface — reaches the active camera's dispatcher\n // through here.\n const wheelSlot = useContext(CameraWheelContext);\n useEffect(() => {\n if (!wheelSlot) return;\n const forward = (e: WheelEvent): void => {\n hostRef.current?.dispatchEvent(new WheelEvent('wheel', e));\n };\n const claim = (): void => {\n if (registry.isActive()) wheelSlot.current = forward;\n };\n claim();\n const off = registry.subscribe(claim);\n return () => {\n off();\n if (wheelSlot.current === forward) wheelSlot.current = null;\n };\n }, [wheelSlot, hostRef, registry]);\n\n return null;\n}\n\n/**\n * Publish the pointer over `hostRef` into the store in scope, in the\n * instrument's world, as `viewId`; clear it when the pointer leaves with no\n * button held.\n */\nexport function usePublishPointer(\n hostRef: RefObject<HTMLElement | null>,\n clientToLocal: (cx: number, cy: number) => Point,\n frame: WorldFrame,\n viewId: string,\n): void {\n const store = usePointerContext();\n const frameRef = useRef(frame);\n frameRef.current = frame;\n const toLocal = useRef(clientToLocal);\n toLocal.current = clientToLocal;\n useEffect(() => {\n const el = hostRef.current;\n if (!el || !store) return;\n const onMove = (e: PointerEvent): void => {\n const w = frameLocalToWorld(toLocal.current(e.clientX, e.clientY), frameRef.current);\n store.set({ worldX: w.x, worldY: w.y, viewId });\n };\n const onLeave = (e: PointerEvent): void => {\n if (e.buttons !== 0) return;\n if (store.get()?.viewId === viewId) store.set(null);\n };\n el.addEventListener('pointermove', onMove);\n el.addEventListener('pointerleave', onLeave);\n return () => {\n el.removeEventListener('pointermove', onMove);\n el.removeEventListener('pointerleave', onLeave);\n };\n }, [hostRef, store, viewId]);\n}\n\n/** The pointer as a frame-local point, or `null` when there is none or it is\n * over `viewId` itself. */\nexport function pointerElsewhere(\n p: PointerWorldPos,\n viewId: string,\n frame: WorldFrame,\n): Point | null {\n if (!p || p.viewId === viewId) return null;\n return frameLocalToWorld({ x: p.worldX, y: p.worldY }, frame);\n}\n\nexport type { ViewportSize };\n"]}
@@ -1,4 +1,5 @@
1
- import { resolveFrame, screenToWorld } from './chunk-2AYGEN57.js';
1
+ import { screenToWorld } from './chunk-AWQG7FCC.js';
2
+ import { resolveFrame } from './chunk-24W5VEJW.js';
2
3
  import { useState, useEffect, useRef, useCallback } from 'react';
3
4
  import { createPortal } from 'react-dom';
4
5
  import { jsx } from 'react/jsx-runtime';
@@ -138,5 +139,5 @@ function Palette({ items, onDragStart, className }) {
138
139
  }
139
140
 
140
141
  export { DragGhost, DragOverlay, Palette, useDragDrop };
141
- //# sourceMappingURL=chunk-RS3HRQWU.js.map
142
- //# sourceMappingURL=chunk-RS3HRQWU.js.map
142
+ //# sourceMappingURL=chunk-WPLAANH4.js.map
143
+ //# sourceMappingURL=chunk-WPLAANH4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dragdrop/DragGhost.tsx","../src/dragdrop/DragDropRuntime.tsx","../src/dragdrop/Palette.tsx"],"names":["useState","useEffect","jsx"],"mappings":"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","file":"chunk-WPLAANH4.js","sourcesContent":["import { type ReactPortal, useEffect, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { PaletteItem, Point } from '../instrument/types';\n\nexport interface DragGhostProps {\n item: PaletteItem;\n screenPos: Point;\n}\n\nexport function DragGhost({ item, screenPos }: DragGhostProps): ReactPortal | null {\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n if (!mounted || typeof document === 'undefined') return null;\n const style = {\n left: `${screenPos.x + 12}px`,\n top: `${screenPos.y + 12}px`,\n };\n return createPortal(\n <div className=\"lk-drag-ghost\" style={style}>\n <span className=\"lk-drag-ghost__label\">{item.label}</span>\n </div>,\n document.body,\n );\n}\n","import { openPointerSession, type PointerSession, useVisibleRaf } from '@weasel-js/core';\nimport {\n type PointerEvent as ReactPointerEvent,\n type RefObject,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport { screenToWorld } from '../canvas/canvasCoords';\nimport { resolveFrame, type WorldSpec } from '../canvas/worldSpec';\nimport type {\n DragDropCapability,\n DragFeedback,\n PaletteItem,\n Point,\n ViewTransform,\n} from '../instrument/types';\nimport { DragGhost } from './DragGhost';\n\nexport interface DragState {\n item: PaletteItem;\n screenPos: Point;\n feedback: DragFeedback | null;\n}\n\nexport interface UseDragDropArgs<TS, TC> {\n capability: DragDropCapability<TS, TC>;\n canvasContainerRef: RefObject<HTMLElement | null>;\n view: ViewTransform;\n /** The instrument's coordinate system, resolved against the container each\n * time a screen point is converted — the container is what knows its size. */\n worldSpec?: WorldSpec;\n state: TS;\n config: TC;\n setState: (next: TS | ((prev: TS) => TS)) => void;\n emit: (event: string) => void;\n}\n\nexport interface UseDragDropResult {\n drag: DragState | null;\n startDrag: (item: PaletteItem, e: ReactPointerEvent) => void;\n}\n\nexport function useDragDrop<TS, TC>({\n capability,\n canvasContainerRef,\n view,\n state,\n config,\n setState,\n emit,\n worldSpec,\n}: UseDragDropArgs<TS, TC>): UseDragDropResult {\n const [drag, setDrag] = useState<DragState | null>(null);\n const dragRef = useRef<DragState | null>(null);\n dragRef.current = drag;\n /** The move the next frame will resolve. Doubles as the throttle's flag:\n * non-null means a frame is already queued. */\n const pendingPos = useRef<Point | null>(null);\n const sessionRef = useRef<PointerSession | null>(null);\n\n /** A session keeps the closure it was opened with, so everything the drop\n * reads has to come from here rather than from that closure. */\n const liveRef = useRef({ capability, state, config, setState, emit, view, worldSpec });\n liveRef.current = { capability, state, config, setState, emit, view, worldSpec };\n\n const isOverCanvas = useCallback(\n (screenPos: Point): boolean => {\n const el = canvasContainerRef.current;\n if (!el) return false;\n const r = el.getBoundingClientRect();\n return (\n screenPos.x >= r.left &&\n screenPos.x <= r.right &&\n screenPos.y >= r.top &&\n screenPos.y <= r.bottom\n );\n },\n [canvasContainerRef],\n );\n\n const screenToWorldFromContainer = useCallback(\n (screenPos: Point): Point | null => {\n const el = canvasContainerRef.current;\n if (!el) return null;\n const r = el.getBoundingClientRect();\n const frame = resolveFrame(liveRef.current.worldSpec, { width: r.width, height: r.height });\n return screenToWorld(\n { x: screenPos.x - r.left, y: screenPos.y - r.top },\n liveRef.current.view,\n frame,\n );\n },\n [canvasContainerRef],\n );\n\n const frameLoop = useVisibleRaf(() => {\n const screenPos = pendingPos.current;\n pendingPos.current = null;\n const active = dragRef.current;\n if (!active || !screenPos) return;\n const live = liveRef.current;\n let feedback: DragFeedback | null = null;\n if (live.capability.onDragOver && isOverCanvas(screenPos)) {\n const world = screenToWorldFromContainer(screenPos);\n if (world) feedback = live.capability.onDragOver(world, active.item, live.state, live.config);\n }\n setDrag({ ...active, screenPos, feedback });\n });\n\n const clearDrag = useCallback(() => {\n pendingPos.current = null;\n frameLoop.cancel();\n sessionRef.current = null;\n dragRef.current = null;\n setDrag(null);\n }, [frameLoop]);\n\n const startDrag = useCallback(\n (item: PaletteItem, e: ReactPointerEvent) => {\n if (sessionRef.current) return;\n const next: DragState = { item, screenPos: { x: e.clientX, y: e.clientY }, feedback: null };\n dragRef.current = next;\n setDrag(next);\n sessionRef.current = openPointerSession(e.currentTarget, e, {\n onMove: (ev) => {\n const screenPos = { x: ev.clientX, y: ev.clientY };\n const current = dragRef.current;\n if (!current) return;\n if (pendingPos.current !== null) {\n // A frame is already queued: keep the ghost under the cursor and\n // let that frame probe the drop target from the newest position.\n pendingPos.current = screenPos;\n dragRef.current = { ...current, screenPos };\n setDrag(dragRef.current);\n return;\n }\n pendingPos.current = screenPos;\n frameLoop.request();\n },\n onEnd: (ev) => {\n const screenPos = { x: ev.clientX, y: ev.clientY };\n const active = dragRef.current;\n clearDrag();\n if (!active || !isOverCanvas(screenPos)) return;\n const world = screenToWorldFromContainer(screenPos);\n if (!world) return;\n const live = liveRef.current;\n live.setState(live.capability.onDrop(world, active.item, live.state, live.config));\n live.emit('canvas.itemAdded');\n },\n // An interrupted gesture never named a destination, so it drops nothing.\n onCancel: clearDrag,\n });\n },\n [clearDrag, frameLoop, isOverCanvas, screenToWorldFromContainer],\n );\n\n useEffect(() => () => sessionRef.current?.cancel(), []);\n\n return { drag, startDrag };\n}\n\nexport interface DragOverlayProps {\n drag: DragState | null;\n}\n\nexport function DragOverlay({ drag }: DragOverlayProps) {\n if (!drag) return null;\n return <DragGhost item={drag.item} screenPos={drag.screenPos} />;\n}\n","import type { PointerEvent as ReactPointerEvent } from 'react';\nimport type { PaletteItem } from '../instrument/types';\n\nexport interface PaletteProps {\n items: PaletteItem[];\n onDragStart: (item: PaletteItem, e: ReactPointerEvent) => void;\n className?: string;\n}\n\nexport function Palette({ items, onDragStart, className }: PaletteProps) {\n return (\n <div className={className ? `lk-palette ${className}` : 'lk-palette'}>\n {items.map((item) => (\n <button\n type=\"button\"\n key={item.id}\n className=\"lk-palette__item\"\n onPointerDown={(e) => {\n e.preventDefault();\n onDragStart(item, e);\n }}\n >\n <span className=\"lk-palette__label\">{item.label}</span>\n </button>\n ))}\n </div>\n );\n}\n"]}
@@ -1,18 +1,11 @@
1
- import { v3 } from './chunk-WUJXJC5O.js';
2
- import { jsx } from 'react/jsx-runtime';
1
+ import { isPlainObject } from '@weasel-js/core';
3
2
 
4
3
  // src/config/auto.ts
5
4
  var auto = /* @__PURE__ */ Symbol("weasel.auto");
6
5
  function isAuto(value) {
7
6
  return value === auto;
8
7
  }
9
-
10
- // src/config/path.ts
11
- function isRecord(value) {
12
- if (typeof value !== "object" || value === null) return false;
13
- const proto = Object.getPrototypeOf(value);
14
- return proto === Object.prototype || proto === null;
15
- }
8
+ var isRecord = isPlainObject;
16
9
  function valueAtPath(config, path) {
17
10
  let cur = config;
18
11
  for (const segment of path.split(".")) {
@@ -145,32 +138,7 @@ function applyConfigWrite(config, autoPaths, path, value) {
145
138
  autoPaths: autoPaths.includes(path) ? autoPaths.filter((p) => p !== path) : autoPaths
146
139
  };
147
140
  }
148
- function summarizeValue(value) {
149
- if (Array.isArray(value)) return value.length === 0 ? "None" : value.join(", ");
150
- if (value === void 0 || value === null || value === "") return "None";
151
- return typeof value === "object" ? JSON.stringify(value) : String(value);
152
- }
153
- function inDialog(body, opts = {}) {
154
- const summary = opts.summary ?? summarizeValue;
155
- return (ctx) => {
156
- const { pref, value, auto: auto2, setAuto } = ctx;
157
- const manual = pref.manual === true;
158
- return /* @__PURE__ */ jsx(
159
- v3,
160
- {
161
- label: pref.name,
162
- description: pref.description,
163
- summary: summary(value),
164
- title: opts.title,
165
- readout: auto2 ? "auto" : void 0,
166
- auto: auto2,
167
- onAutoChange: manual ? void 0 : setAuto,
168
- children: () => body(ctx)
169
- }
170
- );
171
- };
172
- }
173
141
 
174
- export { applyConfigWrite, auto, autoPathsOf, fillConfigDefaults, hasConfigPath, inDialog, isAuto, isRecord, resolveAutoConfig, schemaNodeAtPath, summarizeValue, valueAtPath, withValueAtPath };
175
- //# sourceMappingURL=chunk-AWH3SRPW.js.map
176
- //# sourceMappingURL=chunk-AWH3SRPW.js.map
142
+ export { applyConfigWrite, auto, autoPathsOf, fillConfigDefaults, hasConfigPath, isAuto, isRecord, resolveAutoConfig, schemaNodeAtPath, valueAtPath, withValueAtPath };
143
+ //# sourceMappingURL=chunk-Y23G2VZL.js.map
144
+ //# sourceMappingURL=chunk-Y23G2VZL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/auto.ts","../src/config/path.ts","../src/config/autoConfig.ts"],"names":[],"mappings":"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","file":"chunk-Y23G2VZL.js","sourcesContent":["/**\n * Written at a config path to mean \"do not pin this — let the instrument\n * decide\". Accepted by `setConfig`, by a trial's `configSeed`, and by\n * `.initial()` in a schema. It is normalized into the trial's set of unpinned\n * paths on the way into the store and is never itself stored, so a serialized\n * trial contains no sentinel and the last pinned value survives.\n */\nexport const auto: unique symbol = Symbol('weasel.auto');\n\n/** The type of the `auto` sentinel, for widening a value parameter. */\nexport type Auto = typeof auto;\n\nexport function isAuto(value: unknown): value is Auto {\n return value === auto;\n}\n","import { isPlainObject } from '@weasel-js/core';\nimport type { PrefGroup, PrefLeaf } from '@weasel-js/ui';\n\n/**\n * Reading and writing a config tree by dotted path. A schema's leaves address\n * their values this way, so a leaf nested under `f.group` and a flat one are\n * reached by exactly the same call.\n */\n\n/** A plain object a config tree can be walked into. An array, a `Date`, a\n * `Map` or a class instance is a leaf value: a walk that descended into one\n * would rebuild it as a bare `{}`. */\nexport const isRecord = isPlainObject;\n\n/** The value at a dotted path. `undefined` when a segment is missing or the\n * walk hits something that is not a record. */\nexport function valueAtPath(config: unknown, path: string): unknown {\n let cur = config;\n for (const segment of path.split('.')) {\n if (!isRecord(cur)) return undefined;\n cur = cur[segment];\n }\n return cur;\n}\n\n/** Whether a dotted path is actually present, which a `undefined` value is\n * not enough to tell you. */\nexport function hasConfigPath(config: unknown, path: string): boolean {\n let cur = config;\n for (const segment of path.split('.')) {\n if (!isRecord(cur) || !(segment in cur)) return false;\n cur = cur[segment];\n }\n return true;\n}\n\nfunction writeIn(node: unknown, segments: readonly string[], value: unknown): unknown {\n const [head, ...rest] = segments;\n if (head === undefined) return value;\n const base = isRecord(node) ? node : {};\n return { ...base, [head]: rest.length === 0 ? value : writeIn(base[head], rest, value) };\n}\n\n/** A copy of `config` with `value` written at a dotted path. Every record on\n * the way down is copied and the input is left alone; a missing — or\n * non-record — intermediate is replaced with the branch the path implies. */\nexport function withValueAtPath<T>(config: T, path: string, value: unknown): T {\n return writeIn(config, path.split('.'), value) as T;\n}\n\n/**\n * `stored` with every gap filled from `defaults`, down the whole tree.\n *\n * This is how a config written before its schema grew a branch still loads:\n * the branch arrives at its defaults instead of `undefined`, and a key the\n * defaults no longer mention is kept rather than dropped, so nothing is lost\n * to a schema change the author has not finished making.\n */\nexport function fillConfigDefaults<T>(stored: unknown, defaults: T): T {\n if (!isRecord(defaults)) return (stored === undefined ? defaults : stored) as T;\n const base = isRecord(stored) ? stored : {};\n const out: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(defaults)) {\n out[key] = fillConfigDefaults(base[key], value);\n }\n return out as T;\n}\n\n/** A node in a resolved schema tree, by dotted path. Structural rather than\n * `isPrefLeaf` so this stays free of a runtime import. */\nexport function schemaNodeAtPath(group: PrefGroup, path: string): PrefLeaf | PrefGroup | undefined {\n let node: PrefLeaf | PrefGroup | undefined = group;\n for (const segment of path.split('.')) {\n if (node === undefined || !('children' in node)) return undefined;\n node = node.children[segment];\n }\n return node;\n}\n","import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';\nimport { isAuto } from './auto';\nimport { isRecord, withValueAtPath } from './path';\nimport type { ResolvedConfig } from './types';\n\ntype Resolver = (config: Record<string, unknown>) => unknown;\n\n/** Reads a labkit-only extra off a resolved leaf. `PrefLeaf` has no field for\n * these; they ride as extra keys and survive the resolve walk. */\nfunction extra<T>(leaf: PrefLeaf, key: string): T | undefined {\n return (leaf as unknown as Record<string, T | undefined>)[key];\n}\n\n/** Every leaf in a resolved schema, in schema order, with its dotted path. */\nfunction* leaves(group: PrefGroup, at = ''): Generator<[string, PrefLeaf]> {\n for (const [key, child] of Object.entries(group.children)) {\n const path = at === '' ? key : `${at}.${key}`;\n if ('kind' in child) yield [path, child];\n else yield* leaves(child, path);\n }\n}\n\n/** The dotted paths a schema declares as starting auto — every leaf that said\n * `.initial(auto)`. This is what seeds a new trial's unpinned set. */\nexport function autoPathsOf(resolved: ResolvedConfig): string[] {\n const out: string[] = [];\n for (const [path, leaf] of leaves(resolved.group)) {\n if (extra<boolean>(leaf, 'unpinned')) out.push(path);\n }\n return out;\n}\n\n/**\n * The config an instrument reads: the stored one, with every unpinned path\n * either computed by its resolver or removed.\n *\n * Resolution is demand-driven — a resolver reading another unpinned path\n * forces that one first, whatever order the schema declared them in — so a\n * cycle is a real cycle and not an ordering accident.\n */\nexport function resolveAutoConfig<TC>(\n resolved: ResolvedConfig,\n config: TC,\n autoPaths: ReadonlySet<string>,\n): TC {\n if (autoPaths.size === 0) return config;\n\n const resolvers = new Map<string, Resolver | undefined>();\n for (const [path, leaf] of leaves(resolved.group)) {\n if (autoPaths.has(path)) resolvers.set(path, extra<Resolver>(leaf, 'autoResolve'));\n }\n if (resolvers.size === 0) return config;\n\n // Drop first, so a resolver reading a still-unresolved auto path sees\n // `undefined` rather than a stale pinned value it would silently believe.\n let out = config as unknown as Record<string, unknown>;\n for (const path of resolvers.keys()) out = dropAtPath(out, path);\n\n const inFlight = new Set<string>();\n const done = new Set<string>();\n\n const need = (path: string): void => {\n if (done.has(path)) return;\n if (inFlight.has(path)) {\n throw new Error(`[labkit] auto resolver cycle at \"${path}\"`);\n }\n const resolve = resolvers.get(path);\n if (!resolve) {\n done.add(path);\n return;\n }\n inFlight.add(path);\n try {\n // Read `out` only after the resolver has run: it forces its own\n // dependencies, and each of those replaced `out`.\n const value = resolve(demand(() => out, resolvers, need));\n out = withValueAtPath(out, path, value);\n } finally {\n inFlight.delete(path);\n }\n done.add(path);\n };\n\n for (const path of resolvers.keys()) need(path);\n return out as unknown as TC;\n}\n\n/**\n * The config a resolver reads. A top-level key that is itself unpinned is\n * forced before it is handed back, which is what makes declaration order\n * irrelevant and turns a genuine loop into a named error.\n *\n * Only top-level keys are trapped: a resolver reaching `c.grid.size` gets the\n * already-dropped branch, and a nested auto path it depends on resolves by\n * schema order alone.\n */\nfunction demand(\n read: () => Record<string, unknown>,\n resolvers: ReadonlyMap<string, Resolver | undefined>,\n need: (path: string) => void,\n): Record<string, unknown> {\n return new Proxy(read(), {\n get(_target, key) {\n if (typeof key === 'string' && resolvers.has(key)) need(key);\n return Reflect.get(read(), key);\n },\n has(_target, key) {\n return Reflect.has(read(), key);\n },\n ownKeys() {\n return Reflect.ownKeys(read());\n },\n getOwnPropertyDescriptor(_target, key) {\n return Reflect.getOwnPropertyDescriptor(read(), key);\n },\n });\n}\n\n/** A copy of `config` with the dotted path removed, copying every record on\n * the way down. A path that is not there comes back unchanged. */\nfunction dropAtPath(config: Record<string, unknown>, path: string): Record<string, unknown> {\n const [head, ...rest] = path.split('.');\n if (head === undefined || !(head in config)) return config;\n if (rest.length === 0) {\n const { [head]: _dropped, ...kept } = config;\n return kept;\n }\n const child = config[head];\n if (!isRecord(child)) return config;\n return { ...config, [head]: dropAtPath(child, rest.join('.')) };\n}\n\n/**\n * What writing `value` at `path` makes of a trial's raw config and its set of\n * unpinned paths. `auto` pins nothing and joins the set; any other value is\n * written and leaves it. Both the store and the chrome go through this, so\n * they cannot disagree about what a write meant.\n */\nexport function applyConfigWrite<TC>(\n config: TC,\n autoPaths: readonly string[],\n path: string,\n value: unknown,\n): { config: TC; autoPaths: readonly string[] } {\n if (isAuto(value)) {\n if (autoPaths.includes(path)) return { config, autoPaths };\n return { config, autoPaths: [...autoPaths, path] };\n }\n return {\n config: withValueAtPath(config, path, value),\n autoPaths: autoPaths.includes(path) ? autoPaths.filter((p) => p !== path) : autoPaths,\n };\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { newId, emptyUndoStack, deserializeTrials, openRecords, labPrefix, documentOfRecords, recordsOfDocument, trialRecord, trialValuesPrefix, saveRecord, LAYOUT_RECORD, UNDOCK_RECORD, META_RECORD, parseRecordName, compareTrialOrder, serializeTrials, labStorageKey } from './chunk-RWNDDKOH.js';
2
- import { resolveConfigSchema, useResolvedConfig } from './chunk-BVL7Y574.js';
3
- import { applyConfigWrite, fillConfigDefaults } from './chunk-AWH3SRPW.js';
2
+ import { resolveConfigSchema, useResolvedConfig } from './chunk-FZ7YFQN7.js';
3
+ import { applyConfigWrite, fillConfigDefaults } from './chunk-Y23G2VZL.js';
4
4
  import { LabStoreContext, TrialIdContext } from './chunk-2YILQ7Y5.js';
5
5
  import { createStore } from 'zustand/vanilla';
6
6
  import { useContext } from 'react';
@@ -733,5 +733,5 @@ function useTrialState() {
733
733
  }
734
734
 
735
735
  export { CURRENT_DOCUMENT_VERSION, createLabStore, dockPanel, labDocumentKey, openLabStore, panelKey, quarantineKey, undockPanel, unruledConfigSchema, useTrialState };
736
- //# sourceMappingURL=chunk-MOLIUNVH.js.map
737
- //# sourceMappingURL=chunk-MOLIUNVH.js.map
736
+ //# sourceMappingURL=chunk-YT75VYRD.js.map
737
+ //# sourceMappingURL=chunk-YT75VYRD.js.map