@camstack/ui-library 1.2.178 → 1.2.180
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 +6 -2
- package/dist/index.cjs +177 -84
- package/dist/index.js +177 -84
- package/package.json +1 -1
|
@@ -32,13 +32,17 @@ export interface GridProfileOffer {
|
|
|
32
32
|
readonly profile: 'high' | 'mid' | 'low';
|
|
33
33
|
readonly offered: boolean;
|
|
34
34
|
readonly missingSources: readonly number[];
|
|
35
|
+
/**
|
|
36
|
+
* The canvas this profile composes onto, `WxH`, or empty when it is not
|
|
37
|
+
* offered. DERIVED — the operator no longer types a resolution, so this is
|
|
38
|
+
* the only place the grid's actual size is ever stated.
|
|
39
|
+
*/
|
|
40
|
+
readonly canvas: string;
|
|
35
41
|
}
|
|
36
42
|
export interface GridLayoutView {
|
|
37
43
|
readonly instanceId: string;
|
|
38
44
|
readonly deviceId: number;
|
|
39
45
|
readonly name: string;
|
|
40
|
-
readonly width: number;
|
|
41
|
-
readonly height: number;
|
|
42
46
|
readonly fps: number;
|
|
43
47
|
readonly cells: readonly GridLayoutCell[];
|
|
44
48
|
readonly profiles: readonly GridProfileOffer[];
|
package/dist/index.cjs
CHANGED
|
@@ -16641,7 +16641,8 @@ function readProfileOffer(value) {
|
|
|
16641
16641
|
return {
|
|
16642
16642
|
profile,
|
|
16643
16643
|
offered,
|
|
16644
|
-
missingSources
|
|
16644
|
+
missingSources,
|
|
16645
|
+
canvas: typeof value["canvas"] === "string" ? value["canvas"] : ""
|
|
16645
16646
|
};
|
|
16646
16647
|
}
|
|
16647
16648
|
/**
|
|
@@ -16655,8 +16656,8 @@ function readProfileOffer(value) {
|
|
|
16655
16656
|
function parseGridView(raw) {
|
|
16656
16657
|
if (raw === null) return null;
|
|
16657
16658
|
if (!isRecord(raw)) return "unreadable";
|
|
16658
|
-
const { instanceId, deviceId, name,
|
|
16659
|
-
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof
|
|
16659
|
+
const { instanceId, deviceId, name, fps, cells, profiles } = raw;
|
|
16660
|
+
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
|
|
16660
16661
|
const parsed = [];
|
|
16661
16662
|
for (const entry of cells) {
|
|
16662
16663
|
const cell = readCell(entry);
|
|
@@ -16673,8 +16674,6 @@ function parseGridView(raw) {
|
|
|
16673
16674
|
instanceId,
|
|
16674
16675
|
deviceId,
|
|
16675
16676
|
name,
|
|
16676
|
-
width,
|
|
16677
|
-
height,
|
|
16678
16677
|
fps,
|
|
16679
16678
|
cells: parsed,
|
|
16680
16679
|
profiles: offers
|
|
@@ -16806,6 +16805,25 @@ var FULL_FRAME = {
|
|
|
16806
16805
|
var MAX_CELLS = 16;
|
|
16807
16806
|
/** Hoisted so `useDevices` does not re-subscribe on every render. */
|
|
16808
16807
|
var CAMERA_FILTER = { type: _camstack_types.DeviceType.Camera };
|
|
16808
|
+
/**
|
|
16809
|
+
* A rectangle being dragged must not also PAN THE PAGE.
|
|
16810
|
+
*
|
|
16811
|
+
* On a phone a pointerdown on a `div` is a scroll gesture until the browser is
|
|
16812
|
+
* told otherwise, and `preventDefault` in the handler is too late — the browser
|
|
16813
|
+
* has already decided. `touch-action: none` is the only thing that makes a drag
|
|
16814
|
+
* work with a finger, and every surface that starts one carries it.
|
|
16815
|
+
*/
|
|
16816
|
+
var DRAG_SURFACE = { touchAction: "none" };
|
|
16817
|
+
/**
|
|
16818
|
+
* Finger-sized hit area around a grip.
|
|
16819
|
+
*
|
|
16820
|
+
* The visible square stays small (it sits ON the picture being edited and a big
|
|
16821
|
+
* one hides what it is positioning); the TOUCHABLE box around it is 24px, which
|
|
16822
|
+
* is the smallest target size that does not need a fingernail. The two are
|
|
16823
|
+
* different boxes on purpose — making the dot itself 24px would cover a quarter
|
|
16824
|
+
* of a small cell.
|
|
16825
|
+
*/
|
|
16826
|
+
var HANDLE_HIT = "absolute flex h-6 w-6 items-center justify-center";
|
|
16809
16827
|
/** The eight grips, in the order they are drawn. */
|
|
16810
16828
|
var HANDLES = [
|
|
16811
16829
|
"nw",
|
|
@@ -16818,15 +16836,26 @@ var HANDLES = [
|
|
|
16818
16836
|
"w"
|
|
16819
16837
|
];
|
|
16820
16838
|
var HANDLE_STYLE = {
|
|
16821
|
-
nw: "-top-
|
|
16822
|
-
n: "-top-
|
|
16823
|
-
ne: "-top-
|
|
16824
|
-
e: "top-1/2 -right-
|
|
16825
|
-
se: "-bottom-
|
|
16826
|
-
s: "-bottom-
|
|
16827
|
-
sw: "-bottom-
|
|
16828
|
-
w: "top-1/2 -left-
|
|
16839
|
+
nw: "-top-3 -left-3 cursor-nw-resize",
|
|
16840
|
+
n: "-top-3 left-1/2 -translate-x-1/2 cursor-n-resize",
|
|
16841
|
+
ne: "-top-3 -right-3 cursor-ne-resize",
|
|
16842
|
+
e: "top-1/2 -right-3 -translate-y-1/2 cursor-e-resize",
|
|
16843
|
+
se: "-bottom-3 -right-3 cursor-se-resize",
|
|
16844
|
+
s: "-bottom-3 left-1/2 -translate-x-1/2 cursor-s-resize",
|
|
16845
|
+
sw: "-bottom-3 -left-3 cursor-sw-resize",
|
|
16846
|
+
w: "top-1/2 -left-3 -translate-y-1/2 cursor-w-resize"
|
|
16829
16847
|
};
|
|
16848
|
+
/**
|
|
16849
|
+
* Start a drag and KEEP it, even when the finger leaves the rectangle.
|
|
16850
|
+
*
|
|
16851
|
+
* Without pointer capture a drag ends the moment the pointer crosses the box it
|
|
16852
|
+
* started in — which on a phone is immediately, because the box is small and
|
|
16853
|
+
* the finger is not. With capture the events keep coming to the element that
|
|
16854
|
+
* was grabbed and still bubble to the canvas that does the arithmetic.
|
|
16855
|
+
*/
|
|
16856
|
+
function capturePointer(event) {
|
|
16857
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
16858
|
+
}
|
|
16830
16859
|
/** Move or resize, from one drag and one delta. */
|
|
16831
16860
|
function applyDrag(drag, dx, dy) {
|
|
16832
16861
|
return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
|
|
@@ -16866,8 +16895,6 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16866
16895
|
try {
|
|
16867
16896
|
const outcome = parseGridView(await layout.saveLayout({
|
|
16868
16897
|
name: draft.name,
|
|
16869
|
-
width: draft.width,
|
|
16870
|
-
height: draft.height,
|
|
16871
16898
|
fps: draft.fps,
|
|
16872
16899
|
cells: draft.cells.map((cell) => ({ ...cell }))
|
|
16873
16900
|
}));
|
|
@@ -16890,7 +16917,27 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16890
16917
|
} : cell)
|
|
16891
16918
|
});
|
|
16892
16919
|
}, []);
|
|
16893
|
-
|
|
16920
|
+
/**
|
|
16921
|
+
* The shape the grid will actually come out as.
|
|
16922
|
+
*
|
|
16923
|
+
* The canvas is no longer a field: each profile derives its own from the
|
|
16924
|
+
* cells and the sources' dimensions, and the server reports what it came out
|
|
16925
|
+
* as. Drawing at that aspect is the only way the editor shows the truth —
|
|
16926
|
+
* before, it drew at whatever two numbers had been typed, which is exactly
|
|
16927
|
+
* how a layout could look right here and wrong on screen.
|
|
16928
|
+
*
|
|
16929
|
+
* 16:9 while nothing is offered yet: a grid with no cells has no shape, and
|
|
16930
|
+
* an empty editor has to be drawn at something.
|
|
16931
|
+
*/
|
|
16932
|
+
const canvasAspect = (0, react$1.useMemo)(() => {
|
|
16933
|
+
const offered = loaded === void 0 || loaded === null || loaded === "unreadable" ? [] : loaded.profiles;
|
|
16934
|
+
for (const row of offered) {
|
|
16935
|
+
if (!row.offered) continue;
|
|
16936
|
+
const [width, height] = row.canvas.split("x").map(Number);
|
|
16937
|
+
if (width !== void 0 && height !== void 0 && width > 0 && height > 0) return width / height;
|
|
16938
|
+
}
|
|
16939
|
+
return 16 / 9;
|
|
16940
|
+
}, [loaded]);
|
|
16894
16941
|
if (loaded === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetPanel, {
|
|
16895
16942
|
title: "Grid layout",
|
|
16896
16943
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Grid2x2, { className: "h-3.5 w-3.5" }),
|
|
@@ -16966,12 +17013,15 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16966
17013
|
}),
|
|
16967
17014
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16968
17015
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
16969
|
-
style: {
|
|
17016
|
+
style: {
|
|
17017
|
+
aspectRatio: String(canvasAspect),
|
|
17018
|
+
...DRAG_SURFACE
|
|
17019
|
+
},
|
|
16970
17020
|
onPointerMove,
|
|
16971
17021
|
onPointerUp: () => {
|
|
16972
17022
|
setDrag(null);
|
|
16973
17023
|
},
|
|
16974
|
-
|
|
17024
|
+
onPointerCancel: () => {
|
|
16975
17025
|
setDrag(null);
|
|
16976
17026
|
},
|
|
16977
17027
|
children: [draft.cells.map((cell, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridCellBox, {
|
|
@@ -16998,62 +17048,39 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16998
17048
|
}),
|
|
16999
17049
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17000
17050
|
className: "grid grid-cols-4 gap-2 text-[10.5px]",
|
|
17001
|
-
children: [
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
17033
|
-
|
|
17034
|
-
disabled: draft.cells.length >= MAX_CELLS,
|
|
17035
|
-
onClick: () => {
|
|
17036
|
-
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17037
|
-
if (!first) return;
|
|
17038
|
-
setDraft({
|
|
17039
|
-
...draft,
|
|
17040
|
-
cells: [...draft.cells, {
|
|
17041
|
-
deviceId: first.id,
|
|
17042
|
-
source: FULL_FRAME,
|
|
17043
|
-
cell: clampNormalizedRect({
|
|
17044
|
-
x: .1,
|
|
17045
|
-
y: .1,
|
|
17046
|
-
width: .4,
|
|
17047
|
-
height: .4
|
|
17048
|
-
})
|
|
17049
|
-
}]
|
|
17050
|
-
});
|
|
17051
|
-
setSelected(draft.cells.length);
|
|
17052
|
-
},
|
|
17053
|
-
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17054
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17055
|
-
})
|
|
17056
|
-
]
|
|
17051
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabelledNumber, {
|
|
17052
|
+
label: "FPS",
|
|
17053
|
+
value: draft.fps,
|
|
17054
|
+
onChange: (fps) => {
|
|
17055
|
+
setDraft({
|
|
17056
|
+
...draft,
|
|
17057
|
+
fps
|
|
17058
|
+
});
|
|
17059
|
+
}
|
|
17060
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17061
|
+
type: "button",
|
|
17062
|
+
disabled: draft.cells.length >= MAX_CELLS,
|
|
17063
|
+
onClick: () => {
|
|
17064
|
+
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17065
|
+
if (!first) return;
|
|
17066
|
+
setDraft({
|
|
17067
|
+
...draft,
|
|
17068
|
+
cells: [...draft.cells, {
|
|
17069
|
+
deviceId: first.id,
|
|
17070
|
+
source: FULL_FRAME,
|
|
17071
|
+
cell: clampNormalizedRect({
|
|
17072
|
+
x: .1,
|
|
17073
|
+
y: .1,
|
|
17074
|
+
width: .4,
|
|
17075
|
+
height: .4
|
|
17076
|
+
})
|
|
17077
|
+
}]
|
|
17078
|
+
});
|
|
17079
|
+
setSelected(draft.cells.length);
|
|
17080
|
+
},
|
|
17081
|
+
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17082
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17083
|
+
})]
|
|
17057
17084
|
}),
|
|
17058
17085
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridProfileReport, {
|
|
17059
17086
|
offers: loaded.profiles,
|
|
@@ -17101,7 +17128,11 @@ function GridProfileReport({ offers, cameras }) {
|
|
|
17101
17128
|
children: offer.profile
|
|
17102
17129
|
}), offer.offered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
17103
17130
|
className: "text-foreground-subtle",
|
|
17104
|
-
children: [
|
|
17131
|
+
children: [
|
|
17132
|
+
offer.canvas,
|
|
17133
|
+
" — composed from every source’s ",
|
|
17134
|
+
offer.profile
|
|
17135
|
+
]
|
|
17105
17136
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17106
17137
|
className: "text-danger",
|
|
17107
17138
|
children: offer.missingSources.length === 0 ? "not offered — this grid has no cells yet" : `not offered — no ${offer.profile} on ${offer.missingSources.map(nameOf).join(", ")}`
|
|
@@ -17117,10 +17148,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17117
17148
|
role: "presentation",
|
|
17118
17149
|
onPointerDown: (event) => {
|
|
17119
17150
|
onSelect();
|
|
17151
|
+
capturePointer(event);
|
|
17120
17152
|
onDragStart("move", event);
|
|
17121
17153
|
},
|
|
17122
17154
|
className: `absolute cursor-move border ${selected ? "border-accent" : "border-border/60"}`,
|
|
17123
17155
|
style: {
|
|
17156
|
+
...DRAG_SURFACE,
|
|
17124
17157
|
left: `${String(cell.cell.x * 100)}%`,
|
|
17125
17158
|
top: `${String(cell.cell.y * 100)}%`,
|
|
17126
17159
|
width: `${String(cell.cell.width * 100)}%`,
|
|
@@ -17134,9 +17167,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17134
17167
|
role: "presentation",
|
|
17135
17168
|
onPointerDown: (event) => {
|
|
17136
17169
|
event.stopPropagation();
|
|
17170
|
+
capturePointer(event);
|
|
17137
17171
|
onDragStart(handle, event);
|
|
17138
17172
|
},
|
|
17139
|
-
className:
|
|
17173
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17174
|
+
style: DRAG_SURFACE,
|
|
17175
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17140
17176
|
}, handle))
|
|
17141
17177
|
});
|
|
17142
17178
|
}
|
|
@@ -17181,16 +17217,15 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17181
17217
|
}
|
|
17182
17218
|
}),
|
|
17183
17219
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17184
|
-
className: "grid grid-cols-
|
|
17220
|
+
className: "grid grid-cols-2 gap-x-3 gap-y-1.5",
|
|
17185
17221
|
children: [
|
|
17186
17222
|
"x",
|
|
17187
17223
|
"y",
|
|
17188
17224
|
"width",
|
|
17189
17225
|
"height"
|
|
17190
|
-
].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
17226
|
+
].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CropSlider, {
|
|
17191
17227
|
label: `Crop ${key}`,
|
|
17192
|
-
value:
|
|
17193
|
-
step: .01,
|
|
17228
|
+
value: cell.source[key],
|
|
17194
17229
|
onChange: (next) => {
|
|
17195
17230
|
onChange({ source: clampNormalizedRect({
|
|
17196
17231
|
...cell.source,
|
|
@@ -17218,6 +17253,7 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17218
17253
|
function SourceCropEditor({ deviceId, source, onChange }) {
|
|
17219
17254
|
const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
|
|
17220
17255
|
const [drag, setDrag] = (0, react$1.useState)(null);
|
|
17256
|
+
const [sourceAspect, setSourceAspect] = (0, react$1.useState)(null);
|
|
17221
17257
|
const onPointerMove = (event) => {
|
|
17222
17258
|
if (drag === null) return;
|
|
17223
17259
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
@@ -17247,12 +17283,15 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17247
17283
|
})]
|
|
17248
17284
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17249
17285
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
17250
|
-
style: {
|
|
17286
|
+
style: {
|
|
17287
|
+
aspectRatio: String(sourceAspect ?? 16 / 9),
|
|
17288
|
+
...DRAG_SURFACE
|
|
17289
|
+
},
|
|
17251
17290
|
onPointerMove,
|
|
17252
17291
|
onPointerUp: () => {
|
|
17253
17292
|
setDrag(null);
|
|
17254
17293
|
},
|
|
17255
|
-
|
|
17294
|
+
onPointerCancel: () => {
|
|
17256
17295
|
setDrag(null);
|
|
17257
17296
|
},
|
|
17258
17297
|
children: [
|
|
@@ -17262,12 +17301,17 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17262
17301
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
17263
17302
|
src,
|
|
17264
17303
|
alt: "",
|
|
17265
|
-
className: "absolute inset-0 h-full w-full
|
|
17304
|
+
className: "absolute inset-0 h-full w-full",
|
|
17305
|
+
onLoad: (event) => {
|
|
17306
|
+
const { naturalWidth, naturalHeight } = event.currentTarget;
|
|
17307
|
+
if (naturalWidth > 0 && naturalHeight > 0) setSourceAspect(naturalWidth / naturalHeight);
|
|
17308
|
+
}
|
|
17266
17309
|
}),
|
|
17267
17310
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
|
|
17268
17311
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17269
17312
|
role: "presentation",
|
|
17270
17313
|
onPointerDown: (event) => {
|
|
17314
|
+
capturePointer(event);
|
|
17271
17315
|
setDrag({
|
|
17272
17316
|
mode: "move",
|
|
17273
17317
|
startX: event.clientX,
|
|
@@ -17277,6 +17321,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17277
17321
|
},
|
|
17278
17322
|
className: "absolute cursor-move border border-accent",
|
|
17279
17323
|
style: {
|
|
17324
|
+
...DRAG_SURFACE,
|
|
17280
17325
|
left: `${String(source.x * 100)}%`,
|
|
17281
17326
|
top: `${String(source.y * 100)}%`,
|
|
17282
17327
|
width: `${String(source.width * 100)}%`,
|
|
@@ -17289,6 +17334,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17289
17334
|
role: "presentation",
|
|
17290
17335
|
onPointerDown: (event) => {
|
|
17291
17336
|
event.stopPropagation();
|
|
17337
|
+
capturePointer(event);
|
|
17292
17338
|
setDrag({
|
|
17293
17339
|
mode: handle,
|
|
17294
17340
|
startX: event.clientX,
|
|
@@ -17296,13 +17342,60 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17296
17342
|
origin: source
|
|
17297
17343
|
});
|
|
17298
17344
|
},
|
|
17299
|
-
className:
|
|
17345
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17346
|
+
style: DRAG_SURFACE,
|
|
17347
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17300
17348
|
}, handle))
|
|
17301
17349
|
})
|
|
17302
17350
|
]
|
|
17303
17351
|
})]
|
|
17304
17352
|
});
|
|
17305
17353
|
}
|
|
17354
|
+
/**
|
|
17355
|
+
* One crop number, as a slider AND as a number.
|
|
17356
|
+
*
|
|
17357
|
+
* The slider is what makes this usable with a thumb — a `number` input on a
|
|
17358
|
+
* phone opens a keyboard, and adjusting a crop by typing 0.37 then 0.39 then
|
|
17359
|
+
* 0.38 is not adjusting, it is guessing. The typed field stays because a slider
|
|
17360
|
+
* cannot express "exactly a third" and because a value you can read is how you
|
|
17361
|
+
* copy a crop from one cell to another.
|
|
17362
|
+
*
|
|
17363
|
+
* Both write through `clampNormalizedRect` upstream, so neither can hand the
|
|
17364
|
+
* addon a rectangle that leaves the frame — it THROWS on one, by design.
|
|
17365
|
+
*/
|
|
17366
|
+
function CropSlider({ label, value, onChange }) {
|
|
17367
|
+
const rounded = Math.round(value * 100) / 100;
|
|
17368
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17369
|
+
className: "flex flex-col gap-0.5",
|
|
17370
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
17371
|
+
className: "flex items-baseline justify-between text-foreground-subtle",
|
|
17372
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17373
|
+
type: "number",
|
|
17374
|
+
min: 0,
|
|
17375
|
+
max: 1,
|
|
17376
|
+
step: .01,
|
|
17377
|
+
value: rounded,
|
|
17378
|
+
onChange: (event) => {
|
|
17379
|
+
const next = Number(event.target.value);
|
|
17380
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17381
|
+
},
|
|
17382
|
+
className: "w-12 rounded border border-border bg-transparent px-1 py-0.5 text-right"
|
|
17383
|
+
})]
|
|
17384
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17385
|
+
type: "range",
|
|
17386
|
+
min: 0,
|
|
17387
|
+
max: 1,
|
|
17388
|
+
step: .005,
|
|
17389
|
+
value,
|
|
17390
|
+
onChange: (event) => {
|
|
17391
|
+
const next = Number(event.target.value);
|
|
17392
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17393
|
+
},
|
|
17394
|
+
style: DRAG_SURFACE,
|
|
17395
|
+
className: "h-5 w-full accent-accent"
|
|
17396
|
+
})]
|
|
17397
|
+
});
|
|
17398
|
+
}
|
|
17306
17399
|
function LabelledNumber({ label, value, step = 1, onChange }) {
|
|
17307
17400
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17308
17401
|
className: "flex flex-col gap-0.5",
|
package/dist/index.js
CHANGED
|
@@ -16617,7 +16617,8 @@ function readProfileOffer(value) {
|
|
|
16617
16617
|
return {
|
|
16618
16618
|
profile,
|
|
16619
16619
|
offered,
|
|
16620
|
-
missingSources
|
|
16620
|
+
missingSources,
|
|
16621
|
+
canvas: typeof value["canvas"] === "string" ? value["canvas"] : ""
|
|
16621
16622
|
};
|
|
16622
16623
|
}
|
|
16623
16624
|
/**
|
|
@@ -16631,8 +16632,8 @@ function readProfileOffer(value) {
|
|
|
16631
16632
|
function parseGridView(raw) {
|
|
16632
16633
|
if (raw === null) return null;
|
|
16633
16634
|
if (!isRecord(raw)) return "unreadable";
|
|
16634
|
-
const { instanceId, deviceId, name,
|
|
16635
|
-
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof
|
|
16635
|
+
const { instanceId, deviceId, name, fps, cells, profiles } = raw;
|
|
16636
|
+
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
|
|
16636
16637
|
const parsed = [];
|
|
16637
16638
|
for (const entry of cells) {
|
|
16638
16639
|
const cell = readCell(entry);
|
|
@@ -16649,8 +16650,6 @@ function parseGridView(raw) {
|
|
|
16649
16650
|
instanceId,
|
|
16650
16651
|
deviceId,
|
|
16651
16652
|
name,
|
|
16652
|
-
width,
|
|
16653
|
-
height,
|
|
16654
16653
|
fps,
|
|
16655
16654
|
cells: parsed,
|
|
16656
16655
|
profiles: offers
|
|
@@ -16782,6 +16781,25 @@ var FULL_FRAME = {
|
|
|
16782
16781
|
var MAX_CELLS = 16;
|
|
16783
16782
|
/** Hoisted so `useDevices` does not re-subscribe on every render. */
|
|
16784
16783
|
var CAMERA_FILTER = { type: DeviceType.Camera };
|
|
16784
|
+
/**
|
|
16785
|
+
* A rectangle being dragged must not also PAN THE PAGE.
|
|
16786
|
+
*
|
|
16787
|
+
* On a phone a pointerdown on a `div` is a scroll gesture until the browser is
|
|
16788
|
+
* told otherwise, and `preventDefault` in the handler is too late — the browser
|
|
16789
|
+
* has already decided. `touch-action: none` is the only thing that makes a drag
|
|
16790
|
+
* work with a finger, and every surface that starts one carries it.
|
|
16791
|
+
*/
|
|
16792
|
+
var DRAG_SURFACE = { touchAction: "none" };
|
|
16793
|
+
/**
|
|
16794
|
+
* Finger-sized hit area around a grip.
|
|
16795
|
+
*
|
|
16796
|
+
* The visible square stays small (it sits ON the picture being edited and a big
|
|
16797
|
+
* one hides what it is positioning); the TOUCHABLE box around it is 24px, which
|
|
16798
|
+
* is the smallest target size that does not need a fingernail. The two are
|
|
16799
|
+
* different boxes on purpose — making the dot itself 24px would cover a quarter
|
|
16800
|
+
* of a small cell.
|
|
16801
|
+
*/
|
|
16802
|
+
var HANDLE_HIT = "absolute flex h-6 w-6 items-center justify-center";
|
|
16785
16803
|
/** The eight grips, in the order they are drawn. */
|
|
16786
16804
|
var HANDLES = [
|
|
16787
16805
|
"nw",
|
|
@@ -16794,15 +16812,26 @@ var HANDLES = [
|
|
|
16794
16812
|
"w"
|
|
16795
16813
|
];
|
|
16796
16814
|
var HANDLE_STYLE = {
|
|
16797
|
-
nw: "-top-
|
|
16798
|
-
n: "-top-
|
|
16799
|
-
ne: "-top-
|
|
16800
|
-
e: "top-1/2 -right-
|
|
16801
|
-
se: "-bottom-
|
|
16802
|
-
s: "-bottom-
|
|
16803
|
-
sw: "-bottom-
|
|
16804
|
-
w: "top-1/2 -left-
|
|
16815
|
+
nw: "-top-3 -left-3 cursor-nw-resize",
|
|
16816
|
+
n: "-top-3 left-1/2 -translate-x-1/2 cursor-n-resize",
|
|
16817
|
+
ne: "-top-3 -right-3 cursor-ne-resize",
|
|
16818
|
+
e: "top-1/2 -right-3 -translate-y-1/2 cursor-e-resize",
|
|
16819
|
+
se: "-bottom-3 -right-3 cursor-se-resize",
|
|
16820
|
+
s: "-bottom-3 left-1/2 -translate-x-1/2 cursor-s-resize",
|
|
16821
|
+
sw: "-bottom-3 -left-3 cursor-sw-resize",
|
|
16822
|
+
w: "top-1/2 -left-3 -translate-y-1/2 cursor-w-resize"
|
|
16805
16823
|
};
|
|
16824
|
+
/**
|
|
16825
|
+
* Start a drag and KEEP it, even when the finger leaves the rectangle.
|
|
16826
|
+
*
|
|
16827
|
+
* Without pointer capture a drag ends the moment the pointer crosses the box it
|
|
16828
|
+
* started in — which on a phone is immediately, because the box is small and
|
|
16829
|
+
* the finger is not. With capture the events keep coming to the element that
|
|
16830
|
+
* was grabbed and still bubble to the canvas that does the arithmetic.
|
|
16831
|
+
*/
|
|
16832
|
+
function capturePointer(event) {
|
|
16833
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
16834
|
+
}
|
|
16806
16835
|
/** Move or resize, from one drag and one delta. */
|
|
16807
16836
|
function applyDrag(drag, dx, dy) {
|
|
16808
16837
|
return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
|
|
@@ -16842,8 +16871,6 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16842
16871
|
try {
|
|
16843
16872
|
const outcome = parseGridView(await layout.saveLayout({
|
|
16844
16873
|
name: draft.name,
|
|
16845
|
-
width: draft.width,
|
|
16846
|
-
height: draft.height,
|
|
16847
16874
|
fps: draft.fps,
|
|
16848
16875
|
cells: draft.cells.map((cell) => ({ ...cell }))
|
|
16849
16876
|
}));
|
|
@@ -16866,7 +16893,27 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16866
16893
|
} : cell)
|
|
16867
16894
|
});
|
|
16868
16895
|
}, []);
|
|
16869
|
-
|
|
16896
|
+
/**
|
|
16897
|
+
* The shape the grid will actually come out as.
|
|
16898
|
+
*
|
|
16899
|
+
* The canvas is no longer a field: each profile derives its own from the
|
|
16900
|
+
* cells and the sources' dimensions, and the server reports what it came out
|
|
16901
|
+
* as. Drawing at that aspect is the only way the editor shows the truth —
|
|
16902
|
+
* before, it drew at whatever two numbers had been typed, which is exactly
|
|
16903
|
+
* how a layout could look right here and wrong on screen.
|
|
16904
|
+
*
|
|
16905
|
+
* 16:9 while nothing is offered yet: a grid with no cells has no shape, and
|
|
16906
|
+
* an empty editor has to be drawn at something.
|
|
16907
|
+
*/
|
|
16908
|
+
const canvasAspect = useMemo(() => {
|
|
16909
|
+
const offered = loaded === void 0 || loaded === null || loaded === "unreadable" ? [] : loaded.profiles;
|
|
16910
|
+
for (const row of offered) {
|
|
16911
|
+
if (!row.offered) continue;
|
|
16912
|
+
const [width, height] = row.canvas.split("x").map(Number);
|
|
16913
|
+
if (width !== void 0 && height !== void 0 && width > 0 && height > 0) return width / height;
|
|
16914
|
+
}
|
|
16915
|
+
return 16 / 9;
|
|
16916
|
+
}, [loaded]);
|
|
16870
16917
|
if (loaded === void 0) return /* @__PURE__ */ jsx(WidgetPanel, {
|
|
16871
16918
|
title: "Grid layout",
|
|
16872
16919
|
icon: /* @__PURE__ */ jsx(Grid2x2, { className: "h-3.5 w-3.5" }),
|
|
@@ -16942,12 +16989,15 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16942
16989
|
}),
|
|
16943
16990
|
/* @__PURE__ */ jsxs("div", {
|
|
16944
16991
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
16945
|
-
style: {
|
|
16992
|
+
style: {
|
|
16993
|
+
aspectRatio: String(canvasAspect),
|
|
16994
|
+
...DRAG_SURFACE
|
|
16995
|
+
},
|
|
16946
16996
|
onPointerMove,
|
|
16947
16997
|
onPointerUp: () => {
|
|
16948
16998
|
setDrag(null);
|
|
16949
16999
|
},
|
|
16950
|
-
|
|
17000
|
+
onPointerCancel: () => {
|
|
16951
17001
|
setDrag(null);
|
|
16952
17002
|
},
|
|
16953
17003
|
children: [draft.cells.map((cell, index) => /* @__PURE__ */ jsx(GridCellBox, {
|
|
@@ -16974,62 +17024,39 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16974
17024
|
}),
|
|
16975
17025
|
/* @__PURE__ */ jsxs("div", {
|
|
16976
17026
|
className: "grid grid-cols-4 gap-2 text-[10.5px]",
|
|
16977
|
-
children: [
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
16986
|
-
|
|
16987
|
-
|
|
16988
|
-
|
|
16989
|
-
|
|
16990
|
-
|
|
16991
|
-
|
|
16992
|
-
|
|
16993
|
-
|
|
16994
|
-
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
/* @__PURE__ */
|
|
17009
|
-
|
|
17010
|
-
disabled: draft.cells.length >= MAX_CELLS,
|
|
17011
|
-
onClick: () => {
|
|
17012
|
-
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17013
|
-
if (!first) return;
|
|
17014
|
-
setDraft({
|
|
17015
|
-
...draft,
|
|
17016
|
-
cells: [...draft.cells, {
|
|
17017
|
-
deviceId: first.id,
|
|
17018
|
-
source: FULL_FRAME,
|
|
17019
|
-
cell: clampNormalizedRect({
|
|
17020
|
-
x: .1,
|
|
17021
|
-
y: .1,
|
|
17022
|
-
width: .4,
|
|
17023
|
-
height: .4
|
|
17024
|
-
})
|
|
17025
|
-
}]
|
|
17026
|
-
});
|
|
17027
|
-
setSelected(draft.cells.length);
|
|
17028
|
-
},
|
|
17029
|
-
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17030
|
-
children: [/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17031
|
-
})
|
|
17032
|
-
]
|
|
17027
|
+
children: [/* @__PURE__ */ jsx(LabelledNumber, {
|
|
17028
|
+
label: "FPS",
|
|
17029
|
+
value: draft.fps,
|
|
17030
|
+
onChange: (fps) => {
|
|
17031
|
+
setDraft({
|
|
17032
|
+
...draft,
|
|
17033
|
+
fps
|
|
17034
|
+
});
|
|
17035
|
+
}
|
|
17036
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
17037
|
+
type: "button",
|
|
17038
|
+
disabled: draft.cells.length >= MAX_CELLS,
|
|
17039
|
+
onClick: () => {
|
|
17040
|
+
const first = cameras.find((camera) => camera.id !== deviceId);
|
|
17041
|
+
if (!first) return;
|
|
17042
|
+
setDraft({
|
|
17043
|
+
...draft,
|
|
17044
|
+
cells: [...draft.cells, {
|
|
17045
|
+
deviceId: first.id,
|
|
17046
|
+
source: FULL_FRAME,
|
|
17047
|
+
cell: clampNormalizedRect({
|
|
17048
|
+
x: .1,
|
|
17049
|
+
y: .1,
|
|
17050
|
+
width: .4,
|
|
17051
|
+
height: .4
|
|
17052
|
+
})
|
|
17053
|
+
}]
|
|
17054
|
+
});
|
|
17055
|
+
setSelected(draft.cells.length);
|
|
17056
|
+
},
|
|
17057
|
+
className: "flex items-center justify-center gap-1 self-end rounded border border-border px-2 py-1 uppercase",
|
|
17058
|
+
children: [/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }), " Cell"]
|
|
17059
|
+
})]
|
|
17033
17060
|
}),
|
|
17034
17061
|
/* @__PURE__ */ jsx(GridProfileReport, {
|
|
17035
17062
|
offers: loaded.profiles,
|
|
@@ -17077,7 +17104,11 @@ function GridProfileReport({ offers, cameras }) {
|
|
|
17077
17104
|
children: offer.profile
|
|
17078
17105
|
}), offer.offered ? /* @__PURE__ */ jsxs("span", {
|
|
17079
17106
|
className: "text-foreground-subtle",
|
|
17080
|
-
children: [
|
|
17107
|
+
children: [
|
|
17108
|
+
offer.canvas,
|
|
17109
|
+
" — composed from every source’s ",
|
|
17110
|
+
offer.profile
|
|
17111
|
+
]
|
|
17081
17112
|
}) : /* @__PURE__ */ jsx("span", {
|
|
17082
17113
|
className: "text-danger",
|
|
17083
17114
|
children: offer.missingSources.length === 0 ? "not offered — this grid has no cells yet" : `not offered — no ${offer.profile} on ${offer.missingSources.map(nameOf).join(", ")}`
|
|
@@ -17093,10 +17124,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17093
17124
|
role: "presentation",
|
|
17094
17125
|
onPointerDown: (event) => {
|
|
17095
17126
|
onSelect();
|
|
17127
|
+
capturePointer(event);
|
|
17096
17128
|
onDragStart("move", event);
|
|
17097
17129
|
},
|
|
17098
17130
|
className: `absolute cursor-move border ${selected ? "border-accent" : "border-border/60"}`,
|
|
17099
17131
|
style: {
|
|
17132
|
+
...DRAG_SURFACE,
|
|
17100
17133
|
left: `${String(cell.cell.x * 100)}%`,
|
|
17101
17134
|
top: `${String(cell.cell.y * 100)}%`,
|
|
17102
17135
|
width: `${String(cell.cell.width * 100)}%`,
|
|
@@ -17110,9 +17143,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17110
17143
|
role: "presentation",
|
|
17111
17144
|
onPointerDown: (event) => {
|
|
17112
17145
|
event.stopPropagation();
|
|
17146
|
+
capturePointer(event);
|
|
17113
17147
|
onDragStart(handle, event);
|
|
17114
17148
|
},
|
|
17115
|
-
className:
|
|
17149
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17150
|
+
style: DRAG_SURFACE,
|
|
17151
|
+
children: /* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17116
17152
|
}, handle))
|
|
17117
17153
|
});
|
|
17118
17154
|
}
|
|
@@ -17157,16 +17193,15 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17157
17193
|
}
|
|
17158
17194
|
}),
|
|
17159
17195
|
/* @__PURE__ */ jsx("div", {
|
|
17160
|
-
className: "grid grid-cols-
|
|
17196
|
+
className: "grid grid-cols-2 gap-x-3 gap-y-1.5",
|
|
17161
17197
|
children: [
|
|
17162
17198
|
"x",
|
|
17163
17199
|
"y",
|
|
17164
17200
|
"width",
|
|
17165
17201
|
"height"
|
|
17166
|
-
].map((key) => /* @__PURE__ */ jsx(
|
|
17202
|
+
].map((key) => /* @__PURE__ */ jsx(CropSlider, {
|
|
17167
17203
|
label: `Crop ${key}`,
|
|
17168
|
-
value:
|
|
17169
|
-
step: .01,
|
|
17204
|
+
value: cell.source[key],
|
|
17170
17205
|
onChange: (next) => {
|
|
17171
17206
|
onChange({ source: clampNormalizedRect({
|
|
17172
17207
|
...cell.source,
|
|
@@ -17194,6 +17229,7 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17194
17229
|
function SourceCropEditor({ deviceId, source, onChange }) {
|
|
17195
17230
|
const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
|
|
17196
17231
|
const [drag, setDrag] = useState(null);
|
|
17232
|
+
const [sourceAspect, setSourceAspect] = useState(null);
|
|
17197
17233
|
const onPointerMove = (event) => {
|
|
17198
17234
|
if (drag === null) return;
|
|
17199
17235
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
@@ -17223,12 +17259,15 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17223
17259
|
})]
|
|
17224
17260
|
}), /* @__PURE__ */ jsxs("div", {
|
|
17225
17261
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
17226
|
-
style: {
|
|
17262
|
+
style: {
|
|
17263
|
+
aspectRatio: String(sourceAspect ?? 16 / 9),
|
|
17264
|
+
...DRAG_SURFACE
|
|
17265
|
+
},
|
|
17227
17266
|
onPointerMove,
|
|
17228
17267
|
onPointerUp: () => {
|
|
17229
17268
|
setDrag(null);
|
|
17230
17269
|
},
|
|
17231
|
-
|
|
17270
|
+
onPointerCancel: () => {
|
|
17232
17271
|
setDrag(null);
|
|
17233
17272
|
},
|
|
17234
17273
|
children: [
|
|
@@ -17238,12 +17277,17 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17238
17277
|
}) : /* @__PURE__ */ jsx("img", {
|
|
17239
17278
|
src,
|
|
17240
17279
|
alt: "",
|
|
17241
|
-
className: "absolute inset-0 h-full w-full
|
|
17280
|
+
className: "absolute inset-0 h-full w-full",
|
|
17281
|
+
onLoad: (event) => {
|
|
17282
|
+
const { naturalWidth, naturalHeight } = event.currentTarget;
|
|
17283
|
+
if (naturalWidth > 0 && naturalHeight > 0) setSourceAspect(naturalWidth / naturalHeight);
|
|
17284
|
+
}
|
|
17242
17285
|
}),
|
|
17243
17286
|
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
|
|
17244
17287
|
/* @__PURE__ */ jsx("div", {
|
|
17245
17288
|
role: "presentation",
|
|
17246
17289
|
onPointerDown: (event) => {
|
|
17290
|
+
capturePointer(event);
|
|
17247
17291
|
setDrag({
|
|
17248
17292
|
mode: "move",
|
|
17249
17293
|
startX: event.clientX,
|
|
@@ -17253,6 +17297,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17253
17297
|
},
|
|
17254
17298
|
className: "absolute cursor-move border border-accent",
|
|
17255
17299
|
style: {
|
|
17300
|
+
...DRAG_SURFACE,
|
|
17256
17301
|
left: `${String(source.x * 100)}%`,
|
|
17257
17302
|
top: `${String(source.y * 100)}%`,
|
|
17258
17303
|
width: `${String(source.width * 100)}%`,
|
|
@@ -17265,6 +17310,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17265
17310
|
role: "presentation",
|
|
17266
17311
|
onPointerDown: (event) => {
|
|
17267
17312
|
event.stopPropagation();
|
|
17313
|
+
capturePointer(event);
|
|
17268
17314
|
setDrag({
|
|
17269
17315
|
mode: handle,
|
|
17270
17316
|
startX: event.clientX,
|
|
@@ -17272,13 +17318,60 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17272
17318
|
origin: source
|
|
17273
17319
|
});
|
|
17274
17320
|
},
|
|
17275
|
-
className:
|
|
17321
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17322
|
+
style: DRAG_SURFACE,
|
|
17323
|
+
children: /* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17276
17324
|
}, handle))
|
|
17277
17325
|
})
|
|
17278
17326
|
]
|
|
17279
17327
|
})]
|
|
17280
17328
|
});
|
|
17281
17329
|
}
|
|
17330
|
+
/**
|
|
17331
|
+
* One crop number, as a slider AND as a number.
|
|
17332
|
+
*
|
|
17333
|
+
* The slider is what makes this usable with a thumb — a `number` input on a
|
|
17334
|
+
* phone opens a keyboard, and adjusting a crop by typing 0.37 then 0.39 then
|
|
17335
|
+
* 0.38 is not adjusting, it is guessing. The typed field stays because a slider
|
|
17336
|
+
* cannot express "exactly a third" and because a value you can read is how you
|
|
17337
|
+
* copy a crop from one cell to another.
|
|
17338
|
+
*
|
|
17339
|
+
* Both write through `clampNormalizedRect` upstream, so neither can hand the
|
|
17340
|
+
* addon a rectangle that leaves the frame — it THROWS on one, by design.
|
|
17341
|
+
*/
|
|
17342
|
+
function CropSlider({ label, value, onChange }) {
|
|
17343
|
+
const rounded = Math.round(value * 100) / 100;
|
|
17344
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
17345
|
+
className: "flex flex-col gap-0.5",
|
|
17346
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
17347
|
+
className: "flex items-baseline justify-between text-foreground-subtle",
|
|
17348
|
+
children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("input", {
|
|
17349
|
+
type: "number",
|
|
17350
|
+
min: 0,
|
|
17351
|
+
max: 1,
|
|
17352
|
+
step: .01,
|
|
17353
|
+
value: rounded,
|
|
17354
|
+
onChange: (event) => {
|
|
17355
|
+
const next = Number(event.target.value);
|
|
17356
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17357
|
+
},
|
|
17358
|
+
className: "w-12 rounded border border-border bg-transparent px-1 py-0.5 text-right"
|
|
17359
|
+
})]
|
|
17360
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
17361
|
+
type: "range",
|
|
17362
|
+
min: 0,
|
|
17363
|
+
max: 1,
|
|
17364
|
+
step: .005,
|
|
17365
|
+
value,
|
|
17366
|
+
onChange: (event) => {
|
|
17367
|
+
const next = Number(event.target.value);
|
|
17368
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17369
|
+
},
|
|
17370
|
+
style: DRAG_SURFACE,
|
|
17371
|
+
className: "h-5 w-full accent-accent"
|
|
17372
|
+
})]
|
|
17373
|
+
});
|
|
17374
|
+
}
|
|
17282
17375
|
function LabelledNumber({ label, value, step = 1, onChange }) {
|
|
17283
17376
|
return /* @__PURE__ */ jsxs("label", {
|
|
17284
17377
|
className: "flex flex-col gap-0.5",
|