@camstack/ui-library 1.0.2 → 1.0.4

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.
@@ -0,0 +1,913 @@
1
+ let react = require("react");
2
+ let react_jsx_runtime = require("react/jsx-runtime");
3
+ let react_konva = require("react-konva");
4
+ //#region src/composites/cap-settings/MaskShapeCanvas.tsx
5
+ /**
6
+ * MaskShapeCanvas — ONE shared, Konva-based on-frame "drawing plane" editor
7
+ * that handles ALL four `MaskShape` kinds: `rect`, `polygon`, `grid`, `line`.
8
+ *
9
+ * It is a generalization of the detection `ZoneCanvas` (orchestrator) so that
10
+ * privacy-mask (rect/polygon), motion-zones (grid), and the
11
+ * pipeline-orchestrator zones/lines editor can eventually all share a single
12
+ * editor instead of three bespoke ones. Every coordinate is normalized 0..1 of
13
+ * the camera frame (top-left origin), like every existing on-frame editor.
14
+ *
15
+ * ─────────────────────────────────────────────────────────────────────────────
16
+ * Behaviours ported (the references are NOT edited — this generalizes them):
17
+ *
18
+ * - From `ZoneCanvas` (orchestrator zone-editor):
19
+ * • ResizeObserver-sized `<Stage>`, `transparent` absolute-fill overlay
20
+ * mode for layering over a stream player, optional `backdrop` slot,
21
+ * pointer-events pass-through while not editing.
22
+ * • polygon draw / vertex-drag / body-drag with optimistic local override
23
+ * + the `useEffect` that drops the override once the controlled prop
24
+ * echoes the commit back, the rAF-throttled cursor ghost-line preview,
25
+ * the `Arrow` tripwire, and the memoised export.
26
+ * - From `PrivacyMaskCanvas`:
27
+ * • rect move + bottom-right-handle resize with 0..1 clamp, polygon
28
+ * vertex add (edge midpoint) / remove (✕, gated by min/max), and the
29
+ * `polygonVertices` {min,max} gate (e.g. Hikvision {min:4,max:4}).
30
+ * - From `MotionGridCanvas`:
31
+ * • grid cell-paint gesture (press-drag paints every touched cell to the
32
+ * value the first-touched cell flipped to), row-major `cells`.
33
+ *
34
+ * ─────────────────────────────────────────────────────────────────────────────
35
+ * Usage — each consumer maps its own model onto `MaskShapeItem[]` and gates the
36
+ * editor with `supportedShapes`:
37
+ *
38
+ * @example privacy-mask (rect + polygon)
39
+ * ```tsx
40
+ * const items: MaskShapeItem[] = regions.map((r) => ({ id: r.id, shape: r.shape }))
41
+ * <MaskShapeCanvas
42
+ * transparent
43
+ * items={items}
44
+ * supportedShapes={['rect', 'polygon']}
45
+ * polygonVertices={options.polygonVertices} // Hikvision {min:4,max:4}
46
+ * selectedId={selectedId}
47
+ * onSelect={setSelectedId}
48
+ * onShapeChange={(id, shape) =>
49
+ * onRegionsChange(regions.map((r) => (r.id === id ? { ...r, shape } : r)))}
50
+ * onDrawComplete={(shape) => onRegionsChange([...regions, { id: nextId(), shape }])}
51
+ * drawingKind={drawing ? 'rect' : null}
52
+ * />
53
+ * ```
54
+ *
55
+ * @example motion-zones (grid)
56
+ * ```tsx
57
+ * const items: MaskShapeItem[] = [{
58
+ * id: 'motion',
59
+ * shape: { kind: 'grid', gridWidth: w, gridHeight: h, cells },
60
+ * }]
61
+ * <MaskShapeCanvas
62
+ * transparent
63
+ * items={items}
64
+ * supportedShapes={['grid']}
65
+ * grid={{ width: w, height: h }}
66
+ * selectedId="motion"
67
+ * onSelect={() => {}}
68
+ * onShapeChange={(_id, shape) => {
69
+ * if (shape.kind === 'grid') onCellsChange(shape.cells)
70
+ * }}
71
+ * onDrawComplete={() => {}}
72
+ * drawingKind={null}
73
+ * />
74
+ * ```
75
+ *
76
+ * @example orchestrator (polygon + line/tripwire)
77
+ * ```tsx
78
+ * const items: MaskShapeItem[] = zones.map((z) => ({
79
+ * id: z.id,
80
+ * label: z.name,
81
+ * color: z.color,
82
+ * shape: z.kind === 'tripwire'
83
+ * ? { kind: 'line', points: z.points }
84
+ * : { kind: 'polygon', points: z.points },
85
+ * }))
86
+ * <MaskShapeCanvas
87
+ * transparent
88
+ * items={items}
89
+ * supportedShapes={['polygon', 'line']}
90
+ * selectedId={selectedZoneId}
91
+ * onSelect={onSelectZone}
92
+ * onShapeChange={(id, shape) => {
93
+ * if (shape.kind === 'polygon' || shape.kind === 'line')
94
+ * onZonePointsChange(id, shape.points)
95
+ * }}
96
+ * onDrawComplete={(shape) => onZoneComplete(shape)}
97
+ * drawingKind={drawingKind === 'tripwire' ? 'line' : drawingKind}
98
+ * backdrop={<CameraStreamPlayer ... />}
99
+ * />
100
+ * ```
101
+ *
102
+ * Purely controlled — the parent owns the truth. Every edit fires
103
+ * `onShapeChange(id, shape)` (or `onDrawComplete(shape)` for a freshly-drawn
104
+ * shape) with a brand-new immutable shape object; points and cells are never
105
+ * mutated in place.
106
+ */
107
+ var DEFAULT_COLOR = {
108
+ rect: "#ef4444",
109
+ polygon: "#3b82f6",
110
+ grid: "#6366f1",
111
+ line: "#f59e0b"
112
+ };
113
+ /** Polygon body fill alpha (hex suffix). `#33` ≈ 20%, `#66` ≈ 40%. */
114
+ var FILL_ALPHA = "33";
115
+ var FILL_ALPHA_SELECTED = "66";
116
+ var DRAW_PREVIEW_STROKE = "#6366f1";
117
+ var FLOOR_POLYGON_VERTICES = 3;
118
+ var FLOOR_LINE_VERTICES = 2;
119
+ var CLOSE_HIT_RADIUS = 12;
120
+ var RHYTHM_COLS = 16;
121
+ var RHYTHM_ROWS = 9;
122
+ function toCanvas(p, w, h) {
123
+ return {
124
+ x: p.x * w,
125
+ y: p.y * h
126
+ };
127
+ }
128
+ function toNorm(p, w, h) {
129
+ return {
130
+ x: p.x / w,
131
+ y: p.y / h
132
+ };
133
+ }
134
+ function flatPoints(points, w, h) {
135
+ return points.flatMap((p) => [p.x * w, p.y * h]);
136
+ }
137
+ /** Clamp to the normalized 0..1 range, mapping non-finite values to 0. */
138
+ function clamp01(v) {
139
+ if (!Number.isFinite(v)) return 0;
140
+ if (v < 0) return 0;
141
+ if (v > 1) return 1;
142
+ return v;
143
+ }
144
+ function resolveColor(item) {
145
+ return item.color ?? DEFAULT_COLOR[item.shape.kind];
146
+ }
147
+ /** Insert a vertex at the midpoint of edge `edgeStart → edgeStart+1`. */
148
+ function insertMidpoint(points, edgeStart) {
149
+ const a = points[edgeStart];
150
+ const b = points[(edgeStart + 1) % points.length];
151
+ if (!a || !b) return [...points];
152
+ const mid = {
153
+ x: (a.x + b.x) / 2,
154
+ y: (a.y + b.y) / 2
155
+ };
156
+ return [
157
+ ...points.slice(0, edgeStart + 1),
158
+ mid,
159
+ ...points.slice(edgeStart + 1)
160
+ ];
161
+ }
162
+ /** Float-tolerant point-list equality (used to detect prop catch-up). */
163
+ function pointsEqual(a, b) {
164
+ if (a.length !== b.length) return false;
165
+ return a.every((p, i) => {
166
+ const o = b[i];
167
+ return Math.abs(p.x - o.x) < 1e-6 && Math.abs(p.y - o.y) < 1e-6;
168
+ });
169
+ }
170
+ function isPointShape(shape) {
171
+ return shape.kind === "polygon" || shape.kind === "line";
172
+ }
173
+ function MaskShapeCanvasImpl({ items, selectedId, onSelect, onShapeChange, onDrawComplete, drawingKind, supportedShapes, transparent = false, backdrop, polygonVertices, grid }) {
174
+ const containerRef = (0, react.useRef)(null);
175
+ const [size, setSize] = (0, react.useState)({
176
+ w: 800,
177
+ h: 450
178
+ });
179
+ const [drawPoints, setDrawPoints] = (0, react.useState)([]);
180
+ const [cursorPos, setCursorPos] = (0, react.useState)(null);
181
+ const [draggingItemId, setDraggingItemId] = (0, react.useState)(null);
182
+ const [dragOverride, setDragOverride] = (0, react.useState)(null);
183
+ const isDrawing = drawingKind !== null && supportedShapes.includes(drawingKind);
184
+ (0, react.useEffect)(() => {
185
+ if (!dragOverride) return;
186
+ const it = items.find((i) => i.id === dragOverride.itemId);
187
+ if (!it || !isPointShape(it.shape)) {
188
+ setDragOverride(null);
189
+ return;
190
+ }
191
+ if (pointsEqual(it.shape.points, dragOverride.points)) setDragOverride(null);
192
+ }, [items, dragOverride]);
193
+ (0, react.useEffect)(() => {
194
+ const el = containerRef.current;
195
+ if (!el) return;
196
+ const ro = new ResizeObserver((entries) => {
197
+ const entry = entries[0];
198
+ if (!entry) return;
199
+ const { width, height } = entry.contentRect;
200
+ setSize(transparent ? {
201
+ w: width,
202
+ h: height
203
+ } : {
204
+ w: width,
205
+ h: width * 9 / 16
206
+ });
207
+ });
208
+ ro.observe(el);
209
+ return () => ro.disconnect();
210
+ }, [transparent]);
211
+ (0, react.useEffect)(() => {
212
+ if (!isDrawing && cursorPos !== null) setCursorPos(null);
213
+ }, [isDrawing, cursorPos]);
214
+ const completeDraw = (0, react.useCallback)((shape) => {
215
+ onDrawComplete(shape);
216
+ setDrawPoints([]);
217
+ }, [onDrawComplete]);
218
+ const handleStageClick = (0, react.useCallback)((e) => {
219
+ if (!isDrawing || drawingKind === null) {
220
+ onSelect(null);
221
+ return;
222
+ }
223
+ const stage = e.target.getStage();
224
+ if (!stage) return;
225
+ const pos = stage.getPointerPosition();
226
+ if (!pos) return;
227
+ const norm = toNorm(pos, size.w, size.h);
228
+ if (drawingKind === "rect") {
229
+ const x = clamp01(norm.x);
230
+ const y = clamp01(norm.y);
231
+ completeDraw({
232
+ kind: "rect",
233
+ x,
234
+ y,
235
+ width: Math.min(.2, 1 - x),
236
+ height: Math.min(.2, 1 - y)
237
+ });
238
+ return;
239
+ }
240
+ if (drawingKind === "grid") {
241
+ const gw = grid?.width ?? RHYTHM_COLS;
242
+ const gh = grid?.height ?? RHYTHM_ROWS;
243
+ completeDraw({
244
+ kind: "grid",
245
+ gridWidth: gw,
246
+ gridHeight: gh,
247
+ cells: Array.from({ length: gw * gh }, () => false)
248
+ });
249
+ return;
250
+ }
251
+ if (drawingKind === "line") {
252
+ if (drawPoints.length === 0) {
253
+ setDrawPoints([norm]);
254
+ return;
255
+ }
256
+ completeDraw({
257
+ kind: "line",
258
+ points: [...drawPoints, norm]
259
+ });
260
+ return;
261
+ }
262
+ if (drawingKind === "polygon") {
263
+ if (drawPoints.length === 0) {
264
+ setDrawPoints([norm]);
265
+ return;
266
+ }
267
+ if (drawPoints.length >= FLOOR_POLYGON_VERTICES) {
268
+ const first = toCanvas(drawPoints[0], size.w, size.h);
269
+ if (Math.hypot(pos.x - first.x, pos.y - first.y) < CLOSE_HIT_RADIUS) {
270
+ completeDraw({
271
+ kind: "polygon",
272
+ points: drawPoints
273
+ });
274
+ return;
275
+ }
276
+ }
277
+ setDrawPoints((prev) => [...prev, norm]);
278
+ }
279
+ }, [
280
+ isDrawing,
281
+ drawingKind,
282
+ drawPoints,
283
+ size,
284
+ grid,
285
+ completeDraw,
286
+ onSelect
287
+ ]);
288
+ const handleDblClick = (0, react.useCallback)((e) => {
289
+ if (!isDrawing || drawingKind !== "polygon") return;
290
+ if (drawPoints.length < FLOOR_POLYGON_VERTICES) return;
291
+ e.cancelBubble = true;
292
+ completeDraw({
293
+ kind: "polygon",
294
+ points: drawPoints
295
+ });
296
+ }, [
297
+ isDrawing,
298
+ drawingKind,
299
+ drawPoints,
300
+ completeDraw
301
+ ]);
302
+ const cursorRafRef = (0, react.useRef)(null);
303
+ const pendingCursorRef = (0, react.useRef)(null);
304
+ (0, react.useEffect)(() => () => {
305
+ if (cursorRafRef.current !== null) cancelAnimationFrame(cursorRafRef.current);
306
+ }, []);
307
+ const handleMouseMove = (0, react.useCallback)((e) => {
308
+ if (!isDrawing) return;
309
+ const stage = e.target.getStage();
310
+ if (!stage) return;
311
+ pendingCursorRef.current = stage.getPointerPosition() ?? null;
312
+ if (cursorRafRef.current !== null) return;
313
+ cursorRafRef.current = requestAnimationFrame(() => {
314
+ cursorRafRef.current = null;
315
+ setCursorPos(pendingCursorRef.current);
316
+ });
317
+ }, [isDrawing]);
318
+ const paintingRef = (0, react.useRef)(false);
319
+ const paintValueRef = (0, react.useRef)(true);
320
+ const paintedRef = (0, react.useRef)(/* @__PURE__ */ new Set());
321
+ const paintCell = (0, react.useCallback)((itemId, shape, index, value) => {
322
+ const total = shape.gridWidth * shape.gridHeight;
323
+ if (index < 0 || index >= total) return;
324
+ if (shape.cells[index] === value) return;
325
+ const next = [...shape.cells];
326
+ while (next.length < total) next.push(false);
327
+ next.length = total;
328
+ next[index] = value;
329
+ onShapeChange(itemId, {
330
+ ...shape,
331
+ cells: next
332
+ });
333
+ }, [onShapeChange]);
334
+ const rhythmLines = [];
335
+ if (!transparent) {
336
+ for (let i = 1; i < RHYTHM_COLS; i++) {
337
+ const x = size.w / RHYTHM_COLS * i;
338
+ rhythmLines.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Line, {
339
+ points: [
340
+ x,
341
+ 0,
342
+ x,
343
+ size.h
344
+ ],
345
+ stroke: "#ffffff",
346
+ strokeWidth: .5,
347
+ opacity: .08
348
+ }, `gv-${i}`));
349
+ }
350
+ for (let i = 1; i < RHYTHM_ROWS; i++) {
351
+ const y = size.h / RHYTHM_ROWS * i;
352
+ rhythmLines.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Line, {
353
+ points: [
354
+ 0,
355
+ y,
356
+ size.w,
357
+ y
358
+ ],
359
+ stroke: "#ffffff",
360
+ strokeWidth: .5,
361
+ opacity: .08
362
+ }, `gh-${i}`));
363
+ }
364
+ }
365
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
366
+ ref: containerRef,
367
+ className: transparent ? "absolute inset-0" : "relative w-full bg-zinc-900 rounded-lg overflow-hidden",
368
+ style: { cursor: isDrawing ? "crosshair" : "default" },
369
+ children: [
370
+ backdrop && !transparent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
371
+ className: "absolute inset-0 pointer-events-none [&>*]:w-full [&>*]:h-full [&>img]:object-cover [&>video]:object-cover",
372
+ style: {
373
+ width: size.w,
374
+ height: size.h
375
+ },
376
+ children: backdrop
377
+ }) : null,
378
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_konva.Stage, {
379
+ width: size.w,
380
+ height: size.h,
381
+ onClick: handleStageClick,
382
+ onDblClick: handleDblClick,
383
+ onMouseMove: handleMouseMove,
384
+ children: [
385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_konva.Layer, {
386
+ listening: false,
387
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Line, {
388
+ points: [
389
+ 0,
390
+ 0,
391
+ size.w,
392
+ 0,
393
+ size.w,
394
+ size.h,
395
+ 0,
396
+ size.h,
397
+ 0,
398
+ 0
399
+ ],
400
+ stroke: "transparent",
401
+ strokeWidth: 0
402
+ }), rhythmLines]
403
+ }),
404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Layer, { children: items.map((item) => {
405
+ const selected = item.id === selectedId;
406
+ const dimmed = item.enabled === false;
407
+ const color = resolveColor(item);
408
+ const editable = selected && !isDrawing && !dimmed;
409
+ if (item.shape.kind === "rect") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RectItem, {
410
+ item,
411
+ shape: item.shape,
412
+ color,
413
+ selected,
414
+ dimmed,
415
+ editable,
416
+ isDrawing,
417
+ size,
418
+ onSelect,
419
+ onShapeChange
420
+ }, item.id);
421
+ if (item.shape.kind === "grid") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridItem, {
422
+ item,
423
+ shape: item.shape,
424
+ color,
425
+ dimmed,
426
+ editable,
427
+ size,
428
+ onSelect,
429
+ paintingRef,
430
+ paintValueRef,
431
+ paintedRef,
432
+ paintCell
433
+ }, item.id);
434
+ const isLine = item.shape.kind === "line";
435
+ const minVertices = isLine ? FLOOR_LINE_VERTICES : Math.max(FLOOR_POLYGON_VERTICES, polygonVertices?.min ?? FLOOR_POLYGON_VERTICES);
436
+ const maxVertices = isLine ? FLOOR_LINE_VERTICES : polygonVertices?.max;
437
+ const effectivePoints = dragOverride && dragOverride.itemId === item.id ? dragOverride.points : item.shape.points;
438
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointItem, {
439
+ item,
440
+ shape: item.shape,
441
+ color,
442
+ selected,
443
+ dimmed,
444
+ editable,
445
+ isLine,
446
+ isDrawing,
447
+ size,
448
+ effectivePoints,
449
+ minVertices,
450
+ maxVertices,
451
+ isDragging: draggingItemId === item.id,
452
+ onSelect,
453
+ onShapeChange,
454
+ setDragOverride,
455
+ setDraggingItemId
456
+ }, item.id);
457
+ }) }),
458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Layer, {
459
+ listening: false,
460
+ children: isDrawing && drawPoints.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_konva.Group, { children: [
461
+ drawingKind === "polygon" && drawPoints.length >= 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Line, {
462
+ points: flatPoints(drawPoints, size.w, size.h),
463
+ stroke: DRAW_PREVIEW_STROKE,
464
+ strokeWidth: 1.5,
465
+ dash: [6, 3],
466
+ opacity: .8
467
+ }),
468
+ drawingKind === "line" && drawPoints.length === 1 && cursorPos && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Line, {
469
+ points: [
470
+ drawPoints[0].x * size.w,
471
+ drawPoints[0].y * size.h,
472
+ cursorPos.x,
473
+ cursorPos.y
474
+ ],
475
+ stroke: DEFAULT_COLOR.line,
476
+ strokeWidth: 1.5,
477
+ dash: [6, 3],
478
+ opacity: .7
479
+ }),
480
+ drawingKind === "polygon" && cursorPos && drawPoints.length >= 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Line, {
481
+ points: [
482
+ drawPoints[drawPoints.length - 1].x * size.w,
483
+ drawPoints[drawPoints.length - 1].y * size.h,
484
+ cursorPos.x,
485
+ cursorPos.y
486
+ ],
487
+ stroke: DRAW_PREVIEW_STROKE,
488
+ strokeWidth: 1,
489
+ dash: [4, 4],
490
+ opacity: .5
491
+ }),
492
+ drawPoints.map((p, idx) => {
493
+ const cp = toCanvas(p, size.w, size.h);
494
+ const firstClose = idx === 0 && drawingKind === "polygon" && drawPoints.length >= FLOOR_POLYGON_VERTICES;
495
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Circle, {
496
+ x: cp.x,
497
+ y: cp.y,
498
+ radius: firstClose ? 7 : 4,
499
+ fill: firstClose ? DRAW_PREVIEW_STROKE : "#ffffff",
500
+ stroke: DRAW_PREVIEW_STROKE,
501
+ strokeWidth: 1.5,
502
+ opacity: .9
503
+ }, idx);
504
+ })
505
+ ] })
506
+ })
507
+ ]
508
+ }),
509
+ isDrawing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
510
+ className: "px-3 py-1.5 bg-zinc-800/80 text-[11px] text-zinc-400 border-t border-zinc-700",
511
+ children: drawingKind === "line" ? "Click to set the start point, click again to complete the line" : drawingKind === "rect" ? "Click to drop a rectangle, then drag the corner to resize" : drawingKind === "grid" ? "Click to add the grid, then paint cells" : drawPoints.length < FLOOR_POLYGON_VERTICES ? "Click to add points (min 3). Double-click or click the first point to close." : "Continue adding points. Double-click or click the first point to close."
512
+ }) : null
513
+ ]
514
+ });
515
+ }
516
+ var MIN_RECT_DIM = .02;
517
+ /** Float-tolerant rect equality (used to detect controlled-prop catch-up). */
518
+ function rectGeomEqual(a, b) {
519
+ return Math.abs(a.x - b.x) < 1e-6 && Math.abs(a.y - b.y) < 1e-6 && Math.abs(a.width - b.width) < 1e-6 && Math.abs(a.height - b.height) < 1e-6;
520
+ }
521
+ function RectItem({ item, shape, color, selected, dimmed, editable, isDrawing, size, onSelect, onShapeChange }) {
522
+ const [override, setOverride] = (0, react.useState)(null);
523
+ const propGeom = {
524
+ x: shape.x,
525
+ y: shape.y,
526
+ width: shape.width,
527
+ height: shape.height
528
+ };
529
+ (0, react.useEffect)(() => {
530
+ if (override && rectGeomEqual(propGeom, override)) setOverride(null);
531
+ }, [
532
+ shape.x,
533
+ shape.y,
534
+ shape.width,
535
+ shape.height,
536
+ override
537
+ ]);
538
+ const geom = override ?? propGeom;
539
+ const x = geom.x * size.w;
540
+ const y = geom.y * size.h;
541
+ const w = geom.width * size.w;
542
+ const h = geom.height * size.h;
543
+ const baseOpacity = dimmed ? .35 : selected ? 1 : .85;
544
+ const commit = (g) => {
545
+ setOverride(g);
546
+ onShapeChange(item.id, {
547
+ kind: "rect",
548
+ x: g.x,
549
+ y: g.y,
550
+ width: g.width,
551
+ height: g.height
552
+ });
553
+ };
554
+ const dragBoundFunc = (pos) => ({
555
+ x: Math.max(-x, Math.min(size.w - (x + w), pos.x)),
556
+ y: Math.max(-y, Math.min(size.h - (y + h), pos.y))
557
+ });
558
+ const onBodyDragEnd = (e) => {
559
+ const node = e.target;
560
+ const dx = node.x() / size.w;
561
+ const dy = node.y() / size.h;
562
+ node.position({
563
+ x: 0,
564
+ y: 0
565
+ });
566
+ if (dx === 0 && dy === 0) return;
567
+ const nx = Math.min(Math.max(0, geom.x + dx), Math.max(0, 1 - geom.width));
568
+ const ny = Math.min(Math.max(0, geom.y + dy), Math.max(0, 1 - geom.height));
569
+ commit({
570
+ ...geom,
571
+ x: nx,
572
+ y: ny
573
+ });
574
+ };
575
+ const onHandleDragMove = (e) => {
576
+ const node = e.target;
577
+ const newW = clamp01(node.x() / size.w - geom.x);
578
+ const newH = clamp01(node.y() / size.h - geom.y);
579
+ setOverride({
580
+ ...geom,
581
+ width: Math.max(MIN_RECT_DIM, Math.min(newW, 1 - geom.x)),
582
+ height: Math.max(MIN_RECT_DIM, Math.min(newH, 1 - geom.y))
583
+ });
584
+ };
585
+ const onHandleDragEnd = (e) => {
586
+ const node = e.target;
587
+ const newW = clamp01(node.x() / size.w - geom.x);
588
+ const newH = clamp01(node.y() / size.h - geom.y);
589
+ commit({
590
+ ...geom,
591
+ width: Math.max(MIN_RECT_DIM, Math.min(newW, 1 - geom.x)),
592
+ height: Math.max(MIN_RECT_DIM, Math.min(newH, 1 - geom.y))
593
+ });
594
+ };
595
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Group, {
596
+ onClick: (e) => {
597
+ e.cancelBubble = true;
598
+ if (!isDrawing) onSelect(item.id);
599
+ },
600
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_konva.Group, {
601
+ draggable: editable,
602
+ dragBoundFunc: editable ? dragBoundFunc : void 0,
603
+ onDragEnd: editable ? onBodyDragEnd : void 0,
604
+ onMouseEnter: (e) => {
605
+ if (!editable) return;
606
+ const stage = e.target.getStage();
607
+ if (stage) stage.container().style.cursor = "move";
608
+ },
609
+ onMouseLeave: (e) => {
610
+ if (!editable) return;
611
+ const stage = e.target.getStage();
612
+ if (stage) stage.container().style.cursor = isDrawing ? "crosshair" : "default";
613
+ },
614
+ children: [
615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Rect, {
616
+ x,
617
+ y,
618
+ width: w,
619
+ height: h,
620
+ fill: color + (selected ? FILL_ALPHA_SELECTED : FILL_ALPHA),
621
+ stroke: color,
622
+ strokeWidth: selected ? 2.5 : 1.5,
623
+ opacity: baseOpacity
624
+ }),
625
+ item.label ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Text, {
626
+ x,
627
+ y: y - 16,
628
+ text: item.label,
629
+ fontSize: 12,
630
+ fill: color,
631
+ stroke: "#000000",
632
+ strokeWidth: 3,
633
+ fillAfterStrokeEnabled: true,
634
+ fontStyle: "bold",
635
+ listening: false
636
+ }) : null,
637
+ editable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Circle, {
638
+ x: x + w,
639
+ y: y + h,
640
+ radius: 6,
641
+ fill: color,
642
+ stroke: "#ffffff",
643
+ strokeWidth: 1.5,
644
+ draggable: true,
645
+ dragBoundFunc: (pos) => ({
646
+ x: Math.min(size.w, Math.max(x, pos.x)),
647
+ y: Math.min(size.h, Math.max(y, pos.y))
648
+ }),
649
+ onDragStart: (e) => {
650
+ e.cancelBubble = true;
651
+ },
652
+ onDragMove: onHandleDragMove,
653
+ onDragEnd: onHandleDragEnd,
654
+ onMouseEnter: (e) => {
655
+ const stage = e.target.getStage();
656
+ if (stage) stage.container().style.cursor = "nwse-resize";
657
+ },
658
+ onMouseLeave: (e) => {
659
+ const stage = e.target.getStage();
660
+ if (stage) stage.container().style.cursor = "default";
661
+ }
662
+ }) : null
663
+ ]
664
+ })
665
+ });
666
+ }
667
+ function GridItem({ item, shape, color, dimmed, editable, size, onSelect, paintingRef, paintValueRef, paintedRef, paintCell }) {
668
+ const { gridWidth, gridHeight, cells } = shape;
669
+ const cellW = size.w / gridWidth;
670
+ const cellH = size.h / gridHeight;
671
+ const total = gridWidth * gridHeight;
672
+ const opacity = dimmed ? .3 : 1;
673
+ const onCellDown = (index) => {
674
+ if (!editable) {
675
+ onSelect(item.id);
676
+ return;
677
+ }
678
+ const nextValue = cells[index] !== true;
679
+ paintingRef.current = true;
680
+ paintValueRef.current = nextValue;
681
+ paintedRef.current = new Set([index]);
682
+ paintCell(item.id, shape, index, nextValue);
683
+ };
684
+ const onCellEnter = (index) => {
685
+ if (!paintingRef.current || !editable) return;
686
+ if (paintedRef.current.has(index)) return;
687
+ paintedRef.current.add(index);
688
+ paintCell(item.id, shape, index, paintValueRef.current);
689
+ };
690
+ const endPaint = () => {
691
+ if (!paintingRef.current) return;
692
+ paintingRef.current = false;
693
+ paintedRef.current = /* @__PURE__ */ new Set();
694
+ };
695
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Group, {
696
+ opacity,
697
+ onMouseUp: endPaint,
698
+ onMouseLeave: endPaint,
699
+ children: Array.from({ length: total }, (_, i) => {
700
+ const col = i % gridWidth;
701
+ const row = Math.floor(i / gridWidth);
702
+ const active = cells[i] === true;
703
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Rect, {
704
+ x: col * cellW,
705
+ y: row * cellH,
706
+ width: cellW,
707
+ height: cellH,
708
+ fill: active ? color + FILL_ALPHA_SELECTED : "#ffffff10",
709
+ stroke: active ? color : "#ffffff40",
710
+ strokeWidth: .5,
711
+ onMouseDown: (e) => {
712
+ e.cancelBubble = true;
713
+ onCellDown(i);
714
+ },
715
+ onMouseEnter: () => onCellEnter(i)
716
+ }, i);
717
+ })
718
+ });
719
+ }
720
+ function PointItem({ item, shape, color, selected, dimmed, editable, isLine, isDrawing, size, effectivePoints, minVertices, maxVertices, isDragging, onSelect, onShapeChange, setDragOverride, setDraggingItemId }) {
721
+ const pts = flatPoints(effectivePoints, size.w, size.h);
722
+ const baseOpacity = dimmed ? .35 : selected ? 1 : isLine ? .8 : .85;
723
+ const commitPoints = (points) => isLine ? {
724
+ kind: "line",
725
+ points: [...points]
726
+ } : {
727
+ kind: "polygon",
728
+ points: [...points]
729
+ };
730
+ const xs = effectivePoints.map((p) => p.x * size.w);
731
+ const ys = effectivePoints.map((p) => p.y * size.h);
732
+ const minX = xs.length > 0 ? Math.min(...xs) : 0;
733
+ const maxX = xs.length > 0 ? Math.max(...xs) : 0;
734
+ const minY = ys.length > 0 ? Math.min(...ys) : 0;
735
+ const maxY = ys.length > 0 ? Math.max(...ys) : 0;
736
+ const dragBoundFunc = (pos) => ({
737
+ x: Math.max(-minX, Math.min(size.w - maxX, pos.x)),
738
+ y: Math.max(-minY, Math.min(size.h - maxY, pos.y))
739
+ });
740
+ const onBodyDragEnd = (e) => {
741
+ const node = e.target;
742
+ const dx = node.x() / size.w;
743
+ const dy = node.y() / size.h;
744
+ node.position({
745
+ x: 0,
746
+ y: 0
747
+ });
748
+ setDraggingItemId(null);
749
+ if (dx === 0 && dy === 0) return;
750
+ const updated = shape.points.map((p) => ({
751
+ x: p.x + dx,
752
+ y: p.y + dy
753
+ }));
754
+ setDragOverride({
755
+ itemId: item.id,
756
+ points: updated
757
+ });
758
+ onShapeChange(item.id, commitPoints(updated));
759
+ };
760
+ const canRemoveVertex = !isLine && effectivePoints.length > minVertices;
761
+ const canAddVertex = !isLine && (maxVertices === void 0 || effectivePoints.length < maxVertices);
762
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_konva.Group, {
763
+ onClick: (e) => {
764
+ e.cancelBubble = true;
765
+ if (!isDrawing) onSelect(item.id);
766
+ },
767
+ children: [
768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_konva.Group, {
769
+ draggable: editable,
770
+ dragBoundFunc: editable ? dragBoundFunc : void 0,
771
+ onDragStart: editable ? () => setDraggingItemId(item.id) : void 0,
772
+ onDragEnd: editable ? onBodyDragEnd : void 0,
773
+ onMouseEnter: (e) => {
774
+ if (!editable) return;
775
+ const stage = e.target.getStage();
776
+ if (stage) stage.container().style.cursor = "move";
777
+ },
778
+ onMouseLeave: (e) => {
779
+ if (!editable) return;
780
+ const stage = e.target.getStage();
781
+ if (stage) stage.container().style.cursor = isDrawing ? "crosshair" : "default";
782
+ },
783
+ children: [isLine ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Arrow, {
784
+ points: pts,
785
+ stroke: color,
786
+ strokeWidth: selected ? 3 : 2,
787
+ fill: color,
788
+ pointerLength: 10,
789
+ pointerWidth: 8,
790
+ opacity: baseOpacity
791
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Line, {
792
+ points: pts,
793
+ closed: true,
794
+ fill: color + (selected ? FILL_ALPHA_SELECTED : FILL_ALPHA),
795
+ stroke: color,
796
+ strokeWidth: selected ? 2.5 : 1.5,
797
+ opacity: baseOpacity
798
+ }), item.label && effectivePoints.length > 0 ? (() => {
799
+ const c = toCanvas({
800
+ x: effectivePoints.reduce((s, p) => s + p.x, 0) / effectivePoints.length,
801
+ y: effectivePoints.reduce((s, p) => s + p.y, 0) / effectivePoints.length
802
+ }, size.w, size.h);
803
+ const approxWidth = Math.max(40, item.label.length * 11 * .62);
804
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Text, {
805
+ x: c.x - approxWidth / 2,
806
+ y: c.y - 7,
807
+ width: approxWidth,
808
+ align: "center",
809
+ text: item.label,
810
+ fontSize: 12,
811
+ fill: color,
812
+ stroke: "#000000",
813
+ strokeWidth: 3,
814
+ fillAfterStrokeEnabled: true,
815
+ fontStyle: "bold",
816
+ listening: false
817
+ });
818
+ })() : null]
819
+ }),
820
+ editable && !isDragging && effectivePoints.map((p, idx) => {
821
+ const cp = toCanvas(p, size.w, size.h);
822
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Circle, {
823
+ x: cp.x,
824
+ y: cp.y,
825
+ radius: 5,
826
+ fill: color,
827
+ stroke: "#ffffff",
828
+ strokeWidth: 1.5,
829
+ draggable: true,
830
+ onDragMove: (e) => {
831
+ const newPt = toNorm({
832
+ x: e.target.x(),
833
+ y: e.target.y()
834
+ }, size.w, size.h);
835
+ setDragOverride((prev) => {
836
+ const base = prev && prev.itemId === item.id ? prev.points : shape.points;
837
+ return {
838
+ itemId: item.id,
839
+ points: base.map((pp, i) => i === idx ? newPt : pp)
840
+ };
841
+ });
842
+ },
843
+ onDragEnd: (e) => {
844
+ const newPt = toNorm({
845
+ x: e.target.x(),
846
+ y: e.target.y()
847
+ }, size.w, size.h);
848
+ const updated = shape.points.map((pp, i) => i === idx ? newPt : pp);
849
+ setDragOverride({
850
+ itemId: item.id,
851
+ points: updated
852
+ });
853
+ onShapeChange(item.id, commitPoints(updated));
854
+ },
855
+ onContextMenu: (e) => {
856
+ e.evt.preventDefault();
857
+ e.cancelBubble = true;
858
+ if (!canRemoveVertex) return;
859
+ onShapeChange(item.id, commitPoints(shape.points.filter((_, i) => i !== idx)));
860
+ }
861
+ }, `pt-${idx}`);
862
+ }),
863
+ editable && !isDragging && canAddVertex && effectivePoints.length >= 2 && effectivePoints.map((p, idx) => {
864
+ const next = effectivePoints[(idx + 1) % effectivePoints.length];
865
+ const cp = toCanvas({
866
+ x: (p.x + next.x) / 2,
867
+ y: (p.y + next.y) / 2
868
+ }, size.w, size.h);
869
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_konva.Circle, {
870
+ x: cp.x,
871
+ y: cp.y,
872
+ radius: 4,
873
+ fill: "#ffffff",
874
+ stroke: color,
875
+ strokeWidth: 1.5,
876
+ opacity: .6,
877
+ onMouseEnter: (e) => {
878
+ const node = e.target;
879
+ node.to({
880
+ radius: 6,
881
+ opacity: 1,
882
+ duration: .1
883
+ });
884
+ const stage = node.getStage();
885
+ if (stage) stage.container().style.cursor = "copy";
886
+ },
887
+ onMouseLeave: (e) => {
888
+ const node = e.target;
889
+ node.to({
890
+ radius: 4,
891
+ opacity: .6,
892
+ duration: .1
893
+ });
894
+ const stage = node.getStage();
895
+ if (stage) stage.container().style.cursor = isDrawing ? "crosshair" : "default";
896
+ },
897
+ onClick: (e) => {
898
+ e.cancelBubble = true;
899
+ onShapeChange(item.id, commitPoints(insertMidpoint(shape.points, idx)));
900
+ }
901
+ }, `mid-${idx}`);
902
+ })
903
+ ]
904
+ });
905
+ }
906
+ /**
907
+ * Memoised export. The Konva scene is expensive to re-evaluate (many nodes per
908
+ * item + per-item drag-bound closures) and overlay parents re-render on every
909
+ * detection/motion event. Mirrors ZoneCanvas's `memo`.
910
+ */
911
+ var MaskShapeCanvas = (0, react.memo)(MaskShapeCanvasImpl);
912
+ //#endregion
913
+ exports.MaskShapeCanvas = MaskShapeCanvas;