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