reac-mapper 0.1.1

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.js ADDED
@@ -0,0 +1,1044 @@
1
+ 'use strict';
2
+
3
+ var React2 = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var reactKonva = require('react-konva');
6
+ var useImage = require('use-image');
7
+ var lucideReact = require('lucide-react');
8
+ var styled = require('styled-components');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ function _interopNamespace(e) {
13
+ if (e && e.__esModule) return e;
14
+ var n = Object.create(null);
15
+ if (e) {
16
+ Object.keys(e).forEach(function (k) {
17
+ if (k !== 'default') {
18
+ var d = Object.getOwnPropertyDescriptor(e, k);
19
+ Object.defineProperty(n, k, d.get ? d : {
20
+ enumerable: true,
21
+ get: function () { return e[k]; }
22
+ });
23
+ }
24
+ });
25
+ }
26
+ n.default = e;
27
+ return Object.freeze(n);
28
+ }
29
+
30
+ var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
31
+ var useImage__default = /*#__PURE__*/_interopDefault(useImage);
32
+ var styled__default = /*#__PURE__*/_interopDefault(styled);
33
+
34
+ // src/components/ImageWithPoints.tsx
35
+
36
+ // src/utils/imageMetrics.ts
37
+ async function getImageNaturalSize(src) {
38
+ return new Promise((resolve, reject) => {
39
+ const img = new Image();
40
+ img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });
41
+ img.onerror = reject;
42
+ img.src = src;
43
+ });
44
+ }
45
+ function ImageWithPoints({
46
+ image,
47
+ naturalSize,
48
+ edges = [],
49
+ pointRadius = 5,
50
+ pointFill = "#1f6feb",
51
+ lineWidth = 2,
52
+ lineStroke = "#111",
53
+ className,
54
+ style,
55
+ title,
56
+ desc,
57
+ children,
58
+ points
59
+ }) {
60
+ const [size, setSize] = React2__namespace.useState(naturalSize ?? null);
61
+ React2__namespace.useEffect(() => {
62
+ let alive = true;
63
+ (async () => {
64
+ if (!naturalSize && image.type === "img") {
65
+ try {
66
+ const s = await getImageNaturalSize(image.src);
67
+ if (alive) setSize(s);
68
+ } catch {
69
+ }
70
+ } else if (!naturalSize && image.type === "next") {
71
+ setSize({ width: image.width, height: image.height });
72
+ }
73
+ })();
74
+ return () => {
75
+ alive = false;
76
+ };
77
+ }, [image, naturalSize]);
78
+ if (!size) return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...style, aspectRatio: "16/9", background: "#f6f8fa" } });
79
+ const { width, height } = size;
80
+ return /* @__PURE__ */ jsxRuntime.jsxs(
81
+ "svg",
82
+ {
83
+ className,
84
+ style,
85
+ viewBox: `0 0 ${width} ${height}`,
86
+ role: "img",
87
+ "aria-label": title,
88
+ xmlns: "http://www.w3.org/2000/svg",
89
+ children: [
90
+ title && /* @__PURE__ */ jsxRuntime.jsx("title", { children: title }),
91
+ desc && /* @__PURE__ */ jsxRuntime.jsx("desc", { children: desc }),
92
+ /* @__PURE__ */ jsxRuntime.jsx("image", { href: image.src, x: 0, y: 0, width, height, preserveAspectRatio: "none" }),
93
+ /* @__PURE__ */ jsxRuntime.jsx("g", { stroke: lineStroke, strokeWidth: lineWidth, strokeLinecap: "round", children: edges.map((e) => {
94
+ const a = points.find((p) => p.id === e.from);
95
+ const b = points.find((p) => p.id === e.to);
96
+ if (!a || !b) return null;
97
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { x1: a.x, y1: a.y, x2: b.x, y2: b.y }, e.id);
98
+ }) }),
99
+ /* @__PURE__ */ jsxRuntime.jsx("g", { fill: pointFill, children: points.map((p) => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: p.x, cy: p.y, r: pointRadius }, p.id)) }),
100
+ children
101
+ ]
102
+ }
103
+ );
104
+ }
105
+ function FloorMapDrawer(props) {
106
+ const {
107
+ image,
108
+ naturalSize,
109
+ points,
110
+ edges,
111
+ defaultPoints = [],
112
+ defaultEdges = [],
113
+ onChange,
114
+ onModeChange,
115
+ pointRadius = 5,
116
+ activePointRadius = 6.5,
117
+ pointFill = "#1f6feb",
118
+ activePointFill = "#ff7b72",
119
+ lineWidth = 2,
120
+ lineStroke = "#111",
121
+ connectSequential = true,
122
+ panEnabled = true,
123
+ zoomEnabled = true,
124
+ minZoom = 0.3,
125
+ maxZoom = 6,
126
+ zoomStep = 1.12,
127
+ zoomOnCtrlWheel = false,
128
+ showToolbar = true,
129
+ initialMode = "pan",
130
+ className,
131
+ style,
132
+ svgClassName,
133
+ title,
134
+ desc
135
+ } = props;
136
+ const wrapperRef = React2__namespace.useRef(null);
137
+ const svgRef = React2__namespace.useRef(null);
138
+ const [size, setSize] = React2__namespace.useState(naturalSize ?? null);
139
+ const isControlled = points !== void 0 || edges !== void 0;
140
+ const [localPoints, setLocalPoints] = React2__namespace.useState(defaultPoints);
141
+ const [localEdges, setLocalEdges] = React2__namespace.useState(defaultEdges);
142
+ const pts = points ?? localPoints;
143
+ const eds = edges ?? localEdges;
144
+ const [scale, setScale] = React2__namespace.useState(1);
145
+ const [tx, setTx] = React2__namespace.useState(0);
146
+ const [ty, setTy] = React2__namespace.useState(0);
147
+ const [mode, setMode] = React2__namespace.useState(initialMode);
148
+ const [hoverId, setHoverId] = React2__namespace.useState(null);
149
+ const [drag, setDrag] = React2__namespace.useState(null);
150
+ const spacePanningRef = React2__namespace.useRef(false);
151
+ const [undoStack, setUndoStack] = React2__namespace.useState([]);
152
+ const [redoStack, setRedoStack] = React2__namespace.useState([]);
153
+ React2__namespace.useEffect(() => {
154
+ let alive = true;
155
+ (async () => {
156
+ if (!naturalSize && image.type === "img") {
157
+ try {
158
+ const s = await getImageNaturalSize(image.src);
159
+ if (alive) setSize(s);
160
+ } catch {
161
+ }
162
+ } else if (!naturalSize && image.type === "next") {
163
+ setSize({ width: image.width, height: image.height });
164
+ }
165
+ })();
166
+ return () => {
167
+ alive = false;
168
+ };
169
+ }, [image, naturalSize]);
170
+ const clamp = React2__namespace.useCallback((n, a, b) => Math.max(a, Math.min(b, n)), []);
171
+ const cryptoRandomId = React2__namespace.useCallback(
172
+ () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2),
173
+ []
174
+ );
175
+ const screenToImage = React2__namespace.useCallback((clientX, clientY) => {
176
+ const svg = svgRef.current;
177
+ if (!svg || !size) return null;
178
+ const rect = svg.getBoundingClientRect();
179
+ const cx = clientX - rect.left;
180
+ const cy = clientY - rect.top;
181
+ const x = (cx - tx) / scale;
182
+ const y = (cy - ty) / scale;
183
+ return { x, y };
184
+ }, [size, tx, ty, scale]);
185
+ const pushHistory = React2__namespace.useCallback((entry) => {
186
+ if (isControlled) return;
187
+ setUndoStack((s) => [...s, entry]);
188
+ setRedoStack([]);
189
+ }, [isControlled]);
190
+ const undo = React2__namespace.useCallback(() => {
191
+ if (isControlled) return;
192
+ setUndoStack((s) => {
193
+ if (s.length === 0) return s;
194
+ const prev = s[s.length - 1];
195
+ setRedoStack((r) => [{
196
+ points: localPoints,
197
+ edges: localEdges
198
+ }, ...r]);
199
+ setLocalPoints(prev.points);
200
+ setLocalEdges(prev.edges);
201
+ onChange?.({ points: prev.points, edges: prev.edges });
202
+ return s.slice(0, -1);
203
+ });
204
+ }, [isControlled, localPoints, localEdges, onChange]);
205
+ const redo = React2__namespace.useCallback(() => {
206
+ if (isControlled) return;
207
+ setRedoStack((r) => {
208
+ if (r.length === 0) return r;
209
+ const next = r[0];
210
+ setUndoStack((s) => [...s, {
211
+ points: localPoints,
212
+ edges: localEdges
213
+ }]);
214
+ setLocalPoints(next.points);
215
+ setLocalEdges(next.edges);
216
+ onChange?.({ points: next.points, edges: next.edges });
217
+ return r.slice(1);
218
+ });
219
+ }, [isControlled, localPoints, localEdges, onChange]);
220
+ const setAndEmitMode = React2__namespace.useCallback((m) => {
221
+ setMode(m);
222
+ onModeChange?.(m);
223
+ }, [onModeChange]);
224
+ const update = React2__namespace.useCallback((next) => {
225
+ const newPoints = next.points ?? pts;
226
+ const newEdges = next.edges ?? eds;
227
+ if (!isControlled) {
228
+ pushHistory({ points: pts, edges: eds });
229
+ if (next.points) setLocalPoints(newPoints);
230
+ if (next.edges) setLocalEdges(newEdges);
231
+ }
232
+ onChange?.({ points: newPoints, edges: newEdges });
233
+ }, [isControlled, pts, eds, onChange, pushHistory]);
234
+ const removePoint = React2__namespace.useCallback((id) => {
235
+ update({
236
+ points: pts.filter((p) => p.id !== id),
237
+ edges: eds.filter((e) => e.from !== id && e.to !== id)
238
+ });
239
+ }, [pts, eds, update]);
240
+ const addPointAt = React2__namespace.useCallback((clientX, clientY) => {
241
+ const img = screenToImage(clientX, clientY);
242
+ if (!img || !size) return;
243
+ const x = clamp(img.x, 0, size.width);
244
+ const y = clamp(img.y, 0, size.height);
245
+ const id = cryptoRandomId();
246
+ const newPoint = { id, x, y };
247
+ const newPoints = [...pts, newPoint];
248
+ let newEdges = eds;
249
+ if (connectSequential && pts.length > 0) {
250
+ const last = pts[pts.length - 1];
251
+ const edgeExists = eds.some(
252
+ (e) => e.from === last.id && e.to === id || e.from === id && e.to === last.id
253
+ );
254
+ if (!edgeExists) {
255
+ newEdges = [...eds, { id: cryptoRandomId(), from: last.id, to: id }];
256
+ }
257
+ }
258
+ update({ points: newPoints, edges: newEdges });
259
+ }, [screenToImage, size, clamp, cryptoRandomId, pts, eds, connectSequential, update]);
260
+ const resetZoom = React2__namespace.useCallback(() => {
261
+ setScale(1);
262
+ setTx(0);
263
+ setTy(0);
264
+ }, []);
265
+ const fitToContainer = React2__namespace.useCallback(() => {
266
+ if (!wrapperRef.current || !size) return;
267
+ const rect = wrapperRef.current.getBoundingClientRect();
268
+ const pad = 16;
269
+ const availW = Math.max(1, rect.width - pad * 2);
270
+ const availH = Math.max(1, rect.height - pad * 2);
271
+ const s = Math.min(availW / size.width, availH / size.height);
272
+ setScale(s);
273
+ setTx((rect.width - size.width * s) / 2);
274
+ setTy((rect.height - size.height * s) / 2);
275
+ }, [size]);
276
+ React2__namespace.useEffect(() => {
277
+ const onKeyDown = (e) => {
278
+ if (e.code === "Space") spacePanningRef.current = true;
279
+ if (e.target && e.target.tagName === "INPUT") return;
280
+ if (e.metaKey || e.ctrlKey) {
281
+ if (e.key.toLowerCase() === "z" && !e.shiftKey) {
282
+ e.preventDefault();
283
+ undo();
284
+ }
285
+ if (e.key.toLowerCase() === "z" && e.shiftKey || e.key.toLowerCase() === "y") {
286
+ e.preventDefault();
287
+ redo();
288
+ }
289
+ }
290
+ if (e.key.toLowerCase() === "f") {
291
+ e.preventDefault();
292
+ fitToContainer();
293
+ }
294
+ if (e.key === "1") {
295
+ e.preventDefault();
296
+ resetZoom();
297
+ }
298
+ if (e.key.toLowerCase() === "a") {
299
+ e.preventDefault();
300
+ setAndEmitMode("add");
301
+ }
302
+ if (e.key.toLowerCase() === "m") {
303
+ e.preventDefault();
304
+ setAndEmitMode("move");
305
+ }
306
+ if (e.key.toLowerCase() === "e") {
307
+ e.preventDefault();
308
+ setAndEmitMode("erase");
309
+ }
310
+ };
311
+ const onKeyUp = (e) => {
312
+ if (e.code === "Space") spacePanningRef.current = false;
313
+ };
314
+ window.addEventListener("keydown", onKeyDown);
315
+ window.addEventListener("keyup", onKeyUp);
316
+ return () => {
317
+ window.removeEventListener("keydown", onKeyDown);
318
+ window.removeEventListener("keyup", onKeyUp);
319
+ };
320
+ }, [undo, redo, fitToContainer, resetZoom, setAndEmitMode]);
321
+ const onSvgClick = (e) => {
322
+ if (!size) return;
323
+ if (mode === "add") addPointAt(e.clientX, e.clientY);
324
+ };
325
+ const wantPan = React2__namespace.useCallback(
326
+ () => panEnabled && (mode === "pan" || spacePanningRef.current),
327
+ [panEnabled, mode]
328
+ );
329
+ const onBackgroundPointerDown = (e) => {
330
+ if (!wantPan()) return;
331
+ e.target.setPointerCapture(e.pointerId);
332
+ setDrag({ kind: "panning", lastX: e.clientX, lastY: e.clientY });
333
+ };
334
+ const onPointPointerDown = React2__namespace.useCallback((p) => (e) => {
335
+ if (mode === "erase") {
336
+ e.stopPropagation();
337
+ removePoint(p.id);
338
+ return;
339
+ }
340
+ if (mode !== "move") return;
341
+ e.target.setPointerCapture(e.pointerId);
342
+ setDrag({ kind: "point", id: p.id });
343
+ }, [mode, removePoint]);
344
+ const onPointerMove = (e) => {
345
+ if (!drag) return;
346
+ if (drag.kind === "point") {
347
+ const img = screenToImage(e.clientX, e.clientY);
348
+ if (!img || !size) return;
349
+ const { x, y } = img;
350
+ update({
351
+ points: pts.map(
352
+ (pp) => pp.id === drag.id ? { ...pp, x: clamp(x, 0, size.width), y: clamp(y, 0, size.height) } : pp
353
+ )
354
+ });
355
+ } else if (drag.kind === "panning") {
356
+ const dx = e.clientX - drag.lastX;
357
+ const dy = e.clientY - drag.lastY;
358
+ setTx((t) => t + dx);
359
+ setTy((t) => t + dy);
360
+ setDrag({ kind: "panning", lastX: e.clientX, lastY: e.clientY });
361
+ }
362
+ };
363
+ const onPointerUp = (e) => {
364
+ if (drag) {
365
+ e.target.releasePointerCapture?.(e.pointerId);
366
+ }
367
+ setDrag(null);
368
+ };
369
+ const onWheel = (e) => {
370
+ if (!zoomEnabled) return;
371
+ if (zoomOnCtrlWheel && !e.ctrlKey) return;
372
+ e.preventDefault();
373
+ const direction = e.deltaY < 0 ? 1 : -1;
374
+ const factor = direction > 0 ? zoomStep : 1 / zoomStep;
375
+ const newScale = clamp(scale * factor, minZoom, maxZoom);
376
+ const svg = svgRef.current;
377
+ const rect = svg.getBoundingClientRect();
378
+ const cx = e.clientX - rect.left;
379
+ const cy = e.clientY - rect.top;
380
+ const imgX = (cx - tx) / scale;
381
+ const imgY = (cy - ty) / scale;
382
+ setScale(newScale);
383
+ setTx(cx - imgX * newScale);
384
+ setTy(cy - imgY * newScale);
385
+ };
386
+ React2__namespace.useEffect(() => {
387
+ if (!size || !wrapperRef.current) return;
388
+ const id = requestAnimationFrame(() => fitToContainer());
389
+ return () => cancelAnimationFrame(id);
390
+ }, [size?.width, size?.height, fitToContainer]);
391
+ if (!size) {
392
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: wrapperRef, className, style: { position: "relative", ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { aspectRatio: "16/9", background: "#f6f8fa", width: "100%" } }) });
393
+ }
394
+ const { width, height } = size;
395
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrapperRef, className, style: { position: "relative", ...style }, children: [
396
+ showToolbar && /* @__PURE__ */ jsxRuntime.jsx(
397
+ Toolbar,
398
+ {
399
+ mode,
400
+ onModeChange: setAndEmitMode,
401
+ onFit: fitToContainer,
402
+ onReset: resetZoom,
403
+ onUndo: undo,
404
+ onRedo: redo,
405
+ canUndo: !isControlled && undoStack.length > 0,
406
+ canRedo: !isControlled && redoStack.length > 0
407
+ }
408
+ ),
409
+ /* @__PURE__ */ jsxRuntime.jsxs(
410
+ "svg",
411
+ {
412
+ ref: svgRef,
413
+ className: svgClassName,
414
+ style: { touchAction: "none", display: "block", width: "100%", height: "auto" },
415
+ viewBox: `0 0 ${width} ${height}`,
416
+ onClick: onSvgClick,
417
+ onPointerMove,
418
+ onPointerUp,
419
+ onWheel,
420
+ xmlns: "http://www.w3.org/2000/svg",
421
+ role: "img",
422
+ "aria-label": title,
423
+ children: [
424
+ title && /* @__PURE__ */ jsxRuntime.jsx("title", { children: title }),
425
+ desc && /* @__PURE__ */ jsxRuntime.jsx("desc", { children: desc }),
426
+ /* @__PURE__ */ jsxRuntime.jsx(
427
+ "rect",
428
+ {
429
+ x: 0,
430
+ y: 0,
431
+ width,
432
+ height,
433
+ fill: "transparent",
434
+ onPointerDown: onBackgroundPointerDown,
435
+ style: { cursor: wantPan() ? "grab" : mode === "add" ? "crosshair" : "default" }
436
+ }
437
+ ),
438
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `matrix(${scale},0,0,${scale},${tx},${ty})`, children: [
439
+ /* @__PURE__ */ jsxRuntime.jsx("image", { href: image.src, x: 0, y: 0, width, height, preserveAspectRatio: "none" }),
440
+ /* @__PURE__ */ jsxRuntime.jsx("g", { stroke: lineStroke, strokeWidth: lineWidth, strokeLinecap: "round", children: eds.map((e) => {
441
+ const a = pts.find((p) => p.id === e.from);
442
+ const b = pts.find((p) => p.id === e.to);
443
+ if (!a || !b) return null;
444
+ return /* @__PURE__ */ jsxRuntime.jsx("line", { x1: a.x, y1: a.y, x2: b.x, y2: b.y }, e.id);
445
+ }) }),
446
+ /* @__PURE__ */ jsxRuntime.jsx("g", { children: pts.map((p) => {
447
+ const active = hoverId === p.id;
448
+ const showDelete = mode === "erase" && active;
449
+ return /* @__PURE__ */ jsxRuntime.jsxs(
450
+ "g",
451
+ {
452
+ onPointerDown: onPointPointerDown(p),
453
+ onMouseEnter: () => setHoverId(p.id),
454
+ onMouseLeave: () => setHoverId((curr) => curr === p.id ? null : curr),
455
+ style: { cursor: mode === "move" ? "grab" : mode === "erase" ? "pointer" : "default" },
456
+ children: [
457
+ /* @__PURE__ */ jsxRuntime.jsx(
458
+ "circle",
459
+ {
460
+ cx: p.x,
461
+ cy: p.y,
462
+ r: active ? activePointRadius : pointRadius,
463
+ fill: active ? activePointFill : pointFill
464
+ }
465
+ ),
466
+ showDelete && /* @__PURE__ */ jsxRuntime.jsxs(
467
+ "g",
468
+ {
469
+ onClick: (e) => {
470
+ e.stopPropagation();
471
+ removePoint(p.id);
472
+ },
473
+ style: { cursor: "pointer" },
474
+ children: [
475
+ /* @__PURE__ */ jsxRuntime.jsx(
476
+ "rect",
477
+ {
478
+ x: p.x + 8,
479
+ y: p.y - 16,
480
+ width: 18,
481
+ height: 18,
482
+ rx: 3,
483
+ fill: "#fff",
484
+ stroke: "#d0d7de"
485
+ }
486
+ ),
487
+ /* @__PURE__ */ jsxRuntime.jsx(
488
+ "text",
489
+ {
490
+ x: p.x + 17,
491
+ y: p.y - 3,
492
+ textAnchor: "middle",
493
+ fontFamily: "system-ui",
494
+ fontSize: 12,
495
+ fill: "#cf222e",
496
+ children: "\xD7"
497
+ }
498
+ )
499
+ ]
500
+ }
501
+ )
502
+ ]
503
+ },
504
+ p.id
505
+ );
506
+ }) })
507
+ ] })
508
+ ]
509
+ }
510
+ )
511
+ ] });
512
+ }
513
+ function Toolbar(props) {
514
+ const Btn = ({ children, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx(
515
+ "button",
516
+ {
517
+ ...rest,
518
+ style: {
519
+ padding: "6px 10px",
520
+ borderRadius: 8,
521
+ border: "1px solid #d0d7de",
522
+ background: rest.disabled ? "#f6f8fa" : "#fff",
523
+ cursor: rest.disabled ? "not-allowed" : "pointer",
524
+ font: "600 12px system-ui",
525
+ opacity: rest.disabled ? 0.5 : 1,
526
+ ...rest.style
527
+ },
528
+ children
529
+ }
530
+ );
531
+ const Tab = ({ active, onClick, children }) => /* @__PURE__ */ jsxRuntime.jsx(
532
+ "button",
533
+ {
534
+ onClick,
535
+ style: {
536
+ padding: "6px 10px",
537
+ borderRadius: 8,
538
+ border: active ? "2px solid #1f6feb" : "1px solid #d0d7de",
539
+ background: active ? "#eff6ff" : "#fff",
540
+ font: "600 12px system-ui",
541
+ cursor: "pointer"
542
+ },
543
+ children
544
+ }
545
+ );
546
+ return /* @__PURE__ */ jsxRuntime.jsxs(
547
+ "div",
548
+ {
549
+ style: {
550
+ position: "absolute",
551
+ top: 8,
552
+ left: 8,
553
+ display: "flex",
554
+ gap: 8,
555
+ alignItems: "center",
556
+ background: "rgba(255,255,255,0.9)",
557
+ padding: 8,
558
+ border: "1px solid #d0d7de",
559
+ borderRadius: 12,
560
+ boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
561
+ zIndex: 10
562
+ },
563
+ children: [
564
+ /* @__PURE__ */ jsxRuntime.jsx(Tab, { active: props.mode === "pan", onClick: () => props.onModeChange("pan"), children: "Pan" }),
565
+ /* @__PURE__ */ jsxRuntime.jsx(Tab, { active: props.mode === "add", onClick: () => props.onModeChange("add"), children: "Add" }),
566
+ /* @__PURE__ */ jsxRuntime.jsx(Tab, { active: props.mode === "move", onClick: () => props.onModeChange("move"), children: "Move" }),
567
+ /* @__PURE__ */ jsxRuntime.jsx(Tab, { active: props.mode === "erase", onClick: () => props.onModeChange("erase"), children: "Erase" }),
568
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: 1, height: 22, background: "#d0d7de", margin: "0 6px" } }),
569
+ /* @__PURE__ */ jsxRuntime.jsx(Btn, { onClick: props.onFit, title: "Fit (F)", children: "Fit" }),
570
+ /* @__PURE__ */ jsxRuntime.jsx(Btn, { onClick: props.onReset, title: "1:1 (1)", children: "1:1" }),
571
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: 1, height: 22, background: "#d0d7de", margin: "0 6px" } }),
572
+ /* @__PURE__ */ jsxRuntime.jsx(Btn, { onClick: props.onUndo, disabled: !props.canUndo, title: "Undo (Ctrl/Cmd+Z)", children: "Undo" }),
573
+ /* @__PURE__ */ jsxRuntime.jsx(Btn, { onClick: props.onRedo, disabled: !props.canRedo, title: "Redo (Shift+Ctrl/Cmd+Z)", children: "Redo" })
574
+ ]
575
+ }
576
+ );
577
+ }
578
+
579
+ // src/utils/closest-segment-point.ts
580
+ var getClosestPointOnSegment = (px, py, x1, y1, x2, y2) => {
581
+ const dx = x2 - x1;
582
+ const dy = y2 - y1;
583
+ const lengthSquared = dx * dx + dy * dy;
584
+ if (lengthSquared === 0) return { x: x1, y: y1, t: 0 };
585
+ let t = ((px - x1) * dx + (py - y1) * dy) / lengthSquared;
586
+ t = Math.max(0, Math.min(1, t));
587
+ return {
588
+ x: x1 + t * dx,
589
+ y: y1 + t * dy,
590
+ t
591
+ };
592
+ };
593
+ var usePoints = (initialPoints) => {
594
+ const [points, setPoints] = React2.useState(
595
+ () => initialPoints.map((p, i) => ({ id: i + 1, x: p.x, y: p.y }))
596
+ );
597
+ return { points, setPoints };
598
+ };
599
+ var useZoom = (stage) => {
600
+ const MIN_SCALE = 0.5;
601
+ const MAX_SCALE = 5;
602
+ const DEFAULT_SCALE = 1;
603
+ const [stageScale, setStageScale] = React2.useState(DEFAULT_SCALE);
604
+ return {
605
+ stageScale,
606
+ setStageScale,
607
+ MIN_SCALE,
608
+ MAX_SCALE
609
+ };
610
+ };
611
+ var Container = styled__default.default.div`
612
+ overflow: hidden;
613
+ width:1200px;
614
+ height:1200px;
615
+ display: flex;
616
+ align-items: stretch;
617
+ border:1px solid #ccc;
618
+ border-radius:4px;
619
+ `;
620
+ var NavContainer = styled__default.default.div`
621
+ width:80px;
622
+ height:100%;
623
+ background:red;
624
+ flex-shrink:0;
625
+ display: flex;
626
+ justify-content: flex-start;
627
+ align-items: center;
628
+ flex-direction:column;
629
+ gap:20px;
630
+ padding:20px 0;
631
+ background:#ebebeb;
632
+ border-left:1px solid #ccc;
633
+ `;
634
+ var CanvasContainer = styled__default.default.div`
635
+ width:calc(100% - 80px);
636
+ height:100%;
637
+ overflow: hidden;
638
+ `;
639
+ var ButtonBase = styled__default.default.button`
640
+ padding:2px;
641
+ background:white;
642
+ border:none;
643
+ border-radius:8px;
644
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
645
+ cursor: pointer;
646
+ display: flex;
647
+ justify-content: center;
648
+ align-items: center;
649
+ font-size:12px;
650
+ min-width:50px;
651
+ min-height:50px;
652
+ border-left:1px solid #ccc;
653
+ &:disabled:{
654
+ opacity:0.5;
655
+ cursor:not-allowed;
656
+ }
657
+ `;
658
+ var Button = styled__default.default(ButtonBase)``;
659
+ var Mapper = ({
660
+ src = "https://konvajs.org/assets/line-building.png",
661
+ initialPoints = [],
662
+ maxWidth = 1200
663
+ }) => {
664
+ const stageRef = React2.useRef(null);
665
+ const { points, setPoints } = usePoints(initialPoints);
666
+ const { stageScale, setStageScale, MAX_SCALE, MIN_SCALE } = useZoom(stageRef?.current);
667
+ const SCALE_BY = 1.2;
668
+ const [closed, setClosed] = React2.useState(initialPoints.length >= 3);
669
+ const [hoveringFirst, setHoveringFirst] = React2.useState(false);
670
+ const [stagePosition, setStagePosition] = React2.useState({ x: 0, y: 0 });
671
+ const [dragStart, setDragStart] = React2.useState(null);
672
+ const [isDraggingPoint, setIsDraggingPoint] = React2.useState(false);
673
+ const [image] = useImage__default.default(src, "anonymous");
674
+ const imgW = image?.width || 1;
675
+ const imgH = image?.height || 1;
676
+ const scale = maxWidth / imgW;
677
+ const stageSize = { width: maxWidth, height: imgH * scale };
678
+ const DRAG_THRESHOLD = 5;
679
+ const LINE_HIT_THRESHOLD = 10;
680
+ const handleZoomIn = () => {
681
+ const stage = stageRef.current;
682
+ if (!stage) return;
683
+ const oldScale = stageScale;
684
+ const newScale = Math.min(oldScale * SCALE_BY, MAX_SCALE);
685
+ if (newScale === oldScale) return;
686
+ const centerX = stage.width() / 2;
687
+ const centerY = stage.height() / 2;
688
+ const pointTo = {
689
+ x: (centerX - stagePosition.x) / oldScale,
690
+ y: (centerY - stagePosition.y) / oldScale
691
+ };
692
+ const newPos = {
693
+ x: centerX - pointTo.x * newScale,
694
+ y: centerY - pointTo.y * newScale
695
+ };
696
+ const constrainedPos = getConstrainedPosition(newPos, newScale);
697
+ setStageScale(newScale);
698
+ setStagePosition(constrainedPos);
699
+ };
700
+ const handleZoomOut = () => {
701
+ const stage = stageRef.current;
702
+ if (!stage) return;
703
+ const oldScale = stageScale;
704
+ const newScale = Math.max(oldScale / SCALE_BY, MIN_SCALE);
705
+ if (newScale === oldScale) return;
706
+ const centerX = stage.width() / 2;
707
+ const centerY = stage.height() / 2;
708
+ const pointTo = {
709
+ x: (centerX - stagePosition.x) / oldScale,
710
+ y: (centerY - stagePosition.y) / oldScale
711
+ };
712
+ const newPos = {
713
+ x: centerX - pointTo.x * newScale,
714
+ y: centerY - pointTo.y * newScale
715
+ };
716
+ const constrainedPos = getConstrainedPosition(newPos, newScale);
717
+ setStageScale(newScale);
718
+ setStagePosition(constrainedPos);
719
+ };
720
+ const handleResetZoom = () => {
721
+ const newScale = 1;
722
+ const constrainedPos = getConstrainedPosition({ x: 0, y: 0 }, newScale);
723
+ setStageScale(newScale);
724
+ setStagePosition(constrainedPos);
725
+ };
726
+ const handleWheel = (e) => {
727
+ e.evt.preventDefault();
728
+ const stage = stageRef.current;
729
+ if (!stage) return;
730
+ const oldScale = stageScale;
731
+ const pointer = stage.getPointerPosition();
732
+ if (!pointer) return;
733
+ const mousePointTo = {
734
+ x: (pointer.x - stagePosition.x) / oldScale,
735
+ y: (pointer.y - stagePosition.y) / oldScale
736
+ };
737
+ const direction = e.evt.deltaY > 0 ? -1 : 1;
738
+ const newScale = direction > 0 ? Math.min(oldScale * SCALE_BY, MAX_SCALE) : Math.max(oldScale / SCALE_BY, MIN_SCALE);
739
+ if (newScale === oldScale) return;
740
+ const newPos = {
741
+ x: pointer.x - mousePointTo.x * newScale,
742
+ y: pointer.y - mousePointTo.y * newScale
743
+ };
744
+ const constrainedPos = getConstrainedPosition(newPos, newScale);
745
+ setStageScale(newScale);
746
+ setStagePosition(constrainedPos);
747
+ };
748
+ const getConstrainedPosition = (pos, currentScale) => {
749
+ const stage = stageRef.current;
750
+ if (!stage) return pos;
751
+ const stageWidth = stage.width();
752
+ const stageHeight = stage.height();
753
+ const imageWidth = stageSize.width * currentScale;
754
+ const imageHeight = stageSize.height * currentScale;
755
+ let newX = pos.x;
756
+ let newY = pos.y;
757
+ if (imageWidth > stageWidth) {
758
+ if (newX > 0) newX = 0;
759
+ if (newX < stageWidth - imageWidth) newX = stageWidth - imageWidth;
760
+ } else {
761
+ newX = (stageWidth - imageWidth) / 2;
762
+ }
763
+ if (imageHeight > stageHeight) {
764
+ if (newY > 0) newY = 0;
765
+ if (newY < stageHeight - imageHeight) newY = stageHeight - imageHeight;
766
+ } else {
767
+ newY = (stageHeight - imageHeight) / 2;
768
+ }
769
+ return { x: newX, y: newY };
770
+ };
771
+ const findLineSegmentNearClick = (clickX, clickY) => {
772
+ if (points.length < 2) return null;
773
+ const threshold = LINE_HIT_THRESHOLD / (scale * stageScale);
774
+ const segments = [];
775
+ for (let i = 0; i < points.length - 1; i++) {
776
+ segments.push({ start: i, end: i + 1 });
777
+ }
778
+ if (closed) {
779
+ segments.push({ start: points.length - 1, end: 0 });
780
+ }
781
+ for (const segment of segments) {
782
+ const p1 = points[segment.start];
783
+ const p2 = points[segment.end];
784
+ const closest = getClosestPointOnSegment(clickX, clickY, p1.x, p1.y, p2.x, p2.y);
785
+ const distance = Math.hypot(closest.x - clickX, closest.y - clickY);
786
+ if (distance < threshold) {
787
+ return {
788
+ segmentIndex: segment.start,
789
+ insertPosition: closest.t,
790
+ point: { x: closest.x, y: closest.y }
791
+ };
792
+ }
793
+ }
794
+ return null;
795
+ };
796
+ React2.useEffect(() => {
797
+ if (image) {
798
+ const initialPos = getConstrainedPosition({ x: 0, y: 0 }, 1);
799
+ setStagePosition(initialPos);
800
+ }
801
+ }, [image]);
802
+ React2.useEffect(() => {
803
+ const handleKeyDown = (e) => {
804
+ if (e.ctrlKey || e.metaKey) {
805
+ if (e.key === "=" || e.key === "+") {
806
+ e.preventDefault();
807
+ handleZoomIn();
808
+ } else if (e.key === "-") {
809
+ e.preventDefault();
810
+ handleZoomOut();
811
+ } else if (e.key === "0") {
812
+ e.preventDefault();
813
+ handleResetZoom();
814
+ }
815
+ }
816
+ };
817
+ window.addEventListener("keydown", handleKeyDown);
818
+ return () => window.removeEventListener("keydown", handleKeyDown);
819
+ }, [stageScale, stagePosition]);
820
+ const handleStageDragStart = (e) => {
821
+ const clickedShape = e.target;
822
+ if (clickedShape && clickedShape.name() === "point") {
823
+ e.target.stopDrag();
824
+ return;
825
+ }
826
+ const stage = e.target.getStage();
827
+ if (!stage) return;
828
+ const pos = stage.getPointerPosition();
829
+ if (pos) {
830
+ setDragStart(pos);
831
+ }
832
+ };
833
+ const handleStageDragEnd = (e) => {
834
+ const newPos = {
835
+ x: e.target.x(),
836
+ y: e.target.y()
837
+ };
838
+ const constrainedPos = getConstrainedPosition(newPos, stageScale);
839
+ setStagePosition(constrainedPos);
840
+ setDragStart(null);
841
+ };
842
+ const toImageCoords = (stage) => {
843
+ if (!stage) return { x: 0, y: 0 };
844
+ const pos = stage.getPointerPosition();
845
+ if (!pos) return { x: 0, y: 0 };
846
+ const x = (pos.x - stagePosition.x) / (scale * stageScale);
847
+ const y = (pos.y - stagePosition.y) / (scale * stageScale);
848
+ return { x, y };
849
+ };
850
+ const linePointsScaled = React2.useMemo(() => {
851
+ const flat = points.flatMap((p) => [p.x * scale, p.y * scale]);
852
+ return flat;
853
+ }, [points, scale]);
854
+ const commit = (nextPoints) => {
855
+ setPoints(nextPoints);
856
+ };
857
+ const handlePointDragStart = (e) => {
858
+ e.cancelBubble = true;
859
+ setIsDraggingPoint(true);
860
+ };
861
+ const handlePointDragMove = (id, e) => {
862
+ const newX = e.target.x() / scale;
863
+ const newY = e.target.y() / scale;
864
+ const updatedPoints = points.map(
865
+ (p) => p.id === id ? { ...p, x: newX, y: newY } : p
866
+ );
867
+ setPoints(updatedPoints);
868
+ };
869
+ const handlePointDragEnd = (e) => {
870
+ e.cancelBubble = true;
871
+ setIsDraggingPoint(false);
872
+ };
873
+ const handleStageMouseDown = (e) => {
874
+ if (!image) return;
875
+ const clickedShape = e.target;
876
+ if (clickedShape && clickedShape.name() === "point") {
877
+ if (!closed) {
878
+ const clickedIndex = points.findIndex(
879
+ (p) => Math.abs(p.x * scale - clickedShape.x()) < 1 && Math.abs(p.y * scale - clickedShape.y()) < 1
880
+ );
881
+ if (clickedIndex === 0 && points.length >= 3) {
882
+ setClosed(true);
883
+ setHoveringFirst(false);
884
+ }
885
+ }
886
+ return;
887
+ }
888
+ const stage = e.target.getStage();
889
+ if (!stage) return;
890
+ const pos = stage.getPointerPosition();
891
+ if (pos) {
892
+ setDragStart(pos);
893
+ }
894
+ };
895
+ const handleStageMouseUp = (e) => {
896
+ if (!image || isDraggingPoint) return;
897
+ const clickedShape = e.target;
898
+ if (clickedShape && clickedShape.name() === "point") {
899
+ return;
900
+ }
901
+ if (dragStart) {
902
+ const stage = e.target.getStage();
903
+ if (!stage) return;
904
+ const currentPos = stage.getPointerPosition();
905
+ if (currentPos) {
906
+ const dx = currentPos.x - dragStart.x;
907
+ const dy = currentPos.y - dragStart.y;
908
+ const distance = Math.sqrt(dx * dx + dy * dy);
909
+ if (distance < DRAG_THRESHOLD) {
910
+ const p = toImageCoords(stage);
911
+ const lineHit = findLineSegmentNearClick(p.x, p.y);
912
+ if (lineHit) {
913
+ const newPoint = {
914
+ id: Date.now(),
915
+ x: lineHit.point.x,
916
+ y: lineHit.point.y
917
+ };
918
+ const newPoints = [...points];
919
+ newPoints.splice(lineHit.segmentIndex + 1, 0, newPoint);
920
+ commit(newPoints);
921
+ } else if (!closed) {
922
+ const next = [...points, { id: Date.now(), x: p.x, y: p.y }];
923
+ commit(next);
924
+ }
925
+ }
926
+ }
927
+ setDragStart(null);
928
+ }
929
+ };
930
+ const handleMouseMove = (e) => {
931
+ if (closed || points.length < 3) {
932
+ setHoveringFirst(false);
933
+ return;
934
+ }
935
+ const stage = e.target.getStage();
936
+ const p = toImageCoords(stage);
937
+ const fp = points[0];
938
+ const dx = (p.x - fp.x) * scale * stageScale;
939
+ const dy = (p.y - fp.y) * scale * stageScale;
940
+ const dist = Math.hypot(dx, dy);
941
+ setHoveringFirst(dist < 15);
942
+ };
943
+ return /* @__PURE__ */ jsxRuntime.jsxs(Container, { children: [
944
+ /* @__PURE__ */ jsxRuntime.jsx(CanvasContainer, { children: /* @__PURE__ */ jsxRuntime.jsx(
945
+ reactKonva.Stage,
946
+ {
947
+ ref: stageRef,
948
+ width: 1200,
949
+ height: 1200,
950
+ onMouseDown: handleStageMouseDown,
951
+ onMouseUp: handleStageMouseUp,
952
+ onMouseMove: handleMouseMove,
953
+ onWheel: handleWheel,
954
+ scaleX: stageScale,
955
+ scaleY: stageScale,
956
+ x: stagePosition.x,
957
+ y: stagePosition.y,
958
+ draggable: !isDraggingPoint,
959
+ onDragStart: handleStageDragStart,
960
+ onDragEnd: handleStageDragEnd,
961
+ dragBoundFunc: (pos) => getConstrainedPosition(pos, stageScale),
962
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactKonva.Layer, { children: [
963
+ /* @__PURE__ */ jsxRuntime.jsx(
964
+ reactKonva.Image,
965
+ {
966
+ image,
967
+ width: stageSize.width,
968
+ height: stageSize.height,
969
+ listening: false
970
+ }
971
+ ),
972
+ points.length >= 2 && /* @__PURE__ */ jsxRuntime.jsx(
973
+ reactKonva.Line,
974
+ {
975
+ points: linePointsScaled,
976
+ closed,
977
+ stroke: "#2563eb",
978
+ strokeWidth: 2 / stageScale,
979
+ lineCap: "round",
980
+ lineJoin: "round",
981
+ tension: 0,
982
+ fill: closed ? "rgba(37, 99, 235, 0.3)" : void 0,
983
+ hitStrokeWidth: LINE_HIT_THRESHOLD / stageScale
984
+ }
985
+ ),
986
+ /* @__PURE__ */ jsxRuntime.jsx(reactKonva.Group, { children: points.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
987
+ reactKonva.Circle,
988
+ {
989
+ name: "point",
990
+ x: p.x * scale,
991
+ y: p.y * scale,
992
+ radius: (i === 0 && hoveringFirst ? 10 : 6) / stageScale,
993
+ fill: i === 0 ? "#ef4444" : "#7e9de1",
994
+ stroke: i === 0 && hoveringFirst ? "#fbbf24" : void 0,
995
+ strokeWidth: i === 0 && hoveringFirst ? 2 / stageScale : 0,
996
+ draggable: true,
997
+ onDragStart: handlePointDragStart,
998
+ onDragMove: (e) => handlePointDragMove(p.id, e),
999
+ onDragEnd: handlePointDragEnd
1000
+ },
1001
+ p.id
1002
+ )) })
1003
+ ] })
1004
+ }
1005
+ ) }),
1006
+ /* @__PURE__ */ jsxRuntime.jsxs(NavContainer, { children: [
1007
+ /* @__PURE__ */ jsxRuntime.jsx(
1008
+ Button,
1009
+ {
1010
+ onClick: handleZoomIn,
1011
+ disabled: stageScale >= MAX_SCALE,
1012
+ title: "Zoom In (Ctrl/Cmd + +)",
1013
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZoomIn, { style: { width: "20px", height: "20px" } })
1014
+ }
1015
+ ),
1016
+ /* @__PURE__ */ jsxRuntime.jsx(
1017
+ Button,
1018
+ {
1019
+ onClick: handleZoomOut,
1020
+ disabled: stageScale <= MIN_SCALE,
1021
+ title: "Zoom Out (Ctrl/Cmd + -)",
1022
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZoomOut, { style: { width: "20px", height: "20px" } })
1023
+ }
1024
+ ),
1025
+ /* @__PURE__ */ jsxRuntime.jsxs(
1026
+ Button,
1027
+ {
1028
+ onClick: handleResetZoom,
1029
+ title: "Reset Zoom (Ctrl/Cmd + 0)",
1030
+ children: [
1031
+ Math.round(stageScale * 100),
1032
+ "%"
1033
+ ]
1034
+ }
1035
+ )
1036
+ ] })
1037
+ ] });
1038
+ };
1039
+
1040
+ exports.FloorMapDrawer = FloorMapDrawer;
1041
+ exports.ImageWithPoints = ImageWithPoints;
1042
+ exports.Mapper = Mapper;
1043
+ //# sourceMappingURL=index.js.map
1044
+ //# sourceMappingURL=index.js.map