@camstack/ui-library 1.2.177 → 1.2.178
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/composites/cap-settings/camera-grid-layout.d.ts +33 -0
- package/dist/index.cjs +273 -17
- package/dist/index.js +273 -17
- package/package.json +1 -1
|
@@ -72,3 +72,36 @@ export declare function clampNormalizedRect(rect: GridNormalizedRect): GridNorma
|
|
|
72
72
|
export declare function moveNormalizedRect(rect: GridNormalizedRect, dx: number, dy: number): GridNormalizedRect;
|
|
73
73
|
/** Resize from the bottom-right corner: the origin stays put. */
|
|
74
74
|
export declare function resizeNormalizedRect(rect: GridNormalizedRect, dw: number, dh: number): GridNormalizedRect;
|
|
75
|
+
/** The eight grips on a rectangle. A corner moves two edges, an edge one. */
|
|
76
|
+
export type GridResizeHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
|
|
77
|
+
/**
|
|
78
|
+
* Resize from ANY handle: the opposite side is the anchor.
|
|
79
|
+
*
|
|
80
|
+
* The editor had one grip, in the bottom-right, which meant a cell could only
|
|
81
|
+
* grow away from its origin — to move its top edge an operator had to drag the
|
|
82
|
+
* whole cell and then resize it back. The rectangle is expressed as its two
|
|
83
|
+
* edges here rather than origin+extent, because that is what makes "the
|
|
84
|
+
* opposite edge does not move" fall out instead of being arithmetic to get
|
|
85
|
+
* wrong. `clampNormalizedRect` still has the last word: the addon THROWS on a
|
|
86
|
+
* rectangle outside the unit square.
|
|
87
|
+
*/
|
|
88
|
+
export declare function resizeNormalizedRectFrom(rect: GridNormalizedRect, handle: GridResizeHandle, dx: number, dy: number): GridNormalizedRect;
|
|
89
|
+
/**
|
|
90
|
+
* Tile the whole canvas with `count` cells, leaving no gap.
|
|
91
|
+
*
|
|
92
|
+
* Autofill exists because the useful answer to "put these four cameras up" is
|
|
93
|
+
* almost never the one hand-dragging produces: rectangles land a pixel apart
|
|
94
|
+
* and the composition shows black seams the operator cannot see in the editor
|
|
95
|
+
* but will see in the stream.
|
|
96
|
+
*
|
|
97
|
+
* The shape is the near-square one — `ceil(sqrt(n))` columns — and a short last
|
|
98
|
+
* row STRETCHES to the full width rather than leaving a hole. Three cameras
|
|
99
|
+
* become two over one, not a 2x2 with an empty quarter, which is precisely the
|
|
100
|
+
* layout autofill is asked to avoid.
|
|
101
|
+
*
|
|
102
|
+
* Aspect is deliberately not considered: these are CANVAS fractions, and what a
|
|
103
|
+
* source shows inside its cell is the crop's business (the source rectangle),
|
|
104
|
+
* not the placement's. Mixing the two here would silently re-crop every cell
|
|
105
|
+
* whenever the canvas was resized.
|
|
106
|
+
*/
|
|
107
|
+
export declare function autofillCellRects(count: number): readonly GridNormalizedRect[];
|
package/dist/index.cjs
CHANGED
|
@@ -2365,6 +2365,46 @@ var Laugh = createLucideIcon("laugh", [
|
|
|
2365
2365
|
* This source code is licensed under the ISC license.
|
|
2366
2366
|
* See the LICENSE file in the root directory of this source tree.
|
|
2367
2367
|
*/
|
|
2368
|
+
var LayoutGrid = createLucideIcon("layout-grid", [
|
|
2369
|
+
["rect", {
|
|
2370
|
+
width: "7",
|
|
2371
|
+
height: "7",
|
|
2372
|
+
x: "3",
|
|
2373
|
+
y: "3",
|
|
2374
|
+
rx: "1",
|
|
2375
|
+
key: "1g98yp"
|
|
2376
|
+
}],
|
|
2377
|
+
["rect", {
|
|
2378
|
+
width: "7",
|
|
2379
|
+
height: "7",
|
|
2380
|
+
x: "14",
|
|
2381
|
+
y: "3",
|
|
2382
|
+
rx: "1",
|
|
2383
|
+
key: "6d4xhi"
|
|
2384
|
+
}],
|
|
2385
|
+
["rect", {
|
|
2386
|
+
width: "7",
|
|
2387
|
+
height: "7",
|
|
2388
|
+
x: "14",
|
|
2389
|
+
y: "14",
|
|
2390
|
+
rx: "1",
|
|
2391
|
+
key: "nxv5o0"
|
|
2392
|
+
}],
|
|
2393
|
+
["rect", {
|
|
2394
|
+
width: "7",
|
|
2395
|
+
height: "7",
|
|
2396
|
+
x: "3",
|
|
2397
|
+
y: "14",
|
|
2398
|
+
rx: "1",
|
|
2399
|
+
key: "1bb6yr"
|
|
2400
|
+
}]
|
|
2401
|
+
]);
|
|
2402
|
+
/**
|
|
2403
|
+
* @license lucide-react v0.576.0 - ISC
|
|
2404
|
+
*
|
|
2405
|
+
* This source code is licensed under the ISC license.
|
|
2406
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
2407
|
+
*/
|
|
2368
2408
|
var Leaf = createLucideIcon("leaf", [["path", {
|
|
2369
2409
|
d: "M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z",
|
|
2370
2410
|
key: "nnexq3"
|
|
@@ -16664,13 +16704,75 @@ function moveNormalizedRect(rect, dx, dy) {
|
|
|
16664
16704
|
y: rect.y + dy
|
|
16665
16705
|
});
|
|
16666
16706
|
}
|
|
16667
|
-
/**
|
|
16668
|
-
|
|
16707
|
+
/**
|
|
16708
|
+
* Resize from ANY handle: the opposite side is the anchor.
|
|
16709
|
+
*
|
|
16710
|
+
* The editor had one grip, in the bottom-right, which meant a cell could only
|
|
16711
|
+
* grow away from its origin — to move its top edge an operator had to drag the
|
|
16712
|
+
* whole cell and then resize it back. The rectangle is expressed as its two
|
|
16713
|
+
* edges here rather than origin+extent, because that is what makes "the
|
|
16714
|
+
* opposite edge does not move" fall out instead of being arithmetic to get
|
|
16715
|
+
* wrong. `clampNormalizedRect` still has the last word: the addon THROWS on a
|
|
16716
|
+
* rectangle outside the unit square.
|
|
16717
|
+
*/
|
|
16718
|
+
function resizeNormalizedRectFrom(rect, handle, dx, dy) {
|
|
16719
|
+
const west = handle === "nw" || handle === "w" || handle === "sw";
|
|
16720
|
+
const east = handle === "ne" || handle === "e" || handle === "se";
|
|
16721
|
+
const north = handle === "nw" || handle === "n" || handle === "ne";
|
|
16722
|
+
const south = handle === "sw" || handle === "s" || handle === "se";
|
|
16723
|
+
let left = rect.x;
|
|
16724
|
+
let right = rect.x + rect.width;
|
|
16725
|
+
let top = rect.y;
|
|
16726
|
+
let bottom = rect.y + rect.height;
|
|
16727
|
+
if (west) left = Math.min(right - MIN_EXTENT, Math.max(0, left + dx));
|
|
16728
|
+
if (east) right = Math.max(left + MIN_EXTENT, Math.min(1, right + dx));
|
|
16729
|
+
if (north) top = Math.min(bottom - MIN_EXTENT, Math.max(0, top + dy));
|
|
16730
|
+
if (south) bottom = Math.max(top + MIN_EXTENT, Math.min(1, bottom + dy));
|
|
16669
16731
|
return clampNormalizedRect({
|
|
16670
|
-
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
|
|
16732
|
+
x: left,
|
|
16733
|
+
y: top,
|
|
16734
|
+
width: right - left,
|
|
16735
|
+
height: bottom - top
|
|
16736
|
+
});
|
|
16737
|
+
}
|
|
16738
|
+
/**
|
|
16739
|
+
* Tile the whole canvas with `count` cells, leaving no gap.
|
|
16740
|
+
*
|
|
16741
|
+
* Autofill exists because the useful answer to "put these four cameras up" is
|
|
16742
|
+
* almost never the one hand-dragging produces: rectangles land a pixel apart
|
|
16743
|
+
* and the composition shows black seams the operator cannot see in the editor
|
|
16744
|
+
* but will see in the stream.
|
|
16745
|
+
*
|
|
16746
|
+
* The shape is the near-square one — `ceil(sqrt(n))` columns — and a short last
|
|
16747
|
+
* row STRETCHES to the full width rather than leaving a hole. Three cameras
|
|
16748
|
+
* become two over one, not a 2x2 with an empty quarter, which is precisely the
|
|
16749
|
+
* layout autofill is asked to avoid.
|
|
16750
|
+
*
|
|
16751
|
+
* Aspect is deliberately not considered: these are CANVAS fractions, and what a
|
|
16752
|
+
* source shows inside its cell is the crop's business (the source rectangle),
|
|
16753
|
+
* not the placement's. Mixing the two here would silently re-crop every cell
|
|
16754
|
+
* whenever the canvas was resized.
|
|
16755
|
+
*/
|
|
16756
|
+
function autofillCellRects(count) {
|
|
16757
|
+
if (!Number.isFinite(count) || count <= 0) return [];
|
|
16758
|
+
const cells = Math.floor(count);
|
|
16759
|
+
const columns = Math.ceil(Math.sqrt(cells));
|
|
16760
|
+
const rows = Math.ceil(cells / columns);
|
|
16761
|
+
const rects = [];
|
|
16762
|
+
let placed = 0;
|
|
16763
|
+
for (let row = 0; row < rows; row++) {
|
|
16764
|
+
const inRow = Math.min(columns, cells - placed);
|
|
16765
|
+
const height = 1 / rows;
|
|
16766
|
+
const width = 1 / inRow;
|
|
16767
|
+
for (let column = 0; column < inRow; column++) rects.push({
|
|
16768
|
+
x: column * width,
|
|
16769
|
+
y: row * height,
|
|
16770
|
+
width,
|
|
16771
|
+
height
|
|
16772
|
+
});
|
|
16773
|
+
placed += inRow;
|
|
16774
|
+
}
|
|
16775
|
+
return rects;
|
|
16674
16776
|
}
|
|
16675
16777
|
//#endregion
|
|
16676
16778
|
//#region src/composites/cap-settings/CameraGridLayoutEditor.tsx
|
|
@@ -16704,6 +16806,31 @@ var FULL_FRAME = {
|
|
|
16704
16806
|
var MAX_CELLS = 16;
|
|
16705
16807
|
/** Hoisted so `useDevices` does not re-subscribe on every render. */
|
|
16706
16808
|
var CAMERA_FILTER = { type: _camstack_types.DeviceType.Camera };
|
|
16809
|
+
/** The eight grips, in the order they are drawn. */
|
|
16810
|
+
var HANDLES = [
|
|
16811
|
+
"nw",
|
|
16812
|
+
"n",
|
|
16813
|
+
"ne",
|
|
16814
|
+
"e",
|
|
16815
|
+
"se",
|
|
16816
|
+
"s",
|
|
16817
|
+
"sw",
|
|
16818
|
+
"w"
|
|
16819
|
+
];
|
|
16820
|
+
var HANDLE_STYLE = {
|
|
16821
|
+
nw: "-top-1 -left-1 cursor-nw-resize",
|
|
16822
|
+
n: "-top-1 left-1/2 -translate-x-1/2 cursor-n-resize",
|
|
16823
|
+
ne: "-top-1 -right-1 cursor-ne-resize",
|
|
16824
|
+
e: "top-1/2 -right-1 -translate-y-1/2 cursor-e-resize",
|
|
16825
|
+
se: "-bottom-1 -right-1 cursor-se-resize",
|
|
16826
|
+
s: "-bottom-1 left-1/2 -translate-x-1/2 cursor-s-resize",
|
|
16827
|
+
sw: "-bottom-1 -left-1 cursor-sw-resize",
|
|
16828
|
+
w: "top-1/2 -left-1 -translate-y-1/2 cursor-w-resize"
|
|
16829
|
+
};
|
|
16830
|
+
/** Move or resize, from one drag and one delta. */
|
|
16831
|
+
function applyDrag(drag, dx, dy) {
|
|
16832
|
+
return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
|
|
16833
|
+
}
|
|
16707
16834
|
function CameraGridLayoutEditor({ deviceId }) {
|
|
16708
16835
|
const system = useSystem();
|
|
16709
16836
|
const cameras = (0, react$1.useMemo)(() => system.listDeviceInfos(CAMERA_FILTER), [system, useDevices(CAMERA_FILTER)]);
|
|
@@ -16792,19 +16919,39 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16792
16919
|
if (drag === null) return;
|
|
16793
16920
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
16794
16921
|
if (bounds.width === 0 || bounds.height === 0) return;
|
|
16922
|
+
if (drag.target !== "cell") return;
|
|
16795
16923
|
const dx = (event.clientX - drag.startX) / bounds.width;
|
|
16796
16924
|
const dy = (event.clientY - drag.startY) / bounds.height;
|
|
16797
|
-
patchCell(drag.index, { cell:
|
|
16925
|
+
patchCell(drag.index, { cell: applyDrag(drag, dx, dy) });
|
|
16798
16926
|
};
|
|
16799
16927
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetPanel, {
|
|
16800
16928
|
title: "Grid layout",
|
|
16801
16929
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Grid2x2, { className: "h-3.5 w-3.5" }),
|
|
16802
|
-
headerRight: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16930
|
+
headerRight: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16931
|
+
className: "flex items-center gap-1",
|
|
16932
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
16933
|
+
type: "button",
|
|
16934
|
+
disabled: draft.cells.length === 0,
|
|
16935
|
+
title: "Tile every cell over the whole canvas, leaving no gap",
|
|
16936
|
+
onClick: () => {
|
|
16937
|
+
const rects = autofillCellRects(draft.cells.length);
|
|
16938
|
+
setDraft({
|
|
16939
|
+
...draft,
|
|
16940
|
+
cells: draft.cells.map((cell, index) => ({
|
|
16941
|
+
...cell,
|
|
16942
|
+
cell: rects[index] ?? cell.cell
|
|
16943
|
+
}))
|
|
16944
|
+
});
|
|
16945
|
+
},
|
|
16946
|
+
className: "flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[10px] uppercase",
|
|
16947
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayoutGrid, { className: "h-3 w-3" }), " Autofill"]
|
|
16948
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
16949
|
+
type: "button",
|
|
16950
|
+
disabled: saving,
|
|
16951
|
+
onClick: () => void save(),
|
|
16952
|
+
className: "flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[10px] uppercase",
|
|
16953
|
+
children: [saving ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Save, { className: "h-3 w-3" }), "Save"]
|
|
16954
|
+
})]
|
|
16808
16955
|
}),
|
|
16809
16956
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16810
16957
|
className: "flex flex-col gap-3",
|
|
@@ -16837,6 +16984,7 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16837
16984
|
setSelected(index);
|
|
16838
16985
|
setDrag({
|
|
16839
16986
|
index,
|
|
16987
|
+
target: "cell",
|
|
16840
16988
|
mode,
|
|
16841
16989
|
startX: event.clientX,
|
|
16842
16990
|
startY: event.clientY,
|
|
@@ -16982,14 +17130,14 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
16982
17130
|
backgroundPosition,
|
|
16983
17131
|
backgroundRepeat: "no-repeat"
|
|
16984
17132
|
},
|
|
16985
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17133
|
+
children: selected && HANDLES.map((handle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
16986
17134
|
role: "presentation",
|
|
16987
17135
|
onPointerDown: (event) => {
|
|
16988
17136
|
event.stopPropagation();
|
|
16989
|
-
onDragStart(
|
|
17137
|
+
onDragStart(handle, event);
|
|
16990
17138
|
},
|
|
16991
|
-
className:
|
|
16992
|
-
})
|
|
17139
|
+
className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
|
|
17140
|
+
}, handle))
|
|
16993
17141
|
});
|
|
16994
17142
|
}
|
|
16995
17143
|
function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
@@ -17025,6 +17173,13 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17025
17173
|
}, camera.id))
|
|
17026
17174
|
})]
|
|
17027
17175
|
}),
|
|
17176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SourceCropEditor, {
|
|
17177
|
+
deviceId: cell.deviceId,
|
|
17178
|
+
source: cell.source,
|
|
17179
|
+
onChange: (source) => {
|
|
17180
|
+
onChange({ source });
|
|
17181
|
+
}
|
|
17182
|
+
}),
|
|
17028
17183
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17029
17184
|
className: "grid grid-cols-4 gap-2",
|
|
17030
17185
|
children: [
|
|
@@ -17047,6 +17202,107 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17047
17202
|
]
|
|
17048
17203
|
});
|
|
17049
17204
|
}
|
|
17205
|
+
/**
|
|
17206
|
+
* The crop, on the source camera's own frame.
|
|
17207
|
+
*
|
|
17208
|
+
* The whole snapshot is drawn at full opacity behind a dimmed overlay, and the
|
|
17209
|
+
* selected rectangle is the hole in it: what is INSIDE is what the grid will
|
|
17210
|
+
* show. Drawing it the other way round — bright rectangle on a dark picture —
|
|
17211
|
+
* reads as "this part is highlighted", which is not the same claim.
|
|
17212
|
+
*
|
|
17213
|
+
* Its drag state is local. The canvas above owns cell placement and this owns
|
|
17214
|
+
* the crop; sharing one drag state would mean every pointer move on either
|
|
17215
|
+
* canvas consulted a `target` discriminator to decide whether it was for the
|
|
17216
|
+
* other one.
|
|
17217
|
+
*/
|
|
17218
|
+
function SourceCropEditor({ deviceId, source, onChange }) {
|
|
17219
|
+
const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
|
|
17220
|
+
const [drag, setDrag] = (0, react$1.useState)(null);
|
|
17221
|
+
const onPointerMove = (event) => {
|
|
17222
|
+
if (drag === null) return;
|
|
17223
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
17224
|
+
if (bounds.width === 0 || bounds.height === 0) return;
|
|
17225
|
+
onChange(applyDrag({
|
|
17226
|
+
...drag,
|
|
17227
|
+
index: 0,
|
|
17228
|
+
target: "source"
|
|
17229
|
+
}, (event.clientX - drag.startX) / bounds.width, (event.clientY - drag.startY) / bounds.height));
|
|
17230
|
+
};
|
|
17231
|
+
const full = source.x === 0 && source.y === 0 && source.width === 1 && source.height === 1;
|
|
17232
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17233
|
+
className: "flex flex-col gap-1",
|
|
17234
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17235
|
+
className: "flex items-center justify-between",
|
|
17236
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17237
|
+
className: "uppercase text-foreground-subtle",
|
|
17238
|
+
children: "Visible part of the camera"
|
|
17239
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17240
|
+
type: "button",
|
|
17241
|
+
disabled: full,
|
|
17242
|
+
onClick: () => {
|
|
17243
|
+
onChange(FULL_FRAME);
|
|
17244
|
+
},
|
|
17245
|
+
className: "flex items-center gap-1 rounded border border-border px-1.5 py-0.5 uppercase disabled:opacity-40",
|
|
17246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Maximize2, { className: "h-3 w-3" }), " Whole frame"]
|
|
17247
|
+
})]
|
|
17248
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17249
|
+
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
17250
|
+
style: { aspectRatio: "16 / 9" },
|
|
17251
|
+
onPointerMove,
|
|
17252
|
+
onPointerUp: () => {
|
|
17253
|
+
setDrag(null);
|
|
17254
|
+
},
|
|
17255
|
+
onPointerLeave: () => {
|
|
17256
|
+
setDrag(null);
|
|
17257
|
+
},
|
|
17258
|
+
children: [
|
|
17259
|
+
src === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17260
|
+
className: "absolute inset-0 flex items-center justify-center text-[10px] text-foreground-subtle",
|
|
17261
|
+
children: "No snapshot from this camera yet — the numbers below still apply."
|
|
17262
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
17263
|
+
src,
|
|
17264
|
+
alt: "",
|
|
17265
|
+
className: "absolute inset-0 h-full w-full object-cover"
|
|
17266
|
+
}),
|
|
17267
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
|
|
17268
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17269
|
+
role: "presentation",
|
|
17270
|
+
onPointerDown: (event) => {
|
|
17271
|
+
setDrag({
|
|
17272
|
+
mode: "move",
|
|
17273
|
+
startX: event.clientX,
|
|
17274
|
+
startY: event.clientY,
|
|
17275
|
+
origin: source
|
|
17276
|
+
});
|
|
17277
|
+
},
|
|
17278
|
+
className: "absolute cursor-move border border-accent",
|
|
17279
|
+
style: {
|
|
17280
|
+
left: `${String(source.x * 100)}%`,
|
|
17281
|
+
top: `${String(source.y * 100)}%`,
|
|
17282
|
+
width: `${String(source.width * 100)}%`,
|
|
17283
|
+
height: `${String(source.height * 100)}%`,
|
|
17284
|
+
backgroundImage: src === null ? void 0 : `url(${src})`,
|
|
17285
|
+
backgroundSize: `${String(100 / source.width)}% ${String(100 / source.height)}%`,
|
|
17286
|
+
backgroundPosition: source.width >= 1 && source.height >= 1 ? "center" : `${String(source.x / Math.max(1e-6, 1 - source.width) * 100)}% ${String(source.y / Math.max(1e-6, 1 - source.height) * 100)}%`
|
|
17287
|
+
},
|
|
17288
|
+
children: HANDLES.map((handle) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17289
|
+
role: "presentation",
|
|
17290
|
+
onPointerDown: (event) => {
|
|
17291
|
+
event.stopPropagation();
|
|
17292
|
+
setDrag({
|
|
17293
|
+
mode: handle,
|
|
17294
|
+
startX: event.clientX,
|
|
17295
|
+
startY: event.clientY,
|
|
17296
|
+
origin: source
|
|
17297
|
+
});
|
|
17298
|
+
},
|
|
17299
|
+
className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
|
|
17300
|
+
}, handle))
|
|
17301
|
+
})
|
|
17302
|
+
]
|
|
17303
|
+
})]
|
|
17304
|
+
});
|
|
17305
|
+
}
|
|
17050
17306
|
function LabelledNumber({ label, value, step = 1, onChange }) {
|
|
17051
17307
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17052
17308
|
className: "flex flex-col gap-0.5",
|
package/dist/index.js
CHANGED
|
@@ -2341,6 +2341,46 @@ var Laugh = createLucideIcon("laugh", [
|
|
|
2341
2341
|
* This source code is licensed under the ISC license.
|
|
2342
2342
|
* See the LICENSE file in the root directory of this source tree.
|
|
2343
2343
|
*/
|
|
2344
|
+
var LayoutGrid = createLucideIcon("layout-grid", [
|
|
2345
|
+
["rect", {
|
|
2346
|
+
width: "7",
|
|
2347
|
+
height: "7",
|
|
2348
|
+
x: "3",
|
|
2349
|
+
y: "3",
|
|
2350
|
+
rx: "1",
|
|
2351
|
+
key: "1g98yp"
|
|
2352
|
+
}],
|
|
2353
|
+
["rect", {
|
|
2354
|
+
width: "7",
|
|
2355
|
+
height: "7",
|
|
2356
|
+
x: "14",
|
|
2357
|
+
y: "3",
|
|
2358
|
+
rx: "1",
|
|
2359
|
+
key: "6d4xhi"
|
|
2360
|
+
}],
|
|
2361
|
+
["rect", {
|
|
2362
|
+
width: "7",
|
|
2363
|
+
height: "7",
|
|
2364
|
+
x: "14",
|
|
2365
|
+
y: "14",
|
|
2366
|
+
rx: "1",
|
|
2367
|
+
key: "nxv5o0"
|
|
2368
|
+
}],
|
|
2369
|
+
["rect", {
|
|
2370
|
+
width: "7",
|
|
2371
|
+
height: "7",
|
|
2372
|
+
x: "3",
|
|
2373
|
+
y: "14",
|
|
2374
|
+
rx: "1",
|
|
2375
|
+
key: "1bb6yr"
|
|
2376
|
+
}]
|
|
2377
|
+
]);
|
|
2378
|
+
/**
|
|
2379
|
+
* @license lucide-react v0.576.0 - ISC
|
|
2380
|
+
*
|
|
2381
|
+
* This source code is licensed under the ISC license.
|
|
2382
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
2383
|
+
*/
|
|
2344
2384
|
var Leaf = createLucideIcon("leaf", [["path", {
|
|
2345
2385
|
d: "M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z",
|
|
2346
2386
|
key: "nnexq3"
|
|
@@ -16640,13 +16680,75 @@ function moveNormalizedRect(rect, dx, dy) {
|
|
|
16640
16680
|
y: rect.y + dy
|
|
16641
16681
|
});
|
|
16642
16682
|
}
|
|
16643
|
-
/**
|
|
16644
|
-
|
|
16683
|
+
/**
|
|
16684
|
+
* Resize from ANY handle: the opposite side is the anchor.
|
|
16685
|
+
*
|
|
16686
|
+
* The editor had one grip, in the bottom-right, which meant a cell could only
|
|
16687
|
+
* grow away from its origin — to move its top edge an operator had to drag the
|
|
16688
|
+
* whole cell and then resize it back. The rectangle is expressed as its two
|
|
16689
|
+
* edges here rather than origin+extent, because that is what makes "the
|
|
16690
|
+
* opposite edge does not move" fall out instead of being arithmetic to get
|
|
16691
|
+
* wrong. `clampNormalizedRect` still has the last word: the addon THROWS on a
|
|
16692
|
+
* rectangle outside the unit square.
|
|
16693
|
+
*/
|
|
16694
|
+
function resizeNormalizedRectFrom(rect, handle, dx, dy) {
|
|
16695
|
+
const west = handle === "nw" || handle === "w" || handle === "sw";
|
|
16696
|
+
const east = handle === "ne" || handle === "e" || handle === "se";
|
|
16697
|
+
const north = handle === "nw" || handle === "n" || handle === "ne";
|
|
16698
|
+
const south = handle === "sw" || handle === "s" || handle === "se";
|
|
16699
|
+
let left = rect.x;
|
|
16700
|
+
let right = rect.x + rect.width;
|
|
16701
|
+
let top = rect.y;
|
|
16702
|
+
let bottom = rect.y + rect.height;
|
|
16703
|
+
if (west) left = Math.min(right - MIN_EXTENT, Math.max(0, left + dx));
|
|
16704
|
+
if (east) right = Math.max(left + MIN_EXTENT, Math.min(1, right + dx));
|
|
16705
|
+
if (north) top = Math.min(bottom - MIN_EXTENT, Math.max(0, top + dy));
|
|
16706
|
+
if (south) bottom = Math.max(top + MIN_EXTENT, Math.min(1, bottom + dy));
|
|
16645
16707
|
return clampNormalizedRect({
|
|
16646
|
-
|
|
16647
|
-
|
|
16648
|
-
|
|
16649
|
-
|
|
16708
|
+
x: left,
|
|
16709
|
+
y: top,
|
|
16710
|
+
width: right - left,
|
|
16711
|
+
height: bottom - top
|
|
16712
|
+
});
|
|
16713
|
+
}
|
|
16714
|
+
/**
|
|
16715
|
+
* Tile the whole canvas with `count` cells, leaving no gap.
|
|
16716
|
+
*
|
|
16717
|
+
* Autofill exists because the useful answer to "put these four cameras up" is
|
|
16718
|
+
* almost never the one hand-dragging produces: rectangles land a pixel apart
|
|
16719
|
+
* and the composition shows black seams the operator cannot see in the editor
|
|
16720
|
+
* but will see in the stream.
|
|
16721
|
+
*
|
|
16722
|
+
* The shape is the near-square one — `ceil(sqrt(n))` columns — and a short last
|
|
16723
|
+
* row STRETCHES to the full width rather than leaving a hole. Three cameras
|
|
16724
|
+
* become two over one, not a 2x2 with an empty quarter, which is precisely the
|
|
16725
|
+
* layout autofill is asked to avoid.
|
|
16726
|
+
*
|
|
16727
|
+
* Aspect is deliberately not considered: these are CANVAS fractions, and what a
|
|
16728
|
+
* source shows inside its cell is the crop's business (the source rectangle),
|
|
16729
|
+
* not the placement's. Mixing the two here would silently re-crop every cell
|
|
16730
|
+
* whenever the canvas was resized.
|
|
16731
|
+
*/
|
|
16732
|
+
function autofillCellRects(count) {
|
|
16733
|
+
if (!Number.isFinite(count) || count <= 0) return [];
|
|
16734
|
+
const cells = Math.floor(count);
|
|
16735
|
+
const columns = Math.ceil(Math.sqrt(cells));
|
|
16736
|
+
const rows = Math.ceil(cells / columns);
|
|
16737
|
+
const rects = [];
|
|
16738
|
+
let placed = 0;
|
|
16739
|
+
for (let row = 0; row < rows; row++) {
|
|
16740
|
+
const inRow = Math.min(columns, cells - placed);
|
|
16741
|
+
const height = 1 / rows;
|
|
16742
|
+
const width = 1 / inRow;
|
|
16743
|
+
for (let column = 0; column < inRow; column++) rects.push({
|
|
16744
|
+
x: column * width,
|
|
16745
|
+
y: row * height,
|
|
16746
|
+
width,
|
|
16747
|
+
height
|
|
16748
|
+
});
|
|
16749
|
+
placed += inRow;
|
|
16750
|
+
}
|
|
16751
|
+
return rects;
|
|
16650
16752
|
}
|
|
16651
16753
|
//#endregion
|
|
16652
16754
|
//#region src/composites/cap-settings/CameraGridLayoutEditor.tsx
|
|
@@ -16680,6 +16782,31 @@ var FULL_FRAME = {
|
|
|
16680
16782
|
var MAX_CELLS = 16;
|
|
16681
16783
|
/** Hoisted so `useDevices` does not re-subscribe on every render. */
|
|
16682
16784
|
var CAMERA_FILTER = { type: DeviceType.Camera };
|
|
16785
|
+
/** The eight grips, in the order they are drawn. */
|
|
16786
|
+
var HANDLES = [
|
|
16787
|
+
"nw",
|
|
16788
|
+
"n",
|
|
16789
|
+
"ne",
|
|
16790
|
+
"e",
|
|
16791
|
+
"se",
|
|
16792
|
+
"s",
|
|
16793
|
+
"sw",
|
|
16794
|
+
"w"
|
|
16795
|
+
];
|
|
16796
|
+
var HANDLE_STYLE = {
|
|
16797
|
+
nw: "-top-1 -left-1 cursor-nw-resize",
|
|
16798
|
+
n: "-top-1 left-1/2 -translate-x-1/2 cursor-n-resize",
|
|
16799
|
+
ne: "-top-1 -right-1 cursor-ne-resize",
|
|
16800
|
+
e: "top-1/2 -right-1 -translate-y-1/2 cursor-e-resize",
|
|
16801
|
+
se: "-bottom-1 -right-1 cursor-se-resize",
|
|
16802
|
+
s: "-bottom-1 left-1/2 -translate-x-1/2 cursor-s-resize",
|
|
16803
|
+
sw: "-bottom-1 -left-1 cursor-sw-resize",
|
|
16804
|
+
w: "top-1/2 -left-1 -translate-y-1/2 cursor-w-resize"
|
|
16805
|
+
};
|
|
16806
|
+
/** Move or resize, from one drag and one delta. */
|
|
16807
|
+
function applyDrag(drag, dx, dy) {
|
|
16808
|
+
return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
|
|
16809
|
+
}
|
|
16683
16810
|
function CameraGridLayoutEditor({ deviceId }) {
|
|
16684
16811
|
const system = useSystem();
|
|
16685
16812
|
const cameras = useMemo(() => system.listDeviceInfos(CAMERA_FILTER), [system, useDevices(CAMERA_FILTER)]);
|
|
@@ -16768,19 +16895,39 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16768
16895
|
if (drag === null) return;
|
|
16769
16896
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
16770
16897
|
if (bounds.width === 0 || bounds.height === 0) return;
|
|
16898
|
+
if (drag.target !== "cell") return;
|
|
16771
16899
|
const dx = (event.clientX - drag.startX) / bounds.width;
|
|
16772
16900
|
const dy = (event.clientY - drag.startY) / bounds.height;
|
|
16773
|
-
patchCell(drag.index, { cell:
|
|
16901
|
+
patchCell(drag.index, { cell: applyDrag(drag, dx, dy) });
|
|
16774
16902
|
};
|
|
16775
16903
|
return /* @__PURE__ */ jsx(WidgetPanel, {
|
|
16776
16904
|
title: "Grid layout",
|
|
16777
16905
|
icon: /* @__PURE__ */ jsx(Grid2x2, { className: "h-3.5 w-3.5" }),
|
|
16778
|
-
headerRight: /* @__PURE__ */ jsxs("
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
16783
|
-
|
|
16906
|
+
headerRight: /* @__PURE__ */ jsxs("div", {
|
|
16907
|
+
className: "flex items-center gap-1",
|
|
16908
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
16909
|
+
type: "button",
|
|
16910
|
+
disabled: draft.cells.length === 0,
|
|
16911
|
+
title: "Tile every cell over the whole canvas, leaving no gap",
|
|
16912
|
+
onClick: () => {
|
|
16913
|
+
const rects = autofillCellRects(draft.cells.length);
|
|
16914
|
+
setDraft({
|
|
16915
|
+
...draft,
|
|
16916
|
+
cells: draft.cells.map((cell, index) => ({
|
|
16917
|
+
...cell,
|
|
16918
|
+
cell: rects[index] ?? cell.cell
|
|
16919
|
+
}))
|
|
16920
|
+
});
|
|
16921
|
+
},
|
|
16922
|
+
className: "flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[10px] uppercase",
|
|
16923
|
+
children: [/* @__PURE__ */ jsx(LayoutGrid, { className: "h-3 w-3" }), " Autofill"]
|
|
16924
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
16925
|
+
type: "button",
|
|
16926
|
+
disabled: saving,
|
|
16927
|
+
onClick: () => void save(),
|
|
16928
|
+
className: "flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[10px] uppercase",
|
|
16929
|
+
children: [saving ? /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3 w-3" }), "Save"]
|
|
16930
|
+
})]
|
|
16784
16931
|
}),
|
|
16785
16932
|
children: /* @__PURE__ */ jsxs("div", {
|
|
16786
16933
|
className: "flex flex-col gap-3",
|
|
@@ -16813,6 +16960,7 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16813
16960
|
setSelected(index);
|
|
16814
16961
|
setDrag({
|
|
16815
16962
|
index,
|
|
16963
|
+
target: "cell",
|
|
16816
16964
|
mode,
|
|
16817
16965
|
startX: event.clientX,
|
|
16818
16966
|
startY: event.clientY,
|
|
@@ -16958,14 +17106,14 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
16958
17106
|
backgroundPosition,
|
|
16959
17107
|
backgroundRepeat: "no-repeat"
|
|
16960
17108
|
},
|
|
16961
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
17109
|
+
children: selected && HANDLES.map((handle) => /* @__PURE__ */ jsx("div", {
|
|
16962
17110
|
role: "presentation",
|
|
16963
17111
|
onPointerDown: (event) => {
|
|
16964
17112
|
event.stopPropagation();
|
|
16965
|
-
onDragStart(
|
|
17113
|
+
onDragStart(handle, event);
|
|
16966
17114
|
},
|
|
16967
|
-
className:
|
|
16968
|
-
})
|
|
17115
|
+
className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
|
|
17116
|
+
}, handle))
|
|
16969
17117
|
});
|
|
16970
17118
|
}
|
|
16971
17119
|
function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
@@ -17001,6 +17149,13 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17001
17149
|
}, camera.id))
|
|
17002
17150
|
})]
|
|
17003
17151
|
}),
|
|
17152
|
+
/* @__PURE__ */ jsx(SourceCropEditor, {
|
|
17153
|
+
deviceId: cell.deviceId,
|
|
17154
|
+
source: cell.source,
|
|
17155
|
+
onChange: (source) => {
|
|
17156
|
+
onChange({ source });
|
|
17157
|
+
}
|
|
17158
|
+
}),
|
|
17004
17159
|
/* @__PURE__ */ jsx("div", {
|
|
17005
17160
|
className: "grid grid-cols-4 gap-2",
|
|
17006
17161
|
children: [
|
|
@@ -17023,6 +17178,107 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17023
17178
|
]
|
|
17024
17179
|
});
|
|
17025
17180
|
}
|
|
17181
|
+
/**
|
|
17182
|
+
* The crop, on the source camera's own frame.
|
|
17183
|
+
*
|
|
17184
|
+
* The whole snapshot is drawn at full opacity behind a dimmed overlay, and the
|
|
17185
|
+
* selected rectangle is the hole in it: what is INSIDE is what the grid will
|
|
17186
|
+
* show. Drawing it the other way round — bright rectangle on a dark picture —
|
|
17187
|
+
* reads as "this part is highlighted", which is not the same claim.
|
|
17188
|
+
*
|
|
17189
|
+
* Its drag state is local. The canvas above owns cell placement and this owns
|
|
17190
|
+
* the crop; sharing one drag state would mean every pointer move on either
|
|
17191
|
+
* canvas consulted a `target` discriminator to decide whether it was for the
|
|
17192
|
+
* other one.
|
|
17193
|
+
*/
|
|
17194
|
+
function SourceCropEditor({ deviceId, source, onChange }) {
|
|
17195
|
+
const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
|
|
17196
|
+
const [drag, setDrag] = useState(null);
|
|
17197
|
+
const onPointerMove = (event) => {
|
|
17198
|
+
if (drag === null) return;
|
|
17199
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
17200
|
+
if (bounds.width === 0 || bounds.height === 0) return;
|
|
17201
|
+
onChange(applyDrag({
|
|
17202
|
+
...drag,
|
|
17203
|
+
index: 0,
|
|
17204
|
+
target: "source"
|
|
17205
|
+
}, (event.clientX - drag.startX) / bounds.width, (event.clientY - drag.startY) / bounds.height));
|
|
17206
|
+
};
|
|
17207
|
+
const full = source.x === 0 && source.y === 0 && source.width === 1 && source.height === 1;
|
|
17208
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
17209
|
+
className: "flex flex-col gap-1",
|
|
17210
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
17211
|
+
className: "flex items-center justify-between",
|
|
17212
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
17213
|
+
className: "uppercase text-foreground-subtle",
|
|
17214
|
+
children: "Visible part of the camera"
|
|
17215
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
17216
|
+
type: "button",
|
|
17217
|
+
disabled: full,
|
|
17218
|
+
onClick: () => {
|
|
17219
|
+
onChange(FULL_FRAME);
|
|
17220
|
+
},
|
|
17221
|
+
className: "flex items-center gap-1 rounded border border-border px-1.5 py-0.5 uppercase disabled:opacity-40",
|
|
17222
|
+
children: [/* @__PURE__ */ jsx(Maximize2, { className: "h-3 w-3" }), " Whole frame"]
|
|
17223
|
+
})]
|
|
17224
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
17225
|
+
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
17226
|
+
style: { aspectRatio: "16 / 9" },
|
|
17227
|
+
onPointerMove,
|
|
17228
|
+
onPointerUp: () => {
|
|
17229
|
+
setDrag(null);
|
|
17230
|
+
},
|
|
17231
|
+
onPointerLeave: () => {
|
|
17232
|
+
setDrag(null);
|
|
17233
|
+
},
|
|
17234
|
+
children: [
|
|
17235
|
+
src === null ? /* @__PURE__ */ jsx("div", {
|
|
17236
|
+
className: "absolute inset-0 flex items-center justify-center text-[10px] text-foreground-subtle",
|
|
17237
|
+
children: "No snapshot from this camera yet — the numbers below still apply."
|
|
17238
|
+
}) : /* @__PURE__ */ jsx("img", {
|
|
17239
|
+
src,
|
|
17240
|
+
alt: "",
|
|
17241
|
+
className: "absolute inset-0 h-full w-full object-cover"
|
|
17242
|
+
}),
|
|
17243
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
|
|
17244
|
+
/* @__PURE__ */ jsx("div", {
|
|
17245
|
+
role: "presentation",
|
|
17246
|
+
onPointerDown: (event) => {
|
|
17247
|
+
setDrag({
|
|
17248
|
+
mode: "move",
|
|
17249
|
+
startX: event.clientX,
|
|
17250
|
+
startY: event.clientY,
|
|
17251
|
+
origin: source
|
|
17252
|
+
});
|
|
17253
|
+
},
|
|
17254
|
+
className: "absolute cursor-move border border-accent",
|
|
17255
|
+
style: {
|
|
17256
|
+
left: `${String(source.x * 100)}%`,
|
|
17257
|
+
top: `${String(source.y * 100)}%`,
|
|
17258
|
+
width: `${String(source.width * 100)}%`,
|
|
17259
|
+
height: `${String(source.height * 100)}%`,
|
|
17260
|
+
backgroundImage: src === null ? void 0 : `url(${src})`,
|
|
17261
|
+
backgroundSize: `${String(100 / source.width)}% ${String(100 / source.height)}%`,
|
|
17262
|
+
backgroundPosition: source.width >= 1 && source.height >= 1 ? "center" : `${String(source.x / Math.max(1e-6, 1 - source.width) * 100)}% ${String(source.y / Math.max(1e-6, 1 - source.height) * 100)}%`
|
|
17263
|
+
},
|
|
17264
|
+
children: HANDLES.map((handle) => /* @__PURE__ */ jsx("div", {
|
|
17265
|
+
role: "presentation",
|
|
17266
|
+
onPointerDown: (event) => {
|
|
17267
|
+
event.stopPropagation();
|
|
17268
|
+
setDrag({
|
|
17269
|
+
mode: handle,
|
|
17270
|
+
startX: event.clientX,
|
|
17271
|
+
startY: event.clientY,
|
|
17272
|
+
origin: source
|
|
17273
|
+
});
|
|
17274
|
+
},
|
|
17275
|
+
className: `absolute h-2.5 w-2.5 bg-accent ${HANDLE_STYLE[handle]}`
|
|
17276
|
+
}, handle))
|
|
17277
|
+
})
|
|
17278
|
+
]
|
|
17279
|
+
})]
|
|
17280
|
+
});
|
|
17281
|
+
}
|
|
17026
17282
|
function LabelledNumber({ label, value, step = 1, onChange }) {
|
|
17027
17283
|
return /* @__PURE__ */ jsxs("label", {
|
|
17028
17284
|
className: "flex flex-col gap-0.5",
|