@weasel-js/labkit 1.7.0 → 1.7.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 (208) hide show
  1. package/README.md +3 -3
  2. package/dist/_dts/{CanvasStackContext-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
  3. package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
  4. package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
  5. package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
  6. package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
  7. package/dist/_dts/{useTrialState-CTZPgeZ7.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
  8. package/dist/canvas/index.d.ts +8 -6
  9. package/dist/canvas/index.js +6 -6
  10. package/dist/chrome/index.d.ts +7 -5
  11. package/dist/chrome/index.js +10 -9
  12. package/dist/chunk-2ES4W47N.js +8 -0
  13. package/dist/chunk-2ES4W47N.js.map +1 -0
  14. package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
  15. package/dist/chunk-2N7UBSS2.js.map +1 -0
  16. package/dist/chunk-4KSC7NXL.js +15491 -0
  17. package/dist/chunk-4KSC7NXL.js.map +1 -0
  18. package/dist/{chunk-7BO7HNGF.js → chunk-CEGFXTBD.js} +8 -61
  19. package/dist/chunk-CEGFXTBD.js.map +1 -0
  20. package/dist/{chunk-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
  21. package/dist/chunk-E23Q6A3Y.js.map +1 -0
  22. package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
  23. package/dist/{chunk-GOZLNM4L.js.map → chunk-E6NARGU7.js.map} +1 -1
  24. package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
  25. package/dist/chunk-EESJUSYR.js.map +1 -0
  26. package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
  27. package/dist/chunk-EFXRM6XQ.js.map +1 -0
  28. package/dist/chunk-F4ACIK7Z.js +210 -0
  29. package/dist/chunk-F4ACIK7Z.js.map +1 -0
  30. package/dist/{chunk-FMK7UQ6V.js → chunk-FB4MIZOO.js} +61 -44
  31. package/dist/chunk-FB4MIZOO.js.map +1 -0
  32. package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
  33. package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
  34. package/dist/{chunk-I5DUQ5HM.js → chunk-GAHELLJT.js} +75 -72
  35. package/dist/chunk-GAHELLJT.js.map +1 -0
  36. package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
  37. package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
  38. package/dist/{chunk-T5IBSPAM.js → chunk-JFOJESJF.js} +33 -23
  39. package/dist/chunk-JFOJESJF.js.map +1 -0
  40. package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
  41. package/dist/chunk-PMLZP7RT.js.map +1 -0
  42. package/dist/{chunk-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
  43. package/dist/chunk-READTDQX.js.map +1 -0
  44. package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
  45. package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
  46. package/dist/{chunk-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
  47. package/dist/chunk-T6DLGEJB.js.map +1 -0
  48. package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
  49. package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
  50. package/dist/{chunk-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
  51. package/dist/chunk-VYVGVBGY.js.map +1 -0
  52. package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
  53. package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
  54. package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
  55. package/dist/chunk-XE5HJOPL.js.map +1 -0
  56. package/dist/chunk-XVUKDSYV.js +38 -0
  57. package/dist/chunk-XVUKDSYV.js.map +1 -0
  58. package/dist/{chunk-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
  59. package/dist/chunk-YCFKW5RX.js.map +1 -0
  60. package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
  61. package/dist/chunk-ZQIHJXP4.js.map +1 -0
  62. package/dist/config/index.d.ts +18 -10
  63. package/dist/config/index.js +4 -4
  64. package/dist/controls/index.d.ts +4 -7
  65. package/dist/controls/index.js +6 -5
  66. package/dist/dragdrop/index.d.ts +23 -3
  67. package/dist/dragdrop/index.js +4 -3
  68. package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
  69. package/dist/index.d.ts +60 -714
  70. package/dist/index.js +357 -334
  71. package/dist/index.js.map +1 -1
  72. package/dist/job/index.d.ts +8 -0
  73. package/dist/job/index.js +1 -1
  74. package/dist/layers/index.d.ts +6 -5
  75. package/dist/layers/index.js +3 -2
  76. package/dist/loupe/index.d.ts +100 -30
  77. package/dist/loupe/index.js +585 -6
  78. package/dist/loupe/index.js.map +1 -1
  79. package/dist/overview/index.d.ts +4 -2
  80. package/dist/overview/index.js +15 -14
  81. package/dist/overview/index.js.map +1 -1
  82. package/dist/passthrough/weasel-canvas.d.ts +2 -0
  83. package/dist/passthrough/weasel-ui.d.ts +2 -2871
  84. package/dist/passthrough/weasel-ui.js +3 -2
  85. package/dist/primitives/index.d.ts +213 -3
  86. package/dist/primitives/index.js +8 -7
  87. package/dist/state/index.d.ts +7 -5
  88. package/dist/state/index.js +6 -6
  89. package/dist/styles.css +60 -44
  90. package/dist/surface/index.d.ts +10 -2
  91. package/dist/surface/index.js +1 -1
  92. package/dist/undo/index.d.ts +5 -3
  93. package/package.json +11 -11
  94. package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
  95. package/src/annotations/AnnotationOverlay.tsx +50 -31
  96. package/src/annotations/Annotations.overlay.test.tsx +31 -0
  97. package/src/annotations/capture.browser.test.ts +41 -0
  98. package/src/annotations/capture.ts +14 -7
  99. package/src/annotations/drawOne.ts +2 -2
  100. package/src/annotations/frac.test.ts +44 -1
  101. package/src/annotations/frac.ts +46 -18
  102. package/src/annotations/index.ts +1 -0
  103. package/src/annotations/paint.test.ts +74 -33
  104. package/src/annotations/paint.ts +70 -32
  105. package/src/annotations/store.test.ts +212 -2
  106. package/src/annotations/store.ts +130 -27
  107. package/src/annotations/svgNodes.test.ts +22 -10
  108. package/src/annotations/svgNodes.ts +3 -3
  109. package/src/annotations/types.ts +29 -10
  110. package/src/canvas/CameraInput.test.tsx +30 -0
  111. package/src/canvas/CameraInput.tsx +16 -15
  112. package/src/canvas/CanvasStack.tsx +5 -5
  113. package/src/canvas/CanvasStackContext.ts +4 -1
  114. package/src/canvas/Stage.tsx +3 -3
  115. package/src/canvas/cameraRegistry.ts +1 -0
  116. package/src/canvas/worldSpec.ts +3 -1
  117. package/src/chrome/builtins.test.ts +7 -6
  118. package/src/chrome/builtins.tsx +5 -1
  119. package/src/chrome/index.ts +1 -1
  120. package/src/chrome/merge.test.ts +5 -0
  121. package/src/chrome/merge.ts +4 -2
  122. package/src/chrome/regions/PaletteRegion.test.tsx +2 -3
  123. package/src/chrome/regions/PaletteRegion.tsx +3 -4
  124. package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
  125. package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
  126. package/src/chrome/regions/ViewportRegion.less +6 -0
  127. package/src/chrome/regions/ViewportRegion.tsx +10 -4
  128. package/src/chrome/types.ts +4 -2
  129. package/src/config/auto.ts +1 -0
  130. package/src/config/builder.ts +6 -0
  131. package/src/config/types.ts +2 -1
  132. package/src/controls/ControlPanel.test.tsx +37 -3
  133. package/src/controls/ControlPanel.tsx +12 -3
  134. package/src/dragdrop/DragDropRuntime.tsx +22 -13
  135. package/src/dragdrop/DragGhost.tsx +3 -0
  136. package/src/dragdrop/Palette.tsx +2 -0
  137. package/src/index.test.ts +5 -0
  138. package/src/index.ts +1 -1
  139. package/src/instrument/types.ts +0 -6
  140. package/src/job/useJob.ts +14 -9
  141. package/src/lab/Lab.tsx +5 -1
  142. package/src/lab/LabShell.less +1 -0
  143. package/src/lab/LabSwitcher.browser.test.tsx +27 -0
  144. package/src/lab/LabSwitcher.less +2 -0
  145. package/src/lab/LabZoom.tsx +4 -4
  146. package/src/lab/Workspace.tsx +48 -52
  147. package/src/loupe/AGENTS.md +28 -15
  148. package/src/loupe/CanvasLoupe.tsx +2 -3
  149. package/src/loupe/DomLoupe.tsx +1 -7
  150. package/src/loupe/Loupe.less +16 -0
  151. package/src/loupe/TrialLoupe.tsx +120 -57
  152. package/src/loupe/index.ts +1 -6
  153. package/src/loupe/types.test.ts +3 -3
  154. package/src/loupe/types.ts +24 -27
  155. package/src/loupe/useLoupe.test.tsx +29 -4
  156. package/src/loupe/useLoupe.ts +20 -17
  157. package/src/overview/OverviewMarks.tsx +12 -4
  158. package/src/overview/TrialOverview.tsx +3 -3
  159. package/src/passthrough/weasel-ui.test.ts +1 -1
  160. package/src/passthrough/weasel-ui.ts +1 -1
  161. package/src/primitives/FloatingPanel.tsx +7 -3
  162. package/src/primitives/JobProgress.less +1 -0
  163. package/src/primitives/Readout.test.tsx +2 -2
  164. package/src/primitives/Split.tsx +69 -41
  165. package/src/primitives/Toolbar.tsx +3 -3
  166. package/src/primitives/ZoomControl.less +1 -2
  167. package/src/specimen/Specimen.tsx +0 -1
  168. package/src/state/undock.ts +3 -0
  169. package/src/state/useOpenOnce.ts +20 -16
  170. package/src/state/view.ts +1 -0
  171. package/src/surface/index.ts +1 -1
  172. package/src/surface/useTiledSurface.test.tsx +14 -0
  173. package/src/surface/useTiledSurface.ts +11 -3
  174. package/src/trial/Trial.annotations.test.tsx +73 -0
  175. package/src/trial/Trial.less +0 -16
  176. package/src/trial/Trial.loupe.test.tsx +28 -11
  177. package/src/trial/Trial.tsx +74 -62
  178. package/src/trial/TrialChrome.tsx +6 -3
  179. package/src/trial/index.ts +2 -0
  180. package/src/trial/loupeSwitch.ts +15 -0
  181. package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
  182. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
  183. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
  184. package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
  185. package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
  186. package/dist/chunk-275JRRVD.js.map +0 -1
  187. package/dist/chunk-4LJSC7MB.js +0 -143
  188. package/dist/chunk-4LJSC7MB.js.map +0 -1
  189. package/dist/chunk-7BO7HNGF.js.map +0 -1
  190. package/dist/chunk-CQLQPQ4P.js.map +0 -1
  191. package/dist/chunk-FMK7UQ6V.js.map +0 -1
  192. package/dist/chunk-FZ7YFQN7.js.map +0 -1
  193. package/dist/chunk-HXXTULDN.js.map +0 -1
  194. package/dist/chunk-I5DUQ5HM.js.map +0 -1
  195. package/dist/chunk-IJJUK4FU.js.map +0 -1
  196. package/dist/chunk-J7OWWTCW.js.map +0 -1
  197. package/dist/chunk-M7HP3FTK.js +0 -549
  198. package/dist/chunk-M7HP3FTK.js.map +0 -1
  199. package/dist/chunk-MIM5R4ZT.js +0 -14972
  200. package/dist/chunk-MIM5R4ZT.js.map +0 -1
  201. package/dist/chunk-RWNDDKOH.js.map +0 -1
  202. package/dist/chunk-T5IBSPAM.js.map +0 -1
  203. package/dist/chunk-UNAZSLWE.js.map +0 -1
  204. package/dist/chunk-WPLAANH4.js.map +0 -1
  205. package/dist/chunk-Y23G2VZL.js.map +0 -1
  206. package/dist/chunk-ZIFPHIWZ.js.map +0 -1
  207. package/src/primitives/useRovingTabIndex.test.ts +0 -48
  208. package/src/primitives/useRovingTabIndex.ts +0 -88
@@ -1,8 +1,587 @@
1
- export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LOUPE_MAGNIFY_ID, LOUPE_PEEK_ID, LoupeBubble, LoupeGestures, TrialLoupe, createLoupeActions, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe } from '../chunk-M7HP3FTK.js';
2
- import '../chunk-JVBYV6TL.js';
3
- import '../chunk-AWQG7FCC.js';
4
- import '../chunk-UNAZSLWE.js';
5
- import '../chunk-24W5VEJW.js';
6
- import '../chunk-CQLQPQ4P.js';
1
+ import { LoupeSwitchContext } from '../chunk-2ES4W47N.js';
2
+ import { centerOn, zoomAt } from '../chunk-ROGJWDQA.js';
3
+ import { screenToWorld } from '../chunk-FG3EXZDV.js';
4
+ import { CameraContext, CameraScope, fromCameraView } from '../chunk-VYVGVBGY.js';
5
+ import { resolveFrame, DEFAULT_FRAME } from '../chunk-ULYYNWGP.js';
6
+ import { CanvasStackContext } from '../chunk-ZQIHJXP4.js';
7
+ import { useLatest, useVisibleRaf, useActionsRegistry, useGestureDispatcher, normalizeZoom } from '@weasel-js/core';
8
+ import { useRef, useEffect, useContext, useReducer, useCallback, useMemo, useState } from 'react';
9
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
+
11
+ // src/loupe/canvasLens.ts
12
+ function lensCamera(aim, outer, outerFrame, factor, diameter, worldSpec) {
13
+ const size = { width: diameter, height: diameter };
14
+ const frame = resolveFrame(worldSpec, size);
15
+ const world = screenToWorld(aim, outer, outerFrame);
16
+ return { view: centerOn(world, outer.zoom * factor, size, frame), frame };
17
+ }
18
+ function lensSourceRect(aim, factor, diameter, dpr) {
19
+ const span = diameter / factor;
20
+ return {
21
+ sx: (aim.x - span / 2) * dpr,
22
+ sy: (aim.y - span / 2) * dpr,
23
+ sw: span * dpr,
24
+ sh: span * dpr
25
+ };
26
+ }
27
+ function sampleStack(layers, canvases, p, dpr) {
28
+ for (let i = layers.length - 1; i >= 0; i--) {
29
+ const layer = layers[i];
30
+ if (!layer.visible) continue;
31
+ const canvas = canvases.get(layer.id);
32
+ if (!canvas) continue;
33
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
34
+ if (!ctx) continue;
35
+ const x = Math.floor(p.x * dpr);
36
+ const y = Math.floor(p.y * dpr);
37
+ if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) continue;
38
+ const [r, g, b, a] = ctx.getImageData(x, y, 1, 1).data;
39
+ if (a === 0) continue;
40
+ return `#${[r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
41
+ }
42
+ return null;
43
+ }
44
+ function drawCanvasLens(ctx, opts) {
45
+ const { aim, factor, diameter, dpr, mode, outer, layers, canvases } = opts;
46
+ ctx.save();
47
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
48
+ ctx.clearRect(0, 0, diameter, diameter);
49
+ if (mode === "pixel") {
50
+ const { sx, sy, sw, sh } = lensSourceRect(aim, factor, diameter, dpr);
51
+ ctx.imageSmoothingEnabled = false;
52
+ for (const layer of layers) {
53
+ if (!layer.visible) continue;
54
+ const canvas = canvases.get(layer.id);
55
+ if (!canvas || canvas.width === 0 || canvas.height === 0) continue;
56
+ ctx.drawImage(canvas, sx, sy, sw, sh, 0, 0, diameter, diameter);
57
+ }
58
+ ctx.restore();
59
+ return;
60
+ }
61
+ const lens = lensCamera(
62
+ aim,
63
+ outer,
64
+ opts.outerFrame ?? DEFAULT_FRAME,
65
+ factor,
66
+ diameter,
67
+ opts.worldSpec
68
+ );
69
+ for (const layer of layers) {
70
+ if (!layer.visible) continue;
71
+ ctx.save();
72
+ layer.render(ctx, lens.view, lens.frame);
73
+ ctx.restore();
74
+ }
75
+ ctx.restore();
76
+ }
77
+ function CanvasLoupe({
78
+ aim,
79
+ factor,
80
+ mode,
81
+ diameter,
82
+ surface,
83
+ view,
84
+ frame,
85
+ worldSpec
86
+ }) {
87
+ const canvasRef = useRef(null);
88
+ const dpr = surface.size.dpr;
89
+ const args = { aim, factor, mode, diameter, dpr, view, frame, worldSpec, surface };
90
+ const argsRef = useLatest(args);
91
+ const loop = useVisibleRaf(
92
+ () => {
93
+ loop.request();
94
+ const canvas = canvasRef.current;
95
+ const ctx = canvas?.getContext("2d");
96
+ const canvases = argsRef.current.surface.canvases.current;
97
+ if (!ctx || !canvases) return;
98
+ const a = argsRef.current;
99
+ drawCanvasLens(ctx, {
100
+ aim: a.aim,
101
+ factor: a.factor,
102
+ diameter: a.diameter,
103
+ dpr: a.dpr,
104
+ mode: a.mode,
105
+ outer: a.view,
106
+ outerFrame: a.frame,
107
+ worldSpec: a.worldSpec,
108
+ layers: a.surface.layers,
109
+ canvases
110
+ });
111
+ },
112
+ { target: canvasRef }
113
+ );
114
+ useEffect(() => {
115
+ loop.request();
116
+ return () => loop.cancel();
117
+ }, [loop]);
118
+ return /* @__PURE__ */ jsx(
119
+ "canvas",
120
+ {
121
+ ref: canvasRef,
122
+ className: "lk-loupe__canvas",
123
+ width: Math.max(1, Math.round(diameter * dpr)),
124
+ height: Math.max(1, Math.round(diameter * dpr))
125
+ }
126
+ );
127
+ }
128
+ function DomLoupe({
129
+ aim,
130
+ factor,
131
+ mode,
132
+ diameter,
133
+ size,
134
+ view,
135
+ frame,
136
+ render
137
+ }) {
138
+ const style = {
139
+ width: `${size.width}px`,
140
+ height: `${size.height}px`,
141
+ transform: `translate(${diameter / 2 - aim.x}px, ${diameter / 2 - aim.y}px)`
142
+ };
143
+ return /* @__PURE__ */ jsx("div", { className: "lk-loupe__stage", style, children: render({
144
+ view: zoomAt(view, factor, aim, { frame }),
145
+ factor,
146
+ mode,
147
+ size
148
+ }) });
149
+ }
150
+ function LoupeBubble({ aim, diameter, hostRef, children }) {
151
+ const style = {
152
+ width: `${diameter}px`,
153
+ height: `${diameter}px`,
154
+ transform: `translate(${aim.x - diameter / 2}px, ${aim.y - diameter / 2}px)`
155
+ };
156
+ return /* @__PURE__ */ jsx("div", { ref: hostRef, className: "lk-loupe", style, "aria-hidden": "true", children });
157
+ }
158
+
159
+ // src/loupe/loupeActions.ts
160
+ var LOUPE_PEEK_ID = "loupe.peek";
161
+ var LOUPE_MAGNIFY_ID = "loupe.magnify";
162
+ var WHEEL_RATE = 2e-3;
163
+ function createLoupeActions(input, peekKey) {
164
+ const magnify = {
165
+ id: LOUPE_MAGNIFY_ID,
166
+ label: "Magnify (loupe)",
167
+ group: "loupe",
168
+ // Hotkey scope, so while the lens is up it outranks the trial camera's
169
+ // wheel zoom on the same dispatcher; while it is down `enabled` declines
170
+ // and the zoom runs.
171
+ scope: "hotkey",
172
+ // Every modifier optional: the lens claims the wheel whenever it is up,
173
+ // which is what the hand-rolled capture-phase listener did. A bare spec
174
+ // would forbid modifiers and hand Cmd+wheel back to the lab mid-peek.
175
+ defaultBinding: [
176
+ {
177
+ spec: {
178
+ kind: "wheel",
179
+ mods: { alt: "optional", ctrl: "optional", meta: "optional", shift: "optional" }
180
+ },
181
+ opts: {}
182
+ }
183
+ ],
184
+ enabled: () => input.shown() ? true : "not-applicable",
185
+ invoker: {
186
+ timing: "immediate",
187
+ run(_deps, params) {
188
+ input.magnifyBy(params?.deltaY ?? 0);
189
+ }
190
+ }
191
+ };
192
+ if (peekKey === null) return [magnify];
193
+ const peek = {
194
+ id: LOUPE_PEEK_ID,
195
+ label: "Peek (loupe)",
196
+ group: "loupe",
197
+ // Hotkey scope: peeking must beat whatever the lab's active tool binds to
198
+ // the same key, the way every other held-key trigger does.
199
+ scope: "hotkey",
200
+ // Every modifier optional because the peek key may BE one: the keydown
201
+ // that carries `Alt` — the default — also reports `altKey: true`, and the
202
+ // matcher forbids any modifier a spec does not name.
203
+ defaultBinding: [
204
+ {
205
+ spec: {
206
+ kind: "key-held",
207
+ key: peekKey,
208
+ mods: { alt: "optional", ctrl: "optional", meta: "optional", shift: "optional" }
209
+ },
210
+ opts: {}
211
+ }
212
+ ],
213
+ invoker: {
214
+ timing: "ongoing",
215
+ start() {
216
+ input.setPeeking(true);
217
+ return { onEnd: () => input.setPeeking(false) };
218
+ }
219
+ }
220
+ };
221
+ return [peek, magnify];
222
+ }
223
+ var NO_TOOLS = /* @__PURE__ */ new Map();
224
+ var CHANNELS = { pointer: false, contextMenu: false, ingest: false };
225
+ function LoupeDispatch({
226
+ hostRef,
227
+ input,
228
+ peekKey,
229
+ registry,
230
+ ownDispatcher
231
+ }) {
232
+ const actions = useMemo(() => createLoupeActions(input, peekKey), [input, peekKey]);
233
+ useEffect(() => {
234
+ const offs = actions.map((a) => registry.register(a));
235
+ return () => {
236
+ for (const off of offs) off();
237
+ };
238
+ }, [registry, actions]);
239
+ useGestureDispatcher({
240
+ canvasRef: hostRef,
241
+ actions: registry,
242
+ entriesById: NO_TOOLS,
243
+ channels: CHANNELS,
244
+ enabled: ownDispatcher
245
+ });
246
+ return null;
247
+ }
248
+ function LoupeGestures(props) {
249
+ const registry = useActionsRegistry();
250
+ const camera = useContext(CameraContext);
251
+ if (!registry) return null;
252
+ return /* @__PURE__ */ jsx(LoupeDispatch, { ...props, registry, ownDispatcher: camera === null });
253
+ }
254
+
255
+ // src/loupe/types.ts
256
+ var LOUPE_DEFAULTS = {
257
+ factor: 6,
258
+ minFactor: 2,
259
+ maxFactor: 32,
260
+ mode: "vector",
261
+ diameter: 200,
262
+ peekKey: "Alt"
263
+ };
264
+ function resolveLoupe(options = {}) {
265
+ const minFactor = options.minFactor ?? LOUPE_DEFAULTS.minFactor;
266
+ const maxFactor = options.maxFactor ?? LOUPE_DEFAULTS.maxFactor;
267
+ return {
268
+ render: options.render,
269
+ onColorChange: options.onColorChange,
270
+ minFactor,
271
+ maxFactor,
272
+ factor: Math.min(maxFactor, Math.max(minFactor, options.factor ?? LOUPE_DEFAULTS.factor)),
273
+ diameter: options.diameter ?? LOUPE_DEFAULTS.diameter,
274
+ peekKey: options.peekKey === void 0 ? LOUPE_DEFAULTS.peekKey : options.peekKey,
275
+ // A DOM loupe has no framebuffer, so `pixel` would have nothing to enlarge.
276
+ mode: options.render ? "vector" : options.mode ?? LOUPE_DEFAULTS.mode
277
+ };
278
+ }
279
+ var ZERO = { width: 0, height: 0 };
280
+ function useHostSize(ref) {
281
+ const [size, setSize] = useState(ZERO);
282
+ useEffect(() => {
283
+ const el = ref.current;
284
+ if (!el) return;
285
+ const update = () => {
286
+ const rect = el.getBoundingClientRect();
287
+ setSize(
288
+ (prev) => prev.width === rect.width && prev.height === rect.height ? prev : { width: rect.width, height: rect.height }
289
+ );
290
+ };
291
+ update();
292
+ if (typeof ResizeObserver === "undefined") return;
293
+ const ro = new ResizeObserver(update);
294
+ ro.observe(el);
295
+ return () => ro.disconnect();
296
+ }, [ref]);
297
+ return size;
298
+ }
299
+ function loupeSourcePoint(p, rect, aim, factor) {
300
+ return {
301
+ x: aim.x + (p.x - (rect.x + rect.w / 2)) / factor,
302
+ y: aim.y + (p.y - (rect.y + rect.h / 2)) / factor
303
+ };
304
+ }
305
+ function createLoupeModel(opts) {
306
+ const { surface } = opts;
307
+ let mode = opts.mode ?? "vector";
308
+ let factor = normalizeZoom(opts.factor ?? 8);
309
+ let aim = { x: 0, y: 0 };
310
+ let color = null;
311
+ let disposed = false;
312
+ let aimUnsampled = false;
313
+ const clamp = (n) => normalizeZoom(Math.min(
314
+ opts.maxFactor ?? Number.POSITIVE_INFINITY,
315
+ Math.max(opts.minFactor ?? Number.NEGATIVE_INFINITY, n)
316
+ ));
317
+ const sampleColor = () => {
318
+ const hex = surface.sample(aim);
319
+ if (hex === null || hex === color) return;
320
+ color = hex;
321
+ opts.onColorChange?.(hex);
322
+ };
323
+ const unsubscribeFrame = surface.subscribeFrame?.(() => {
324
+ if (!aimUnsampled) return;
325
+ aimUnsampled = false;
326
+ sampleColor();
327
+ });
328
+ const teardown = () => {
329
+ if (disposed) return;
330
+ disposed = true;
331
+ unsubscribeFrame?.();
332
+ opts.onDispose?.();
333
+ };
334
+ return {
335
+ get mode() {
336
+ return mode;
337
+ },
338
+ get factor() {
339
+ return factor;
340
+ },
341
+ get aim() {
342
+ return aim;
343
+ },
344
+ get color() {
345
+ return color;
346
+ },
347
+ setMode(next) {
348
+ if (disposed || next === mode) return;
349
+ mode = next;
350
+ surface.changed();
351
+ },
352
+ setFactor(next) {
353
+ if (disposed) return;
354
+ factor = clamp(next);
355
+ surface.changed();
356
+ },
357
+ aimAt(p) {
358
+ if (disposed) return;
359
+ if (surface.gone()) {
360
+ teardown();
361
+ return;
362
+ }
363
+ if (surface.hidden()) return;
364
+ if (surface.covers(p)) return;
365
+ aim = p;
366
+ if (unsubscribeFrame) aimUnsampled = true;
367
+ else sampleColor();
368
+ surface.changed();
369
+ },
370
+ pick(p) {
371
+ if (disposed) return null;
372
+ let at = aim;
373
+ if (p) {
374
+ const rect = surface.lens();
375
+ if (!rect) return null;
376
+ at = loupeSourcePoint(p, rect, aim, factor);
377
+ if (surface.covers(at)) return null;
378
+ }
379
+ const hex = surface.sample(at);
380
+ if (hex !== null) opts.onPick?.(hex);
381
+ return hex;
382
+ },
383
+ dispose: teardown
384
+ };
385
+ }
386
+ function lensShown(over, enabled, peeking) {
387
+ return over && (enabled || peeking);
388
+ }
389
+ function useLoupe({ options, hostRef, enabled, sample }) {
390
+ const [, bump] = useReducer((n) => n + 1, 0);
391
+ const overRef = useRef(false);
392
+ const peekingRef = useRef(false);
393
+ const enabledRef = useLatest(enabled);
394
+ const sampleRef = useLatest(sample);
395
+ const onColorChangeRef = useLatest(options.onColorChange);
396
+ const shown = useCallback(
397
+ () => lensShown(overRef.current, enabledRef.current, peekingRef.current),
398
+ [enabledRef]
399
+ );
400
+ const modelRef = useRef(null);
401
+ if (modelRef.current === null) {
402
+ modelRef.current = createLoupeModel({
403
+ mode: options.mode,
404
+ factor: options.factor,
405
+ minFactor: options.minFactor,
406
+ maxFactor: options.maxFactor,
407
+ onColorChange: (hex) => onColorChangeRef.current?.(hex),
408
+ surface: {
409
+ lens: () => {
410
+ const d = diameterRef.current;
411
+ const { x, y } = modelRef.current?.aim ?? { x: 0, y: 0 };
412
+ return { x: x - d / 2, y: y - d / 2, w: d, h: d };
413
+ },
414
+ // The lens follows the pointer and is painted over the host rather than
415
+ // into it, so it is never part of the picture being magnified.
416
+ covers: () => false,
417
+ sample: (p) => sampleRef.current?.(p) ?? null,
418
+ hidden: () => !shown(),
419
+ gone: () => goneRef.current,
420
+ changed: bump
421
+ }
422
+ });
423
+ }
424
+ const model = modelRef.current;
425
+ const diameterRef = useLatest(options.diameter);
426
+ const goneRef = useRef(false);
427
+ useEffect(() => {
428
+ goneRef.current = false;
429
+ return () => {
430
+ goneRef.current = true;
431
+ };
432
+ }, []);
433
+ useEffect(() => {
434
+ if (!enabled && !peekingRef.current) bump();
435
+ }, [enabled]);
436
+ useEffect(() => {
437
+ const host = hostRef.current;
438
+ if (!host) return;
439
+ const onPointerMove = (e) => {
440
+ overRef.current = true;
441
+ const rect = host.getBoundingClientRect();
442
+ model.aimAt({ x: e.clientX - rect.left, y: e.clientY - rect.top });
443
+ if (!shown()) return;
444
+ bump();
445
+ };
446
+ const onPointerLeave = () => {
447
+ overRef.current = false;
448
+ bump();
449
+ };
450
+ host.addEventListener("pointermove", onPointerMove);
451
+ host.addEventListener("pointerleave", onPointerLeave);
452
+ return () => {
453
+ host.removeEventListener("pointermove", onPointerMove);
454
+ host.removeEventListener("pointerleave", onPointerLeave);
455
+ };
456
+ }, [hostRef, model, shown]);
457
+ const inputRef = useRef(null);
458
+ if (inputRef.current === null) {
459
+ inputRef.current = {
460
+ shown,
461
+ setPeeking: (on) => {
462
+ if (peekingRef.current === on) return;
463
+ peekingRef.current = on;
464
+ bump();
465
+ },
466
+ magnifyBy: (deltaY) => {
467
+ const m = modelRef.current;
468
+ if (m) m.setFactor(m.factor * Math.exp(-deltaY * WHEEL_RATE));
469
+ }
470
+ };
471
+ }
472
+ return {
473
+ // This render's `enabled`: `enabledRef` holds the last committed one.
474
+ visible: lensShown(overRef.current, enabled, peekingRef.current),
475
+ aim: model.aim,
476
+ factor: model.factor,
477
+ mode: model.mode,
478
+ color: model.color,
479
+ setMode: model.setMode,
480
+ setFactor: model.setFactor,
481
+ pick: model.pick,
482
+ input: inputRef.current
483
+ };
484
+ }
485
+ var IDENTITY = { zoom: 1, pan: { x: 0, y: 0 } };
486
+ function TrialLoupe({
487
+ enabled,
488
+ view,
489
+ hostRef,
490
+ children,
491
+ render,
492
+ factor,
493
+ minFactor,
494
+ maxFactor,
495
+ mode,
496
+ diameter,
497
+ peekKey,
498
+ onColorChange
499
+ }) {
500
+ const options = useMemo(
501
+ () => resolveLoupe({
502
+ render,
503
+ factor,
504
+ minFactor,
505
+ maxFactor,
506
+ mode,
507
+ diameter,
508
+ peekKey,
509
+ onColorChange
510
+ }),
511
+ [render, factor, minFactor, maxFactor, mode, diameter, peekKey, onColorChange]
512
+ );
513
+ const stack = useContext(CanvasStackContext);
514
+ const camera = useContext(CameraContext);
515
+ const loupeSwitch = useContext(LoupeSwitchContext);
516
+ const surface = options.render ? void 0 : stack?.surface;
517
+ const ownHost = useRef(null);
518
+ const cameraHost = useMemo(
519
+ () => camera ? {
520
+ get current() {
521
+ return camera.element();
522
+ }
523
+ } : null,
524
+ [camera]
525
+ );
526
+ const host = surface?.element ?? hostRef ?? cameraHost ?? ownHost;
527
+ const wraps = host === ownHost;
528
+ const mount = enabled === void 0 ? loupeSwitch?.mount : void 0;
529
+ useEffect(() => mount?.(), [mount]);
530
+ const sample = useMemo(() => {
531
+ if (!surface) return void 0;
532
+ return (p) => {
533
+ const canvases = surface.canvases.current;
534
+ return canvases ? sampleStack(surface.layers, canvases, p, surface.size.dpr) : null;
535
+ };
536
+ }, [surface]);
537
+ const loupe = useLoupe({
538
+ options,
539
+ hostRef: host,
540
+ enabled: enabled ?? loupeSwitch?.on ?? false,
541
+ sample
542
+ });
543
+ const measured = useHostSize(host);
544
+ const size = surface?.size ?? measured;
545
+ const gestures = /* @__PURE__ */ jsx(CameraScope, { children: /* @__PURE__ */ jsx(LoupeGestures, { hostRef: host, input: loupe.input, peekKey: options.peekKey ?? null }) });
546
+ const lens = loupe.visible ? /* @__PURE__ */ jsx(LoupeBubble, { aim: loupe.aim, diameter: options.diameter, children: options.render ? /* @__PURE__ */ jsx(
547
+ DomLoupe,
548
+ {
549
+ aim: loupe.aim,
550
+ factor: loupe.factor,
551
+ mode: loupe.mode,
552
+ diameter: options.diameter,
553
+ size,
554
+ view: view ?? stack?.view ?? (camera ? fromCameraView(camera.view.get(), camera.frame) : IDENTITY),
555
+ frame: stack?.frame ?? camera?.frame,
556
+ render: options.render
557
+ }
558
+ ) : surface && stack ? /* @__PURE__ */ jsx(
559
+ CanvasLoupe,
560
+ {
561
+ aim: loupe.aim,
562
+ factor: loupe.factor,
563
+ mode: loupe.mode,
564
+ diameter: options.diameter,
565
+ surface,
566
+ view: stack.view,
567
+ frame: stack.frame,
568
+ worldSpec: surface.worldSpec
569
+ }
570
+ ) : null }) : null;
571
+ if (wraps) {
572
+ return /* @__PURE__ */ jsxs("div", { ref: ownHost, className: "lk-loupe-host", children: [
573
+ children,
574
+ gestures,
575
+ lens
576
+ ] });
577
+ }
578
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
579
+ children,
580
+ gestures,
581
+ lens
582
+ ] });
583
+ }
584
+
585
+ export { CanvasLoupe, DomLoupe, LOUPE_DEFAULTS, LOUPE_MAGNIFY_ID, LOUPE_PEEK_ID, LoupeBubble, LoupeGestures, TrialLoupe, createLoupeActions, drawCanvasLens, lensCamera, lensSourceRect, resolveLoupe, sampleStack, useLoupe };
7
586
  //# sourceMappingURL=index.js.map
8
587
  //# sourceMappingURL=index.js.map