@weasel-js/labkit 1.4.4 → 1.5.1

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 (278) hide show
  1. package/README.md +54 -6
  2. package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
  3. package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
  4. package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
  5. package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
  6. package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
  7. package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
  8. package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
  9. package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
  10. package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
  11. package/dist/canvas/index.d.ts +18 -129
  12. package/dist/canvas/index.js +4 -2
  13. package/dist/chrome/index.d.ts +5 -93
  14. package/dist/chrome/index.js +10 -6
  15. package/dist/chunk-7PFH3CNM.js +150 -0
  16. package/dist/chunk-7PFH3CNM.js.map +1 -0
  17. package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
  18. package/dist/chunk-CEIUBPPW.js.map +1 -0
  19. package/dist/chunk-EH7UEQV7.js +590 -0
  20. package/dist/chunk-EH7UEQV7.js.map +1 -0
  21. package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
  22. package/dist/chunk-EHGVROVV.js.map +1 -0
  23. package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
  24. package/dist/chunk-ERFMC6WU.js.map +1 -0
  25. package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
  26. package/dist/chunk-H6ZAOWNE.js.map +1 -0
  27. package/dist/{chunk-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
  28. package/dist/chunk-IHUVQ3TX.js.map +1 -0
  29. package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
  30. package/dist/chunk-IJJUK4FU.js.map +1 -0
  31. package/dist/chunk-IXTQI7JX.js +737 -0
  32. package/dist/chunk-IXTQI7JX.js.map +1 -0
  33. package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
  34. package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
  35. package/dist/chunk-PHK4W562.js +72 -0
  36. package/dist/chunk-PHK4W562.js.map +1 -0
  37. package/dist/chunk-PJ7WEQBJ.js +18 -0
  38. package/dist/chunk-PJ7WEQBJ.js.map +1 -0
  39. package/dist/chunk-RWNDDKOH.js +682 -0
  40. package/dist/chunk-RWNDDKOH.js.map +1 -0
  41. package/dist/chunk-T3EC2DXD.js +12628 -0
  42. package/dist/chunk-T3EC2DXD.js.map +1 -0
  43. package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
  44. package/dist/chunk-UN4U4P63.js.map +1 -0
  45. package/dist/chunk-WIG6XHJ7.js +35 -0
  46. package/dist/chunk-WIG6XHJ7.js.map +1 -0
  47. package/dist/chunk-Z76KEZAX.js +15 -0
  48. package/dist/chunk-Z76KEZAX.js.map +1 -0
  49. package/dist/chunk-ZDHY47EV.js +363 -0
  50. package/dist/chunk-ZDHY47EV.js.map +1 -0
  51. package/dist/config/index.d.ts +276 -0
  52. package/dist/config/index.js +8 -0
  53. package/dist/config/index.js.map +1 -0
  54. package/dist/controls/index.d.ts +3 -4
  55. package/dist/controls/index.js +4 -3
  56. package/dist/dragdrop/index.d.ts +3 -5
  57. package/dist/index.d.ts +266 -413
  58. package/dist/index.js +1775 -2251
  59. package/dist/index.js.map +1 -1
  60. package/dist/layers/index.d.ts +4 -6
  61. package/dist/layers/index.js +2 -2
  62. package/dist/loupe/index.d.ts +69 -20
  63. package/dist/loupe/index.js +2 -2
  64. package/dist/passthrough/weasel-ui.d.ts +175 -309
  65. package/dist/passthrough/weasel-ui.js +1 -1
  66. package/dist/primitives/index.d.ts +55 -6
  67. package/dist/primitives/index.js +6 -4
  68. package/dist/state/index.d.ts +13 -8
  69. package/dist/state/index.js +18 -13
  70. package/dist/state/index.js.map +1 -1
  71. package/dist/styles.css +223 -76
  72. package/dist/surface/index.d.ts +44 -7
  73. package/dist/surface/index.js +2 -1
  74. package/dist/ui/layers/index.js +1 -1
  75. package/dist/undo/index.d.ts +3 -5
  76. package/package.json +15 -8
  77. package/src/annotations/AnnotationOverlay.tsx +56 -5
  78. package/src/annotations/Annotations.less +4 -0
  79. package/src/annotations/Annotations.overlay.test.tsx +105 -6
  80. package/src/annotations/drawOne.test.ts +1 -1
  81. package/src/annotations/drawOne.ts +2 -2
  82. package/src/annotations/paint.test.ts +2 -2
  83. package/src/annotations/paint.ts +1 -1
  84. package/src/annotations/preload.ts +6 -0
  85. package/src/annotations/store.test.ts +18 -0
  86. package/src/annotations/store.ts +5 -4
  87. package/src/annotations/svgNodes.test.ts +2 -2
  88. package/src/annotations/svgNodes.ts +1 -0
  89. package/src/annotations/toolMap.test.ts +9 -5
  90. package/src/annotations/toolMap.ts +6 -3
  91. package/src/annotations/types.ts +6 -3
  92. package/src/canvas/CameraWheelContext.ts +12 -0
  93. package/src/canvas/CanvasStack.stories.tsx +1 -1
  94. package/src/canvas/CanvasStack.test.tsx +25 -0
  95. package/src/canvas/CanvasStack.tsx +31 -4
  96. package/src/canvas/Stage.less +18 -0
  97. package/src/canvas/Stage.test.tsx +186 -0
  98. package/src/canvas/Stage.tsx +134 -0
  99. package/src/canvas/camera.test.ts +17 -2
  100. package/src/canvas/camera.ts +9 -9
  101. package/src/canvas/index.ts +4 -0
  102. package/src/canvas/usePanZoom.test.ts +28 -0
  103. package/src/canvas/usePanZoom.ts +21 -9
  104. package/src/chrome/ChromeRegions.stories.tsx +1 -4
  105. package/src/chrome/LabChrome.tsx +130 -0
  106. package/src/chrome/builtins.test.ts +1 -0
  107. package/src/chrome/builtins.tsx +21 -31
  108. package/src/chrome/index.ts +16 -0
  109. package/src/chrome/labRegions.test.tsx +208 -0
  110. package/src/chrome/labTypes.ts +50 -0
  111. package/src/chrome/merge.ts +7 -9
  112. package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
  113. package/src/chrome/regions/PaletteRegion.tsx +16 -9
  114. package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
  115. package/src/chrome/regions/SidebarRegion.tsx +25 -13
  116. package/src/chrome/regions/StatusRegion.tsx +15 -8
  117. package/src/chrome/regions/ToolbarRegion.tsx +34 -16
  118. package/src/chrome/types.ts +50 -20
  119. package/src/config/auto.test.ts +12 -0
  120. package/src/config/auto.ts +15 -0
  121. package/src/config/autoConfig.test.ts +107 -0
  122. package/src/config/autoConfig.ts +153 -0
  123. package/src/config/builder.test.ts +42 -0
  124. package/src/config/builder.ts +68 -17
  125. package/src/config/declarationEmit.fixture.ts +11 -0
  126. package/src/config/declarationEmit.test.ts +51 -0
  127. package/src/config/entry.test.ts +46 -0
  128. package/src/config/index.ts +16 -1
  129. package/src/config/nodeClasses.test.ts +18 -0
  130. package/src/config/path.ts +7 -2
  131. package/src/config/resolve.test.ts +25 -0
  132. package/src/config/resolve.ts +17 -1
  133. package/src/config/types.ts +34 -2
  134. package/src/config/useResolvedConfig.ts +26 -0
  135. package/src/controls/AutoControls.stories.tsx +88 -0
  136. package/src/controls/ControlPanel.stories.tsx +42 -1
  137. package/src/controls/ControlPanel.test.tsx +530 -4
  138. package/src/controls/ControlPanel.tsx +562 -82
  139. package/src/fake-indexeddb-auto.d.ts +5 -0
  140. package/src/index.test.ts +7 -0
  141. package/src/index.ts +66 -5
  142. package/src/instrument/serializers.test.ts +22 -0
  143. package/src/instrument/serializers.ts +40 -1
  144. package/src/instrument/types.ts +25 -1
  145. package/src/lab/Lab.chrome.test.tsx +16 -0
  146. package/src/lab/Lab.less +4 -13
  147. package/src/lab/Lab.persist.test.tsx +147 -0
  148. package/src/lab/Lab.stories.tsx +1 -3
  149. package/src/lab/Lab.surface.test.tsx +71 -3
  150. package/src/lab/Lab.test.tsx +168 -16
  151. package/src/lab/Lab.tsx +434 -129
  152. package/src/lab/LabContext.ts +8 -0
  153. package/src/lab/LabFit.stories.less +18 -0
  154. package/src/lab/LabFit.stories.tsx +219 -0
  155. package/src/lab/LabFullChrome.stories.tsx +1 -4
  156. package/src/lab/LabHeader.test.tsx +40 -9
  157. package/src/lab/LabHeader.tsx +7 -14
  158. package/src/lab/LabPalette.tsx +13 -26
  159. package/src/lab/LabRoot.test.tsx +56 -0
  160. package/src/lab/LabRoot.tsx +47 -0
  161. package/src/lab/LabShell.less +73 -15
  162. package/src/lab/LabShell.stories.tsx +50 -1
  163. package/src/lab/LabShell.tsx +18 -27
  164. package/src/lab/LabSwitcher.less +18 -11
  165. package/src/lab/LabSwitcher.test.tsx +14 -9
  166. package/src/lab/LabSwitcher.tsx +8 -4
  167. package/src/lab/Workspace.less +1 -1
  168. package/src/lab/Workspace.stories.tsx +1 -1
  169. package/src/lab/Workspace.surface.test.tsx +1 -0
  170. package/src/lab/fitCheck.test.ts +57 -0
  171. package/src/lab/fitCheck.ts +125 -0
  172. package/src/lab/index.ts +2 -0
  173. package/src/layers/LayerList.stories.tsx +1 -1
  174. package/src/loupe/AGENTS.md +17 -5
  175. package/src/loupe/Loupe.less +1 -1
  176. package/src/loupe/LoupeGestures.tsx +67 -0
  177. package/src/loupe/TrialLoupe.tsx +43 -28
  178. package/src/loupe/canvasLens.ts +1 -1
  179. package/src/loupe/index.ts +4 -0
  180. package/src/loupe/loupeActions.ts +98 -0
  181. package/src/loupe/types.ts +1 -1
  182. package/src/loupe/useLoupe.test.tsx +60 -3
  183. package/src/loupe/useLoupe.ts +29 -49
  184. package/src/passthrough/weasel-ui.ts +9 -0
  185. package/src/primitives/FloatingPanel.stories.tsx +14 -2
  186. package/src/primitives/FloatingPanel.test.tsx +47 -16
  187. package/src/primitives/FloatingPanel.tsx +11 -25
  188. package/src/primitives/FpsMeter.stories.tsx +1 -1
  189. package/src/primitives/JobProgress.stories.tsx +1 -1
  190. package/src/primitives/Legend.stories.tsx +1 -1
  191. package/src/primitives/ScaleIndicator.stories.tsx +1 -1
  192. package/src/primitives/Sidebar.less +7 -0
  193. package/src/primitives/Sidebar.stories.tsx +1 -1
  194. package/src/primitives/Split.less +14 -0
  195. package/src/primitives/Split.test.tsx +133 -0
  196. package/src/primitives/Split.tsx +175 -0
  197. package/src/primitives/StatusBar.stories.tsx +1 -1
  198. package/src/primitives/Toolbar.stories.tsx +1 -1
  199. package/src/primitives/ZoomControl.tsx +5 -2
  200. package/src/primitives/index.ts +2 -0
  201. package/src/specimen/Specimen.less +95 -0
  202. package/src/specimen/Specimen.stories.tsx +13 -0
  203. package/src/specimen/Specimen.test.tsx +13 -0
  204. package/src/specimen/Specimen.tsx +888 -0
  205. package/src/state/Persistence.stories.tsx +75 -0
  206. package/src/state/Persistence.tsx +33 -0
  207. package/src/state/SingletonExperiment.test.tsx +47 -53
  208. package/src/state/SingletonExperiment.tsx +24 -13
  209. package/src/state/adapterContract.ts +116 -0
  210. package/src/state/adapters.test.ts +113 -61
  211. package/src/state/adapters.ts +328 -69
  212. package/src/state/document.test.ts +71 -96
  213. package/src/state/document.ts +66 -52
  214. package/src/state/helpers.test.ts +1 -1
  215. package/src/state/index.ts +8 -0
  216. package/src/state/labRecords.test.ts +105 -0
  217. package/src/state/labRecords.ts +160 -0
  218. package/src/state/openLabStore.test.ts +658 -0
  219. package/src/state/openLabStore.ts +403 -0
  220. package/src/state/records.test.ts +234 -0
  221. package/src/state/records.ts +219 -0
  222. package/src/state/store.test.ts +169 -635
  223. package/src/state/store.ts +146 -163
  224. package/src/state/toolSlot.test.ts +1 -1
  225. package/src/state/types.ts +65 -17
  226. package/src/state/useOpenOnce.ts +68 -0
  227. package/src/state/usePersistedState.test.tsx +125 -0
  228. package/src/state/usePersistedState.ts +66 -0
  229. package/src/state/useTrialState.test.tsx +87 -3
  230. package/src/state/useTrialState.ts +9 -1
  231. package/src/state/view.test.ts +49 -15
  232. package/src/state/view.ts +18 -1
  233. package/src/styles.less +3 -0
  234. package/src/surface/AGENTS.md +39 -7
  235. package/src/surface/SurfaceContext.ts +26 -3
  236. package/src/surface/index.ts +2 -0
  237. package/src/surface/useSurfaceTile.test.tsx +1 -0
  238. package/src/surface/useSurfaceTile.ts +6 -5
  239. package/src/surface/useTiledSurface.test.tsx +231 -1
  240. package/src/surface/useTiledSurface.ts +76 -15
  241. package/src/theme/Interstellar.stories.tsx +9 -9
  242. package/src/theme/interstellar.test.ts +17 -6
  243. package/src/theme/interstellar.theme.json +180 -0
  244. package/src/theme/interstellar.ts +8 -5
  245. package/src/tools/labTool.ts +11 -0
  246. package/src/trial/Trial.annotations.persist.test.tsx +100 -58
  247. package/src/trial/Trial.annotations.test.tsx +59 -7
  248. package/src/trial/Trial.canvas.test.tsx +32 -0
  249. package/src/trial/Trial.config.test.tsx +75 -2
  250. package/src/trial/Trial.job.test.tsx +3 -3
  251. package/src/trial/Trial.less +5 -26
  252. package/src/trial/Trial.stories.tsx +5 -3
  253. package/src/trial/Trial.test.tsx +31 -4
  254. package/src/trial/Trial.trialId.test.tsx +23 -1
  255. package/src/trial/Trial.tsx +139 -52
  256. package/src/trial/TrialBody.tsx +17 -141
  257. package/src/trial/TrialChrome.tsx +24 -7
  258. package/src/trial/trialOps.test.ts +101 -1
  259. package/src/trial/trialOps.ts +67 -7
  260. package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
  261. package/dist/_dts/types-x92Kfeme.d.ts +0 -62
  262. package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
  263. package/dist/chunk-2P6PP5N4.js +0 -553
  264. package/dist/chunk-2P6PP5N4.js.map +0 -1
  265. package/dist/chunk-64ZCN3DA.js.map +0 -1
  266. package/dist/chunk-CTRKTLYZ.js.map +0 -1
  267. package/dist/chunk-E44SU6XS.js +0 -50
  268. package/dist/chunk-E44SU6XS.js.map +0 -1
  269. package/dist/chunk-FQMJUVHJ.js.map +0 -1
  270. package/dist/chunk-I6JCVE24.js.map +0 -1
  271. package/dist/chunk-MOM3GOVY.js.map +0 -1
  272. package/dist/chunk-SMHP6XZ4.js.map +0 -1
  273. package/dist/chunk-TJ7QY3OC.js.map +0 -1
  274. package/dist/chunk-U3IYHIAE.js +0 -343
  275. package/dist/chunk-U3IYHIAE.js.map +0 -1
  276. package/dist/chunk-W3ECWC2K.js +0 -7335
  277. package/dist/chunk-W3ECWC2K.js.map +0 -1
  278. package/src/theme/interstellar.tokens.json +0 -61
@@ -1,9 +1,9 @@
1
- import { centerOn, zoomAt } from './chunk-CTRKTLYZ.js';
1
+ import { centerOn, zoomAt } from './chunk-H6ZAOWNE.js';
2
2
  import { resolveFrame, screenToWorld, DEFAULT_FRAME } from './chunk-2AYGEN57.js';
3
3
  import { CanvasStackContext } from './chunk-CQLQPQ4P.js';
4
- import { useVisibleRaf } from '@weasel-js/core';
4
+ import { useVisibleRaf, useActionsRegistry, WeaselProvider, useGestureDispatcher, normalizeZoom } from '@weasel-js/core';
5
5
  import { useRef, useEffect, useReducer, useCallback, useContext, useMemo, useState } from 'react';
6
- import { jsx } from 'react/jsx-runtime';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  // src/loupe/canvasLens.ts
9
9
  function lensCamera(aim, outer, outerFrame, factor, diameter, worldSpec) {
@@ -157,6 +157,95 @@ function LoupeBubble({ aim, diameter, hostRef, children }) {
157
157
  };
158
158
  return /* @__PURE__ */ jsx("div", { ref: hostRef, className: "lk-loupe", style, "aria-hidden": "true", children });
159
159
  }
160
+
161
+ // src/loupe/loupeActions.ts
162
+ var LOUPE_PEEK_ID = "loupe.peek";
163
+ var LOUPE_MAGNIFY_ID = "loupe.magnify";
164
+ var WHEEL_RATE = 2e-3;
165
+ function createLoupeActions(input, peekKey) {
166
+ const magnify = {
167
+ id: LOUPE_MAGNIFY_ID,
168
+ label: "Magnify (loupe)",
169
+ group: "loupe",
170
+ // Every modifier optional: the lens claims the wheel whenever it is up,
171
+ // which is what the hand-rolled capture-phase listener did. A bare spec
172
+ // would forbid modifiers and hand Cmd+wheel back to the lab mid-peek.
173
+ defaultBinding: [
174
+ {
175
+ spec: {
176
+ kind: "wheel",
177
+ mods: { alt: "optional", ctrl: "optional", meta: "optional", shift: "optional" }
178
+ },
179
+ opts: {}
180
+ }
181
+ ],
182
+ enabled: () => input.shown() ? true : "not-applicable",
183
+ invoker: {
184
+ timing: "immediate",
185
+ run(_deps, params) {
186
+ input.magnifyBy(params?.deltaY ?? 0);
187
+ }
188
+ }
189
+ };
190
+ if (peekKey === null) return [magnify];
191
+ const peek = {
192
+ id: LOUPE_PEEK_ID,
193
+ label: "Peek (loupe)",
194
+ group: "loupe",
195
+ // Hotkey scope: peeking must beat whatever the lab's active tool binds to
196
+ // the same key, the way every other held-key trigger does.
197
+ scope: "hotkey",
198
+ // Every modifier optional because the peek key may BE one: the keydown
199
+ // that carries `Alt` — the default — also reports `altKey: true`, and the
200
+ // matcher forbids any modifier a spec does not name.
201
+ defaultBinding: [
202
+ {
203
+ spec: {
204
+ kind: "key-held",
205
+ key: peekKey,
206
+ mods: { alt: "optional", ctrl: "optional", meta: "optional", shift: "optional" }
207
+ },
208
+ opts: {}
209
+ }
210
+ ],
211
+ invoker: {
212
+ timing: "ongoing",
213
+ start() {
214
+ input.setPeeking(true);
215
+ return { onEnd: () => input.setPeeking(false) };
216
+ }
217
+ }
218
+ };
219
+ return [peek, magnify];
220
+ }
221
+ var NO_TOOLS = /* @__PURE__ */ new Map();
222
+ var CHANNELS = { pointer: false, contextMenu: false, ingest: false };
223
+ function LoupeDispatch({
224
+ hostRef,
225
+ input,
226
+ peekKey,
227
+ registry
228
+ }) {
229
+ const actions = useMemo(() => createLoupeActions(input, peekKey), [input, peekKey]);
230
+ useEffect(() => {
231
+ const offs = actions.map((a) => registry.register(a));
232
+ return () => {
233
+ for (const off of offs) off();
234
+ };
235
+ }, [registry, actions]);
236
+ useGestureDispatcher({
237
+ canvasRef: hostRef,
238
+ actions: registry,
239
+ toolsById: NO_TOOLS,
240
+ channels: CHANNELS
241
+ });
242
+ return null;
243
+ }
244
+ function LoupeGestures(props) {
245
+ const registry = useActionsRegistry();
246
+ if (!registry) return null;
247
+ return /* @__PURE__ */ jsx(LoupeDispatch, { ...props, registry });
248
+ }
160
249
  var ZERO = { width: 0, height: 0 };
161
250
  function useHostSize(ref) {
162
251
  const [size, setSize] = useState(ZERO);
@@ -177,8 +266,6 @@ function useHostSize(ref) {
177
266
  }, [ref]);
178
267
  return size;
179
268
  }
180
-
181
- // ../loupe/dist/index.js
182
269
  function loupeSourcePoint(p, rect, aim, factor) {
183
270
  return {
184
271
  x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,
@@ -188,25 +275,32 @@ function loupeSourcePoint(p, rect, aim, factor) {
188
275
  function createLoupeModel(opts) {
189
276
  const { surface } = opts;
190
277
  let mode = opts.mode ?? "vector";
191
- let factor = opts.factor ?? 8;
278
+ let factor = normalizeZoom(opts.factor ?? 8);
192
279
  let aim = { x: 0, y: 0 };
193
280
  let color = null;
194
281
  let disposed = false;
195
- const clamp = (n) => Math.min(
282
+ let aimUnsampled = false;
283
+ const clamp = (n) => normalizeZoom(Math.min(
196
284
  opts.maxFactor ?? Number.POSITIVE_INFINITY,
197
285
  Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)
198
- );
199
- const teardown = () => {
200
- if (disposed) return;
201
- disposed = true;
202
- opts.onDispose?.();
203
- };
286
+ ));
204
287
  const sampleColor = () => {
205
288
  const hex = surface.sample(aim);
206
289
  if (hex === null || hex === color) return;
207
290
  color = hex;
208
291
  opts.onColorChange?.(hex);
209
292
  };
293
+ const unsubscribeFrame = surface.subscribeFrame?.(() => {
294
+ if (!aimUnsampled) return;
295
+ aimUnsampled = false;
296
+ sampleColor();
297
+ });
298
+ const teardown = () => {
299
+ if (disposed) return;
300
+ disposed = true;
301
+ unsubscribeFrame?.();
302
+ opts.onDispose?.();
303
+ };
210
304
  return {
211
305
  get mode() {
212
306
  return mode;
@@ -239,7 +333,8 @@ function createLoupeModel(opts) {
239
333
  if (surface.hidden()) return;
240
334
  if (surface.covers(p)) return;
241
335
  aim = p;
242
- sampleColor();
336
+ if (unsubscribeFrame) aimUnsampled = true;
337
+ else sampleColor();
243
338
  surface.changed();
244
339
  },
245
340
  pick(p) {
@@ -258,7 +353,6 @@ function createLoupeModel(opts) {
258
353
  dispose: teardown
259
354
  };
260
355
  }
261
- var WHEEL_RATE = 2e-3;
262
356
  function useLoupe({ capability, hostRef, enabled, sample }) {
263
357
  const [, bump] = useReducer((n) => n + 1, 0);
264
358
  const overRef = useRef(false);
@@ -324,45 +418,28 @@ function useLoupe({ capability, hostRef, enabled, sample }) {
324
418
  overRef.current = false;
325
419
  bump();
326
420
  };
327
- const onWheel = (e) => {
328
- if (!shown()) return;
329
- e.preventDefault();
330
- e.stopPropagation();
331
- model.setFactor(model.factor * Math.exp(-e.deltaY * WHEEL_RATE));
332
- };
333
421
  host.addEventListener("pointermove", onPointerMove);
334
422
  host.addEventListener("pointerleave", onPointerLeave);
335
- host.addEventListener("wheel", onWheel, { capture: true, passive: false });
336
423
  return () => {
337
424
  host.removeEventListener("pointermove", onPointerMove);
338
425
  host.removeEventListener("pointerleave", onPointerLeave);
339
- host.removeEventListener("wheel", onWheel, { capture: true });
340
426
  };
341
427
  }, [hostRef, model, shown]);
342
- const peekKey = capability.peekKey;
343
- useEffect(() => {
344
- if (peekKey == null) return;
345
- const setPeeking = (next) => {
346
- if (peekingRef.current === next) return;
347
- peekingRef.current = next;
348
- bump();
349
- };
350
- const onKeyDown = (e) => {
351
- if (e.key === peekKey) setPeeking(true);
352
- };
353
- const onKeyUp = (e) => {
354
- if (e.key === peekKey) setPeeking(false);
355
- };
356
- const onBlur = () => setPeeking(false);
357
- window.addEventListener("keydown", onKeyDown);
358
- window.addEventListener("keyup", onKeyUp);
359
- window.addEventListener("blur", onBlur);
360
- return () => {
361
- window.removeEventListener("keydown", onKeyDown);
362
- window.removeEventListener("keyup", onKeyUp);
363
- window.removeEventListener("blur", onBlur);
428
+ const inputRef = useRef(null);
429
+ if (inputRef.current === null) {
430
+ inputRef.current = {
431
+ shown,
432
+ setPeeking: (on) => {
433
+ if (peekingRef.current === on) return;
434
+ peekingRef.current = on;
435
+ bump();
436
+ },
437
+ magnifyBy: (deltaY) => {
438
+ const m = modelRef.current;
439
+ if (m) m.setFactor(m.factor * Math.exp(-deltaY * WHEEL_RATE));
440
+ }
364
441
  };
365
- }, [peekKey]);
442
+ }
366
443
  return {
367
444
  visible: shown(),
368
445
  aim: model.aim,
@@ -371,7 +448,8 @@ function useLoupe({ capability, hostRef, enabled, sample }) {
371
448
  color: model.color,
372
449
  setMode: model.setMode,
373
450
  setFactor: model.setFactor,
374
- pick: model.pick
451
+ pick: model.pick,
452
+ input: inputRef.current
375
453
  };
376
454
  }
377
455
  var NO_HOST = { current: null };
@@ -397,34 +475,38 @@ function TrialLoupe({
397
475
  const loupe = useLoupe({ capability, hostRef: host, enabled, sample });
398
476
  const measured = useHostSize(host);
399
477
  const size = surface?.size ?? measured;
400
- if (!loupe.visible) return null;
401
- return /* @__PURE__ */ jsx(LoupeBubble, { aim: loupe.aim, diameter: capability.diameter, children: capability.render ? /* @__PURE__ */ jsx(
402
- DomLoupe,
403
- {
404
- aim: loupe.aim,
405
- factor: loupe.factor,
406
- mode: loupe.mode,
407
- diameter: capability.diameter,
408
- size,
409
- view,
410
- frame: stack?.frame,
411
- state,
412
- config,
413
- render: capability.render
414
- }
415
- ) : surface && stack ? /* @__PURE__ */ jsx(
416
- CanvasLoupe,
417
- {
418
- aim: loupe.aim,
419
- factor: loupe.factor,
420
- mode: loupe.mode,
421
- diameter: capability.diameter,
422
- surface,
423
- view: stack.view,
424
- frame: stack.frame,
425
- worldSpec
426
- }
427
- ) : null });
478
+ const gestures = /* @__PURE__ */ jsx(WeaselProvider, { isolate: true, children: /* @__PURE__ */ jsx(LoupeGestures, { hostRef: host, input: loupe.input, peekKey: capability.peekKey ?? null }) });
479
+ if (!loupe.visible) return gestures;
480
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
481
+ gestures,
482
+ /* @__PURE__ */ jsx(LoupeBubble, { aim: loupe.aim, diameter: capability.diameter, children: capability.render ? /* @__PURE__ */ jsx(
483
+ DomLoupe,
484
+ {
485
+ aim: loupe.aim,
486
+ factor: loupe.factor,
487
+ mode: loupe.mode,
488
+ diameter: capability.diameter,
489
+ size,
490
+ view,
491
+ frame: stack?.frame,
492
+ state,
493
+ config,
494
+ render: capability.render
495
+ }
496
+ ) : surface && stack ? /* @__PURE__ */ jsx(
497
+ CanvasLoupe,
498
+ {
499
+ aim: loupe.aim,
500
+ factor: loupe.factor,
501
+ mode: loupe.mode,
502
+ diameter: capability.diameter,
503
+ surface,
504
+ view: stack.view,
505
+ frame: stack.frame,
506
+ worldSpec
507
+ }
508
+ ) : null })
509
+ ] });
428
510
  }
429
511
 
430
512
  // src/loupe/types.ts
@@ -453,6 +535,6 @@ function resolveLoupe(declared) {
453
535
  };
454
536
  }
455
537
 
456
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LoupeBubble, TrialLoupe, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe };
457
- //# sourceMappingURL=chunk-TJ7QY3OC.js.map
458
- //# sourceMappingURL=chunk-TJ7QY3OC.js.map
538
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LOUPE_MAGNIFY_ID, LOUPE_PEEK_ID, LoupeBubble, LoupeGestures, TrialLoupe, createLoupeActions, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe };
539
+ //# sourceMappingURL=chunk-ERFMC6WU.js.map
540
+ //# sourceMappingURL=chunk-ERFMC6WU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/loupe/canvasLens.ts","../src/loupe/CanvasLoupe.tsx","../src/loupe/DomLoupe.tsx","../src/loupe/LoupeBubble.tsx","../src/loupe/loupeActions.ts","../src/loupe/LoupeGestures.tsx","../src/loupe/useHostSize.ts","../../loupe/src/geometry.ts","../../loupe/src/model.ts","../src/loupe/useLoupe.ts","../src/loupe/TrialLoupe.tsx","../src/loupe/types.ts"],"names":["jsx","useEffect","useRef","useMemo"],"mappings":"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","file":"chunk-ERFMC6WU.js","sourcesContent":["import type { LoupePoint } from '@weasel-js/loupe';\nimport { centerOn } from '../canvas/camera';\nimport { screenToWorld } from '../canvas/canvasCoords';\nimport type { CanvasLayerDescriptor } from '../canvas/useLayerScheduler';\nimport { DEFAULT_FRAME, resolveFrame, type WorldFrame, type WorldSpec } from '../canvas/worldSpec';\nimport type { ViewTransform } from '../instrument/types';\n\n/** The camera a round lens of `diameter` shows a magnified region through, and\n * the coordinate system it is read in — the instrument's own `WorldSpec`,\n * resolved against the lens rather than the stack. */\nexport interface LensCamera {\n view: ViewTransform;\n frame: WorldFrame;\n}\n\n/**\n * What a lens aimed at `aim` renders the stack's layers through.\n *\n * The lens is its own viewport, so the world spec resolves against its box: an\n * instrument centred on its viewport is centred in the lens too.\n */\nexport function lensCamera(\n aim: LoupePoint,\n outer: ViewTransform,\n outerFrame: WorldFrame,\n factor: number,\n diameter: number,\n worldSpec?: WorldSpec,\n): LensCamera {\n const size = { width: diameter, height: diameter };\n const frame = resolveFrame(worldSpec, size);\n const world = screenToWorld(aim, outer, outerFrame);\n return { view: centerOn(world, outer.zoom * factor, size, frame), frame };\n}\n\n/** A rectangle in a backing store's own device pixels. */\nexport interface SourceRect {\n sx: number;\n sy: number;\n sw: number;\n sh: number;\n}\n\n/**\n * The region a pixel-mode lens copies out of a presented canvas: the `diameter\n * / factor` CSS px around `aim`, in that canvas' backing-store pixels.\n */\nexport function lensSourceRect(\n aim: LoupePoint,\n factor: number,\n diameter: number,\n dpr: number,\n): SourceRect {\n const span = diameter / factor;\n return {\n sx: (aim.x - span / 2) * dpr,\n sy: (aim.y - span / 2) * dpr,\n sw: span * dpr,\n sh: span * dpr,\n };\n}\n\n/**\n * The color a stack presents at a point: the topmost visible layer with\n * anything opaque there, or `null` when every layer is transparent — which is\n * the honest answer for a point showing nothing but the workspace behind.\n */\nexport function sampleStack(\n layers: readonly CanvasLayerDescriptor[],\n canvases: Map<string, HTMLCanvasElement>,\n p: LoupePoint,\n dpr: number,\n): string | null {\n for (let i = layers.length - 1; i >= 0; i--) {\n const layer = layers[i];\n if (!layer.visible) continue;\n const canvas = canvases.get(layer.id);\n if (!canvas) continue;\n const ctx = canvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) continue;\n const x = Math.floor(p.x * dpr);\n const y = Math.floor(p.y * dpr);\n if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) continue;\n const [r, g, b, a] = ctx.getImageData(x, y, 1, 1).data;\n if (a === 0) continue;\n return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;\n }\n return null;\n}\n\n/** Redraw the stack into a lens' own canvas. `mode` decides whether the layers\n * are re-run at the lens camera or their presented pixels are enlarged. */\nexport function drawCanvasLens(\n ctx: CanvasRenderingContext2D,\n opts: {\n aim: LoupePoint;\n factor: number;\n diameter: number;\n dpr: number;\n mode: 'vector' | 'pixel';\n outer: ViewTransform;\n outerFrame?: WorldFrame;\n worldSpec?: WorldSpec;\n layers: readonly CanvasLayerDescriptor[];\n canvases: Map<string, HTMLCanvasElement>;\n },\n): void {\n const { aim, factor, diameter, dpr, mode, outer, layers, canvases } = opts;\n ctx.save();\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, diameter, diameter);\n\n if (mode === 'pixel') {\n const { sx, sy, sw, sh } = lensSourceRect(aim, factor, diameter, dpr);\n ctx.imageSmoothingEnabled = false;\n for (const layer of layers) {\n if (!layer.visible) continue;\n const canvas = canvases.get(layer.id);\n if (!canvas || canvas.width === 0 || canvas.height === 0) continue;\n ctx.drawImage(canvas, sx, sy, sw, sh, 0, 0, diameter, diameter);\n }\n ctx.restore();\n return;\n }\n\n const lens = lensCamera(\n aim,\n outer,\n opts.outerFrame ?? DEFAULT_FRAME,\n factor,\n diameter,\n opts.worldSpec,\n );\n for (const layer of layers) {\n if (!layer.visible) continue;\n ctx.save();\n layer.render(ctx, lens.view, lens.frame);\n ctx.restore();\n }\n ctx.restore();\n}\n","import { useVisibleRaf } from '@weasel-js/core';\nimport type { LoupeMode, LoupePoint } from '@weasel-js/loupe';\nimport { useEffect, useRef } from 'react';\nimport type { CanvasStackSurface } from '../canvas/CanvasStackContext';\nimport type { WorldFrame, WorldSpec } from '../canvas/worldSpec';\nimport type { ViewTransform } from '../instrument/types';\nimport { drawCanvasLens } from './canvasLens';\n\n/** Props for `<CanvasLoupe>`. */\nexport interface CanvasLoupeProps {\n aim: LoupePoint;\n factor: number;\n mode: LoupeMode;\n diameter: number;\n /** The stack being magnified: its layers, its pixels, and its measured box. */\n surface: CanvasStackSurface;\n view: ViewTransform;\n frame: WorldFrame;\n worldSpec?: WorldSpec;\n}\n\n/**\n * Paints a lens over a `<CanvasStack>` by re-drawing the stack's layers through\n * a zoomed camera — so the magnified content is as sharp as the original, at\n * any factor. `pixel` mode enlarges what the stack presented instead.\n */\nexport function CanvasLoupe({\n aim,\n factor,\n mode,\n diameter,\n surface,\n view,\n frame,\n worldSpec,\n}: CanvasLoupeProps) {\n const canvasRef = useRef<HTMLCanvasElement | null>(null);\n const dpr = surface.size.dpr;\n\n // Whatever the stack redraws every frame, the lens has to redraw too — an\n // instrument that animates from its own loop never re-renders this component.\n const args = { aim, factor, mode, diameter, dpr, view, frame, worldSpec, surface };\n const argsRef = useRef(args);\n argsRef.current = args;\n\n const loop = useVisibleRaf(\n () => {\n loop.request();\n const canvas = canvasRef.current;\n const ctx = canvas?.getContext('2d');\n const canvases = argsRef.current.surface.canvases.current;\n if (!ctx || !canvases) return;\n const a = argsRef.current;\n drawCanvasLens(ctx, {\n aim: a.aim,\n factor: a.factor,\n diameter: a.diameter,\n dpr: a.dpr,\n mode: a.mode,\n outer: a.view,\n outerFrame: a.frame,\n worldSpec: a.worldSpec,\n layers: a.surface.layers,\n canvases,\n });\n },\n { target: canvasRef },\n );\n\n useEffect(() => {\n loop.request();\n return () => loop.cancel();\n }, [loop]);\n\n return (\n <canvas\n ref={canvasRef}\n className=\"lk-loupe__canvas\"\n width={Math.max(1, Math.round(diameter * dpr))}\n height={Math.max(1, Math.round(diameter * dpr))}\n />\n );\n}\n","import type { LoupeMode, LoupePoint } from '@weasel-js/loupe';\nimport type { CSSProperties } from 'react';\nimport { zoomAt } from '../canvas/camera';\nimport type { ViewportSize, WorldFrame } from '../canvas/worldSpec';\nimport type { ViewTransform } from '../instrument/types';\nimport type { LoupeRenderArgs } from './types';\n\n/** Props for `<DomLoupe>`. */\nexport interface DomLoupeProps {\n aim: LoupePoint;\n factor: number;\n mode: LoupeMode;\n diameter: number;\n /** The host the instrument draws into, which the stage reproduces. */\n size: ViewportSize;\n /** The trial's own camera. */\n view: ViewTransform;\n frame?: WorldFrame;\n state: unknown;\n config: unknown;\n render: (args: LoupeRenderArgs) => React.ReactNode;\n}\n\n/**\n * Paints a lens over DOM content by asking the instrument to draw itself again\n * at a magnified camera.\n *\n * The stage is a full copy of the host, so the instrument's own layout still\n * holds; the camera is composed about the aimed point, which keeps that point\n * where it already was, and the stage is then slid by `diameter / 2 - aim` to\n * bring it to the middle of the lens.\n */\nexport function DomLoupe({\n aim,\n factor,\n mode,\n diameter,\n size,\n view,\n frame,\n state,\n config,\n render,\n}: DomLoupeProps) {\n const style: CSSProperties = {\n width: `${size.width}px`,\n height: `${size.height}px`,\n transform: `translate(${diameter / 2 - aim.x}px, ${diameter / 2 - aim.y}px)`,\n };\n return (\n <div className=\"lk-loupe__stage\" style={style}>\n {render({\n state,\n config,\n view: zoomAt(view, factor, aim, { frame }),\n factor,\n mode,\n size,\n })}\n </div>\n );\n}\n","import type { LoupePoint } from '@weasel-js/loupe';\nimport type { CSSProperties, ReactNode, RefObject } from 'react';\n\n/** Props for `<LoupeBubble>`. */\nexport interface LoupeBubbleProps {\n /** Where the lens is aimed, in its container's own pixels. */\n aim: LoupePoint;\n diameter: number;\n hostRef?: RefObject<HTMLDivElement | null>;\n children: ReactNode;\n}\n\n/**\n * The lens itself: a circle centred on the aimed point, clipping whatever a\n * painter draws into it.\n *\n * It takes no pointer events, so the pan, the wheel and anything underneath\n * keep working while it is up — and it is `aria-hidden`, since it magnifies\n * content already on the page rather than adding any.\n */\nexport function LoupeBubble({ aim, diameter, hostRef, children }: LoupeBubbleProps) {\n const style: CSSProperties = {\n width: `${diameter}px`,\n height: `${diameter}px`,\n transform: `translate(${aim.x - diameter / 2}px, ${aim.y - diameter / 2}px)`,\n };\n return (\n <div ref={hostRef} className=\"lk-loupe\" style={style} aria-hidden=\"true\">\n {children}\n </div>\n );\n}\n","/**\n * The loupe's two bindable interactions, as `Action` descriptors the gesture\n * dispatcher routes: hold a key to peek, and walk the magnification with the\n * wheel.\n *\n * Free of React so the routing can be tested without a DOM.\n */\nimport type { Action } from '@weasel-js/core';\n\n/** Canonical id of the hold-to-peek action. */\nexport const LOUPE_PEEK_ID = 'loupe.peek';\n/** Canonical id of the wheel-magnify action. */\nexport const LOUPE_MAGNIFY_ID = 'loupe.magnify';\n\n/** How fast the wheel walks the magnification. Gentler than the pan-zoom\n * wheel: the lens' whole range is one order of magnitude. */\nexport const WHEEL_RATE = 0.002;\n\n/** What the actions do to a live loupe. Every member is read through the\n * object on each invocation, so one stable instance serves the life of the\n * registration. */\nexport interface LoupeInputApi {\n /** Whether the lens is up. The wheel belongs to the loupe only while it is;\n * otherwise `loupe.magnify` declines and the dispatcher leaves the event\n * unhandled, which is what hands it back to the lab's own pan/zoom. */\n shown: () => boolean;\n /** Raise or drop the hold-to-peek flag. */\n setPeeking: (on: boolean) => void;\n /** Multiply the magnification by one wheel notch of `deltaY`. */\n magnifyBy: (deltaY: number) => void;\n}\n\n/**\n * Build the loupe's actions. `peekKey` is a `KeyboardEvent.key`; passing\n * `null` omits `loupe.peek` entirely rather than registering a binding no key\n * can match.\n */\nexport function createLoupeActions(\n input: LoupeInputApi,\n peekKey: string | null,\n): readonly Action[] {\n const magnify: Action = {\n id: LOUPE_MAGNIFY_ID,\n label: 'Magnify (loupe)',\n group: 'loupe',\n // Every modifier optional: the lens claims the wheel whenever it is up,\n // which is what the hand-rolled capture-phase listener did. A bare spec\n // would forbid modifiers and hand Cmd+wheel back to the lab mid-peek.\n defaultBinding: [\n {\n spec: {\n kind: 'wheel',\n mods: { alt: 'optional', ctrl: 'optional', meta: 'optional', shift: 'optional' },\n },\n opts: {},\n },\n ],\n enabled: () => (input.shown() ? true : 'not-applicable'),\n invoker: {\n timing: 'immediate',\n run(_deps, params) {\n input.magnifyBy((params?.deltaY as number | undefined) ?? 0);\n },\n },\n };\n\n if (peekKey === null) return [magnify];\n\n const peek: Action = {\n id: LOUPE_PEEK_ID,\n label: 'Peek (loupe)',\n group: 'loupe',\n // Hotkey scope: peeking must beat whatever the lab's active tool binds to\n // the same key, the way every other held-key trigger does.\n scope: 'hotkey',\n // Every modifier optional because the peek key may BE one: the keydown\n // that carries `Alt` — the default — also reports `altKey: true`, and the\n // matcher forbids any modifier a spec does not name.\n defaultBinding: [\n {\n spec: {\n kind: 'key-held',\n key: peekKey,\n mods: { alt: 'optional', ctrl: 'optional', meta: 'optional', shift: 'optional' },\n },\n opts: {},\n },\n ],\n invoker: {\n timing: 'ongoing',\n start() {\n input.setPeeking(true);\n return { onEnd: () => input.setPeeking(false) };\n },\n },\n };\n return [peek, magnify];\n}\n","/**\n * Mounts the gesture dispatcher on a loupe's host and registers the loupe's\n * actions on it, so the peek key and the wheel route the way every other\n * weasel binding does.\n *\n * Three of the dispatcher's four element channels are off. Only `wheel` is\n * wanted here; `contextMenu` suppresses the native menu unconditionally and\n * `ingest` makes the host a file-drop target, neither of which a lab asked\n * for by turning a magnifier on.\n */\nimport {\n type ActionsRegistry,\n type Tool,\n useActionsRegistry,\n useGestureDispatcher,\n} from '@weasel-js/core';\nimport { type RefObject, useEffect, useMemo } from 'react';\nimport { createLoupeActions, type LoupeInputApi } from './loupeActions';\n\n/** Props for `<LoupeGestures>`. */\nexport interface LoupeGesturesProps {\n /** The element the dispatcher listens on — the same one the lens tracks. */\n hostRef: RefObject<HTMLElement | null>;\n input: LoupeInputApi;\n /** `KeyboardEvent.key` for hold-to-peek, or `null` for no peek binding. */\n peekKey: string | null;\n}\n\nconst NO_TOOLS: ReadonlyMap<string, Tool> = new Map();\n\nconst CHANNELS = { pointer: false, contextMenu: false, ingest: false } as const;\n\nfunction LoupeDispatch({\n hostRef,\n input,\n peekKey,\n registry,\n}: LoupeGesturesProps & { registry: ActionsRegistry }) {\n const actions = useMemo(() => createLoupeActions(input, peekKey), [input, peekKey]);\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 useGestureDispatcher({\n canvasRef: hostRef,\n actions: registry,\n toolsById: NO_TOOLS,\n channels: CHANNELS,\n });\n\n return null;\n}\n\n/**\n * The loupe's input, routed. Render it inside a `<WeaselProvider isolate>` —\n * with no registry in scope it renders nothing rather than registering\n * actions that could never fire.\n */\nexport function LoupeGestures(props: LoupeGesturesProps) {\n const registry = useActionsRegistry();\n if (!registry) return null;\n return <LoupeDispatch {...props} registry={registry} />;\n}\n","import { type RefObject, useEffect, useState } from 'react';\nimport type { ViewportSize } from '../canvas/worldSpec';\n\nconst ZERO: ViewportSize = { width: 0, height: 0 };\n\n/** An element's CSS-pixel box, kept current. `{0, 0}` until it is measured, and\n * wherever `ResizeObserver` is absent. */\nexport function useHostSize(ref: RefObject<HTMLElement | null>): ViewportSize {\n const [size, setSize] = useState<ViewportSize>(ZERO);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n const update = (): void => {\n const rect = el.getBoundingClientRect();\n setSize((prev) =>\n prev.width === rect.width && prev.height === rect.height\n ? prev\n : { width: rect.width, height: rect.height },\n );\n };\n update();\n if (typeof ResizeObserver === 'undefined') return;\n const ro = new ResizeObserver(update);\n ro.observe(el);\n return () => ro.disconnect();\n }, [ref]);\n\n return size;\n}\n","import type { View } from '@weasel-js/core';\n\n/** A point on the magnified surface, in its own CSS pixels. */\nexport interface LoupePoint {\n x: number;\n y: number;\n}\n\n/** The lens' rectangle on that surface. */\nexport interface LoupeRect extends LoupePoint {\n w: number;\n h: number;\n}\n\n/**\n * The inner `View` a loupe renders its content through: the outer view's\n * magnification times `factor`, positioned so `target` sits at the center of\n * a viewport the size of `rect`.\n */\nexport function loupeInnerView(\n target: LoupePoint,\n outer: View,\n rect: LoupeRect,\n factor: number,\n): View {\n const scale = { x: outer.scale.x * factor, y: outer.scale.y * factor };\n return {\n x: target.x - rect.w / 2 / scale.x,\n y: target.y - rect.h / 2 / scale.y,\n scale,\n };\n}\n\n/**\n * Where on the outer surface a point inside the lens is looking, in\n * screen-space CSS px.\n *\n * The inverse of {@link loupeInnerView}: the lens centers `aim` in `rect` and\n * magnifies by `factor`, and the outer view's own scale cancels out of the\n * round trip — so this needs no `View`.\n */\nexport function loupeSourcePoint(\n p: LoupePoint,\n rect: LoupeRect,\n aim: LoupePoint,\n factor: number,\n): LoupePoint {\n return {\n x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,\n y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor,\n };\n}\n","import { normalizeZoom } from '@weasel-js/core';\nimport { type LoupePoint, type LoupeRect, loupeSourcePoint } from './geometry';\n\n/** How a loupe magnifies. `'vector'` re-renders the source through a zoomed-in\n * view, so content stays sharp at any factor; `'pixel'` blows up the actual\n * pixels the surface presented. */\nexport type LoupeMode = 'vector' | 'pixel';\n\n/**\n * What a loupe needs from the surface it magnifies. A painter implements it,\n * and the model asks nothing about how the lens is drawn — only where it is,\n * what colour is under a point, and whether anyone can still see it.\n */\nexport interface LoupeSurface {\n /** The lens' rectangle, or `null` while it has none. */\n lens(): LoupeRect | null;\n /**\n * Does the lens itself cover this point? A stationary lens does over its own\n * frame, which is what the freeze rule below is for; a lens that follows the\n * pointer never covers it and answers `false`.\n */\n covers(p: LoupePoint): boolean;\n /** Hex colour at a surface point, or `null` when the surface cannot say. */\n sample(p: LoupePoint): string | null;\n /** Is the lens off screen? Aims are ignored while it is. */\n hidden(): boolean;\n /** Is the lens gone for good? The model tears itself down when it is. */\n gone(): boolean;\n /** Something the painter must redraw for has changed. */\n changed(): void;\n /**\n * Run `fn` after each frame the surface lands; returns an unsubscribe.\n * Implement it when `sample` reads pixels that only show an aim once a\n * frame has painted — a framebuffer read back. The model then samples the\n * aim on the next landed frame, so `color` settles a frame after the aim.\n * Omit it when `sample` can answer for a fresh aim right away.\n */\n subscribeFrame?(fn: () => void): () => void;\n}\n\n/** Options for {@link createLoupeModel}. */\nexport interface LoupeModelOptions {\n surface: LoupeSurface;\n mode?: LoupeMode;\n /** Magnification. Default 8. Always positive and finite: a factor that is\n * not becomes `ZOOM_FLOOR`, the same rule a view's zoom follows. */\n factor?: number;\n /** Bounds `setFactor` clamps to. Unset means bounded only by that rule. */\n minFactor?: number;\n maxFactor?: number;\n /** Called with the hex colour under the aim point whenever it changes. */\n onColorChange?: (hex: string) => void;\n /** Called when a pick lands — an eyedropper on the magnified surface. Where\n * the colour goes is the consumer's business. */\n onPick?: (hex: string) => void;\n /** Called when the model tears itself down because the surface reported the\n * lens gone, so the painter can drop what it was holding. */\n onDispose?: () => void;\n}\n\n/** Where a loupe is aimed, how far it magnifies, and what colour it is over. */\nexport interface LoupeModel {\n readonly mode: LoupeMode;\n readonly factor: number;\n /** Last aimed point, in the surface's CSS px. */\n readonly aim: LoupePoint;\n /** Hex colour under the aim point. `null` until something answers. */\n readonly color: string | null;\n setMode(mode: LoupeMode): void;\n setFactor(factor: number): void;\n /** Aim at a surface point. Ignored while the lens covers it, is hidden, or\n * the model is disposed. */\n aimAt(p: LoupePoint): void;\n /** Sample what the lens shows at `p`, a point inside it, and report that to\n * `onPick`. Omit `p` to pick at the aim point. Leaves the aim, and `color`,\n * alone. */\n pick(p?: LoupePoint): string | null;\n dispose(): void;\n}\n\n/**\n * Build the loupe's model over a surface. It holds the state a magnifier has —\n * aim, factor, mode, colour — and none of the drawing.\n */\nexport function createLoupeModel(opts: LoupeModelOptions): LoupeModel {\n const { surface } = opts;\n let mode: LoupeMode = opts.mode ?? 'vector';\n let factor = normalizeZoom(opts.factor ?? 8);\n let aim: LoupePoint = { x: 0, y: 0 };\n let color: string | null = null;\n let disposed = false;\n let aimUnsampled = false;\n\n const clamp = (n: number): number =>\n normalizeZoom(Math.min(opts.maxFactor ?? Number.POSITIVE_INFINITY,\n Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)));\n\n const sampleColor = () => {\n const hex = surface.sample(aim);\n if (hex === null || hex === color) return;\n color = hex;\n opts.onColorChange?.(hex);\n };\n\n const unsubscribeFrame = surface.subscribeFrame?.(() => {\n if (!aimUnsampled) return;\n aimUnsampled = false;\n sampleColor();\n });\n\n const teardown = () => {\n if (disposed) return;\n disposed = true;\n unsubscribeFrame?.();\n opts.onDispose?.();\n };\n\n return {\n get mode() { return mode; },\n get factor() { return factor; },\n get aim() { return aim; },\n get color() { return color; },\n\n setMode(next) {\n if (disposed || next === mode) return;\n mode = next;\n surface.changed();\n },\n\n setFactor(next) {\n if (disposed) return;\n factor = clamp(next);\n surface.changed();\n },\n\n aimAt(p) {\n if (disposed) return;\n // A lens can be taken away by whatever owns it, without coming back\n // through `dispose`. Follow it down rather than keep sampling for a\n // magnifier nobody can see.\n if (surface.gone()) { teardown(); return; }\n if (surface.hidden()) return;\n if (surface.covers(p)) return;\n aim = p;\n if (unsubscribeFrame) aimUnsampled = true;\n else sampleColor();\n surface.changed();\n },\n\n pick(p) {\n if (disposed) return null;\n let at = aim;\n if (p) {\n const rect = surface.lens();\n if (!rect) return null;\n at = loupeSourcePoint(p, rect, aim, factor);\n // The lens is part of the picture the surface presents. A point near\n // its frame maps back underneath it, and sampling there reports the\n // lens' own chrome as artwork.\n if (surface.covers(at)) return null;\n }\n const hex = surface.sample(at);\n if (hex !== null) opts.onPick?.(hex);\n return hex;\n },\n\n dispose: teardown,\n };\n}\n","import {\n createLoupeModel,\n type LoupeMode,\n type LoupeModel,\n type LoupePoint,\n} from '@weasel-js/loupe';\nimport { type RefObject, useCallback, useEffect, useReducer, useRef } from 'react';\nimport type { LoupeInputApi } from './loupeActions';\nimport { WHEEL_RATE } from './loupeActions';\nimport type { ResolvedLoupe } from './types';\n\n/** Options for {@link useLoupe}. */\nexport interface UseLoupeOptions {\n capability: ResolvedLoupe;\n /** The element the lens tracks the pointer across. */\n hostRef: RefObject<HTMLElement | null>;\n /** Whether the loupe is turned on. Hold-to-peek shows it regardless. */\n enabled: boolean;\n /** Hex color at a host point. Omitted, the loupe reports no color — which\n * is the honest answer for a surface with no pixels to read. */\n sample?: (p: LoupePoint) => string | null;\n}\n\n/** A loupe as a React view reads it. */\nexport interface LoupeState {\n /** Whether the lens should be drawn. */\n visible: boolean;\n /** Where it is aimed, in the host's own pixels. */\n aim: LoupePoint;\n factor: number;\n mode: LoupeMode;\n color: string | null;\n setMode: (mode: LoupeMode) => void;\n setFactor: (factor: number) => void;\n /** Sample what the lens shows at a point inside it. */\n pick: (p?: LoupePoint) => string | null;\n /** What `<LoupeGestures>` drives the lens through. Stable for the life of\n * the hook, so registering the actions on it does not churn. */\n input: LoupeInputApi;\n}\n\n/**\n * Binds `@weasel-js/loupe`'s model to a host element and tracks the pointer\n * across it.\n *\n * The peek key and the wheel are not here: they are `loupe.peek` and\n * `loupe.magnify`, routed through the gesture dispatcher that\n * `<LoupeGestures>` mounts on the same host. Aiming stays a plain listener\n * because a hover is not a gesture the grammar names — `GESTURE_DESCRIPTORS`\n * has no continuous-motion entry, and inventing one to carry the lens' aim\n * would be an input taxonomy change, not a loupe change.\n */\nexport function useLoupe({ capability, hostRef, enabled, sample }: UseLoupeOptions): LoupeState {\n const [, bump] = useReducer((n: number) => n + 1, 0);\n\n const overRef = useRef(false);\n const peekingRef = useRef(false);\n const enabledRef = useRef(enabled);\n enabledRef.current = enabled;\n const sampleRef = useRef(sample);\n sampleRef.current = sample;\n const onColorChangeRef = useRef(capability.onColorChange);\n onColorChangeRef.current = capability.onColorChange;\n\n // Reads nothing but refs, so it is stable and an effect may depend on it.\n const shown = useCallback(\n (): boolean => overRef.current && (enabledRef.current || peekingRef.current),\n [],\n );\n\n const modelRef = useRef<LoupeModel | null>(null);\n if (modelRef.current === null) {\n modelRef.current = createLoupeModel({\n mode: capability.mode,\n factor: capability.factor,\n minFactor: capability.minFactor,\n maxFactor: capability.maxFactor,\n onColorChange: (hex) => onColorChangeRef.current?.(hex),\n surface: {\n lens: () => {\n const d = diameterRef.current;\n const { x, y } = modelRef.current?.aim ?? { x: 0, y: 0 };\n return { x: x - d / 2, y: y - d / 2, w: d, h: d };\n },\n // The lens follows the pointer and is painted over the host rather than\n // into it, so it is never part of the picture being magnified.\n covers: () => false,\n sample: (p) => sampleRef.current?.(p) ?? null,\n hidden: () => !shown(),\n gone: () => goneRef.current,\n changed: bump,\n },\n });\n }\n const model = modelRef.current;\n\n const diameterRef = useRef(capability.diameter);\n diameterRef.current = capability.diameter;\n const goneRef = useRef(false);\n\n // The model holds no resources, and `dispose` is one-way — so unmounting only\n // reports the lens gone. Disposing here instead leaves React's mount / unmount\n // / remount in StrictMode with a permanently dead model that silently ignores\n // every aim.\n useEffect(() => {\n goneRef.current = false;\n return () => {\n goneRef.current = true;\n };\n }, []);\n\n // Turning the loupe off with the pointer still inside must put the lens away,\n // and the model only reconsiders on an aim.\n useEffect(() => {\n if (!enabled && !peekingRef.current) bump();\n }, [enabled]);\n\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n\n const onPointerMove = (e: PointerEvent): void => {\n overRef.current = true;\n const rect = host.getBoundingClientRect();\n model.aimAt({ x: e.clientX - rect.left, y: e.clientY - rect.top });\n if (!shown()) return;\n bump();\n };\n const onPointerLeave = (): void => {\n overRef.current = false;\n bump();\n };\n\n host.addEventListener('pointermove', onPointerMove);\n host.addEventListener('pointerleave', onPointerLeave);\n return () => {\n host.removeEventListener('pointermove', onPointerMove);\n host.removeEventListener('pointerleave', onPointerLeave);\n };\n }, [hostRef, model, shown]);\n\n const inputRef = useRef<LoupeInputApi | null>(null);\n if (inputRef.current === null) {\n inputRef.current = {\n shown,\n setPeeking: (on) => {\n if (peekingRef.current === on) return;\n peekingRef.current = on;\n bump();\n },\n magnifyBy: (deltaY) => {\n const m = modelRef.current;\n if (m) m.setFactor(m.factor * Math.exp(-deltaY * WHEEL_RATE));\n },\n };\n }\n\n return {\n visible: shown(),\n aim: model.aim,\n factor: model.factor,\n mode: model.mode,\n color: model.color,\n setMode: model.setMode,\n setFactor: model.setFactor,\n pick: model.pick,\n input: inputRef.current,\n };\n}\n","import { WeaselProvider } from '@weasel-js/core';\nimport { type RefObject, useContext, useMemo } from 'react';\nimport { CanvasStackContext } from '../canvas/CanvasStackContext';\nimport type { WorldSpec } from '../canvas/worldSpec';\nimport type { ViewTransform } from '../instrument/types';\nimport { CanvasLoupe } from './CanvasLoupe';\nimport { sampleStack } from './canvasLens';\nimport { DomLoupe } from './DomLoupe';\nimport { LoupeBubble } from './LoupeBubble';\nimport { LoupeGestures } from './LoupeGestures';\nimport type { ResolvedLoupe } from './types';\nimport { useHostSize } from './useHostSize';\nimport { useLoupe } from './useLoupe';\n\n/** Props for `<TrialLoupe>`. */\nexport interface TrialLoupeProps {\n capability: ResolvedLoupe;\n /** Whether the trial's loupe is turned on. Hold-to-peek shows it anyway. */\n enabled: boolean;\n state: unknown;\n config: unknown;\n /** The trial's own camera. */\n view: ViewTransform;\n worldSpec?: WorldSpec;\n /** The element the lens tracks. A loupe mounted inside a `<CanvasStack>`\n * takes the stack's own element instead, and this goes unused. */\n hostRef?: RefObject<HTMLElement | null>;\n}\n\n/** Stable stand-in, so a loupe with nothing to track does not re-bind its\n * listeners on every render. */\nconst NO_HOST: RefObject<HTMLElement | null> = { current: null };\n\n/**\n * A trial's loupe, painted by whichever painter suits its content: the canvas\n * stack it is mounted inside, or the instrument's own `render` at a magnified\n * camera.\n */\nexport function TrialLoupe({\n capability,\n enabled,\n state,\n config,\n view,\n worldSpec,\n hostRef,\n}: TrialLoupeProps) {\n const stack = useContext(CanvasStackContext);\n const surface = capability.render ? undefined : stack?.surface;\n const host = surface?.element ?? hostRef ?? NO_HOST;\n\n const sample = useMemo(() => {\n if (!surface) return undefined;\n return (p: { x: number; y: number }): string | null => {\n const canvases = surface.canvases.current;\n return canvases ? sampleStack(surface.layers, canvases, p, surface.size.dpr) : null;\n };\n }, [surface]);\n\n const loupe = useLoupe({ capability, hostRef: host, enabled, sample });\n const measured = useHostSize(host);\n const size = surface?.size ?? measured;\n\n // The gestures mount outside the visibility gate: hold-to-peek is what\n // raises a lens that is down, so its binding has to be live while it is.\n // One isolated scope per loupe, for the reason `<AnnotationOverlay>` gives\n // — an actions registry holds one dispatcher.\n const gestures = (\n <WeaselProvider isolate>\n <LoupeGestures hostRef={host} input={loupe.input} peekKey={capability.peekKey ?? null} />\n </WeaselProvider>\n );\n\n if (!loupe.visible) return gestures;\n\n return (\n <>\n {gestures}\n <LoupeBubble aim={loupe.aim} diameter={capability.diameter}>\n {capability.render ? (\n <DomLoupe\n aim={loupe.aim}\n factor={loupe.factor}\n mode={loupe.mode}\n diameter={capability.diameter}\n size={size}\n view={view}\n frame={stack?.frame}\n state={state}\n config={config}\n render={capability.render}\n />\n ) : surface && stack ? (\n <CanvasLoupe\n aim={loupe.aim}\n factor={loupe.factor}\n mode={loupe.mode}\n diameter={capability.diameter}\n surface={surface}\n view={stack.view}\n frame={stack.frame}\n worldSpec={worldSpec}\n />\n ) : null}\n </LoupeBubble>\n </>\n );\n}\n","import type { LoupeMode } from '@weasel-js/loupe';\nimport type { ReactNode } from 'react';\nimport type { ViewportSize } from '../canvas/worldSpec';\nimport type { ViewTransform } from '../instrument/types';\n\n/** What a DOM instrument's loupe `render` is handed: the same data its own\n * `render` gets, and the camera to draw it through. */\nexport interface LoupeRenderArgs<TS = unknown, TC = unknown> {\n state: TS;\n config: TC;\n /** The trial's own camera composed with the magnification, about the aimed\n * point — so drawing the same content through it magnifies in place. */\n view: ViewTransform;\n /** The magnification on its own, for whatever must not scale with the\n * camera. */\n factor: number;\n mode: LoupeMode;\n /** The size of the viewport `view` is written for: the trial's content well,\n * not the lens. The lens shows a circle cut out of it. */\n size: ViewportSize;\n}\n\n/**\n * Declares that an instrument can be magnified.\n *\n * With no `render`, the loupe re-draws the instrument's canvas layers through a\n * zoomed camera, so it stays sharp at any factor. An instrument whose content is\n * DOM supplies `render` instead: given a camera, draw me again.\n */\nexport interface LoupeCapability<TS = unknown, TC = unknown> {\n render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;\n /** Opening magnification, clamped to the bounds below. Default 6. */\n factor?: number;\n /** What the wheel clamps to. Defaults 2 and 32. */\n minFactor?: number;\n maxFactor?: number;\n /** `'vector'` (default) re-renders the content magnified; `'pixel'` blows up\n * the pixels the instrument presented. A `render` loupe is always vector —\n * DOM has no framebuffer to enlarge. */\n mode?: LoupeMode;\n /** Lens diameter in CSS px. Default 200. */\n diameter?: number;\n /** Held for a momentary peek while the loupe is off. Default `'Alt'`; `null`\n * turns hold-to-peek off. Matched against `KeyboardEvent.key`. */\n peekKey?: string | null;\n /** Called with the color under the aim, wherever the surface can say. The\n * canvas painter reads it back; a DOM loupe has no pixels to sample. */\n onColorChange?: (hex: string) => void;\n}\n\n/** An instrument's loupe declaration. `true` takes every default. */\nexport type LoupeDeclaration<TS = unknown, TC = unknown> = true | LoupeCapability<TS, TC>;\n\n/** A {@link LoupeCapability} with every default filled in. */\nexport interface ResolvedLoupe<TS = unknown, TC = unknown> {\n render?: (args: LoupeRenderArgs<TS, TC>) => ReactNode;\n onColorChange?: (hex: string) => void;\n factor: number;\n minFactor: number;\n maxFactor: number;\n mode: LoupeMode;\n diameter: number;\n peekKey: string | null;\n}\n\nexport const LOUPE_DEFAULTS = {\n factor: 6,\n minFactor: 2,\n maxFactor: 32,\n mode: 'vector',\n diameter: 200,\n peekKey: 'Alt',\n} as const satisfies Omit<ResolvedLoupe, 'render' | 'onColorChange'>;\n\nexport function resolveLoupe<TS, TC>(declared: LoupeDeclaration<TS, TC>): ResolvedLoupe<TS, TC> {\n const cap = declared === true ? {} : declared;\n const minFactor = cap.minFactor ?? LOUPE_DEFAULTS.minFactor;\n const maxFactor = cap.maxFactor ?? LOUPE_DEFAULTS.maxFactor;\n return {\n render: cap.render,\n onColorChange: cap.onColorChange,\n minFactor,\n maxFactor,\n factor: Math.min(maxFactor, Math.max(minFactor, cap.factor ?? LOUPE_DEFAULTS.factor)),\n diameter: cap.diameter ?? LOUPE_DEFAULTS.diameter,\n peekKey: cap.peekKey === undefined ? LOUPE_DEFAULTS.peekKey : cap.peekKey,\n // A DOM loupe has no framebuffer, so `pixel` would have nothing to enlarge.\n mode: cap.render ? 'vector' : (cap.mode ?? LOUPE_DEFAULTS.mode),\n };\n}\n"]}
@@ -1,14 +1,13 @@
1
1
  import { DEFAULT_FRAME } from './chunk-2AYGEN57.js';
2
+ import { normalizeZoom } from '@weasel-js/core';
2
3
 
3
- // src/canvas/camera.ts
4
- function usableZoom(zoom) {
5
- return Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
6
- }
7
4
  function zoomAt(view, factor, at, { frame = DEFAULT_FRAME, min, max } = {}) {
8
5
  const anchorX = at.x - frame.originPx.x;
9
6
  const anchorY = at.y - frame.originPx.y;
10
- const current = usableZoom(view.zoom);
11
- const zoom = Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min ?? 0, current * factor));
7
+ const current = normalizeZoom(view.zoom);
8
+ const zoom = normalizeZoom(
9
+ Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min ?? 0, current * factor))
10
+ );
12
11
  const ratio = zoom / current;
13
12
  return {
14
13
  zoom,
@@ -19,15 +18,16 @@ function zoomAt(view, factor, at, { frame = DEFAULT_FRAME, min, max } = {}) {
19
18
  };
20
19
  }
21
20
  function centerOn(world, zoom, size, frame = DEFAULT_FRAME) {
21
+ const z = normalizeZoom(zoom);
22
22
  return {
23
- zoom,
23
+ zoom: z,
24
24
  pan: {
25
- x: size.width / 2 - frame.originPx.x - world.x * zoom,
26
- y: size.height / 2 - frame.originPx.y - world.y * zoom * frame.yDir
25
+ x: size.width / 2 - frame.originPx.x - world.x * z,
26
+ y: size.height / 2 - frame.originPx.y - world.y * z * frame.yDir
27
27
  }
28
28
  };
29
29
  }
30
30
 
31
31
  export { centerOn, zoomAt };
32
- //# sourceMappingURL=chunk-CTRKTLYZ.js.map
33
- //# sourceMappingURL=chunk-CTRKTLYZ.js.map
32
+ //# sourceMappingURL=chunk-H6ZAOWNE.js.map
33
+ //# sourceMappingURL=chunk-H6ZAOWNE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/canvas/camera.ts"],"names":[],"mappings":";;;AAqBO,SAAS,MAAA,CACd,IAAA,EACA,MAAA,EACA,EAAA,EACA,EAAE,KAAA,GAAQ,aAAA,EAAe,GAAA,EAAK,GAAA,EAAI,GAAmB,EAAC,EACvC;AAGf,EAAA,MAAM,OAAA,GAAU,EAAA,CAAG,CAAA,GAAI,KAAA,CAAM,QAAA,CAAS,CAAA;AACtC,EAAA,MAAM,OAAA,GAAU,EAAA,CAAG,CAAA,GAAI,KAAA,CAAM,QAAA,CAAS,CAAA;AACtC,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,IAAA,CAAK,IAAI,CAAA;AACvC,EAAA,MAAM,IAAA,GAAO,aAAA;AAAA,IACX,IAAA,CAAK,GAAA,CAAI,GAAA,IAAO,MAAA,CAAO,iBAAA,EAAmB,IAAA,CAAK,GAAA,CAAI,GAAA,IAAO,CAAA,EAAG,OAAA,GAAU,MAAM,CAAC;AAAA,GAChF;AACA,EAAA,MAAM,QAAQ,IAAA,GAAO,OAAA;AACrB,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,GAAA,EAAK;AAAA,MACH,CAAA,EAAG,OAAA,GAAA,CAAW,OAAA,GAAU,IAAA,CAAK,IAAI,CAAA,IAAK,KAAA;AAAA,MACtC,CAAA,EAAG,OAAA,GAAA,CAAW,OAAA,GAAU,IAAA,CAAK,IAAI,CAAA,IAAK;AAAA;AACxC,GACF;AACF;AAIO,SAAS,QAAA,CACd,KAAA,EACA,IAAA,EACA,IAAA,EACA,QAAoB,aAAA,EACL;AACf,EAAA,MAAM,CAAA,GAAI,cAAc,IAAI,CAAA;AAC5B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,CAAA;AAAA,IACN,GAAA,EAAK;AAAA,MACH,CAAA,EAAG,KAAK,KAAA,GAAQ,CAAA,GAAI,MAAM,QAAA,CAAS,CAAA,GAAI,MAAM,CAAA,GAAI,CAAA;AAAA,MACjD,CAAA,EAAG,IAAA,CAAK,MAAA,GAAS,CAAA,GAAI,KAAA,CAAM,SAAS,CAAA,GAAI,KAAA,CAAM,CAAA,GAAI,CAAA,GAAI,KAAA,CAAM;AAAA;AAC9D,GACF;AACF","file":"chunk-H6ZAOWNE.js","sourcesContent":["import { normalizeZoom } from '@weasel-js/core';\nimport type { Point, ViewTransform } from '../instrument/types';\nimport { DEFAULT_FRAME, type ViewportSize, type WorldFrame } from './worldSpec';\n\n/** Bounds and coordinate system a {@link zoomAt} works in. */\nexport interface ZoomAtOptions {\n /** The instrument's coordinate system, resolved against the viewport.\n * Omitted, `at` is measured from the element's top-left. */\n frame?: WorldFrame;\n min?: number;\n max?: number;\n}\n\n/**\n * Zoom a view by `factor` about a fixed point, so whatever world coordinate\n * `at` was over stays under it.\n *\n * `at` is in the element's own pixels — a cursor position, or the centre of a\n * lens. The clamp is applied before the pan is solved, so a zoom that hits a\n * bound still anchors on the zoom it actually reached.\n */\nexport function zoomAt(\n view: ViewTransform,\n factor: number,\n at: Point,\n { frame = DEFAULT_FRAME, min, max }: ZoomAtOptions = {},\n): ViewTransform {\n // `pan` is measured from the frame's origin, so anchoring at the raw point\n // drifts by `(1 - ratio) * originPx` per step on any frame that moves it.\n const anchorX = at.x - frame.originPx.x;\n const anchorY = at.y - frame.originPx.y;\n const current = normalizeZoom(view.zoom);\n const zoom = normalizeZoom(\n Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min ?? 0, current * factor)),\n );\n const ratio = zoom / current;\n return {\n zoom,\n pan: {\n x: anchorX - (anchorX - view.pan.x) * ratio,\n y: anchorY - (anchorY - view.pan.y) * ratio,\n },\n };\n}\n\n/** The view that puts a world point at the centre of a `size` viewport, at\n * `zoom`. What a lens showing a region around one point renders through. */\nexport function centerOn(\n world: Point,\n zoom: number,\n size: ViewportSize,\n frame: WorldFrame = DEFAULT_FRAME,\n): ViewTransform {\n const z = normalizeZoom(zoom);\n return {\n zoom: z,\n pan: {\n x: size.width / 2 - frame.originPx.x - world.x * z,\n y: size.height / 2 - frame.originPx.y - world.y * z * frame.yDir,\n },\n };\n}\n"]}