@weasel-js/hud 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/attach.d.ts CHANGED
@@ -6,5 +6,13 @@ export interface AttachHudOptions {
6
6
  * default mode; pass `useTheme().resolved` to follow a live theme. */
7
7
  readonly theme?: ResolvedTheme;
8
8
  }
9
+ /**
10
+ * Bind a HUD to a canvas: register its render layer, route pointer input to
11
+ * the widget under the cursor, and kick off default-font registration. Returns
12
+ * the detach function.
13
+ *
14
+ * Throws if the HUD is already attached — a HUD belongs to one canvas at a
15
+ * time.
16
+ */
9
17
  export declare function attachHud(api: CanvasExtensionApi, hud: Hud, options?: AttachHudOptions): () => void;
10
18
  //# sourceMappingURL=attach.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,KAAK,EAAE,kBAAkB,EAA+B,MAAM,iBAAiB,CAAC;AAOvF,OAAO,EAA6B,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjF,MAAM,WAAW,gBAAgB;IAC/B;2EACuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED,wBAAgB,SAAS,CACvB,GAAG,EAAE,kBAAkB,EACvB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,IAAI,CAwHZ"}
1
+ {"version":3,"file":"attach.d.ts","sourceRoot":"","sources":["../src/attach.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AACjC,OAAO,KAAK,EAAE,kBAAkB,EAA+B,MAAM,iBAAiB,CAAC;AAOvF,OAAO,EAA6B,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjF,MAAM,WAAW,gBAAgB;IAC/B;2EACuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,kBAAkB,EACvB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,IAAI,CAkIZ"}
@@ -1,3 +1,7 @@
1
+ /** Family name the bundled HUD font registers under. Widgets that name no
2
+ * family draw with it. */
1
3
  export declare const DEFAULT_FONT_FAMILY = "weasel-hud-default";
4
+ /** Register the bundled Inter atlas under `DEFAULT_FONT_FAMILY`. `attachHud`
5
+ * calls this; calling it again is a no-op. */
2
6
  export declare function registerDefaultFont(): Promise<void>;
3
7
  //# sourceMappingURL=registerDefaultFont.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registerDefaultFont.d.ts","sourceRoot":"","sources":["../../src/fonts/registerDefaultFont.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AAExD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEzD"}
1
+ {"version":3,"file":"registerDefaultFont.d.ts","sourceRoot":"","sources":["../../src/fonts/registerDefaultFont.ts"],"names":[],"mappings":"AAOA;2BAC2B;AAC3B,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AAExD;+CAC+C;AAC/C,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEzD"}
package/dist/host.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { RenderLayer } from '@weasel-js/core';
2
+ /** What a HUD needs from whatever is hosting it: somewhere to register its
3
+ * render layer, and a way to ask for a repaint. `attachHud` supplies this
4
+ * from a canvas; a test or a headless renderer can supply its own. */
2
5
  export interface HudHost {
3
6
  requestRedraw(): void;
4
7
  registerLayer(layer: RenderLayer<unknown>): () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,WAAW,OAAO;IACtB,aAAa,IAAI,IAAI,CAAC;IACtB,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC;CACxD"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD;;uEAEuE;AACvE,MAAM,WAAW,OAAO;IACtB,aAAa,IAAI,IAAI,CAAC;IACtB,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC;CACxD"}
package/dist/hud.d.ts CHANGED
@@ -6,6 +6,15 @@ import { type ImageOptions, type ImageWidget } from './widgets/image';
6
6
  import { type LabelOptions, type LabelWidget } from './widgets/label';
7
7
  import { type ButtonOptions, type ButtonWidget } from './widgets/button';
8
8
  import { type WindowOptions, type WindowWidget } from './widgets/window/window';
9
+ /**
10
+ * A HUD: an ordered set of widgets drawn in screen space over the canvas, and
11
+ * the factories that create them. The factory methods (`rect`, `text`, …) also
12
+ * add the widget and wire its mutations to a redraw; the bare `createRect`-style
13
+ * factories do neither, leaving redraw scheduling to the caller.
14
+ *
15
+ * A HUD holds no GL state of its own. It becomes visible by binding to a host
16
+ * — normally through `attachHud`.
17
+ */
9
18
  export interface Hud {
10
19
  add(widget: Widget): void;
11
20
  remove(widget: Widget): void;
@@ -28,5 +37,6 @@ export interface Hud {
28
37
  /** Create a window widget, add it to the HUD, and wire onChange → markDirty. */
29
38
  window(opts: WindowOptions): WindowWidget;
30
39
  }
40
+ /** Create an empty, unbound HUD. */
31
41
  export declare function createHud(): Hud;
32
42
  //# sourceMappingURL=hud.d.ts.map
package/dist/hud.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hud.d.ts","sourceRoot":"","sources":["../src/hud.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAc,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EAAc,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,EAAgB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EAAgB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE9F,MAAM,WAAW,GAAG;IAClB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,IAAI,SAAS,MAAM,EAAE,CAAC;IAC7B,SAAS,IAAI,IAAI,CAAC;IAClB,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,MAAM,IAAI,IAAI,CAAC;IACf,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,8EAA8E;IAC9E,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC;IACpC,8EAA8E;IAC9E,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC;IACpC,gFAAgF;IAChF,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAAC;IACvC,+EAA+E;IAC/E,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAAC;IACvC,gFAAgF;IAChF,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAAC;IAC1C,gFAAgF;IAChF,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAAC;CAC3C;AAED,wBAAgB,SAAS,IAAI,GAAG,CA0G/B"}
1
+ {"version":3,"file":"hud.d.ts","sourceRoot":"","sources":["../src/hud.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAc,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EAAc,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnF,OAAO,EAAgB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACvF,OAAO,EAAgB,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE9F;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAG;IAClB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,IAAI,SAAS,MAAM,EAAE,CAAC;IAC7B,SAAS,IAAI,IAAI,CAAC;IAClB,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1B,MAAM,IAAI,IAAI,CAAC;IACf,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,8EAA8E;IAC9E,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC;IACpC,8EAA8E;IAC9E,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC;IACpC,gFAAgF;IAChF,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAAC;IACvC,+EAA+E;IAC/E,KAAK,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAAC;IACvC,gFAAgF;IAChF,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAAC;IAC1C,gFAAgF;IAChF,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAAC;CAC3C;AAED,oCAAoC;AACpC,wBAAgB,SAAS,IAAI,GAAG,CAwG/B"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./tool-DyPy2h0T.js";
1
+ import { a as e, c as t, i as n, l as r, n as i, o as a, r as o, s, t as c, u as l } from "./tool-Dta4BYKu.js";
2
2
  import { createViewportLayer as u, screenToWorld as d, viewToTransform as f } from "@weasel-js/core";
3
3
  //#region src/loupe/innerView.ts
4
4
  function p(e, t, n, r) {
@@ -116,7 +116,15 @@ function g(e) {
116
116
  y: e.clientY - t.top
117
117
  });
118
118
  }, E = (e) => {
119
- w.hidden || w.hitTest(e.x, e.y) || (s = e, C(), a === "pixel" && S(), i());
119
+ if (!_) {
120
+ if (w.disposed) {
121
+ D();
122
+ return;
123
+ }
124
+ w.hidden || w.hitTest(e.x, e.y) || (s = e, C(), a === "pixel" && S(), i());
125
+ }
126
+ }, D = () => {
127
+ _ = !0, n.removeEventListener("pointermove", T), l?.close(), l = null;
120
128
  };
121
129
  return n.addEventListener("pointermove", T), {
122
130
  window: w,
@@ -140,7 +148,7 @@ function g(e) {
140
148
  },
141
149
  aimAt: E,
142
150
  dispose() {
143
- _ = !0, n.removeEventListener("pointermove", T), l?.close(), l = null, w.dispose();
151
+ D(), w.dispose();
144
152
  }
145
153
  };
146
154
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/loupe/innerView.ts","../src/loupe/readback.ts","../src/loupe/createLoupe.ts"],"sourcesContent":["import type { View } from '@weasel-js/core';\nimport type { WidgetBounds } from '../widget';\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: { x: number; y: number },\n outer: View,\n rect: WidgetBounds,\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 * Read a `rw × rh` device-pixel region of the drawing buffer centered on\n * `pointer` (CSS px relative to the canvas), returned top-down.\n *\n * GL reports rows bottom-up; every consumer here wants top-down, so both the\n * read origin and the returned rows are flipped.\n */\nexport function readbackRegion(\n gl: WebGL2RenderingContext,\n buffer: { width: number; height: number },\n pointer: { x: number; y: number },\n dpr: number,\n rw: number,\n rh: number,\n): ImageData {\n const cx = Math.round(pointer.x * dpr);\n const cy = Math.round(pointer.y * dpr);\n const gx = clamp(cx - Math.floor(rw / 2), 0, Math.max(0, buffer.width - rw));\n const gyTop = clamp(cy - Math.floor(rh / 2), 0, Math.max(0, buffer.height - rh));\n const gy = buffer.height - gyTop - rh;\n\n const raw = new Uint8Array(rw * rh * 4);\n gl.readPixels(gx, gy, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, raw);\n\n const flipped = new Uint8ClampedArray(rw * rh * 4);\n const stride = rw * 4;\n for (let row = 0; row < rh; row++) {\n const src = (rh - 1 - row) * stride;\n flipped.set(raw.subarray(src, src + stride), row * stride);\n }\n return new ImageData(flipped, rw, rh);\n}\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return v < lo ? lo : v > hi ? hi : v;\n}\n","import {\n createViewportLayer, screenToWorld, viewToTransform,\n type RenderLayer, type View,\n} from '@weasel-js/core';\nimport type { DrawCommand } from '@weasel-js/core/renderer';\nimport type { Hud } from '../hud';\nimport type { WindowWidget } from '../widgets/window/window';\nimport type { HudContentCtx, WidgetBounds } from '../widget';\nimport { loupeInnerView } from './innerView';\nimport { readbackRegion } from './readback';\n\nexport type LoupeMode = 'vector' | 'pixel';\n\nexport interface LoupeOptions {\n hud: Hud;\n /** The live canvas. Supplies the GL context for pixel mode and the pointer\n * feed for aiming. */\n element: HTMLCanvasElement;\n /** Layers re-rendered through the magnified inner view in vector mode. */\n source: RenderLayer<unknown>[];\n requestRedraw: () => void;\n mode?: LoupeMode;\n factor?: number;\n bounds?: WidgetBounds;\n title?: string;\n /** Draw the window's titlebar and close box. Default `true`. Pass `false`\n * for a bare lens; the interior then doubles as the move handle. */\n titlebar?: boolean;\n /** Called with the hex color under the aim point whenever it changes.\n * Sampled from the framebuffer in both modes — vector content is a\n * re-render whose edge colors differ from the screen's, so reading the\n * color off it would report something the user cannot see. */\n onColorChange?: (hex: string) => void;\n /** Fired by the window's close box. The window does not hide itself —\n * whoever owns the loupe's visibility decides what closing means. */\n onClose?: () => void;\n /** Opaque backdrop behind vector content, so the outer canvas does not\n * show through where the inner view is empty. */\n background?: string;\n}\n\nexport interface LoupeHandle {\n readonly window: WindowWidget;\n readonly mode: LoupeMode;\n readonly factor: number;\n /** Last aimed point, in screen-space CSS px relative to the canvas. */\n readonly aim: { x: number; y: number };\n /** Hex color under the aim point, read off the framebuffer. `null` before\n * the first aim, or when the GL context is unavailable. */\n readonly color: string | null;\n setMode(mode: LoupeMode): void;\n setFactor(factor: number): void;\n /** Aim at a screen point. Ignored while the point is over the window —\n * that is the freeze rule that makes the borders reachable. */\n aimAt(p: { x: number; y: number }): void;\n dispose(): void;\n}\n\nexport function createLoupe(opts: LoupeOptions): LoupeHandle {\n const { hud, element, source, requestRedraw } = opts;\n let mode: LoupeMode = opts.mode ?? 'vector';\n let factor = opts.factor ?? 8;\n let aim = { x: 0, y: 0 };\n let color: string | null = null;\n let pixels: ImageBitmap | null = null;\n let pixelsPending = false;\n let pixelsStale = false;\n let disposed = false;\n\n const b = opts.bounds ?? { x: 24, y: 24, w: 220, h: 200 };\n\n const content = (ctx: HudContentCtx): DrawCommand[] =>\n mode === 'vector' ? drawVector(ctx) : drawPixels(ctx);\n\n const drawVector = (ctx: HudContentCtx): DrawCommand[] => {\n const world = screenToWorld(aim.x, aim.y, viewToTransform(ctx.view));\n const inner: View = loupeInnerView(\n { x: world[0], y: world[1] }, ctx.view, ctx.rect, factor,\n );\n // A fresh lens per frame: CreateViewportLayerOpts.view is static, and the\n // inner view moves with the pointer.\n const lens = createViewportLayer<unknown>({\n id: `${win.id}:lens`,\n label: 'Loupe content',\n source,\n view: inner,\n bounds: () => ctx.rect,\n background: opts.background ?? ctx.tokens['--wzl-surface'],\n });\n return lens.draw(ctx.data, ctx.view, ctx.dims);\n };\n\n const drawPixels = (ctx: HudContentCtx): DrawCommand[] => {\n if (!pixels) return [];\n return [{\n kind: 'image',\n image: pixels,\n x: ctx.rect.x, y: ctx.rect.y, w: ctx.rect.w, h: ctx.rect.h,\n sampling: 'nearest',\n }];\n };\n\n const refreshPixels = () => {\n if (disposed || mode !== 'pixel') return;\n // A readback requested mid-flight is remembered rather than dropped: the\n // aim that arrives during a fast drag is the one the user ends on.\n if (pixelsPending) { pixelsStale = true; return; }\n const gl = element.getContext('webgl2');\n if (!gl) return;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return;\n const dpr = element.width / cssRect.width;\n const rect = win.contentRect;\n const rw = Math.max(1, Math.round((rect.w * dpr) / factor));\n const rh = Math.max(1, Math.round((rect.h * dpr) / factor));\n const data = readbackRegion(\n gl, { width: element.width, height: element.height }, aim, dpr, rw, rh,\n );\n pixelsPending = true;\n pixelsStale = false;\n createImageBitmap(data)\n .then((bmp) => {\n if (disposed) { bmp.close(); return; }\n pixels?.close();\n pixels = bmp;\n pixelsPending = false;\n requestRedraw();\n if (pixelsStale) refreshPixels();\n })\n .catch(() => {\n pixelsPending = false;\n if (pixelsStale) refreshPixels();\n });\n };\n\n const sampleColor = () => {\n const gl = element.getContext('webgl2');\n if (!gl) return;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return;\n const dpr = element.width / cssRect.width;\n const px = readbackRegion(\n gl, { width: element.width, height: element.height }, aim, dpr, 1, 1,\n );\n const hex = '#' + [px.data[0], px.data[1], px.data[2]]\n .map((c) => c.toString(16).padStart(2, '0')).join('');\n if (hex !== color) {\n color = hex;\n opts.onColorChange?.(hex);\n }\n };\n\n const win = hud.window({\n id: 'weasel-loupe',\n x: b.x, y: b.y, w: b.w, h: b.h,\n title: opts.title ?? 'Loupe',\n titlebar: opts.titlebar,\n content,\n onResize: () => { refreshPixels(); },\n ...(opts.onClose ? { onClose: opts.onClose } : {}),\n });\n\n const onPointerMove = (evt: PointerEvent) => {\n const r = element.getBoundingClientRect();\n handleAim({ x: evt.clientX - r.left, y: evt.clientY - r.top });\n };\n\n const handleAim = (p: { x: number; y: number }) => {\n if (win.hidden) return;\n if (win.hitTest(p.x, p.y)) return; // freeze rule\n aim = p;\n sampleColor();\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n };\n\n element.addEventListener('pointermove', onPointerMove);\n\n return {\n window: win,\n get mode() { return mode; },\n get factor() { return factor; },\n get aim() { return aim; },\n get color() { return color; },\n setMode(next) {\n if (next === mode) return;\n mode = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n setFactor(next) {\n factor = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n aimAt: handleAim,\n dispose() {\n disposed = true;\n element.removeEventListener('pointermove', onPointerMove);\n pixels?.close();\n pixels = null;\n win.dispose();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/loupe/innerView.ts","../src/loupe/readback.ts","../src/loupe/createLoupe.ts"],"sourcesContent":["import type { View } from '@weasel-js/core';\nimport type { WidgetBounds } from '../widget';\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: { x: number; y: number },\n outer: View,\n rect: WidgetBounds,\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 * Read a `rw × rh` device-pixel region of the drawing buffer centered on\n * `pointer` (CSS px relative to the canvas), returned top-down.\n *\n * GL reports rows bottom-up; every consumer here wants top-down, so both the\n * read origin and the returned rows are flipped.\n */\nexport function readbackRegion(\n gl: WebGL2RenderingContext,\n buffer: { width: number; height: number },\n pointer: { x: number; y: number },\n dpr: number,\n rw: number,\n rh: number,\n): ImageData {\n const cx = Math.round(pointer.x * dpr);\n const cy = Math.round(pointer.y * dpr);\n const gx = clamp(cx - Math.floor(rw / 2), 0, Math.max(0, buffer.width - rw));\n const gyTop = clamp(cy - Math.floor(rh / 2), 0, Math.max(0, buffer.height - rh));\n const gy = buffer.height - gyTop - rh;\n\n const raw = new Uint8Array(rw * rh * 4);\n gl.readPixels(gx, gy, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, raw);\n\n const flipped = new Uint8ClampedArray(rw * rh * 4);\n const stride = rw * 4;\n for (let row = 0; row < rh; row++) {\n const src = (rh - 1 - row) * stride;\n flipped.set(raw.subarray(src, src + stride), row * stride);\n }\n return new ImageData(flipped, rw, rh);\n}\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return v < lo ? lo : v > hi ? hi : v;\n}\n","import {\n createViewportLayer, screenToWorld, viewToTransform,\n type RenderLayer, type View,\n} from '@weasel-js/core';\nimport type { DrawCommand } from '@weasel-js/core/renderer';\nimport type { Hud } from '../hud';\nimport type { WindowWidget } from '../widgets/window/window';\nimport type { HudContentCtx, WidgetBounds } from '../widget';\nimport { loupeInnerView } from './innerView';\nimport { readbackRegion } from './readback';\n\n/** How a loupe magnifies. `'vector'` re-renders the source layers through a\n * zoomed-in view, so content stays sharp at any factor; `'pixel'` reads the\n * framebuffer back and blows up the actual pixels. */\nexport type LoupeMode = 'vector' | 'pixel';\n\n/** Options for `createLoupe`. */\nexport interface LoupeOptions {\n hud: Hud;\n /** The live canvas. Supplies the GL context for pixel mode and the pointer\n * feed for aiming. */\n element: HTMLCanvasElement;\n /** Layers re-rendered through the magnified inner view in vector mode. */\n source: RenderLayer<unknown>[];\n requestRedraw: () => void;\n mode?: LoupeMode;\n factor?: number;\n bounds?: WidgetBounds;\n title?: string;\n /** Draw the window's titlebar and close box. Default `true`. Pass `false`\n * for a bare lens; the interior then doubles as the move handle. */\n titlebar?: boolean;\n /** Called with the hex color under the aim point whenever it changes.\n * Sampled from the framebuffer in both modes — vector content is a\n * re-render whose edge colors differ from the screen's, so reading the\n * color off it would report something the user cannot see. */\n onColorChange?: (hex: string) => void;\n /** Fired by the window's close box. The window does not hide itself —\n * whoever owns the loupe's visibility decides what closing means. */\n onClose?: () => void;\n /** Opaque backdrop behind vector content, so the outer canvas does not\n * show through where the inner view is empty. */\n background?: string;\n}\n\n/** Control surface for a live loupe: where it is aimed, how it magnifies,\n * what color it is over, and the window it lives in. */\nexport interface LoupeHandle {\n readonly window: WindowWidget;\n readonly mode: LoupeMode;\n readonly factor: number;\n /** Last aimed point, in screen-space CSS px relative to the canvas. */\n readonly aim: { x: number; y: number };\n /** Hex color under the aim point, read off the framebuffer. `null` before\n * the first aim, or when the GL context is unavailable. */\n readonly color: string | null;\n setMode(mode: LoupeMode): void;\n setFactor(factor: number): void;\n /** Aim at a screen point. Ignored while the point is over the window —\n * that is the freeze rule that makes the borders reachable. */\n aimAt(p: { x: number; y: number }): void;\n dispose(): void;\n}\n\n/**\n * Build a magnifier: a HUD window whose interior shows the canvas around an\n * aim point, enlarged. Adds the window to the HUD and starts following the\n * pointer; `dispose` removes it again.\n */\nexport function createLoupe(opts: LoupeOptions): LoupeHandle {\n const { hud, element, source, requestRedraw } = opts;\n let mode: LoupeMode = opts.mode ?? 'vector';\n let factor = opts.factor ?? 8;\n let aim = { x: 0, y: 0 };\n let color: string | null = null;\n let pixels: ImageBitmap | null = null;\n let pixelsPending = false;\n let pixelsStale = false;\n let disposed = false;\n\n const b = opts.bounds ?? { x: 24, y: 24, w: 220, h: 200 };\n\n const content = (ctx: HudContentCtx): DrawCommand[] =>\n mode === 'vector' ? drawVector(ctx) : drawPixels(ctx);\n\n const drawVector = (ctx: HudContentCtx): DrawCommand[] => {\n const world = screenToWorld(aim.x, aim.y, viewToTransform(ctx.view));\n const inner: View = loupeInnerView(\n { x: world[0], y: world[1] }, ctx.view, ctx.rect, factor,\n );\n // A fresh lens per frame: CreateViewportLayerOpts.view is static, and the\n // inner view moves with the pointer.\n const lens = createViewportLayer<unknown>({\n id: `${win.id}:lens`,\n label: 'Loupe content',\n source,\n view: inner,\n bounds: () => ctx.rect,\n background: opts.background ?? ctx.tokens['--wzl-surface'],\n });\n return lens.draw(ctx.data, ctx.view, ctx.dims);\n };\n\n const drawPixels = (ctx: HudContentCtx): DrawCommand[] => {\n if (!pixels) return [];\n return [{\n kind: 'image',\n image: pixels,\n x: ctx.rect.x, y: ctx.rect.y, w: ctx.rect.w, h: ctx.rect.h,\n sampling: 'nearest',\n }];\n };\n\n const refreshPixels = () => {\n if (disposed || mode !== 'pixel') return;\n // A readback requested mid-flight is remembered rather than dropped: the\n // aim that arrives during a fast drag is the one the user ends on.\n if (pixelsPending) { pixelsStale = true; return; }\n const gl = element.getContext('webgl2');\n if (!gl) return;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return;\n const dpr = element.width / cssRect.width;\n const rect = win.contentRect;\n const rw = Math.max(1, Math.round((rect.w * dpr) / factor));\n const rh = Math.max(1, Math.round((rect.h * dpr) / factor));\n const data = readbackRegion(\n gl, { width: element.width, height: element.height }, aim, dpr, rw, rh,\n );\n pixelsPending = true;\n pixelsStale = false;\n createImageBitmap(data)\n .then((bmp) => {\n if (disposed) { bmp.close(); return; }\n pixels?.close();\n pixels = bmp;\n pixelsPending = false;\n requestRedraw();\n if (pixelsStale) refreshPixels();\n })\n .catch(() => {\n pixelsPending = false;\n if (pixelsStale) refreshPixels();\n });\n };\n\n const sampleColor = () => {\n const gl = element.getContext('webgl2');\n if (!gl) return;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return;\n const dpr = element.width / cssRect.width;\n const px = readbackRegion(\n gl, { width: element.width, height: element.height }, aim, dpr, 1, 1,\n );\n const hex = '#' + [px.data[0], px.data[1], px.data[2]]\n .map((c) => c.toString(16).padStart(2, '0')).join('');\n if (hex !== color) {\n color = hex;\n opts.onColorChange?.(hex);\n }\n };\n\n const win = hud.window({\n id: 'weasel-loupe',\n x: b.x, y: b.y, w: b.w, h: b.h,\n title: opts.title ?? 'Loupe',\n titlebar: opts.titlebar,\n content,\n onResize: () => { refreshPixels(); },\n ...(opts.onClose ? { onClose: opts.onClose } : {}),\n });\n\n const onPointerMove = (evt: PointerEvent) => {\n const r = element.getBoundingClientRect();\n handleAim({ x: evt.clientX - r.left, y: evt.clientY - r.top });\n };\n\n const handleAim = (p: { x: number; y: number }) => {\n if (disposed) return;\n // The window can be removed through the HUD, which disposes it without\n // going through `LoupeHandle.dispose`. Follow it down rather than keep\n // reading pixels back for a loupe nobody can see.\n if (win.disposed) { teardown(); return; }\n if (win.hidden) return;\n if (win.hitTest(p.x, p.y)) return; // freeze rule\n aim = p;\n sampleColor();\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n };\n\n const teardown = () => {\n disposed = true;\n element.removeEventListener('pointermove', onPointerMove);\n pixels?.close();\n pixels = null;\n };\n\n element.addEventListener('pointermove', onPointerMove);\n\n return {\n window: win,\n get mode() { return mode; },\n get factor() { return factor; },\n get aim() { return aim; },\n get color() { return color; },\n setMode(next) {\n if (next === mode) return;\n mode = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n setFactor(next) {\n factor = next;\n if (mode === 'pixel') refreshPixels();\n requestRedraw();\n },\n aimAt: handleAim,\n dispose() {\n teardown();\n win.dispose();\n },\n };\n}\n"],"mappings":"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"}
@@ -2,7 +2,11 @@ import { type RenderLayer } from '@weasel-js/core';
2
2
  import type { Hud } from '../hud';
3
3
  import type { WindowWidget } from '../widgets/window/window';
4
4
  import type { WidgetBounds } from '../widget';
5
+ /** How a loupe magnifies. `'vector'` re-renders the source layers through a
6
+ * zoomed-in view, so content stays sharp at any factor; `'pixel'` reads the
7
+ * framebuffer back and blows up the actual pixels. */
5
8
  export type LoupeMode = 'vector' | 'pixel';
9
+ /** Options for `createLoupe`. */
6
10
  export interface LoupeOptions {
7
11
  hud: Hud;
8
12
  /** The live canvas. Supplies the GL context for pixel mode and the pointer
@@ -30,6 +34,8 @@ export interface LoupeOptions {
30
34
  * show through where the inner view is empty. */
31
35
  background?: string;
32
36
  }
37
+ /** Control surface for a live loupe: where it is aimed, how it magnifies,
38
+ * what color it is over, and the window it lives in. */
33
39
  export interface LoupeHandle {
34
40
  readonly window: WindowWidget;
35
41
  readonly mode: LoupeMode;
@@ -52,5 +58,10 @@ export interface LoupeHandle {
52
58
  }): void;
53
59
  dispose(): void;
54
60
  }
61
+ /**
62
+ * Build a magnifier: a HUD window whose interior shows the canvas around an
63
+ * aim point, enlarged. Adds the window to the HUD and starts following the
64
+ * pointer; `dispose` removes it again.
65
+ */
55
66
  export declare function createLoupe(opts: LoupeOptions): LoupeHandle;
56
67
  //# sourceMappingURL=createLoupe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createLoupe.d.ts","sourceRoot":"","sources":["../../src/loupe/createLoupe.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,WAAW,CAAC;AAI7D,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT;2BACuB;IACvB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;yEACqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;mEAG+D;IAC/D,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;0EACsE;IACtE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;sDACkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC;gEAC4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;oEACgE;IAChE,KAAK,CAAC,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CAkJ3D"}
1
+ {"version":3,"file":"createLoupe.d.ts","sourceRoot":"","sources":["../../src/loupe/createLoupe.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,WAAW,CAAC;AAI7D;;uDAEuD;AACvD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C,iCAAiC;AACjC,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT;2BACuB;IACvB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,0EAA0E;IAC1E,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;yEACqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;mEAG+D;IAC/D,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC;0EACsE;IACtE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;sDACkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;yDACyD;AACzD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,GAAG,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC;gEAC4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;oEACgE;IAChE,KAAK,CAAC,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CA2J3D"}
@@ -1,4 +1,4 @@
1
- import { c as e, n as t, r as n } from "../tool-DyPy2h0T.js";
1
+ import { c as e, n as t, r as n } from "../tool-Dta4BYKu.js";
2
2
  import { resolveTheme as r, weaselTheme as i } from "@weasel-js/theme";
3
3
  import { useEffect as a, useMemo as o, useState as s } from "react";
4
4
  import { useThemeOptional as c } from "@weasel-js/theme/react";
@@ -20,6 +20,9 @@ function s(e) {
20
20
  get hidden() {
21
21
  return r;
22
22
  },
23
+ get disposed() {
24
+ return t;
25
+ },
23
26
  setBounds(t) {
24
27
  a(), n = { ...t }, e.onChange?.();
25
28
  },
@@ -75,6 +78,9 @@ function c(e) {
75
78
  get hidden() {
76
79
  return a;
77
80
  },
81
+ get disposed() {
82
+ return n;
83
+ },
78
84
  setBounds(t) {
79
85
  o(), r = { ...t }, e.onChange?.();
80
86
  },
@@ -125,6 +131,9 @@ function l(e) {
125
131
  get hidden() {
126
132
  return i;
127
133
  },
134
+ get disposed() {
135
+ return t;
136
+ },
128
137
  setBounds(t) {
129
138
  a(), n = { ...t }, e.onChange?.();
130
139
  },
@@ -196,6 +205,9 @@ function d(e) {
196
205
  get hidden() {
197
206
  return i;
198
207
  },
208
+ get disposed() {
209
+ return n;
210
+ },
199
211
  setBounds(t) {
200
212
  f(), r = { ...t }, e.onChange?.();
201
213
  },
@@ -361,6 +373,9 @@ function _(n) {
361
373
  get hidden() {
362
374
  return u;
363
375
  },
376
+ get disposed() {
377
+ return l;
378
+ },
364
379
  get contentRect() {
365
380
  return m(_, a);
366
381
  },
@@ -497,17 +512,19 @@ function v() {
497
512
  if (!n) return;
498
513
  let i = e.indexOf(n);
499
514
  i !== -1 && (e.splice(i, 1), r());
515
+ }, a = (t, i) => {
516
+ if (n) {
517
+ console.warn(`weasel-hud: ${i} called on a detached HUD; ignored.`);
518
+ return;
519
+ }
520
+ e.push(t), r();
500
521
  };
501
522
  return {
502
523
  get attached() {
503
524
  return t !== null;
504
525
  },
505
- add(t) {
506
- if (n) {
507
- console.warn("weasel-hud: add() called on a detached HUD; ignored.");
508
- return;
509
- }
510
- e.push(t), r();
526
+ add(e) {
527
+ a(e, "add()");
511
528
  },
512
529
  remove(t) {
513
530
  let n = e.indexOf(t);
@@ -534,53 +551,53 @@ function v() {
534
551
  unbind() {
535
552
  t = null, n = !0;
536
553
  },
537
- rect(t) {
538
- let n = null, a = i(() => n);
539
- return n = s({
540
- ...t,
554
+ rect(e) {
555
+ let t = null, n = i(() => t);
556
+ return t = s({
557
+ ...e,
541
558
  onChange: () => r(),
542
- removeFromHud: a
543
- }), e.push(n), r(), n;
559
+ removeFromHud: n
560
+ }), a(t, "rect()"), t;
544
561
  },
545
- text(t) {
546
- let n = null, a = i(() => n);
547
- return n = c({
548
- ...t,
562
+ text(e) {
563
+ let t = null, n = i(() => t);
564
+ return t = c({
565
+ ...e,
549
566
  onChange: () => r(),
550
- removeFromHud: a
551
- }), e.push(n), r(), n;
567
+ removeFromHud: n
568
+ }), a(t, "text()"), t;
552
569
  },
553
- image(t) {
554
- let n = null, a = i(() => n);
555
- return n = l({
556
- ...t,
570
+ image(e) {
571
+ let t = null, n = i(() => t);
572
+ return t = l({
573
+ ...e,
557
574
  onChange: () => r(),
558
- removeFromHud: a
559
- }), e.push(n), r(), n;
575
+ removeFromHud: n
576
+ }), a(t, "image()"), t;
560
577
  },
561
- label(t) {
562
- let n = null, a = i(() => n);
563
- return n = u({
564
- ...t,
578
+ label(e) {
579
+ let t = null, n = i(() => t);
580
+ return t = u({
581
+ ...e,
565
582
  onChange: () => r(),
566
- removeFromHud: a
567
- }), e.push(n), r(), n;
583
+ removeFromHud: n
584
+ }), a(t, "label()"), t;
568
585
  },
569
- button(t) {
570
- let n = null, a = i(() => n);
571
- return n = d({
572
- ...t,
586
+ button(e) {
587
+ let t = null, n = i(() => t);
588
+ return t = d({
589
+ ...e,
573
590
  onChange: () => r(),
574
- removeFromHud: a
575
- }), e.push(n), r(), n;
591
+ removeFromHud: n
592
+ }), a(t, "button()"), t;
576
593
  },
577
- window(t) {
578
- let n = null, a = i(() => n);
579
- return n = _({
580
- ...t,
594
+ window(e) {
595
+ let t = null, n = i(() => t);
596
+ return t = _({
597
+ ...e,
581
598
  onChange: () => r(),
582
- removeFromHud: a
583
- }), e.push(n), r(), n;
599
+ removeFromHud: n
600
+ }), a(t, "window()"), t;
584
601
  }
585
602
  };
586
603
  }
@@ -672,12 +689,12 @@ function E(e, t, i = {}) {
672
689
  d !== c && (c && c.onPointer({
673
690
  type: "hoverleave",
674
691
  native: a
675
- }), d && d.onPointer({
692
+ }), c = d, e.requestRedraw()), d && d.onPointer({
676
693
  type: "hovermove",
677
694
  x: s,
678
695
  y: u,
679
696
  native: a
680
- }), c = d, e.requestRedraw());
697
+ });
681
698
  },
682
699
  onUncapturedLeave: () => {
683
700
  c && (c.onPointer({
@@ -690,7 +707,10 @@ function E(e, t, i = {}) {
690
707
  requestRedraw: e.requestRedraw,
691
708
  registerLayer: () => () => {}
692
709
  }), () => {
693
- u(), t.unbind();
710
+ c &&= (c.onPointer({
711
+ type: "hoverleave",
712
+ native: null
713
+ }), null), u(), t.unbind();
694
714
  };
695
715
  }
696
716
  //#endregion
@@ -919,4 +939,4 @@ function R() {
919
939
  //#endregion
920
940
  export { w as a, v as c, C as i, f as l, R as n, x as o, E as r, S as s, D as t, g as u };
921
941
 
922
- //# sourceMappingURL=tool-DyPy2h0T.js.map
942
+ //# sourceMappingURL=tool-Dta4BYKu.js.map