@camstack/ui-library 1.2.178 → 1.2.179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +114 -19
- package/dist/index.js +114 -19
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -16806,6 +16806,25 @@ var FULL_FRAME = {
|
|
|
16806
16806
|
var MAX_CELLS = 16;
|
|
16807
16807
|
/** Hoisted so `useDevices` does not re-subscribe on every render. */
|
|
16808
16808
|
var CAMERA_FILTER = { type: _camstack_types.DeviceType.Camera };
|
|
16809
|
+
/**
|
|
16810
|
+
* A rectangle being dragged must not also PAN THE PAGE.
|
|
16811
|
+
*
|
|
16812
|
+
* On a phone a pointerdown on a `div` is a scroll gesture until the browser is
|
|
16813
|
+
* told otherwise, and `preventDefault` in the handler is too late — the browser
|
|
16814
|
+
* has already decided. `touch-action: none` is the only thing that makes a drag
|
|
16815
|
+
* work with a finger, and every surface that starts one carries it.
|
|
16816
|
+
*/
|
|
16817
|
+
var DRAG_SURFACE = { touchAction: "none" };
|
|
16818
|
+
/**
|
|
16819
|
+
* Finger-sized hit area around a grip.
|
|
16820
|
+
*
|
|
16821
|
+
* The visible square stays small (it sits ON the picture being edited and a big
|
|
16822
|
+
* one hides what it is positioning); the TOUCHABLE box around it is 24px, which
|
|
16823
|
+
* is the smallest target size that does not need a fingernail. The two are
|
|
16824
|
+
* different boxes on purpose — making the dot itself 24px would cover a quarter
|
|
16825
|
+
* of a small cell.
|
|
16826
|
+
*/
|
|
16827
|
+
var HANDLE_HIT = "absolute flex h-6 w-6 items-center justify-center";
|
|
16809
16828
|
/** The eight grips, in the order they are drawn. */
|
|
16810
16829
|
var HANDLES = [
|
|
16811
16830
|
"nw",
|
|
@@ -16818,15 +16837,26 @@ var HANDLES = [
|
|
|
16818
16837
|
"w"
|
|
16819
16838
|
];
|
|
16820
16839
|
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-
|
|
16840
|
+
nw: "-top-3 -left-3 cursor-nw-resize",
|
|
16841
|
+
n: "-top-3 left-1/2 -translate-x-1/2 cursor-n-resize",
|
|
16842
|
+
ne: "-top-3 -right-3 cursor-ne-resize",
|
|
16843
|
+
e: "top-1/2 -right-3 -translate-y-1/2 cursor-e-resize",
|
|
16844
|
+
se: "-bottom-3 -right-3 cursor-se-resize",
|
|
16845
|
+
s: "-bottom-3 left-1/2 -translate-x-1/2 cursor-s-resize",
|
|
16846
|
+
sw: "-bottom-3 -left-3 cursor-sw-resize",
|
|
16847
|
+
w: "top-1/2 -left-3 -translate-y-1/2 cursor-w-resize"
|
|
16829
16848
|
};
|
|
16849
|
+
/**
|
|
16850
|
+
* Start a drag and KEEP it, even when the finger leaves the rectangle.
|
|
16851
|
+
*
|
|
16852
|
+
* Without pointer capture a drag ends the moment the pointer crosses the box it
|
|
16853
|
+
* started in — which on a phone is immediately, because the box is small and
|
|
16854
|
+
* the finger is not. With capture the events keep coming to the element that
|
|
16855
|
+
* was grabbed and still bubble to the canvas that does the arithmetic.
|
|
16856
|
+
*/
|
|
16857
|
+
function capturePointer(event) {
|
|
16858
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
16859
|
+
}
|
|
16830
16860
|
/** Move or resize, from one drag and one delta. */
|
|
16831
16861
|
function applyDrag(drag, dx, dy) {
|
|
16832
16862
|
return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
|
|
@@ -16966,12 +16996,15 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16966
16996
|
}),
|
|
16967
16997
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16968
16998
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
16969
|
-
style: {
|
|
16999
|
+
style: {
|
|
17000
|
+
aspectRatio: String(canvasAspect),
|
|
17001
|
+
...DRAG_SURFACE
|
|
17002
|
+
},
|
|
16970
17003
|
onPointerMove,
|
|
16971
17004
|
onPointerUp: () => {
|
|
16972
17005
|
setDrag(null);
|
|
16973
17006
|
},
|
|
16974
|
-
|
|
17007
|
+
onPointerCancel: () => {
|
|
16975
17008
|
setDrag(null);
|
|
16976
17009
|
},
|
|
16977
17010
|
children: [draft.cells.map((cell, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridCellBox, {
|
|
@@ -17117,10 +17150,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17117
17150
|
role: "presentation",
|
|
17118
17151
|
onPointerDown: (event) => {
|
|
17119
17152
|
onSelect();
|
|
17153
|
+
capturePointer(event);
|
|
17120
17154
|
onDragStart("move", event);
|
|
17121
17155
|
},
|
|
17122
17156
|
className: `absolute cursor-move border ${selected ? "border-accent" : "border-border/60"}`,
|
|
17123
17157
|
style: {
|
|
17158
|
+
...DRAG_SURFACE,
|
|
17124
17159
|
left: `${String(cell.cell.x * 100)}%`,
|
|
17125
17160
|
top: `${String(cell.cell.y * 100)}%`,
|
|
17126
17161
|
width: `${String(cell.cell.width * 100)}%`,
|
|
@@ -17134,9 +17169,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17134
17169
|
role: "presentation",
|
|
17135
17170
|
onPointerDown: (event) => {
|
|
17136
17171
|
event.stopPropagation();
|
|
17172
|
+
capturePointer(event);
|
|
17137
17173
|
onDragStart(handle, event);
|
|
17138
17174
|
},
|
|
17139
|
-
className:
|
|
17175
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17176
|
+
style: DRAG_SURFACE,
|
|
17177
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17140
17178
|
}, handle))
|
|
17141
17179
|
});
|
|
17142
17180
|
}
|
|
@@ -17181,16 +17219,15 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17181
17219
|
}
|
|
17182
17220
|
}),
|
|
17183
17221
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17184
|
-
className: "grid grid-cols-
|
|
17222
|
+
className: "grid grid-cols-2 gap-x-3 gap-y-1.5",
|
|
17185
17223
|
children: [
|
|
17186
17224
|
"x",
|
|
17187
17225
|
"y",
|
|
17188
17226
|
"width",
|
|
17189
17227
|
"height"
|
|
17190
|
-
].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
17228
|
+
].map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CropSlider, {
|
|
17191
17229
|
label: `Crop ${key}`,
|
|
17192
|
-
value:
|
|
17193
|
-
step: .01,
|
|
17230
|
+
value: cell.source[key],
|
|
17194
17231
|
onChange: (next) => {
|
|
17195
17232
|
onChange({ source: clampNormalizedRect({
|
|
17196
17233
|
...cell.source,
|
|
@@ -17218,6 +17255,7 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17218
17255
|
function SourceCropEditor({ deviceId, source, onChange }) {
|
|
17219
17256
|
const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
|
|
17220
17257
|
const [drag, setDrag] = (0, react$1.useState)(null);
|
|
17258
|
+
const [sourceAspect, setSourceAspect] = (0, react$1.useState)(null);
|
|
17221
17259
|
const onPointerMove = (event) => {
|
|
17222
17260
|
if (drag === null) return;
|
|
17223
17261
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
@@ -17247,12 +17285,15 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17247
17285
|
})]
|
|
17248
17286
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17249
17287
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
17250
|
-
style: {
|
|
17288
|
+
style: {
|
|
17289
|
+
aspectRatio: String(sourceAspect ?? 16 / 9),
|
|
17290
|
+
...DRAG_SURFACE
|
|
17291
|
+
},
|
|
17251
17292
|
onPointerMove,
|
|
17252
17293
|
onPointerUp: () => {
|
|
17253
17294
|
setDrag(null);
|
|
17254
17295
|
},
|
|
17255
|
-
|
|
17296
|
+
onPointerCancel: () => {
|
|
17256
17297
|
setDrag(null);
|
|
17257
17298
|
},
|
|
17258
17299
|
children: [
|
|
@@ -17262,12 +17303,17 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17262
17303
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
17263
17304
|
src,
|
|
17264
17305
|
alt: "",
|
|
17265
|
-
className: "absolute inset-0 h-full w-full
|
|
17306
|
+
className: "absolute inset-0 h-full w-full",
|
|
17307
|
+
onLoad: (event) => {
|
|
17308
|
+
const { naturalWidth, naturalHeight } = event.currentTarget;
|
|
17309
|
+
if (naturalWidth > 0 && naturalHeight > 0) setSourceAspect(naturalWidth / naturalHeight);
|
|
17310
|
+
}
|
|
17266
17311
|
}),
|
|
17267
17312
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
|
|
17268
17313
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17269
17314
|
role: "presentation",
|
|
17270
17315
|
onPointerDown: (event) => {
|
|
17316
|
+
capturePointer(event);
|
|
17271
17317
|
setDrag({
|
|
17272
17318
|
mode: "move",
|
|
17273
17319
|
startX: event.clientX,
|
|
@@ -17277,6 +17323,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17277
17323
|
},
|
|
17278
17324
|
className: "absolute cursor-move border border-accent",
|
|
17279
17325
|
style: {
|
|
17326
|
+
...DRAG_SURFACE,
|
|
17280
17327
|
left: `${String(source.x * 100)}%`,
|
|
17281
17328
|
top: `${String(source.y * 100)}%`,
|
|
17282
17329
|
width: `${String(source.width * 100)}%`,
|
|
@@ -17289,6 +17336,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17289
17336
|
role: "presentation",
|
|
17290
17337
|
onPointerDown: (event) => {
|
|
17291
17338
|
event.stopPropagation();
|
|
17339
|
+
capturePointer(event);
|
|
17292
17340
|
setDrag({
|
|
17293
17341
|
mode: handle,
|
|
17294
17342
|
startX: event.clientX,
|
|
@@ -17296,13 +17344,60 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17296
17344
|
origin: source
|
|
17297
17345
|
});
|
|
17298
17346
|
},
|
|
17299
|
-
className:
|
|
17347
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17348
|
+
style: DRAG_SURFACE,
|
|
17349
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17300
17350
|
}, handle))
|
|
17301
17351
|
})
|
|
17302
17352
|
]
|
|
17303
17353
|
})]
|
|
17304
17354
|
});
|
|
17305
17355
|
}
|
|
17356
|
+
/**
|
|
17357
|
+
* One crop number, as a slider AND as a number.
|
|
17358
|
+
*
|
|
17359
|
+
* The slider is what makes this usable with a thumb — a `number` input on a
|
|
17360
|
+
* phone opens a keyboard, and adjusting a crop by typing 0.37 then 0.39 then
|
|
17361
|
+
* 0.38 is not adjusting, it is guessing. The typed field stays because a slider
|
|
17362
|
+
* cannot express "exactly a third" and because a value you can read is how you
|
|
17363
|
+
* copy a crop from one cell to another.
|
|
17364
|
+
*
|
|
17365
|
+
* Both write through `clampNormalizedRect` upstream, so neither can hand the
|
|
17366
|
+
* addon a rectangle that leaves the frame — it THROWS on one, by design.
|
|
17367
|
+
*/
|
|
17368
|
+
function CropSlider({ label, value, onChange }) {
|
|
17369
|
+
const rounded = Math.round(value * 100) / 100;
|
|
17370
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17371
|
+
className: "flex flex-col gap-0.5",
|
|
17372
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
17373
|
+
className: "flex items-baseline justify-between text-foreground-subtle",
|
|
17374
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17375
|
+
type: "number",
|
|
17376
|
+
min: 0,
|
|
17377
|
+
max: 1,
|
|
17378
|
+
step: .01,
|
|
17379
|
+
value: rounded,
|
|
17380
|
+
onChange: (event) => {
|
|
17381
|
+
const next = Number(event.target.value);
|
|
17382
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17383
|
+
},
|
|
17384
|
+
className: "w-12 rounded border border-border bg-transparent px-1 py-0.5 text-right"
|
|
17385
|
+
})]
|
|
17386
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17387
|
+
type: "range",
|
|
17388
|
+
min: 0,
|
|
17389
|
+
max: 1,
|
|
17390
|
+
step: .005,
|
|
17391
|
+
value,
|
|
17392
|
+
onChange: (event) => {
|
|
17393
|
+
const next = Number(event.target.value);
|
|
17394
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17395
|
+
},
|
|
17396
|
+
style: DRAG_SURFACE,
|
|
17397
|
+
className: "h-5 w-full accent-accent"
|
|
17398
|
+
})]
|
|
17399
|
+
});
|
|
17400
|
+
}
|
|
17306
17401
|
function LabelledNumber({ label, value, step = 1, onChange }) {
|
|
17307
17402
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17308
17403
|
className: "flex flex-col gap-0.5",
|
package/dist/index.js
CHANGED
|
@@ -16782,6 +16782,25 @@ var FULL_FRAME = {
|
|
|
16782
16782
|
var MAX_CELLS = 16;
|
|
16783
16783
|
/** Hoisted so `useDevices` does not re-subscribe on every render. */
|
|
16784
16784
|
var CAMERA_FILTER = { type: DeviceType.Camera };
|
|
16785
|
+
/**
|
|
16786
|
+
* A rectangle being dragged must not also PAN THE PAGE.
|
|
16787
|
+
*
|
|
16788
|
+
* On a phone a pointerdown on a `div` is a scroll gesture until the browser is
|
|
16789
|
+
* told otherwise, and `preventDefault` in the handler is too late — the browser
|
|
16790
|
+
* has already decided. `touch-action: none` is the only thing that makes a drag
|
|
16791
|
+
* work with a finger, and every surface that starts one carries it.
|
|
16792
|
+
*/
|
|
16793
|
+
var DRAG_SURFACE = { touchAction: "none" };
|
|
16794
|
+
/**
|
|
16795
|
+
* Finger-sized hit area around a grip.
|
|
16796
|
+
*
|
|
16797
|
+
* The visible square stays small (it sits ON the picture being edited and a big
|
|
16798
|
+
* one hides what it is positioning); the TOUCHABLE box around it is 24px, which
|
|
16799
|
+
* is the smallest target size that does not need a fingernail. The two are
|
|
16800
|
+
* different boxes on purpose — making the dot itself 24px would cover a quarter
|
|
16801
|
+
* of a small cell.
|
|
16802
|
+
*/
|
|
16803
|
+
var HANDLE_HIT = "absolute flex h-6 w-6 items-center justify-center";
|
|
16785
16804
|
/** The eight grips, in the order they are drawn. */
|
|
16786
16805
|
var HANDLES = [
|
|
16787
16806
|
"nw",
|
|
@@ -16794,15 +16813,26 @@ var HANDLES = [
|
|
|
16794
16813
|
"w"
|
|
16795
16814
|
];
|
|
16796
16815
|
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-
|
|
16816
|
+
nw: "-top-3 -left-3 cursor-nw-resize",
|
|
16817
|
+
n: "-top-3 left-1/2 -translate-x-1/2 cursor-n-resize",
|
|
16818
|
+
ne: "-top-3 -right-3 cursor-ne-resize",
|
|
16819
|
+
e: "top-1/2 -right-3 -translate-y-1/2 cursor-e-resize",
|
|
16820
|
+
se: "-bottom-3 -right-3 cursor-se-resize",
|
|
16821
|
+
s: "-bottom-3 left-1/2 -translate-x-1/2 cursor-s-resize",
|
|
16822
|
+
sw: "-bottom-3 -left-3 cursor-sw-resize",
|
|
16823
|
+
w: "top-1/2 -left-3 -translate-y-1/2 cursor-w-resize"
|
|
16805
16824
|
};
|
|
16825
|
+
/**
|
|
16826
|
+
* Start a drag and KEEP it, even when the finger leaves the rectangle.
|
|
16827
|
+
*
|
|
16828
|
+
* Without pointer capture a drag ends the moment the pointer crosses the box it
|
|
16829
|
+
* started in — which on a phone is immediately, because the box is small and
|
|
16830
|
+
* the finger is not. With capture the events keep coming to the element that
|
|
16831
|
+
* was grabbed and still bubble to the canvas that does the arithmetic.
|
|
16832
|
+
*/
|
|
16833
|
+
function capturePointer(event) {
|
|
16834
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
16835
|
+
}
|
|
16806
16836
|
/** Move or resize, from one drag and one delta. */
|
|
16807
16837
|
function applyDrag(drag, dx, dy) {
|
|
16808
16838
|
return drag.mode === "move" ? moveNormalizedRect(drag.origin, dx, dy) : resizeNormalizedRectFrom(drag.origin, drag.mode, dx, dy);
|
|
@@ -16942,12 +16972,15 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16942
16972
|
}),
|
|
16943
16973
|
/* @__PURE__ */ jsxs("div", {
|
|
16944
16974
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
16945
|
-
style: {
|
|
16975
|
+
style: {
|
|
16976
|
+
aspectRatio: String(canvasAspect),
|
|
16977
|
+
...DRAG_SURFACE
|
|
16978
|
+
},
|
|
16946
16979
|
onPointerMove,
|
|
16947
16980
|
onPointerUp: () => {
|
|
16948
16981
|
setDrag(null);
|
|
16949
16982
|
},
|
|
16950
|
-
|
|
16983
|
+
onPointerCancel: () => {
|
|
16951
16984
|
setDrag(null);
|
|
16952
16985
|
},
|
|
16953
16986
|
children: [draft.cells.map((cell, index) => /* @__PURE__ */ jsx(GridCellBox, {
|
|
@@ -17093,10 +17126,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17093
17126
|
role: "presentation",
|
|
17094
17127
|
onPointerDown: (event) => {
|
|
17095
17128
|
onSelect();
|
|
17129
|
+
capturePointer(event);
|
|
17096
17130
|
onDragStart("move", event);
|
|
17097
17131
|
},
|
|
17098
17132
|
className: `absolute cursor-move border ${selected ? "border-accent" : "border-border/60"}`,
|
|
17099
17133
|
style: {
|
|
17134
|
+
...DRAG_SURFACE,
|
|
17100
17135
|
left: `${String(cell.cell.x * 100)}%`,
|
|
17101
17136
|
top: `${String(cell.cell.y * 100)}%`,
|
|
17102
17137
|
width: `${String(cell.cell.width * 100)}%`,
|
|
@@ -17110,9 +17145,12 @@ function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
|
17110
17145
|
role: "presentation",
|
|
17111
17146
|
onPointerDown: (event) => {
|
|
17112
17147
|
event.stopPropagation();
|
|
17148
|
+
capturePointer(event);
|
|
17113
17149
|
onDragStart(handle, event);
|
|
17114
17150
|
},
|
|
17115
|
-
className:
|
|
17151
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17152
|
+
style: DRAG_SURFACE,
|
|
17153
|
+
children: /* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17116
17154
|
}, handle))
|
|
17117
17155
|
});
|
|
17118
17156
|
}
|
|
@@ -17157,16 +17195,15 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17157
17195
|
}
|
|
17158
17196
|
}),
|
|
17159
17197
|
/* @__PURE__ */ jsx("div", {
|
|
17160
|
-
className: "grid grid-cols-
|
|
17198
|
+
className: "grid grid-cols-2 gap-x-3 gap-y-1.5",
|
|
17161
17199
|
children: [
|
|
17162
17200
|
"x",
|
|
17163
17201
|
"y",
|
|
17164
17202
|
"width",
|
|
17165
17203
|
"height"
|
|
17166
|
-
].map((key) => /* @__PURE__ */ jsx(
|
|
17204
|
+
].map((key) => /* @__PURE__ */ jsx(CropSlider, {
|
|
17167
17205
|
label: `Crop ${key}`,
|
|
17168
|
-
value:
|
|
17169
|
-
step: .01,
|
|
17206
|
+
value: cell.source[key],
|
|
17170
17207
|
onChange: (next) => {
|
|
17171
17208
|
onChange({ source: clampNormalizedRect({
|
|
17172
17209
|
...cell.source,
|
|
@@ -17194,6 +17231,7 @@ function CellInspector({ cell, index, cameras, onChange, onRemove }) {
|
|
|
17194
17231
|
function SourceCropEditor({ deviceId, source, onChange }) {
|
|
17195
17232
|
const { src } = useDeviceSnapshotImage(deviceId, { width: 640 });
|
|
17196
17233
|
const [drag, setDrag] = useState(null);
|
|
17234
|
+
const [sourceAspect, setSourceAspect] = useState(null);
|
|
17197
17235
|
const onPointerMove = (event) => {
|
|
17198
17236
|
if (drag === null) return;
|
|
17199
17237
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
@@ -17223,12 +17261,15 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17223
17261
|
})]
|
|
17224
17262
|
}), /* @__PURE__ */ jsxs("div", {
|
|
17225
17263
|
className: "relative w-full select-none overflow-hidden rounded border border-border bg-black",
|
|
17226
|
-
style: {
|
|
17264
|
+
style: {
|
|
17265
|
+
aspectRatio: String(sourceAspect ?? 16 / 9),
|
|
17266
|
+
...DRAG_SURFACE
|
|
17267
|
+
},
|
|
17227
17268
|
onPointerMove,
|
|
17228
17269
|
onPointerUp: () => {
|
|
17229
17270
|
setDrag(null);
|
|
17230
17271
|
},
|
|
17231
|
-
|
|
17272
|
+
onPointerCancel: () => {
|
|
17232
17273
|
setDrag(null);
|
|
17233
17274
|
},
|
|
17234
17275
|
children: [
|
|
@@ -17238,12 +17279,17 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17238
17279
|
}) : /* @__PURE__ */ jsx("img", {
|
|
17239
17280
|
src,
|
|
17240
17281
|
alt: "",
|
|
17241
|
-
className: "absolute inset-0 h-full w-full
|
|
17282
|
+
className: "absolute inset-0 h-full w-full",
|
|
17283
|
+
onLoad: (event) => {
|
|
17284
|
+
const { naturalWidth, naturalHeight } = event.currentTarget;
|
|
17285
|
+
if (naturalWidth > 0 && naturalHeight > 0) setSourceAspect(naturalWidth / naturalHeight);
|
|
17286
|
+
}
|
|
17242
17287
|
}),
|
|
17243
17288
|
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 bg-black/55" }),
|
|
17244
17289
|
/* @__PURE__ */ jsx("div", {
|
|
17245
17290
|
role: "presentation",
|
|
17246
17291
|
onPointerDown: (event) => {
|
|
17292
|
+
capturePointer(event);
|
|
17247
17293
|
setDrag({
|
|
17248
17294
|
mode: "move",
|
|
17249
17295
|
startX: event.clientX,
|
|
@@ -17253,6 +17299,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17253
17299
|
},
|
|
17254
17300
|
className: "absolute cursor-move border border-accent",
|
|
17255
17301
|
style: {
|
|
17302
|
+
...DRAG_SURFACE,
|
|
17256
17303
|
left: `${String(source.x * 100)}%`,
|
|
17257
17304
|
top: `${String(source.y * 100)}%`,
|
|
17258
17305
|
width: `${String(source.width * 100)}%`,
|
|
@@ -17265,6 +17312,7 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17265
17312
|
role: "presentation",
|
|
17266
17313
|
onPointerDown: (event) => {
|
|
17267
17314
|
event.stopPropagation();
|
|
17315
|
+
capturePointer(event);
|
|
17268
17316
|
setDrag({
|
|
17269
17317
|
mode: handle,
|
|
17270
17318
|
startX: event.clientX,
|
|
@@ -17272,13 +17320,60 @@ function SourceCropEditor({ deviceId, source, onChange }) {
|
|
|
17272
17320
|
origin: source
|
|
17273
17321
|
});
|
|
17274
17322
|
},
|
|
17275
|
-
className:
|
|
17323
|
+
className: `${HANDLE_HIT} ${HANDLE_STYLE[handle]}`,
|
|
17324
|
+
style: DRAG_SURFACE,
|
|
17325
|
+
children: /* @__PURE__ */ jsx("span", { className: "h-2.5 w-2.5 bg-accent" })
|
|
17276
17326
|
}, handle))
|
|
17277
17327
|
})
|
|
17278
17328
|
]
|
|
17279
17329
|
})]
|
|
17280
17330
|
});
|
|
17281
17331
|
}
|
|
17332
|
+
/**
|
|
17333
|
+
* One crop number, as a slider AND as a number.
|
|
17334
|
+
*
|
|
17335
|
+
* The slider is what makes this usable with a thumb — a `number` input on a
|
|
17336
|
+
* phone opens a keyboard, and adjusting a crop by typing 0.37 then 0.39 then
|
|
17337
|
+
* 0.38 is not adjusting, it is guessing. The typed field stays because a slider
|
|
17338
|
+
* cannot express "exactly a third" and because a value you can read is how you
|
|
17339
|
+
* copy a crop from one cell to another.
|
|
17340
|
+
*
|
|
17341
|
+
* Both write through `clampNormalizedRect` upstream, so neither can hand the
|
|
17342
|
+
* addon a rectangle that leaves the frame — it THROWS on one, by design.
|
|
17343
|
+
*/
|
|
17344
|
+
function CropSlider({ label, value, onChange }) {
|
|
17345
|
+
const rounded = Math.round(value * 100) / 100;
|
|
17346
|
+
return /* @__PURE__ */ jsxs("label", {
|
|
17347
|
+
className: "flex flex-col gap-0.5",
|
|
17348
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
17349
|
+
className: "flex items-baseline justify-between text-foreground-subtle",
|
|
17350
|
+
children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("input", {
|
|
17351
|
+
type: "number",
|
|
17352
|
+
min: 0,
|
|
17353
|
+
max: 1,
|
|
17354
|
+
step: .01,
|
|
17355
|
+
value: rounded,
|
|
17356
|
+
onChange: (event) => {
|
|
17357
|
+
const next = Number(event.target.value);
|
|
17358
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17359
|
+
},
|
|
17360
|
+
className: "w-12 rounded border border-border bg-transparent px-1 py-0.5 text-right"
|
|
17361
|
+
})]
|
|
17362
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
17363
|
+
type: "range",
|
|
17364
|
+
min: 0,
|
|
17365
|
+
max: 1,
|
|
17366
|
+
step: .005,
|
|
17367
|
+
value,
|
|
17368
|
+
onChange: (event) => {
|
|
17369
|
+
const next = Number(event.target.value);
|
|
17370
|
+
if (Number.isFinite(next)) onChange(next);
|
|
17371
|
+
},
|
|
17372
|
+
style: DRAG_SURFACE,
|
|
17373
|
+
className: "h-5 w-full accent-accent"
|
|
17374
|
+
})]
|
|
17375
|
+
});
|
|
17376
|
+
}
|
|
17282
17377
|
function LabelledNumber({ label, value, step = 1, onChange }) {
|
|
17283
17378
|
return /* @__PURE__ */ jsxs("label", {
|
|
17284
17379
|
className: "flex flex-col gap-0.5",
|