@camstack/ui-library 1.0.1 → 1.0.3
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/MaskShapeCanvas-BByN3jvt.cjs +913 -0
- package/dist/MaskShapeCanvas-DI4BY7W2.js +913 -0
- package/dist/MotionZonesSettings-C1EEbk2V.js +438 -0
- package/dist/MotionZonesSettings-Ci1mzrki.cjs +438 -0
- package/dist/PrivacyMaskSettings-APgPLF7p.js +384 -0
- package/dist/PrivacyMaskSettings-yC-UPYPg.cjs +384 -0
- package/dist/composites/index.d.ts +6 -2
- package/dist/hls-CckKbjjD.cjs +28320 -0
- package/dist/hls-CfgsaJjd.js +28320 -0
- package/dist/index.cjs +410 -30407
- package/dist/index.d.ts +7 -0
- package/dist/index.js +404 -30408
- package/package.json +3 -3
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { TEXT_HINT, WidgetPanel, i as createLucideIcon, isAbsentProvider, useDeviceProxy, usePlayerOverlayLayer, useSystem } from "./index.js";
|
|
2
|
+
import { MaskShapeCanvas } from "./MaskShapeCanvas-DI4BY7W2.js";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
/**
|
|
6
|
+
* @license lucide-react v0.576.0 - ISC
|
|
7
|
+
*
|
|
8
|
+
* This source code is licensed under the ISC license.
|
|
9
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
10
|
+
*/
|
|
11
|
+
var Grid2x2 = createLucideIcon("grid-2x2", [
|
|
12
|
+
["path", {
|
|
13
|
+
d: "M12 3v18",
|
|
14
|
+
key: "108xh3"
|
|
15
|
+
}],
|
|
16
|
+
["path", {
|
|
17
|
+
d: "M3 12h18",
|
|
18
|
+
key: "1i2n21"
|
|
19
|
+
}],
|
|
20
|
+
["rect", {
|
|
21
|
+
x: "3",
|
|
22
|
+
y: "3",
|
|
23
|
+
width: "18",
|
|
24
|
+
height: "18",
|
|
25
|
+
rx: "2",
|
|
26
|
+
key: "h1oib"
|
|
27
|
+
}]
|
|
28
|
+
]);
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/composites/cap-settings/MotionZonesSettings.tsx
|
|
31
|
+
/**
|
|
32
|
+
* MotionZonesSettings — management surface for the on-camera
|
|
33
|
+
* `motion-zones` capability. A cap-settings component (ui-library),
|
|
34
|
+
* mounted into the device-detail Config tab via the cap-UI
|
|
35
|
+
* contribution mechanism (the `motion-zones` cap declares a `ui` block).
|
|
36
|
+
*
|
|
37
|
+
* The on-camera motion mask is a SINGLE `grid` region in the shared
|
|
38
|
+
* MaskShape vocabulary (a row-major boolean cell lattice). This component
|
|
39
|
+
* owns its own control panel (Edit toggle, All on / All off / Invert,
|
|
40
|
+
* count, Revert, Save) and paints the editable lattice over the live frame
|
|
41
|
+
* with the shared `MaskShapeCanvas` drawing-plane — registered as a
|
|
42
|
+
* player-overlay layer ONLY while "Edit grid" is toggled on (OFF by
|
|
43
|
+
* default — nothing draws on mount).
|
|
44
|
+
*
|
|
45
|
+
* Detection enable + sensitivity are NOT edited here — they are the
|
|
46
|
+
* camera's on-board motion master switch, owned by the driver's
|
|
47
|
+
* "On-camera motion" settings section. Saving sends a `cells`-only patch
|
|
48
|
+
* (one grid region); enable / sensitivity are left untouched camera-side.
|
|
49
|
+
*
|
|
50
|
+
* ── Resizable input grid (resampled to the camera grid on save) ──────────────
|
|
51
|
+
* The camera's native motion grid (`getOptions().grid`, e.g. 22×18) is the
|
|
52
|
+
* FINEST resolution that round-trips — painting finer than it can't be stored
|
|
53
|
+
* (not reconstructible). So the camera grid is the FLOOR (×1) and the operator
|
|
54
|
+
* picks BIGGER cells: an integer cell-size multiplier (×1/×2/×3) where a UI cell
|
|
55
|
+
* spans an m×m block of camera cells (×2 → quarter as many, ×3 → ninth). The
|
|
56
|
+
* CAMERA grid stays authoritative:
|
|
57
|
+
* - LOAD: the camera grid is COARSENED into the chosen UI resolution (a UI
|
|
58
|
+
* cell is `true` if ANY camera cell in its block is painted) so existing
|
|
59
|
+
* masks display on the bigger-celled surface.
|
|
60
|
+
* - SAVE: the UI grid is EXPANDED back to camera gridWidth×gridHeight — every
|
|
61
|
+
* camera cell takes the value of the UI cell whose block covers it.
|
|
62
|
+
* At ×1 both transforms are the identity, so the camera-grid round-trip is
|
|
63
|
+
* exact; at higher multipliers `coarsen(expand(uiCells)) === uiCells`, so an
|
|
64
|
+
* edit made at the chosen resolution survives the round-trip exactly. The
|
|
65
|
+
* component's draft (`uiCells`) lives at the UI resolution; the last-saved truth
|
|
66
|
+
* (`committedCamera`) and every comparison stay anchored to the camera-native
|
|
67
|
+
* grid so `dirty`/Save/Revert math is resolution-independent.
|
|
68
|
+
*/
|
|
69
|
+
var MOTION_ZONES_OVERLAY_ORDER = 110;
|
|
70
|
+
var OVERLAY_ID = "motion-zones";
|
|
71
|
+
var GRID_REGION_ID = 0;
|
|
72
|
+
var GRID_MULTIPLIERS = [
|
|
73
|
+
1,
|
|
74
|
+
2,
|
|
75
|
+
3
|
|
76
|
+
];
|
|
77
|
+
var DEFAULT_MULTIPLIER = 1;
|
|
78
|
+
var BTN_NEUTRAL = "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors";
|
|
79
|
+
var BTN_PRIMARY = "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors";
|
|
80
|
+
/** Coerce an arbitrary `cells` array to exactly `width*height` booleans. */
|
|
81
|
+
function normaliseCells(cells, width, height) {
|
|
82
|
+
const total = width * height;
|
|
83
|
+
const next = new Array(total);
|
|
84
|
+
for (let i = 0; i < total; i += 1) next[i] = cells[i] === true;
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
function cellsEqual(a, b) {
|
|
88
|
+
if (a.length !== b.length) return false;
|
|
89
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
/** UI-grid dimension for a camera dimension at cell-size multiplier m. */
|
|
93
|
+
function coarseDim(cameraDim, m) {
|
|
94
|
+
return Math.ceil(cameraDim / m);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Map a camera-grid index to the UI-grid cell that SPATIALLY covers it, by the
|
|
98
|
+
* camera cell's centre (`(c + 0.5)/cameraDim`). Index-based `floor(c/m)` drifts
|
|
99
|
+
* when the camera dim isn't a multiple of m (e.g. 22 ÷ 3) — the painted UI cell
|
|
100
|
+
* no longer lines up with the camera region it masks. The centre map keeps them
|
|
101
|
+
* aligned and is a non-decreasing surjection onto [0, uiDim), so every UI cell
|
|
102
|
+
* owns ≥1 camera cell — which is what makes `coarsen(expand(x)) === x` hold.
|
|
103
|
+
*/
|
|
104
|
+
function cameraToUi(c, cameraDim, uiDim) {
|
|
105
|
+
return Math.min(uiDim - 1, Math.floor((c + .5) / cameraDim * uiDim));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* COARSEN a camera-native grid (cw×ch, row-major) into a coarser UI grid at
|
|
109
|
+
* ceil(cw/m)×ceil(ch/m): a UI cell is `true` if ANY camera cell mapped into it
|
|
110
|
+
* is painted (the inclusive "any" rule — motion masks are inclusive, so a
|
|
111
|
+
* painted sub-region arms the whole bigger cell). At m === 1 this is the
|
|
112
|
+
* identity. Used on LOAD/Revert so an existing camera mask shows up on the
|
|
113
|
+
* chosen (bigger-celled) painting surface.
|
|
114
|
+
*/
|
|
115
|
+
function coarsenCells(cameraCells, cw, ch, m) {
|
|
116
|
+
const uw = coarseDim(cw, m);
|
|
117
|
+
const uh = coarseDim(ch, m);
|
|
118
|
+
const out = new Array(uw * uh).fill(false);
|
|
119
|
+
for (let cy = 0; cy < ch; cy += 1) {
|
|
120
|
+
const uy = cameraToUi(cy, ch, uh);
|
|
121
|
+
for (let cx = 0; cx < cw; cx += 1) {
|
|
122
|
+
if (cameraCells[cy * cw + cx] !== true) continue;
|
|
123
|
+
out[uy * uw + cameraToUi(cx, cw, uw)] = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* EXPAND a coarser UI grid at ceil(cw/m)×ceil(ch/m) back to the camera-native
|
|
130
|
+
* grid (cw×ch): every camera cell takes the value of the UI cell that spatially
|
|
131
|
+
* covers it. At m === 1 this is the identity, so the camera-grid round-trip is
|
|
132
|
+
* exact. Used on SAVE, on multiplier changes (to preserve painted area), and to
|
|
133
|
+
* compute `dirty`. Note `coarsen(expand(uiCells)) === uiCells`, so an edit made
|
|
134
|
+
* at the chosen resolution survives the round-trip exactly.
|
|
135
|
+
*/
|
|
136
|
+
function expandCells(uiCells, cw, ch, m) {
|
|
137
|
+
const uw = coarseDim(cw, m);
|
|
138
|
+
const uh = coarseDim(ch, m);
|
|
139
|
+
const out = new Array(cw * ch).fill(false);
|
|
140
|
+
for (let cy = 0; cy < ch; cy += 1) {
|
|
141
|
+
const uy = cameraToUi(cy, ch, uh);
|
|
142
|
+
for (let cx = 0; cx < cw; cx += 1) out[cy * cw + cx] = uiCells[uy * uw + cameraToUi(cx, cw, uw)] === true;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function MotionZonesSettings({ deviceId }) {
|
|
147
|
+
const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
|
|
148
|
+
const [options, setOptions] = useState(null);
|
|
149
|
+
const [unsupported, setUnsupported] = useState(false);
|
|
150
|
+
const [uiCells, setUiCells] = useState(null);
|
|
151
|
+
const [committedCamera, setCommittedCamera] = useState(null);
|
|
152
|
+
const [multiplier, setMultiplier] = useState(DEFAULT_MULTIPLIER);
|
|
153
|
+
const [saving, setSaving] = useState(false);
|
|
154
|
+
const [editing, setEditing] = useState(false);
|
|
155
|
+
const seededRef = useRef(false);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!dev) return void 0;
|
|
158
|
+
let cancelled = false;
|
|
159
|
+
seededRef.current = false;
|
|
160
|
+
setOptions(null);
|
|
161
|
+
setUnsupported(false);
|
|
162
|
+
setUiCells(null);
|
|
163
|
+
setCommittedCamera(null);
|
|
164
|
+
setMultiplier(DEFAULT_MULTIPLIER);
|
|
165
|
+
setEditing(false);
|
|
166
|
+
(async () => {
|
|
167
|
+
try {
|
|
168
|
+
const opts = await dev.motionZones?.getOptions({});
|
|
169
|
+
if (cancelled) return;
|
|
170
|
+
if (!opts) throw new Error("device proxy not ready");
|
|
171
|
+
setOptions(opts);
|
|
172
|
+
if (seededRef.current) return;
|
|
173
|
+
const status = await dev.motionZones?.getStatus({});
|
|
174
|
+
if (cancelled) return;
|
|
175
|
+
if (!status) throw new Error("device proxy not ready");
|
|
176
|
+
seededRef.current = true;
|
|
177
|
+
const region = status.regions.find((r) => r.shape.kind === "grid");
|
|
178
|
+
const norm = normaliseCells(region ? region.shape.cells : [], opts.grid.width, opts.grid.height);
|
|
179
|
+
setCommittedCamera(norm);
|
|
180
|
+
setUiCells(coarsenCells(norm, opts.grid.width, opts.grid.height, DEFAULT_MULTIPLIER));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (cancelled) return;
|
|
183
|
+
if (isAbsentProvider(err)) setUnsupported(true);
|
|
184
|
+
else console.error("Motion Zones load failed", err);
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
return () => {
|
|
188
|
+
cancelled = true;
|
|
189
|
+
};
|
|
190
|
+
}, [dev]);
|
|
191
|
+
const cw = options ? options.grid.width : 0;
|
|
192
|
+
const ch = options ? options.grid.height : 0;
|
|
193
|
+
const uiWidth = coarseDim(cw, multiplier);
|
|
194
|
+
const uiHeight = coarseDim(ch, multiplier);
|
|
195
|
+
const draftCamera = useMemo(() => uiCells && options ? expandCells(uiCells, cw, ch, multiplier) : null, [
|
|
196
|
+
uiCells,
|
|
197
|
+
options,
|
|
198
|
+
cw,
|
|
199
|
+
ch,
|
|
200
|
+
multiplier
|
|
201
|
+
]);
|
|
202
|
+
const dirty = useMemo(() => draftCamera !== null && committedCamera !== null && !cellsEqual(draftCamera, committedCamera), [draftCamera, committedCamera]);
|
|
203
|
+
const activeCount = useMemo(() => draftCamera ? draftCamera.reduce((n, c) => c ? n + 1 : n, 0) : 0, [draftCamera]);
|
|
204
|
+
const cameraTotal = cw * ch;
|
|
205
|
+
const uiTotal = uiWidth * uiHeight;
|
|
206
|
+
const setAll = useCallback((value) => {
|
|
207
|
+
if (uiTotal > 0) setUiCells(new Array(uiTotal).fill(value));
|
|
208
|
+
}, [uiTotal]);
|
|
209
|
+
const invert = useCallback(() => {
|
|
210
|
+
setUiCells((prev) => prev ? prev.map((c) => !c) : prev);
|
|
211
|
+
}, []);
|
|
212
|
+
const revert = useCallback(() => {
|
|
213
|
+
if (committedCamera && options) setUiCells(coarsenCells(committedCamera, cw, ch, multiplier));
|
|
214
|
+
}, [
|
|
215
|
+
committedCamera,
|
|
216
|
+
options,
|
|
217
|
+
cw,
|
|
218
|
+
ch,
|
|
219
|
+
multiplier
|
|
220
|
+
]);
|
|
221
|
+
const changeMultiplier = useCallback((next) => {
|
|
222
|
+
if (next === multiplier || !options) return;
|
|
223
|
+
setUiCells((prev) => {
|
|
224
|
+
if (!prev) {
|
|
225
|
+
setMultiplier(next);
|
|
226
|
+
return prev;
|
|
227
|
+
}
|
|
228
|
+
const recoarsened = coarsenCells(expandCells(prev, cw, ch, multiplier), cw, ch, next);
|
|
229
|
+
setMultiplier(next);
|
|
230
|
+
return recoarsened;
|
|
231
|
+
});
|
|
232
|
+
}, [
|
|
233
|
+
multiplier,
|
|
234
|
+
options,
|
|
235
|
+
cw,
|
|
236
|
+
ch
|
|
237
|
+
]);
|
|
238
|
+
const items = useMemo(() => options && uiCells ? [{
|
|
239
|
+
id: GRID_REGION_ID,
|
|
240
|
+
shape: {
|
|
241
|
+
kind: "grid",
|
|
242
|
+
gridWidth: uiWidth,
|
|
243
|
+
gridHeight: uiHeight,
|
|
244
|
+
cells: [...uiCells]
|
|
245
|
+
}
|
|
246
|
+
}] : [], [
|
|
247
|
+
options,
|
|
248
|
+
uiCells,
|
|
249
|
+
uiWidth,
|
|
250
|
+
uiHeight
|
|
251
|
+
]);
|
|
252
|
+
const onShapeChange = useCallback((_id, shape) => {
|
|
253
|
+
if (shape.kind !== "grid") return;
|
|
254
|
+
setUiCells(shape.cells);
|
|
255
|
+
}, []);
|
|
256
|
+
const save = useCallback(async () => {
|
|
257
|
+
if (!dev || !uiCells || !options) return;
|
|
258
|
+
setSaving(true);
|
|
259
|
+
try {
|
|
260
|
+
const cameraCells = expandCells(uiCells, options.grid.width, options.grid.height, multiplier);
|
|
261
|
+
const shape = {
|
|
262
|
+
kind: "grid",
|
|
263
|
+
gridWidth: options.grid.width,
|
|
264
|
+
gridHeight: options.grid.height,
|
|
265
|
+
cells: cameraCells
|
|
266
|
+
};
|
|
267
|
+
await dev.motionZones?.setZone({ patch: { regions: [{
|
|
268
|
+
id: GRID_REGION_ID,
|
|
269
|
+
enabled: true,
|
|
270
|
+
shape
|
|
271
|
+
}] } });
|
|
272
|
+
const fresh = await dev.motionZones?.getStatus({});
|
|
273
|
+
if (fresh) {
|
|
274
|
+
const region = fresh.regions.find((r) => r.shape.kind === "grid");
|
|
275
|
+
const norm = normaliseCells(region ? region.shape.cells : [], options.grid.width, options.grid.height);
|
|
276
|
+
setCommittedCamera(norm);
|
|
277
|
+
setUiCells(coarsenCells(norm, options.grid.width, options.grid.height, multiplier));
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error("Motion Zones save failed", err);
|
|
281
|
+
} finally {
|
|
282
|
+
setSaving(false);
|
|
283
|
+
}
|
|
284
|
+
}, [
|
|
285
|
+
dev,
|
|
286
|
+
uiCells,
|
|
287
|
+
options,
|
|
288
|
+
multiplier
|
|
289
|
+
]);
|
|
290
|
+
usePlayerOverlayLayer(useMemo(() => editing && !unsupported && options && uiCells ? {
|
|
291
|
+
id: OVERLAY_ID,
|
|
292
|
+
order: MOTION_ZONES_OVERLAY_ORDER,
|
|
293
|
+
node: /* @__PURE__ */ jsx(MaskShapeCanvas, {
|
|
294
|
+
transparent: true,
|
|
295
|
+
items,
|
|
296
|
+
supportedShapes: ["grid"],
|
|
297
|
+
grid: {
|
|
298
|
+
width: uiWidth,
|
|
299
|
+
height: uiHeight
|
|
300
|
+
},
|
|
301
|
+
selectedId: GRID_REGION_ID,
|
|
302
|
+
onSelect: () => {},
|
|
303
|
+
onShapeChange,
|
|
304
|
+
onDrawComplete: () => {},
|
|
305
|
+
drawingKind: null
|
|
306
|
+
})
|
|
307
|
+
} : null, [
|
|
308
|
+
editing,
|
|
309
|
+
unsupported,
|
|
310
|
+
options,
|
|
311
|
+
uiCells,
|
|
312
|
+
items,
|
|
313
|
+
onShapeChange,
|
|
314
|
+
uiWidth,
|
|
315
|
+
uiHeight
|
|
316
|
+
]));
|
|
317
|
+
const ready = !unsupported && options !== null && uiCells !== null;
|
|
318
|
+
if (!dev) return null;
|
|
319
|
+
return /* @__PURE__ */ jsx(WidgetPanel, {
|
|
320
|
+
title: "Motion Zones",
|
|
321
|
+
icon: /* @__PURE__ */ jsx(Grid2x2, { className: "h-3.5 w-3.5 text-foreground-subtle" }),
|
|
322
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
323
|
+
className: "flex flex-col gap-3",
|
|
324
|
+
children: unsupported ? /* @__PURE__ */ jsx("p", {
|
|
325
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
326
|
+
children: "This camera doesn't expose an on-board motion zones grid."
|
|
327
|
+
}) : !ready ? /* @__PURE__ */ jsx("p", {
|
|
328
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
329
|
+
children: "Loading the camera's grid…"
|
|
330
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("p", {
|
|
331
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
332
|
+
children: [
|
|
333
|
+
"Toggle ",
|
|
334
|
+
/* @__PURE__ */ jsx("strong", {
|
|
335
|
+
className: "text-foreground",
|
|
336
|
+
children: "Edit grid"
|
|
337
|
+
}),
|
|
338
|
+
" to paint the region directly on the live frame, then ",
|
|
339
|
+
/* @__PURE__ */ jsx("strong", {
|
|
340
|
+
className: "text-foreground",
|
|
341
|
+
children: "Save"
|
|
342
|
+
}),
|
|
343
|
+
" to push the mask to the camera. Pick a bigger",
|
|
344
|
+
" ",
|
|
345
|
+
/* @__PURE__ */ jsx("strong", {
|
|
346
|
+
className: "text-foreground",
|
|
347
|
+
children: "Cell size"
|
|
348
|
+
}),
|
|
349
|
+
" for quicker, broad-stroke painting — it's resampled to the camera's native ",
|
|
350
|
+
options.grid.width,
|
|
351
|
+
"×",
|
|
352
|
+
options.grid.height,
|
|
353
|
+
" grid on save (×1 is the finest)."
|
|
354
|
+
]
|
|
355
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
356
|
+
className: "flex items-center gap-2 flex-wrap",
|
|
357
|
+
children: [
|
|
358
|
+
/* @__PURE__ */ jsx("button", {
|
|
359
|
+
type: "button",
|
|
360
|
+
onClick: () => setEditing((v) => !v),
|
|
361
|
+
disabled: saving,
|
|
362
|
+
"aria-pressed": editing,
|
|
363
|
+
className: editing ? BTN_PRIMARY : BTN_NEUTRAL,
|
|
364
|
+
children: editing ? "Done editing" : "Edit grid"
|
|
365
|
+
}),
|
|
366
|
+
/* @__PURE__ */ jsx("button", {
|
|
367
|
+
type: "button",
|
|
368
|
+
onClick: () => setAll(true),
|
|
369
|
+
disabled: saving,
|
|
370
|
+
className: BTN_NEUTRAL,
|
|
371
|
+
children: "All on"
|
|
372
|
+
}),
|
|
373
|
+
/* @__PURE__ */ jsx("button", {
|
|
374
|
+
type: "button",
|
|
375
|
+
onClick: () => setAll(false),
|
|
376
|
+
disabled: saving,
|
|
377
|
+
className: BTN_NEUTRAL,
|
|
378
|
+
children: "All off"
|
|
379
|
+
}),
|
|
380
|
+
/* @__PURE__ */ jsx("button", {
|
|
381
|
+
type: "button",
|
|
382
|
+
onClick: invert,
|
|
383
|
+
disabled: saving,
|
|
384
|
+
className: BTN_NEUTRAL,
|
|
385
|
+
children: "Invert"
|
|
386
|
+
}),
|
|
387
|
+
/* @__PURE__ */ jsxs("div", {
|
|
388
|
+
className: "flex items-center gap-1 ml-1",
|
|
389
|
+
role: "group",
|
|
390
|
+
"aria-label": "Cell size",
|
|
391
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
392
|
+
className: `${TEXT_HINT} mr-0.5`,
|
|
393
|
+
children: "Cell size"
|
|
394
|
+
}), GRID_MULTIPLIERS.map((m) => /* @__PURE__ */ jsxs("button", {
|
|
395
|
+
type: "button",
|
|
396
|
+
onClick: () => changeMultiplier(m),
|
|
397
|
+
disabled: saving,
|
|
398
|
+
"aria-pressed": multiplier === m,
|
|
399
|
+
title: m === 1 ? `Camera grid ${cw}×${ch} (finest)` : `${coarseDim(cw, m)}×${coarseDim(ch, m)} painting grid · cells ×${m} bigger`,
|
|
400
|
+
className: multiplier === m ? BTN_PRIMARY : BTN_NEUTRAL,
|
|
401
|
+
children: ["×", m]
|
|
402
|
+
}, m))]
|
|
403
|
+
}),
|
|
404
|
+
/* @__PURE__ */ jsxs("span", {
|
|
405
|
+
className: `${TEXT_HINT} ml-1 tabular-nums`,
|
|
406
|
+
children: [
|
|
407
|
+
activeCount,
|
|
408
|
+
" / ",
|
|
409
|
+
cameraTotal,
|
|
410
|
+
" cells · ",
|
|
411
|
+
options.grid.width,
|
|
412
|
+
"×",
|
|
413
|
+
options.grid.height,
|
|
414
|
+
multiplier !== 1 ? ` · paint ${uiWidth}×${uiHeight}` : ""
|
|
415
|
+
]
|
|
416
|
+
}),
|
|
417
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
418
|
+
/* @__PURE__ */ jsx("button", {
|
|
419
|
+
type: "button",
|
|
420
|
+
onClick: revert,
|
|
421
|
+
disabled: saving || !dirty,
|
|
422
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
|
|
423
|
+
children: "Revert"
|
|
424
|
+
}),
|
|
425
|
+
/* @__PURE__ */ jsx("button", {
|
|
426
|
+
type: "button",
|
|
427
|
+
onClick: () => void save(),
|
|
428
|
+
disabled: saving || !dirty,
|
|
429
|
+
className: BTN_PRIMARY,
|
|
430
|
+
children: saving ? "Saving…" : "Save"
|
|
431
|
+
})
|
|
432
|
+
]
|
|
433
|
+
})] })
|
|
434
|
+
})
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
export { MotionZonesSettings };
|