@weasel-js/hud 1.3.0-pre.0 → 1.4.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +69 -97
- package/dist/index.js.map +1 -1
- package/dist/loupe/createLoupe.d.ts +9 -7
- package/dist/loupe/createLoupe.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/{tool-DJRTf-Oy.js → tool-B6ZliNf3.js} +165 -145
- package/dist/tool-B6ZliNf3.js.map +1 -0
- package/dist/widgets/window/window.d.ts.map +1 -1
- package/package.json +6 -4
- package/dist/loupe/innerView.d.ts +0 -12
- package/dist/loupe/innerView.d.ts.map +0 -1
- package/dist/loupe/pick.d.ts +0 -20
- package/dist/loupe/pick.d.ts.map +0 -1
- package/dist/tool-DJRTf-Oy.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ export type { WindowOptions, WindowWidget } from './widgets/window/window';
|
|
|
14
14
|
export { DEFAULT_WINDOW_METRICS, cursorForZone, type WindowZone, type WindowMetrics, } from './widgets/window/zones';
|
|
15
15
|
export type { HudContentCtx } from './widget';
|
|
16
16
|
export { createLoupe, type LoupeOptions, type LoupeHandle, type LoupeMode } from './loupe/createLoupe';
|
|
17
|
-
export { loupeInnerView } from '
|
|
17
|
+
export { loupeInnerView } from '@weasel-js/loupe';
|
|
18
18
|
export type { ResolvedTheme } from '@weasel-js/theme';
|
|
19
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACxF,YAAY,EACV,MAAM,EACN,YAAY,EACZ,UAAU,EACV,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAC3D,YAAY,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvF,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAChG,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EACL,sBAAsB,EAAE,aAAa,EACrC,KAAK,UAAU,EAAE,KAAK,aAAa,GACpC,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACvG,OAAO,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACxF,YAAY,EACV,MAAM,EACN,YAAY,EACZ,UAAU,EACV,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAC3D,YAAY,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvF,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAChG,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EACL,sBAAsB,EAAE,aAAa,EACrC,KAAK,UAAU,EAAE,KAAK,aAAa,GACpC,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACvG,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,29 +1,9 @@
|
|
|
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-
|
|
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-B6ZliNf3.js";
|
|
2
2
|
import { createViewportLayer as u, screenToWorld as d, viewToTransform as f } from "@weasel-js/core";
|
|
3
|
-
|
|
4
|
-
function p(e, t, n, r) {
|
|
5
|
-
let i = {
|
|
6
|
-
x: t.scale.x * r,
|
|
7
|
-
y: t.scale.y * r
|
|
8
|
-
};
|
|
9
|
-
return {
|
|
10
|
-
x: e.x - n.w / 2 / i.x,
|
|
11
|
-
y: e.y - n.h / 2 / i.y,
|
|
12
|
-
scale: i
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
//#endregion
|
|
16
|
-
//#region src/loupe/pick.ts
|
|
17
|
-
function m(e, t, n, r) {
|
|
18
|
-
return {
|
|
19
|
-
x: n.x + (e.x - (t.x + t.w / 2)) / r,
|
|
20
|
-
y: n.y + (e.y - (t.y + t.h / 2)) / r
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
3
|
+
import { createLoupeModel as p, loupeInnerView as m, loupeInnerView as h } from "@weasel-js/loupe";
|
|
24
4
|
//#region src/loupe/readback.ts
|
|
25
|
-
function
|
|
26
|
-
let o = Math.round(n.x * r), s = Math.round(n.y * r), c =
|
|
5
|
+
function g(e, t, n, r, i, a) {
|
|
6
|
+
let o = Math.round(n.x * r), s = Math.round(n.y * r), c = _(o - Math.floor(i / 2), 0, Math.max(0, t.width - i)), l = _(s - Math.floor(a / 2), 0, Math.max(0, t.height - a)), u = t.height - l - a, d = new Uint8Array(i * a * 4);
|
|
27
7
|
e.readPixels(c, u, i, a, e.RGBA, e.UNSIGNED_BYTE, d);
|
|
28
8
|
let f = new Uint8ClampedArray(i * a * 4), p = i * 4;
|
|
29
9
|
for (let e = 0; e < a; e++) {
|
|
@@ -32,34 +12,31 @@ function h(e, t, n, r, i, a) {
|
|
|
32
12
|
}
|
|
33
13
|
return new ImageData(f, i, a);
|
|
34
14
|
}
|
|
35
|
-
function
|
|
15
|
+
function _(e, t, n) {
|
|
36
16
|
return e < t ? t : e > n ? n : e;
|
|
37
17
|
}
|
|
38
18
|
//#endregion
|
|
39
19
|
//#region src/loupe/createLoupe.ts
|
|
40
|
-
function
|
|
41
|
-
let { hud: t,
|
|
42
|
-
x: 0,
|
|
43
|
-
y: 0
|
|
44
|
-
}, c = null, l = null, g = !1, _ = !1, v = !1, y = e.bounds ?? {
|
|
20
|
+
function v(e) {
|
|
21
|
+
let { hud: t, canvas: n, source: r, requestRedraw: i } = e, a = e.input ?? n, o = null, s = !1, c = !1, l = !1, m = e.bounds ?? {
|
|
45
22
|
x: 24,
|
|
46
23
|
y: 24,
|
|
47
24
|
w: 220,
|
|
48
25
|
h: 200
|
|
49
|
-
},
|
|
50
|
-
let t = d(
|
|
26
|
+
}, _ = (e) => w.mode === "vector" ? y(e) : b(e), v = (t) => e.background ?? t.tokens["--wzl-surface"], y = (e) => {
|
|
27
|
+
let t = d(w.aim.x, w.aim.y, f(e.view)), n = h({
|
|
51
28
|
x: t[0],
|
|
52
29
|
y: t[1]
|
|
53
|
-
}, e.view, e.rect,
|
|
30
|
+
}, e.view, e.rect, w.factor);
|
|
54
31
|
return u({
|
|
55
|
-
id: `${
|
|
32
|
+
id: `${C.id}:lens`,
|
|
56
33
|
label: "Loupe content",
|
|
57
34
|
source: r,
|
|
58
35
|
view: n,
|
|
59
36
|
bounds: () => e.rect,
|
|
60
|
-
background:
|
|
37
|
+
background: v(e)
|
|
61
38
|
}).draw(e.data, e.view, e.dims);
|
|
62
|
-
},
|
|
39
|
+
}, b = (e) => {
|
|
63
40
|
let t = [{
|
|
64
41
|
kind: "path",
|
|
65
42
|
path: {
|
|
@@ -71,47 +48,47 @@ function _(e) {
|
|
|
71
48
|
},
|
|
72
49
|
fill: {
|
|
73
50
|
fill: "solid",
|
|
74
|
-
color:
|
|
51
|
+
color: v(e)
|
|
75
52
|
}
|
|
76
53
|
}];
|
|
77
|
-
return
|
|
54
|
+
return o && t.push({
|
|
78
55
|
kind: "image",
|
|
79
|
-
image:
|
|
56
|
+
image: o,
|
|
80
57
|
x: e.rect.x,
|
|
81
58
|
y: e.rect.y,
|
|
82
59
|
w: e.rect.w,
|
|
83
60
|
h: e.rect.h,
|
|
84
61
|
sampling: "nearest"
|
|
85
62
|
}), t;
|
|
86
|
-
},
|
|
87
|
-
if (
|
|
88
|
-
if (
|
|
89
|
-
|
|
63
|
+
}, x = () => {
|
|
64
|
+
if (l || w.mode !== "pixel") return;
|
|
65
|
+
if (s) {
|
|
66
|
+
c = !0;
|
|
90
67
|
return;
|
|
91
68
|
}
|
|
92
69
|
let e = n.getContext("webgl2");
|
|
93
70
|
if (!e) return;
|
|
94
71
|
let t = n.getBoundingClientRect();
|
|
95
72
|
if (t.width === 0) return;
|
|
96
|
-
let r = n.width / t.width,
|
|
73
|
+
let r = n.width / t.width, a = C.contentRect, u = Math.max(1, Math.round(a.w * r / w.factor)), d = Math.max(1, Math.round(a.h * r / w.factor)), f = g(e, {
|
|
97
74
|
width: n.width,
|
|
98
75
|
height: n.height
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
if (
|
|
76
|
+
}, w.aim, r, u, d);
|
|
77
|
+
s = !0, c = !1, createImageBitmap(f).then((e) => {
|
|
78
|
+
if (l) {
|
|
102
79
|
e.close();
|
|
103
80
|
return;
|
|
104
81
|
}
|
|
105
|
-
|
|
82
|
+
o?.close(), o = e, s = !1, i(), c && x();
|
|
106
83
|
}).catch(() => {
|
|
107
|
-
|
|
84
|
+
s = !1, c && x();
|
|
108
85
|
});
|
|
109
|
-
},
|
|
86
|
+
}, S = (e) => {
|
|
110
87
|
let t = n.getContext("webgl2");
|
|
111
88
|
if (!t) return null;
|
|
112
89
|
let r = n.getBoundingClientRect();
|
|
113
90
|
if (r.width === 0) return null;
|
|
114
|
-
let i = n.width / r.width, a =
|
|
91
|
+
let i = n.width / r.width, a = g(t, {
|
|
115
92
|
width: n.width,
|
|
116
93
|
height: n.height
|
|
117
94
|
}, e, i, 1, 1);
|
|
@@ -120,76 +97,71 @@ function _(e) {
|
|
|
120
97
|
a.data[1],
|
|
121
98
|
a.data[2]
|
|
122
99
|
].map((e) => e.toString(16).padStart(2, "0")).join("");
|
|
123
|
-
},
|
|
124
|
-
let t = T(s);
|
|
125
|
-
t === null || t === c || (c = t, e.onColorChange?.(t));
|
|
126
|
-
}, D = t.window({
|
|
100
|
+
}, C = t.window({
|
|
127
101
|
id: "weasel-loupe",
|
|
128
|
-
x:
|
|
129
|
-
y:
|
|
130
|
-
w:
|
|
131
|
-
h:
|
|
102
|
+
x: m.x,
|
|
103
|
+
y: m.y,
|
|
104
|
+
w: m.w,
|
|
105
|
+
h: m.h,
|
|
132
106
|
title: e.title ?? "Loupe",
|
|
133
107
|
titlebar: e.titlebar,
|
|
134
|
-
content:
|
|
108
|
+
content: _,
|
|
135
109
|
onResize: () => {
|
|
136
|
-
|
|
110
|
+
x();
|
|
137
111
|
},
|
|
138
112
|
onContentClick: (e) => {
|
|
139
|
-
|
|
113
|
+
w.pick(e);
|
|
140
114
|
},
|
|
141
115
|
...e.onClose ? { onClose: e.onClose } : {}
|
|
142
|
-
}),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
116
|
+
}), w = p({
|
|
117
|
+
surface: {
|
|
118
|
+
lens: () => C.contentRect,
|
|
119
|
+
covers: (e) => C.hitTest(e.x, e.y),
|
|
120
|
+
sample: S,
|
|
121
|
+
hidden: () => C.hidden,
|
|
122
|
+
gone: () => C.disposed === !0,
|
|
123
|
+
changed: () => {
|
|
124
|
+
x(), i();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
...e.mode ? { mode: e.mode } : {},
|
|
128
|
+
...e.factor === void 0 ? {} : { factor: e.factor },
|
|
129
|
+
...e.onColorChange ? { onColorChange: e.onColorChange } : {},
|
|
130
|
+
...e.onPick ? { onPick: e.onPick } : {},
|
|
131
|
+
onDispose: () => E()
|
|
132
|
+
}), T = (e) => {
|
|
133
|
+
let t = a.getBoundingClientRect();
|
|
134
|
+
w.aimAt({
|
|
151
135
|
x: e.clientX - t.left,
|
|
152
136
|
y: e.clientY - t.top
|
|
153
137
|
});
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
if (D.disposed) {
|
|
157
|
-
j();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
D.hidden || D.hitTest(e.x, e.y) || (s = e, E(), a === "pixel" && w(), i());
|
|
161
|
-
}
|
|
162
|
-
}, j = () => {
|
|
163
|
-
v = !0, n.removeEventListener("pointermove", k), l?.close(), l = null;
|
|
138
|
+
}, E = () => {
|
|
139
|
+
l = !0, a.removeEventListener("pointermove", T), o?.close(), o = null;
|
|
164
140
|
};
|
|
165
|
-
return
|
|
166
|
-
window:
|
|
141
|
+
return a.addEventListener("pointermove", T), {
|
|
142
|
+
window: C,
|
|
167
143
|
get mode() {
|
|
168
|
-
return
|
|
144
|
+
return w.mode;
|
|
169
145
|
},
|
|
170
146
|
get factor() {
|
|
171
|
-
return
|
|
147
|
+
return w.factor;
|
|
172
148
|
},
|
|
173
149
|
get aim() {
|
|
174
|
-
return
|
|
150
|
+
return w.aim;
|
|
175
151
|
},
|
|
176
152
|
get color() {
|
|
177
|
-
return
|
|
178
|
-
},
|
|
179
|
-
setMode(e) {
|
|
180
|
-
e !== a && (a = e, a === "pixel" && w(), i());
|
|
181
|
-
},
|
|
182
|
-
setFactor(e) {
|
|
183
|
-
o = e, a === "pixel" && w(), i();
|
|
153
|
+
return w.color;
|
|
184
154
|
},
|
|
185
|
-
|
|
186
|
-
|
|
155
|
+
setMode: w.setMode,
|
|
156
|
+
setFactor: w.setFactor,
|
|
157
|
+
aimAt: w.aimAt,
|
|
158
|
+
pick: w.pick,
|
|
187
159
|
dispose() {
|
|
188
|
-
|
|
160
|
+
w.dispose(), C.dispose();
|
|
189
161
|
}
|
|
190
162
|
};
|
|
191
163
|
}
|
|
192
164
|
//#endregion
|
|
193
|
-
export { a as DEFAULT_FONT_FAMILY, n as DEFAULT_WIDGET_CLAIMS, r as DEFAULT_WINDOW_METRICS, c as HUD_AFFORDANCE_KIND, o as attachHud, e as claimsOf, t as createHud, i as createHudContribution,
|
|
165
|
+
export { a as DEFAULT_FONT_FAMILY, n as DEFAULT_WIDGET_CLAIMS, r as DEFAULT_WINDOW_METRICS, c as HUD_AFFORDANCE_KIND, o as attachHud, e as claimsOf, t as createHud, i as createHudContribution, v as createLoupe, l as cursorForZone, m as loupeInnerView, s as registerDefaultFont };
|
|
194
166
|
|
|
195
167
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/loupe/innerView.ts","../src/loupe/pick.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","import type { WidgetBounds } from '../widget';\n\n/**\n * Where on the outer canvas 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: { x: number; y: number },\n rect: WidgetBounds,\n aim: { x: number; y: number },\n factor: number,\n): { x: number; y: number } {\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","/**\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 { loupeSourcePoint } from './pick';\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 when a click inside the lens picks a color — an eyedropper on the\n * magnified surface. Where the color goes is the consumer's: the loupe\n * reports the pick and nothing else. */\n onPick?: (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 /** Backdrop painted behind the magnified content in both modes, so the\n * outer canvas does not show through where the lens is empty. Defaults to\n * the theme's surface color. */\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 /** Sample the color the lens is showing at `p` — a screen point inside the\n * lens — and report it to `onPick`. Omit `p` to pick at the aim point.\n * Returns the hex, or `null` when the framebuffer cannot answer for that\n * point. Leaves the aim, and `color`, alone. */\n pick(p?: { x: number; y: number }): string | null;\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 backdropColor = (ctx: HudContentCtx): string =>\n opts.background ?? ctx.tokens['--wzl-surface'];\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: backdropColor(ctx),\n });\n return lens.draw(ctx.data, ctx.view, ctx.dims);\n };\n\n const drawPixels = (ctx: HudContentCtx): DrawCommand[] => {\n // The backdrop paints even with a bitmap in hand: a readback of a\n // transparent framebuffer is transparent, and before the first one\n // settles there is nothing at all, either of which leaves the lens a\n // hole onto the unmagnified canvas.\n const out: DrawCommand[] = [{\n kind: 'path',\n path: { kind: 'rect', x: ctx.rect.x, y: ctx.rect.y, width: ctx.rect.w, height: ctx.rect.h },\n fill: { fill: 'solid', color: backdropColor(ctx) },\n }];\n if (pixels) {\n out.push({\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 return out;\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 readHex = (at: { x: number; y: number }): string | null => {\n const gl = element.getContext('webgl2');\n if (!gl) return null;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return null;\n const dpr = element.width / cssRect.width;\n const px = readbackRegion(\n gl, { width: element.width, height: element.height }, at, dpr, 1, 1,\n );\n return '#' + [px.data[0], px.data[1], px.data[2]]\n .map((c) => c.toString(16).padStart(2, '0')).join('');\n };\n\n const sampleColor = () => {\n const hex = readHex(aim);\n if (hex === null || hex === color) return;\n color = hex;\n opts.onColorChange?.(hex);\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 onContentClick: (p) => { pick(p); },\n ...(opts.onClose ? { onClose: opts.onClose } : {}),\n });\n\n const pick = (p?: { x: number; y: number }): string | null => {\n if (disposed) return null;\n const src = p ? loupeSourcePoint(p, win.contentRect, aim, factor) : aim;\n // The window is in the framebuffer too. A lens point near enough to the\n // frame maps back under it, and reading there reports chrome as artwork.\n if (p && win.hitTest(src.x, src.y)) return null;\n const hex = readHex(src);\n if (hex !== null) opts.onPick?.(hex);\n return hex;\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 pick,\n dispose() {\n teardown();\n win.dispose();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/loupe/readback.ts","../src/loupe/createLoupe.ts"],"sourcesContent":["/**\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 {\n createLoupeModel, loupeInnerView,\n type LoupeMode, type LoupeModel, type LoupeSurface,\n} from '@weasel-js/loupe';\nimport type { Hud } from '../hud';\nimport type { WindowWidget } from '../widgets/window/window';\nimport type { HudContentCtx, WidgetBounds } from '../widget';\nimport { readbackRegion } from './readback';\n\nexport type { LoupeMode };\n\n/** Options for `createLoupe`. */\nexport interface LoupeOptions {\n hud: Hud;\n /** The canvas pixel mode reads back from. Supplies the GL context and the\n * drawing-buffer dimensions. */\n canvas: HTMLCanvasElement;\n /** Where the aim comes from: the element pointer events are taken from and\n * the rect client coords are measured against. Defaults to `canvas`, which\n * is right whenever the canvas is also the thing under the pointer. */\n input?: HTMLElement;\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 when a click inside the lens picks a color — an eyedropper on the\n * magnified surface. Where the color goes is the consumer's: the loupe\n * reports the pick and nothing else. */\n onPick?: (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 /** Backdrop painted behind the magnified content in both modes, so the\n * outer canvas does not show through where the lens is empty. Defaults to\n * the theme's surface color. */\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 /** Sample the color the lens is showing at `p` — a screen point inside the\n * lens — and report it to `onPick`. Omit `p` to pick at the aim point.\n * Returns the hex, or `null` when the framebuffer cannot answer for that\n * point. Leaves the aim, and `color`, alone. */\n pick(p?: { x: number; y: number }): string | null;\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, canvas: element, source, requestRedraw } = opts;\n const input = opts.input ?? element;\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 model.mode === 'vector' ? drawVector(ctx) : drawPixels(ctx);\n\n const backdropColor = (ctx: HudContentCtx): string =>\n opts.background ?? ctx.tokens['--wzl-surface'];\n\n const drawVector = (ctx: HudContentCtx): DrawCommand[] => {\n const world = screenToWorld(model.aim.x, model.aim.y, viewToTransform(ctx.view));\n const inner: View = loupeInnerView(\n { x: world[0], y: world[1] }, ctx.view, ctx.rect, model.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: backdropColor(ctx),\n });\n return lens.draw(ctx.data, ctx.view, ctx.dims);\n };\n\n const drawPixels = (ctx: HudContentCtx): DrawCommand[] => {\n // The backdrop paints even with a bitmap in hand: a readback of a\n // transparent framebuffer is transparent, and before the first one\n // settles there is nothing at all, either of which leaves the lens a\n // hole onto the unmagnified canvas.\n const out: DrawCommand[] = [{\n kind: 'path',\n path: { kind: 'rect', x: ctx.rect.x, y: ctx.rect.y, width: ctx.rect.w, height: ctx.rect.h },\n fill: { fill: 'solid', color: backdropColor(ctx) },\n }];\n if (pixels) {\n out.push({\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 return out;\n };\n\n const refreshPixels = () => {\n if (disposed || model.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) / model.factor));\n const rh = Math.max(1, Math.round((rect.h * dpr) / model.factor));\n const data = readbackRegion(\n gl, { width: element.width, height: element.height }, model.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 readHex = (at: { x: number; y: number }): string | null => {\n const gl = element.getContext('webgl2');\n if (!gl) return null;\n const cssRect = element.getBoundingClientRect();\n if (cssRect.width === 0) return null;\n const dpr = element.width / cssRect.width;\n const px = readbackRegion(\n gl, { width: element.width, height: element.height }, at, dpr, 1, 1,\n );\n return '#' + [px.data[0], px.data[1], px.data[2]]\n .map((c) => c.toString(16).padStart(2, '0')).join('');\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 onContentClick: (p) => { model.pick(p); },\n ...(opts.onClose ? { onClose: opts.onClose } : {}),\n });\n\n const surface: LoupeSurface = {\n lens: () => win.contentRect,\n covers: (p) => win.hitTest(p.x, p.y),\n sample: readHex,\n hidden: () => win.hidden,\n gone: () => win.disposed === true,\n changed: () => { refreshPixels(); requestRedraw(); },\n };\n\n const model: LoupeModel = createLoupeModel({\n surface,\n ...(opts.mode ? { mode: opts.mode } : {}),\n ...(opts.factor !== undefined ? { factor: opts.factor } : {}),\n ...(opts.onColorChange ? { onColorChange: opts.onColorChange } : {}),\n ...(opts.onPick ? { onPick: opts.onPick } : {}),\n onDispose: () => teardown(),\n });\n\n const onPointerMove = (evt: PointerEvent) => {\n const r = input.getBoundingClientRect();\n model.aimAt({ x: evt.clientX - r.left, y: evt.clientY - r.top });\n };\n\n const teardown = () => {\n disposed = true;\n input.removeEventListener('pointermove', onPointerMove);\n pixels?.close();\n pixels = null;\n };\n\n input.addEventListener('pointermove', onPointerMove);\n\n return {\n window: win,\n get mode() { return model.mode; },\n get factor() { return model.factor; },\n get aim() { return model.aim; },\n get color() { return model.color; },\n setMode: model.setMode,\n setFactor: model.setFactor,\n aimAt: model.aimAt,\n pick: model.pick,\n dispose() {\n model.dispose();\n win.dispose();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { type RenderLayer } from '@weasel-js/core';
|
|
2
|
+
import { type LoupeMode } from '@weasel-js/loupe';
|
|
2
3
|
import type { Hud } from '../hud';
|
|
3
4
|
import type { WindowWidget } from '../widgets/window/window';
|
|
4
5
|
import type { WidgetBounds } from '../widget';
|
|
5
|
-
|
|
6
|
-
* zoomed-in view, so content stays sharp at any factor; `'pixel'` reads the
|
|
7
|
-
* framebuffer back and blows up the actual pixels. */
|
|
8
|
-
export type LoupeMode = 'vector' | 'pixel';
|
|
6
|
+
export type { LoupeMode };
|
|
9
7
|
/** Options for `createLoupe`. */
|
|
10
8
|
export interface LoupeOptions {
|
|
11
9
|
hud: Hud;
|
|
12
|
-
/** The
|
|
13
|
-
*
|
|
14
|
-
|
|
10
|
+
/** The canvas pixel mode reads back from. Supplies the GL context and the
|
|
11
|
+
* drawing-buffer dimensions. */
|
|
12
|
+
canvas: HTMLCanvasElement;
|
|
13
|
+
/** Where the aim comes from: the element pointer events are taken from and
|
|
14
|
+
* the rect client coords are measured against. Defaults to `canvas`, which
|
|
15
|
+
* is right whenever the canvas is also the thing under the pointer. */
|
|
16
|
+
input?: HTMLElement;
|
|
15
17
|
/** Layers re-rendered through the magnified inner view in vector mode. */
|
|
16
18
|
source: RenderLayer<unknown>[];
|
|
17
19
|
requestRedraw: () => void;
|
|
@@ -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;
|
|
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,EAEL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAC1B,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;AAG7D,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,iCAAiC;AACjC,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,GAAG,CAAC;IACT;qCACiC;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;4EAEwE;IACxE,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,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;;6CAEyC;IACzC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B;0EACsE;IACtE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;qCAEiC;IACjC,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;;;qDAGiD;IACjD,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,GAAG,IAAI,CAAC;IAClD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,WAAW,CA+J3D"}
|
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as e, n as t, r as n } from "../tool-
|
|
1
|
+
import { c as e, n as t, r as n } from "../tool-B6ZliNf3.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";
|