@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.
- package/dist/MaskShapeCanvas-BByN3jvt.cjs +913 -0
- package/dist/MaskShapeCanvas-DI4BY7W2.js +913 -0
- package/dist/MotionZonesSettings-C1EEbk2V.js +438 -0
- package/dist/MotionZonesSettings-Ci1mzrki.cjs +438 -0
- package/dist/PrivacyMaskSettings-APgPLF7p.js +384 -0
- package/dist/PrivacyMaskSettings-yC-UPYPg.cjs +384 -0
- package/dist/composites/index.d.ts +6 -2
- package/dist/generated/system-hooks.d.ts +8 -8
- package/dist/hls-CckKbjjD.cjs +28320 -0
- package/dist/hls-CfgsaJjd.js +28320 -0
- package/dist/index.cjs +422 -30419
- package/dist/index.d.ts +7 -0
- package/dist/index.js +412 -30416
- package/package.json +1 -1
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Arrow, Circle, Group, Layer, Line, Rect, Stage, Text } from "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 = useRef(null);
|
|
175
|
+
const [size, setSize] = useState({
|
|
176
|
+
w: 800,
|
|
177
|
+
h: 450
|
|
178
|
+
});
|
|
179
|
+
const [drawPoints, setDrawPoints] = useState([]);
|
|
180
|
+
const [cursorPos, setCursorPos] = useState(null);
|
|
181
|
+
const [draggingItemId, setDraggingItemId] = useState(null);
|
|
182
|
+
const [dragOverride, setDragOverride] = useState(null);
|
|
183
|
+
const isDrawing = drawingKind !== null && supportedShapes.includes(drawingKind);
|
|
184
|
+
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
|
+
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
|
+
useEffect(() => {
|
|
212
|
+
if (!isDrawing && cursorPos !== null) setCursorPos(null);
|
|
213
|
+
}, [isDrawing, cursorPos]);
|
|
214
|
+
const completeDraw = useCallback((shape) => {
|
|
215
|
+
onDrawComplete(shape);
|
|
216
|
+
setDrawPoints([]);
|
|
217
|
+
}, [onDrawComplete]);
|
|
218
|
+
const handleStageClick = 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 = 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 = useRef(null);
|
|
303
|
+
const pendingCursorRef = useRef(null);
|
|
304
|
+
useEffect(() => () => {
|
|
305
|
+
if (cursorRafRef.current !== null) cancelAnimationFrame(cursorRafRef.current);
|
|
306
|
+
}, []);
|
|
307
|
+
const handleMouseMove = 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 = useRef(false);
|
|
319
|
+
const paintValueRef = useRef(true);
|
|
320
|
+
const paintedRef = useRef(/* @__PURE__ */ new Set());
|
|
321
|
+
const paintCell = 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__ */ jsx(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__ */ jsx(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__ */ 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__ */ 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__ */ jsxs(Stage, {
|
|
379
|
+
width: size.w,
|
|
380
|
+
height: size.h,
|
|
381
|
+
onClick: handleStageClick,
|
|
382
|
+
onDblClick: handleDblClick,
|
|
383
|
+
onMouseMove: handleMouseMove,
|
|
384
|
+
children: [
|
|
385
|
+
/* @__PURE__ */ jsxs(Layer, {
|
|
386
|
+
listening: false,
|
|
387
|
+
children: [/* @__PURE__ */ jsx(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__ */ jsx(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__ */ 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__ */ 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__ */ 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__ */ jsx(Layer, {
|
|
459
|
+
listening: false,
|
|
460
|
+
children: isDrawing && drawPoints.length > 0 && /* @__PURE__ */ jsxs(Group, { children: [
|
|
461
|
+
drawingKind === "polygon" && drawPoints.length >= 2 && /* @__PURE__ */ jsx(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__ */ jsx(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__ */ jsx(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__ */ jsx(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__ */ 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] = useState(null);
|
|
523
|
+
const propGeom = {
|
|
524
|
+
x: shape.x,
|
|
525
|
+
y: shape.y,
|
|
526
|
+
width: shape.width,
|
|
527
|
+
height: shape.height
|
|
528
|
+
};
|
|
529
|
+
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__ */ jsx(Group, {
|
|
596
|
+
onClick: (e) => {
|
|
597
|
+
e.cancelBubble = true;
|
|
598
|
+
if (!isDrawing) onSelect(item.id);
|
|
599
|
+
},
|
|
600
|
+
children: /* @__PURE__ */ jsxs(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__ */ jsx(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__ */ jsx(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__ */ jsx(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__ */ jsx(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__ */ jsx(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__ */ jsxs(Group, {
|
|
763
|
+
onClick: (e) => {
|
|
764
|
+
e.cancelBubble = true;
|
|
765
|
+
if (!isDrawing) onSelect(item.id);
|
|
766
|
+
},
|
|
767
|
+
children: [
|
|
768
|
+
/* @__PURE__ */ jsxs(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__ */ jsx(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__ */ jsx(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__ */ jsx(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__ */ jsx(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__ */ jsx(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 = memo(MaskShapeCanvasImpl);
|
|
912
|
+
//#endregion
|
|
913
|
+
export { MaskShapeCanvas };
|