neogestify-ui-components 1.2.21 → 2.0.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.
Files changed (52) hide show
  1. package/README.md +393 -2
  2. package/dist/components/VenueMapEditor/index.d.mts +202 -0
  3. package/dist/components/VenueMapEditor/index.d.ts +202 -0
  4. package/dist/components/VenueMapEditor/index.js +2684 -0
  5. package/dist/components/VenueMapEditor/index.js.map +1 -0
  6. package/dist/components/VenueMapEditor/index.mjs +2676 -0
  7. package/dist/components/VenueMapEditor/index.mjs.map +1 -0
  8. package/dist/components/alerts/index.js.map +1 -1
  9. package/dist/components/alerts/index.mjs.map +1 -1
  10. package/dist/components/html/index.d.mts +2 -0
  11. package/dist/components/html/index.d.ts +2 -0
  12. package/dist/components/html/index.js +24 -58
  13. package/dist/components/html/index.js.map +1 -1
  14. package/dist/components/html/index.mjs +24 -58
  15. package/dist/components/html/index.mjs.map +1 -1
  16. package/dist/components/icons/index.d.mts +18 -2
  17. package/dist/components/icons/index.d.ts +18 -2
  18. package/dist/components/icons/index.js +97 -11
  19. package/dist/components/icons/index.js.map +1 -1
  20. package/dist/components/icons/index.mjs +82 -12
  21. package/dist/components/icons/index.mjs.map +1 -1
  22. package/dist/context/theme/index.js.map +1 -1
  23. package/dist/context/theme/index.mjs.map +1 -1
  24. package/dist/index.d.mts +2 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.js +2734 -69
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +2713 -71
  29. package/dist/index.mjs.map +1 -1
  30. package/package.json +9 -4
  31. package/src/components/VenueMapEditor/VenueMapEditor.tsx +851 -0
  32. package/src/components/VenueMapEditor/VenueMapViewer.tsx +13 -0
  33. package/src/components/VenueMapEditor/components/Artboard.tsx +405 -0
  34. package/src/components/VenueMapEditor/components/EditorCanvas.tsx +472 -0
  35. package/src/components/VenueMapEditor/components/ElementNode.tsx +357 -0
  36. package/src/components/VenueMapEditor/components/FloorTabs.tsx +137 -0
  37. package/src/components/VenueMapEditor/components/GridOverlay.tsx +67 -0
  38. package/src/components/VenueMapEditor/components/PropertiesPanel.tsx +198 -0
  39. package/src/components/VenueMapEditor/components/Toolbar.tsx +254 -0
  40. package/src/components/VenueMapEditor/components/WallLayer.tsx +117 -0
  41. package/src/components/VenueMapEditor/hooks/useDrag.ts +79 -0
  42. package/src/components/VenueMapEditor/hooks/useHistory.ts +74 -0
  43. package/src/components/VenueMapEditor/hooks/usePanZoom.ts +114 -0
  44. package/src/components/VenueMapEditor/hooks/useSelection.ts +42 -0
  45. package/src/components/VenueMapEditor/index.ts +34 -0
  46. package/src/components/VenueMapEditor/types.ts +173 -0
  47. package/src/components/VenueMapEditor/utils/idGen.ts +2 -0
  48. package/src/components/VenueMapEditor/utils/snapUtils.ts +38 -0
  49. package/src/components/VenueMapEditor/utils/wallGeometry.ts +83 -0
  50. package/src/components/html/Input.tsx +48 -80
  51. package/src/components/icons/icons.tsx +153 -14
  52. package/src/index.ts +1 -0
@@ -0,0 +1,2684 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/components/VenueMapEditor/VenueMapEditor.tsx
7
+ function IconCursor({ className }) {
8
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 1l12 5.5-5.5 1.5L7 13.5 2 1z" }) });
9
+ }
10
+ function IconHand({ className }) {
11
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: [
12
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 1a1 1 0 011 1v4.586l1.293-1.293a1 1 0 111.414 1.414L8 10.414 4.293 6.707a1 1 0 111.414-1.414L7 6.586V2a1 1 0 011-1z" }),
13
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 8a1 1 0 011-1h.5V4.5a1 1 0 012 0V7h1V3.5a1 1 0 012 0V7h1V4.5a1 1 0 012 0V9a5 5 0 01-5 5H6A3 3 0 013 11V8z" })
14
+ ] });
15
+ }
16
+ function IconGrid({ className }) {
17
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx(
18
+ "path",
19
+ {
20
+ fillRule: "evenodd",
21
+ d: "M1 1h6v6H1V1zm8 0h6v6H9V1zM1 9h6v6H1V9zm8 0h6v6H9V9z",
22
+ clipRule: "evenodd",
23
+ opacity: 0.7
24
+ }
25
+ ) });
26
+ }
27
+ function IconZoomIn({ className }) {
28
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.5 1a5.5 5.5 0 104.39 8.803l3.154 3.153a.75.75 0 001.06-1.06l-3.153-3.154A5.5 5.5 0 006.5 1zM2.5 6.5a4 4 0 118 0 4 4 0 01-8 0zM6 4.75a.75.75 0 011.5 0V6h1.25a.75.75 0 010 1.5H7.5v1.25a.75.75 0 01-1.5 0V7.5H4.75a.75.75 0 010-1.5H6V4.75z" }) });
29
+ }
30
+ function IconZoomOut({ className }) {
31
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.5 1a5.5 5.5 0 104.39 8.803l3.154 3.153a.75.75 0 001.06-1.06l-3.153-3.154A5.5 5.5 0 006.5 1zM2.5 6.5a4 4 0 118 0 4 4 0 01-8 0zM4.75 6a.75.75 0 000 1.5h3.5a.75.75 0 000-1.5h-3.5z" }) });
32
+ }
33
+ function IconReset({ className }) {
34
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 1a7 7 0 100 14A7 7 0 008 1zm0 1.5a5.5 5.5 0 110 11 5.5 5.5 0 010-11zM8 4a.75.75 0 01.75.75v3.19l1.28 1.28a.75.75 0 01-1.06 1.06l-1.5-1.5A.75.75 0 017.25 8V4.75A.75.75 0 018 4z" }) });
35
+ }
36
+ function IconUndo({ className }) {
37
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2.5 5.5A.5.5 0 013 5h5a5 5 0 110 10H3a.5.5 0 010-1h5a4 4 0 100-8H3.707l1.647 1.646a.5.5 0 01-.708.708l-2.5-2.5a.5.5 0 010-.708l2.5-2.5a.5.5 0 01.708.708L3.207 5H3a.5.5 0 01-.5-.5z" }) });
38
+ }
39
+ function IconRedo({ className }) {
40
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M13.5 5.5A.5.5 0 0113 5H8a4 4 0 100 8h5a.5.5 0 010 1H8A5 5 0 118 5h4.293l-1.647-1.646a.5.5 0 01.708-.708l2.5 2.5a.5.5 0 010 .708l-2.5 2.5a.5.5 0 01-.708-.708L12.793 6H13a.5.5 0 01.5.5z" }) });
41
+ }
42
+ function IconPlace({ className }) {
43
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 2a.5.5 0 01.5-.5h2a.5.5 0 010 1H3v1.5a.5.5 0 01-1 0V2zm11 0a.5.5 0 00-.5-.5h-2a.5.5 0 000 1H12v1.5a.5.5 0 001 0V2zM2 14a.5.5 0 00.5.5h2a.5.5 0 000-1H3v-1.5a.5.5 0 00-1 0V14zm11 0a.5.5 0 01-.5.5h-2a.5.5 0 010-1H12v-1.5a.5.5 0 011 0V14zM8 4.5a.5.5 0 000 1V7H6.5a.5.5 0 000 1H8v1.5a.5.5 0 001 0V8h1.5a.5.5 0 000-1H9V5.5a.5.5 0 00-1 0z" }) });
44
+ }
45
+ function IconErase({ className }) {
46
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8.086 2.207a2 2 0 012.828 0l2.879 2.878a2 2 0 010 2.83l-7.513 7.51A2 2 0 014.872 16H2.4a1 1 0 01-.966-.741L.8 13.2a2 2 0 01.5-1.946l7.786-9.047zM7.586 5L5 7.586 8.414 11 11 8.414 7.586 5zM6 12L4 10l-1.5 1.5a1 1 0 000 1.414l.587.587A1 1 0 003.793 15H5l1-1-1-1 1-1z" }) });
47
+ }
48
+ function IconWall({ className }) {
49
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 16 16", className, fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", children: [
50
+ /* @__PURE__ */ jsxRuntime.jsx("path", { strokeWidth: "2", d: "M3 14 L3 2 L14 2" }),
51
+ /* @__PURE__ */ jsxRuntime.jsx("path", { strokeWidth: "2", d: "M6 14 L6 5 L14 5" })
52
+ ] });
53
+ }
54
+ function IconDownload({ className }) {
55
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: [
56
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M.5 9.9a.5.5 0 01.5.5v2.5a1 1 0 001 1h12a1 1 0 001-1v-2.5a.5.5 0 011 0v2.5a2 2 0 01-2 2H2a2 2 0 01-2-2v-2.5a.5.5 0 01.5-.5z" }),
57
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.646 11.854a.5.5 0 00.708 0l3-3a.5.5 0 00-.708-.708L8.5 10.293V1.5a.5.5 0 00-1 0v8.793L5.354 8.146a.5.5 0 10-.708.708l3 3z" })
58
+ ] });
59
+ }
60
+ function IconUpload({ className }) {
61
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: [
62
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M.5 9.9a.5.5 0 01.5.5v2.5a1 1 0 001 1h12a1 1 0 001-1v-2.5a.5.5 0 011 0v2.5a2 2 0 01-2 2H2a2 2 0 01-2-2v-2.5a.5.5 0 01.5-.5z" }),
63
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.646 1.146a.5.5 0 01.708 0l3 3a.5.5 0 01-.708.708L8.5 2.707V11.5a.5.5 0 01-1 0V2.707L5.354 4.854a.5.5 0 11-.708-.708l3-3z" })
64
+ ] });
65
+ }
66
+ function IconLayers({ className }) {
67
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", className, fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8.235 1.559a.5.5 0 0 0-.47 0l-7.5 4a.5.5 0 0 0 0 .882L3.188 8 .265 9.559a.5.5 0 0 0 0 .882l7.5 4a.5.5 0 0 0 .47 0l7.5-4a.5.5 0 0 0 0-.882L12.813 8l2.922-1.559a.5.5 0 0 0 0-.882l-7.5-4zm3.515 7.008L14.438 10 8 13.433 1.562 10 4.25 8.567l3.515 1.874a.5.5 0 0 0 .47 0l3.515-1.874zM8 9.433 1.562 6 8 2.567 14.438 6 8 9.433z" }) });
68
+ }
69
+ function ToolButton({ active, disabled, title, onClick, children }) {
70
+ return /* @__PURE__ */ jsxRuntime.jsx(
71
+ "button",
72
+ {
73
+ title,
74
+ onClick,
75
+ disabled,
76
+ className: [
77
+ "flex items-center justify-center w-8 h-8 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed",
78
+ active ? "bg-blue-100 text-blue-700 ring-1 ring-blue-400" : "text-slate-600 hover:bg-slate-100 hover:text-slate-800"
79
+ ].join(" "),
80
+ children
81
+ }
82
+ );
83
+ }
84
+ function Sep() {
85
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px h-6 bg-slate-200 mx-1" });
86
+ }
87
+ function TypeChip({ typeDef, active, onClick }) {
88
+ return /* @__PURE__ */ jsxRuntime.jsxs(
89
+ "button",
90
+ {
91
+ title: typeDef.label,
92
+ onClick,
93
+ className: [
94
+ "flex items-center gap-1.5 px-2 py-1 rounded border text-xs whitespace-nowrap transition-colors",
95
+ active ? "border-blue-400 bg-blue-50 text-blue-700 font-medium" : "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50"
96
+ ].join(" "),
97
+ children: [
98
+ /* @__PURE__ */ jsxRuntime.jsx(
99
+ "span",
100
+ {
101
+ className: "w-2.5 h-2.5 rounded-sm shrink-0",
102
+ style: { background: typeDef.color, border: `1px solid ${typeDef.strokeColor}` }
103
+ }
104
+ ),
105
+ typeDef.label
106
+ ]
107
+ }
108
+ );
109
+ }
110
+ function Toolbar({
111
+ tool,
112
+ onToolChange,
113
+ showGrid,
114
+ onToggleGrid,
115
+ zoom,
116
+ onZoomIn,
117
+ onZoomOut,
118
+ onResetView,
119
+ canUndo,
120
+ canRedo,
121
+ onUndo,
122
+ onRedo,
123
+ paletteGroups,
124
+ activePlaceTypeId,
125
+ onActivePlaceTypeChange,
126
+ areaShape,
127
+ onToggleAreaShape,
128
+ onExportMap,
129
+ onImportMap,
130
+ onLoadLibrary,
131
+ onRemoveLibraryGroup
132
+ }) {
133
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col bg-white border-b border-slate-200 shadow-sm shrink-0", children: [
134
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 px-2 py-1.5", children: [
135
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Seleccionar (V)", active: tool === "SELECT", onClick: () => onToolChange("SELECT"), children: /* @__PURE__ */ jsxRuntime.jsx(IconCursor, { className: "w-4 h-4" }) }),
136
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Desplazar (H)", active: tool === "PAN", onClick: () => onToolChange("PAN"), children: /* @__PURE__ */ jsxRuntime.jsx(IconHand, { className: "w-4 h-4" }) }),
137
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Dibujar pared (W)", active: tool === "WALL", onClick: () => onToolChange("WALL"), children: /* @__PURE__ */ jsxRuntime.jsx(IconWall, { className: "w-4 h-4" }) }),
138
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Colocar elemento (P)", active: tool === "PLACE", onClick: () => onToolChange("PLACE"), children: /* @__PURE__ */ jsxRuntime.jsx(IconPlace, { className: "w-4 h-4" }) }),
139
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Borrar (E)", active: tool === "ERASE", onClick: () => onToolChange("ERASE"), children: /* @__PURE__ */ jsxRuntime.jsx(IconErase, { className: "w-4 h-4" }) }),
140
+ /* @__PURE__ */ jsxRuntime.jsx(Sep, {}),
141
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Deshacer (Ctrl+Z)", disabled: !canUndo, onClick: onUndo, children: /* @__PURE__ */ jsxRuntime.jsx(IconUndo, { className: "w-4 h-4" }) }),
142
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Rehacer (Ctrl+Y)", disabled: !canRedo, onClick: onRedo, children: /* @__PURE__ */ jsxRuntime.jsx(IconRedo, { className: "w-4 h-4" }) }),
143
+ /* @__PURE__ */ jsxRuntime.jsx(Sep, {}),
144
+ /* @__PURE__ */ jsxRuntime.jsx(
145
+ ToolButton,
146
+ {
147
+ title: showGrid ? "Ocultar cuadr\xEDcula" : "Mostrar cuadr\xEDcula",
148
+ active: showGrid,
149
+ onClick: onToggleGrid,
150
+ children: /* @__PURE__ */ jsxRuntime.jsx(IconGrid, { className: "w-4 h-4" })
151
+ }
152
+ ),
153
+ /* @__PURE__ */ jsxRuntime.jsx(Sep, {}),
154
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Acercar (+)", onClick: onZoomIn, children: /* @__PURE__ */ jsxRuntime.jsx(IconZoomIn, { className: "w-4 h-4" }) }),
155
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-slate-500 w-10 text-center tabular-nums select-none", children: [
156
+ Math.round(zoom * 100),
157
+ "%"
158
+ ] }),
159
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Alejar (-)", onClick: onZoomOut, children: /* @__PURE__ */ jsxRuntime.jsx(IconZoomOut, { className: "w-4 h-4" }) }),
160
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Restablecer vista", onClick: onResetView, children: /* @__PURE__ */ jsxRuntime.jsx(IconReset, { className: "w-4 h-4" }) }),
161
+ /* @__PURE__ */ jsxRuntime.jsx(Sep, {}),
162
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Exportar mapa JSON", onClick: () => onExportMap?.(), children: /* @__PURE__ */ jsxRuntime.jsx(IconDownload, { className: "w-4 h-4" }) }),
163
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Importar mapa JSON", onClick: () => onImportMap?.(), children: /* @__PURE__ */ jsxRuntime.jsx(IconUpload, { className: "w-4 h-4" }) }),
164
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: "Cargar librer\xEDa de elementos (.json)", onClick: () => onLoadLibrary?.(), children: /* @__PURE__ */ jsxRuntime.jsx(IconLayers, { className: "w-4 h-4" }) }),
165
+ areaShape !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
166
+ /* @__PURE__ */ jsxRuntime.jsx(Sep, {}),
167
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { title: areaShape === "polygon" ? "Cambiar a rect\xE1ngulo" : "Cambiar a pol\xEDgono", onClick: () => onToggleAreaShape?.(), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium", children: areaShape === "polygon" ? "Poly" : "Rect" }) })
168
+ ] })
169
+ ] }),
170
+ tool === "PLACE" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-stretch gap-0 border-t border-slate-100 bg-slate-50 overflow-x-auto", children: paletteGroups.map((group, gi) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center shrink-0", children: [
171
+ gi > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-px self-stretch bg-slate-200 mx-1" }),
172
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 px-1.5 shrink-0", children: [
173
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-slate-400 font-medium whitespace-nowrap select-none", children: group.name }),
174
+ !group.isBase && onRemoveLibraryGroup && /* @__PURE__ */ jsxRuntime.jsx(
175
+ "button",
176
+ {
177
+ title: `Eliminar grupo "${group.name}"`,
178
+ onClick: () => onRemoveLibraryGroup(group.id),
179
+ className: "text-slate-300 hover:text-red-400 leading-none text-xs ml-0.5 transition-colors",
180
+ children: "\xD7"
181
+ }
182
+ )
183
+ ] }),
184
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1 px-1 py-1.5", children: group.types.map((typeDef) => /* @__PURE__ */ jsxRuntime.jsx(
185
+ TypeChip,
186
+ {
187
+ typeDef,
188
+ active: activePlaceTypeId === typeDef.id,
189
+ onClick: () => onActivePlaceTypeChange(typeDef.id)
190
+ },
191
+ typeDef.id
192
+ )) })
193
+ ] }, group.id)) })
194
+ ] });
195
+ }
196
+ var ZOOM_MIN = 0.1;
197
+ var ZOOM_MAX = 10;
198
+ var ZOOM_FACTOR = 1.1;
199
+ function usePanZoom(initialZoom = 1, leftClickPan = false) {
200
+ const [state, setState] = react.useState({
201
+ panX: 80,
202
+ panY: 80,
203
+ zoom: initialZoom
204
+ });
205
+ const isPanningRef = react.useRef(false);
206
+ const [isPanning, setIsPanning] = react.useState(false);
207
+ const lastPosRef = react.useRef({ x: 0, y: 0 });
208
+ const handleWheel = react.useCallback((e) => {
209
+ const factor = e.deltaY < 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;
210
+ const svgEl = e.currentTarget;
211
+ const rect = svgEl.getBoundingClientRect();
212
+ const mouseX = e.clientX - rect.left;
213
+ const mouseY = e.clientY - rect.top;
214
+ setState((prev) => {
215
+ const newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, prev.zoom * factor));
216
+ const canvasX = (mouseX - prev.panX) / prev.zoom;
217
+ const canvasY = (mouseY - prev.panY) / prev.zoom;
218
+ return {
219
+ panX: mouseX - canvasX * newZoom,
220
+ panY: mouseY - canvasY * newZoom,
221
+ zoom: newZoom
222
+ };
223
+ });
224
+ }, []);
225
+ const handleMouseDown = react.useCallback((e) => {
226
+ const valid = leftClickPan ? e.button === 0 || e.button === 1 : e.button === 1;
227
+ if (!valid) return;
228
+ e.preventDefault();
229
+ isPanningRef.current = true;
230
+ setIsPanning(true);
231
+ lastPosRef.current = { x: e.clientX, y: e.clientY };
232
+ }, [leftClickPan]);
233
+ const handleMouseMove = react.useCallback((e) => {
234
+ if (!isPanningRef.current) return;
235
+ const dx = e.clientX - lastPosRef.current.x;
236
+ const dy = e.clientY - lastPosRef.current.y;
237
+ lastPosRef.current = { x: e.clientX, y: e.clientY };
238
+ setState((prev) => ({ ...prev, panX: prev.panX + dx, panY: prev.panY + dy }));
239
+ }, []);
240
+ const stopPan = react.useCallback((_e) => {
241
+ if (!isPanningRef.current) return;
242
+ isPanningRef.current = false;
243
+ setIsPanning(false);
244
+ }, []);
245
+ const handleMouseLeave = react.useCallback(() => {
246
+ if (isPanningRef.current) {
247
+ isPanningRef.current = false;
248
+ setIsPanning(false);
249
+ }
250
+ }, []);
251
+ const zoomBy = react.useCallback((factor, cx, cy) => {
252
+ setState((prev) => {
253
+ const newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, prev.zoom * factor));
254
+ if (cx !== void 0 && cy !== void 0) {
255
+ const canvasX = (cx - prev.panX) / prev.zoom;
256
+ const canvasY = (cy - prev.panY) / prev.zoom;
257
+ return { panX: cx - canvasX * newZoom, panY: cy - canvasY * newZoom, zoom: newZoom };
258
+ }
259
+ return { ...prev, zoom: newZoom };
260
+ });
261
+ }, []);
262
+ const resetView = react.useCallback(() => {
263
+ setState({ panX: 80, panY: 80, zoom: 1 });
264
+ }, []);
265
+ return {
266
+ state,
267
+ setState,
268
+ isPanning,
269
+ handleWheel,
270
+ handleMouseDown,
271
+ handleMouseMove,
272
+ handleMouseUp: stopPan,
273
+ handleMouseLeave,
274
+ zoomBy,
275
+ resetView
276
+ };
277
+ }
278
+
279
+ // src/components/VenueMapEditor/utils/snapUtils.ts
280
+ var snapToGrid = (value, gridSize) => Math.round(value / gridSize) * gridSize;
281
+ var snapPoint = (x, y, gridSize, enabled) => ({
282
+ x: enabled ? snapToGrid(x, gridSize) : x,
283
+ y: enabled ? snapToGrid(y, gridSize) : y
284
+ });
285
+ var findNearestNode = (x, y, nodes, threshold) => {
286
+ let best = null;
287
+ let bestDist = threshold;
288
+ for (const node of nodes) {
289
+ const dist = Math.hypot(node.x - x, node.y - y);
290
+ if (dist < bestDist) {
291
+ bestDist = dist;
292
+ best = node;
293
+ }
294
+ }
295
+ return best;
296
+ };
297
+
298
+ // src/components/VenueMapEditor/utils/wallGeometry.ts
299
+ function norm(v) {
300
+ const len = Math.hypot(v.x, v.y);
301
+ return len < 1e-10 ? { x: 1, y: 0 } : { x: v.x / len, y: v.y / len };
302
+ }
303
+ function perp(v) {
304
+ return { x: -v.y, y: v.x };
305
+ }
306
+ function add(a, b) {
307
+ return { x: a.x + b.x, y: a.y + b.y };
308
+ }
309
+ function scale(v, s) {
310
+ return { x: v.x * s, y: v.y * s };
311
+ }
312
+ function lineIntersect(p1, d1, p2, d2) {
313
+ const det = d1.x * d2.y - d1.y * d2.x;
314
+ if (Math.abs(det) < 1e-8) return null;
315
+ const dx = p2.x - p1.x, dy = p2.y - p1.y;
316
+ const s = (dx * d2.y - dy * d2.x) / det;
317
+ return { x: p1.x + s * d1.x, y: p1.y + s * d1.y };
318
+ }
319
+ function wallSegmentPath(ax, ay, bx, by, thickness, adjDirAtA, adjDirAtB) {
320
+ const dir = norm({ x: bx - ax, y: by - ay });
321
+ const n = perp(dir);
322
+ const h = thickness / 2;
323
+ const A = { x: ax, y: ay };
324
+ const B = { x: bx, y: by };
325
+ let lA = add(A, scale(n, h));
326
+ let rA = add(A, scale(n, -h));
327
+ let lB = add(B, scale(n, h));
328
+ let rB = add(B, scale(n, -h));
329
+ if (adjDirAtA) {
330
+ const n2 = perp(adjDirAtA);
331
+ const mL = lineIntersect(add(A, scale(n, h)), dir, add(A, scale(n2, h)), adjDirAtA);
332
+ const mR = lineIntersect(add(A, scale(n, -h)), dir, add(A, scale(n2, -h)), adjDirAtA);
333
+ if (mL) lA = mL;
334
+ if (mR) rA = mR;
335
+ }
336
+ if (adjDirAtB) {
337
+ const n2 = perp(adjDirAtB);
338
+ const mL = lineIntersect(add(B, scale(n, h)), dir, add(B, scale(n2, h)), adjDirAtB);
339
+ const mR = lineIntersect(add(B, scale(n, -h)), dir, add(B, scale(n2, -h)), adjDirAtB);
340
+ if (mL) lB = mL;
341
+ if (mR) rB = mR;
342
+ }
343
+ return `M ${lA.x} ${lA.y} L ${lB.x} ${lB.y} L ${rB.x} ${rB.y} L ${rA.x} ${rA.y} Z`;
344
+ }
345
+ function GridOverlay({ gridSize }) {
346
+ const majorSize = gridSize * 5;
347
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
348
+ /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
349
+ /* @__PURE__ */ jsxRuntime.jsx(
350
+ "pattern",
351
+ {
352
+ id: "vme-grid-minor",
353
+ width: gridSize,
354
+ height: gridSize,
355
+ patternUnits: "userSpaceOnUse",
356
+ children: /* @__PURE__ */ jsxRuntime.jsx(
357
+ "path",
358
+ {
359
+ d: `M ${gridSize} 0 L 0 0 0 ${gridSize}`,
360
+ fill: "none",
361
+ stroke: "#e2e8f0",
362
+ strokeWidth: 0.5
363
+ }
364
+ )
365
+ }
366
+ ),
367
+ /* @__PURE__ */ jsxRuntime.jsxs(
368
+ "pattern",
369
+ {
370
+ id: "vme-grid-major",
371
+ width: majorSize,
372
+ height: majorSize,
373
+ patternUnits: "userSpaceOnUse",
374
+ children: [
375
+ /* @__PURE__ */ jsxRuntime.jsx(
376
+ "rect",
377
+ {
378
+ width: majorSize,
379
+ height: majorSize,
380
+ fill: "url(#vme-grid-minor)"
381
+ }
382
+ ),
383
+ /* @__PURE__ */ jsxRuntime.jsx(
384
+ "path",
385
+ {
386
+ d: `M ${majorSize} 0 L 0 0 0 ${majorSize}`,
387
+ fill: "none",
388
+ stroke: "#cbd5e1",
389
+ strokeWidth: 1
390
+ }
391
+ )
392
+ ]
393
+ }
394
+ )
395
+ ] }),
396
+ /* @__PURE__ */ jsxRuntime.jsx(
397
+ "rect",
398
+ {
399
+ x: -5e4,
400
+ y: -5e4,
401
+ width: 1e5,
402
+ height: 1e5,
403
+ fill: "url(#vme-grid-major)"
404
+ }
405
+ )
406
+ ] });
407
+ }
408
+ function useDrag(svgRef, panZoomRef, callbacks) {
409
+ const cbRef = react.useRef(callbacks);
410
+ cbRef.current = callbacks;
411
+ const lastCanvas = react.useRef({ x: 0, y: 0 });
412
+ const toCanvas = react.useCallback(
413
+ (clientX, clientY) => {
414
+ const rect = svgRef.current?.getBoundingClientRect() ?? { left: 0, top: 0 };
415
+ const { panX, panY, zoom } = panZoomRef.current;
416
+ return {
417
+ x: (clientX - rect.left - panX) / zoom,
418
+ y: (clientY - rect.top - panY) / zoom
419
+ };
420
+ },
421
+ [svgRef, panZoomRef]
422
+ );
423
+ const handleMouseDown = react.useCallback(
424
+ (e) => {
425
+ if (e.button !== 0) return;
426
+ e.stopPropagation();
427
+ e.preventDefault();
428
+ const canvas = toCanvas(e.clientX, e.clientY);
429
+ lastCanvas.current = canvas;
430
+ cbRef.current.onDragStart?.(canvas.x, canvas.y);
431
+ const onMove = (ev) => {
432
+ const c = toCanvas(ev.clientX, ev.clientY);
433
+ const dx = c.x - lastCanvas.current.x;
434
+ const dy = c.y - lastCanvas.current.y;
435
+ lastCanvas.current = c;
436
+ cbRef.current.onDragMove(dx, dy, c.x, c.y);
437
+ };
438
+ const onUp = (ev) => {
439
+ const c = toCanvas(ev.clientX, ev.clientY);
440
+ cbRef.current.onDragEnd?.(c.x, c.y);
441
+ window.removeEventListener("mousemove", onMove);
442
+ window.removeEventListener("mouseup", onUp);
443
+ };
444
+ window.addEventListener("mousemove", onMove);
445
+ window.addEventListener("mouseup", onUp);
446
+ },
447
+ [toCanvas]
448
+ );
449
+ return { handleMouseDown };
450
+ }
451
+ var HANDLE_CURSORS = {
452
+ nw: "nwse-resize",
453
+ ne: "nesw-resize",
454
+ se: "nwse-resize",
455
+ sw: "nesw-resize",
456
+ n: "ns-resize",
457
+ s: "ns-resize",
458
+ e: "ew-resize",
459
+ w: "ew-resize"
460
+ };
461
+ var MIN_SIZE = 50;
462
+ var HANDLE_PX = 8;
463
+ function applyHandleDelta(area, handle, dx, dy) {
464
+ const ax = area.x ?? 0;
465
+ const ay = area.y ?? 0;
466
+ const aw = area.width ?? 400;
467
+ const ah = area.height ?? 300;
468
+ const right = ax + aw;
469
+ const bottom = ay + ah;
470
+ let nx = ax, ny = ay, nw = aw, nh = ah;
471
+ switch (handle) {
472
+ case "nw":
473
+ nw = Math.max(MIN_SIZE, aw - dx);
474
+ nh = Math.max(MIN_SIZE, ah - dy);
475
+ nx = right - nw;
476
+ ny = bottom - nh;
477
+ break;
478
+ case "n":
479
+ nh = Math.max(MIN_SIZE, ah - dy);
480
+ ny = bottom - nh;
481
+ break;
482
+ case "ne":
483
+ nw = Math.max(MIN_SIZE, aw + dx);
484
+ nh = Math.max(MIN_SIZE, ah - dy);
485
+ ny = bottom - nh;
486
+ break;
487
+ case "e":
488
+ nw = Math.max(MIN_SIZE, aw + dx);
489
+ break;
490
+ case "se":
491
+ nw = Math.max(MIN_SIZE, aw + dx);
492
+ nh = Math.max(MIN_SIZE, ah + dy);
493
+ break;
494
+ case "s":
495
+ nh = Math.max(MIN_SIZE, ah + dy);
496
+ break;
497
+ case "sw":
498
+ nw = Math.max(MIN_SIZE, aw - dx);
499
+ nh = Math.max(MIN_SIZE, ah + dy);
500
+ nx = right - nw;
501
+ break;
502
+ case "w":
503
+ nw = Math.max(MIN_SIZE, aw - dx);
504
+ nx = right - nw;
505
+ break;
506
+ }
507
+ return { ...area, x: nx, y: ny, width: nw, height: nh };
508
+ }
509
+ function PolygonArtboard({
510
+ area,
511
+ onResize,
512
+ onMove,
513
+ onResizeCommit,
514
+ svgRef,
515
+ panZoomRef,
516
+ zoom,
517
+ readOnly = false
518
+ }) {
519
+ const pts = area.points ?? [];
520
+ const areaRef = react.useRef(area);
521
+ areaRef.current = area;
522
+ const activeVertex = react.useRef(null);
523
+ const vertexStart = react.useRef({ vx: 0, vy: 0, mx: 0, my: 0 });
524
+ const { handleMouseDown: handleVertexDown } = useDrag(svgRef, panZoomRef, {
525
+ onDragStart: (mx, my) => {
526
+ const idx = activeVertex.current;
527
+ if (idx === null) return;
528
+ const currentPts = areaRef.current.points ?? [];
529
+ vertexStart.current = { vx: currentPts[idx][0], vy: currentPts[idx][1], mx, my };
530
+ },
531
+ onDragMove: (_dx, _dy, canvasX, canvasY) => {
532
+ const idx = activeVertex.current;
533
+ if (idx === null) return;
534
+ const { vx, vy, mx, my } = vertexStart.current;
535
+ const newX = vx + (canvasX - mx);
536
+ const newY = vy + (canvasY - my);
537
+ const currentPts = areaRef.current.points ?? [];
538
+ const newPts = currentPts.map(
539
+ (p, i) => i === idx ? [newX, newY] : p
540
+ );
541
+ const newArea = { ...areaRef.current, points: newPts };
542
+ onResize(newArea);
543
+ },
544
+ onDragEnd: () => {
545
+ onResizeCommit?.(areaRef.current);
546
+ activeVertex.current = null;
547
+ }
548
+ });
549
+ const startVertexDrag = react.useCallback(
550
+ (e, idx) => {
551
+ activeVertex.current = idx;
552
+ handleVertexDown(e);
553
+ },
554
+ [handleVertexDown]
555
+ );
556
+ const { handleMouseDown: handleBodyDown } = useDrag(svgRef, panZoomRef, {
557
+ onDragMove: (dx, dy) => {
558
+ onMove?.(dx, dy);
559
+ }
560
+ });
561
+ const handleDeleteVertex = react.useCallback(
562
+ (e, idx) => {
563
+ e.stopPropagation();
564
+ const currentPts = areaRef.current.points ?? [];
565
+ if (currentPts.length <= 3) return;
566
+ const newPts = currentPts.filter((_, i) => i !== idx);
567
+ const newArea = { ...areaRef.current, points: newPts };
568
+ onResize(newArea);
569
+ onResizeCommit?.(newArea);
570
+ },
571
+ [onResize, onResizeCommit]
572
+ );
573
+ const handleAddVertex = react.useCallback(
574
+ (e, insertAfterIdx) => {
575
+ e.stopPropagation();
576
+ const currentPts = areaRef.current.points ?? [];
577
+ const a = currentPts[insertAfterIdx];
578
+ const b = currentPts[(insertAfterIdx + 1) % currentPts.length];
579
+ const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
580
+ const newPts = [
581
+ ...currentPts.slice(0, insertAfterIdx + 1),
582
+ mid,
583
+ ...currentPts.slice(insertAfterIdx + 1)
584
+ ];
585
+ const newArea = { ...areaRef.current, points: newPts };
586
+ onResize(newArea);
587
+ onResizeCommit?.(newArea);
588
+ },
589
+ [onResize, onResizeCommit]
590
+ );
591
+ if (pts.length < 3) return null;
592
+ const pointsStr = pts.map(([x, y]) => `${x},${y}`).join(" ");
593
+ const hs = HANDLE_PX / zoom;
594
+ const sw = 1.5 / zoom;
595
+ const dash = `${6 / zoom},${3 / zoom}`;
596
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
597
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("filter", { id: "vme-artboard-shadow", x: "-4%", y: "-4%", width: "108%", height: "108%", children: /* @__PURE__ */ jsxRuntime.jsx("feDropShadow", { dx: 0, dy: 3 / zoom, stdDeviation: 6 / zoom, floodOpacity: 0.12 }) }) }),
598
+ /* @__PURE__ */ jsxRuntime.jsx(
599
+ "polygon",
600
+ {
601
+ points: pointsStr,
602
+ fill: "#fafaf9",
603
+ stroke: "none",
604
+ filter: "url(#vme-artboard-shadow)"
605
+ }
606
+ ),
607
+ /* @__PURE__ */ jsxRuntime.jsx(
608
+ "polygon",
609
+ {
610
+ points: pointsStr,
611
+ fill: "transparent",
612
+ stroke: "#94a3b8",
613
+ strokeWidth: sw,
614
+ strokeDasharray: dash,
615
+ style: { cursor: readOnly ? "default" : "move" },
616
+ onMouseDown: readOnly ? void 0 : handleBodyDown
617
+ }
618
+ ),
619
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
620
+ pts.map(([ax, ay], i) => {
621
+ const [bx, by] = pts[(i + 1) % pts.length];
622
+ const mx = (ax + bx) / 2;
623
+ const my = (ay + by) / 2;
624
+ return /* @__PURE__ */ jsxRuntime.jsx(
625
+ "rect",
626
+ {
627
+ x: mx - hs * 0.75,
628
+ y: my - hs * 0.75,
629
+ width: hs * 1.5,
630
+ height: hs * 1.5,
631
+ fill: "white",
632
+ stroke: "#94a3b8",
633
+ strokeWidth: sw,
634
+ style: { cursor: "copy", transform: `rotate(45deg)`, transformOrigin: `${mx}px ${my}px` },
635
+ onClick: (e) => handleAddVertex(e, i)
636
+ },
637
+ `mid-${i}`
638
+ );
639
+ }),
640
+ pts.map(([vx, vy], i) => /* @__PURE__ */ jsxRuntime.jsx(
641
+ "rect",
642
+ {
643
+ x: vx - hs,
644
+ y: vy - hs,
645
+ width: hs * 2,
646
+ height: hs * 2,
647
+ rx: 1 / zoom,
648
+ fill: "white",
649
+ stroke: "#3b82f6",
650
+ strokeWidth: sw,
651
+ style: { cursor: "move" },
652
+ onMouseDown: (e) => startVertexDrag(e, i),
653
+ onDoubleClick: (e) => handleDeleteVertex(e, i)
654
+ },
655
+ `v-${i}`
656
+ ))
657
+ ] })
658
+ ] });
659
+ }
660
+ function RectArtboard({
661
+ area,
662
+ onResize,
663
+ onMove,
664
+ onResizeCommit,
665
+ svgRef,
666
+ panZoomRef,
667
+ zoom,
668
+ readOnly = false
669
+ }) {
670
+ const activeHandle = react.useRef(null);
671
+ const areaRef = react.useRef(area);
672
+ areaRef.current = area;
673
+ const { handleMouseDown: handleHandleDown } = useDrag(svgRef, panZoomRef, {
674
+ onDragMove: (dx, dy) => {
675
+ if (!activeHandle.current) return;
676
+ onResize(applyHandleDelta(areaRef.current, activeHandle.current, dx, dy));
677
+ },
678
+ onDragEnd: () => {
679
+ onResizeCommit?.(areaRef.current);
680
+ activeHandle.current = null;
681
+ }
682
+ });
683
+ const startHandleDrag = react.useCallback(
684
+ (e, type) => {
685
+ activeHandle.current = type;
686
+ handleHandleDown(e);
687
+ },
688
+ [handleHandleDown]
689
+ );
690
+ const { handleMouseDown: handleBodyDown } = useDrag(svgRef, panZoomRef, {
691
+ onDragMove: (dx, dy) => {
692
+ onMove?.(dx, dy);
693
+ }
694
+ });
695
+ const ax = area.x ?? 0;
696
+ const ay = area.y ?? 0;
697
+ const aw = area.width ?? 400;
698
+ const ah = area.height ?? 300;
699
+ const hs = HANDLE_PX / zoom;
700
+ const sw = 1.5 / zoom;
701
+ const dash = `${6 / zoom},${3 / zoom}`;
702
+ const handles = [
703
+ { type: "nw", cx: ax, cy: ay },
704
+ { type: "n", cx: ax + aw / 2, cy: ay },
705
+ { type: "ne", cx: ax + aw, cy: ay },
706
+ { type: "e", cx: ax + aw, cy: ay + ah / 2 },
707
+ { type: "se", cx: ax + aw, cy: ay + ah },
708
+ { type: "s", cx: ax + aw / 2, cy: ay + ah },
709
+ { type: "sw", cx: ax, cy: ay + ah },
710
+ { type: "w", cx: ax, cy: ay + ah / 2 }
711
+ ];
712
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
713
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("filter", { id: "vme-artboard-shadow", x: "-4%", y: "-4%", width: "108%", height: "108%", children: /* @__PURE__ */ jsxRuntime.jsx(
714
+ "feDropShadow",
715
+ {
716
+ dx: 0,
717
+ dy: 3 / zoom,
718
+ stdDeviation: 6 / zoom,
719
+ floodOpacity: 0.12
720
+ }
721
+ ) }) }),
722
+ /* @__PURE__ */ jsxRuntime.jsx(
723
+ "rect",
724
+ {
725
+ x: ax,
726
+ y: ay,
727
+ width: aw,
728
+ height: ah,
729
+ fill: "#fafaf9",
730
+ stroke: "none",
731
+ filter: "url(#vme-artboard-shadow)"
732
+ }
733
+ ),
734
+ /* @__PURE__ */ jsxRuntime.jsx(
735
+ "rect",
736
+ {
737
+ x: ax,
738
+ y: ay,
739
+ width: aw,
740
+ height: ah,
741
+ fill: "transparent",
742
+ stroke: "#94a3b8",
743
+ strokeWidth: sw,
744
+ strokeDasharray: dash,
745
+ style: { cursor: readOnly ? "default" : "move" },
746
+ onMouseDown: readOnly ? void 0 : handleBodyDown
747
+ }
748
+ ),
749
+ !readOnly && handles.map(({ type, cx, cy }) => /* @__PURE__ */ jsxRuntime.jsx(
750
+ "rect",
751
+ {
752
+ x: cx - hs,
753
+ y: cy - hs,
754
+ width: hs * 2,
755
+ height: hs * 2,
756
+ rx: 1 / zoom,
757
+ fill: "white",
758
+ stroke: "#3b82f6",
759
+ strokeWidth: sw,
760
+ style: { cursor: HANDLE_CURSORS[type] },
761
+ onMouseDown: (e) => startHandleDrag(e, type)
762
+ },
763
+ type
764
+ ))
765
+ ] });
766
+ }
767
+ function Artboard(props) {
768
+ if (props.area.shape === "polygon") {
769
+ return /* @__PURE__ */ jsxRuntime.jsx(PolygonArtboard, { ...props });
770
+ }
771
+ return /* @__PURE__ */ jsxRuntime.jsx(RectArtboard, { ...props });
772
+ }
773
+ function vecNorm(v) {
774
+ const len = Math.hypot(v.x, v.y);
775
+ return len < 1e-10 ? { x: 1, y: 0 } : { x: v.x / len, y: v.y / len };
776
+ }
777
+ function WallLayer({ nodes, walls, zoom, tool, onDeleteWall }) {
778
+ if (walls.length === 0 && nodes.length === 0) return null;
779
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
780
+ const nodeWalls = /* @__PURE__ */ new Map();
781
+ for (const n of nodes) nodeWalls.set(n.id, []);
782
+ for (const wall of walls) {
783
+ nodeWalls.get(wall.nodeAId)?.push(wall);
784
+ nodeWalls.get(wall.nodeBId)?.push(wall);
785
+ }
786
+ const sw = 0.75 / zoom;
787
+ const isErase = tool === "ERASE";
788
+ const items = walls.map((wall) => {
789
+ const nodeA = nodeMap.get(wall.nodeAId);
790
+ const nodeB = nodeMap.get(wall.nodeBId);
791
+ if (!nodeA || !nodeB) return null;
792
+ const wallsAtA = (nodeWalls.get(wall.nodeAId) ?? []).filter((w) => w.id !== wall.id);
793
+ let adjDirAtA = null;
794
+ if (wallsAtA.length === 1) {
795
+ const w2 = wallsAtA[0];
796
+ const otherId = w2.nodeAId === wall.nodeAId ? w2.nodeBId : w2.nodeAId;
797
+ const other = nodeMap.get(otherId);
798
+ if (other) adjDirAtA = vecNorm({ x: other.x - nodeA.x, y: other.y - nodeA.y });
799
+ }
800
+ const wallsAtB = (nodeWalls.get(wall.nodeBId) ?? []).filter((w) => w.id !== wall.id);
801
+ let adjDirAtB = null;
802
+ if (wallsAtB.length === 1) {
803
+ const w2 = wallsAtB[0];
804
+ const otherId = w2.nodeAId === wall.nodeBId ? w2.nodeBId : w2.nodeAId;
805
+ const other = nodeMap.get(otherId);
806
+ if (other) adjDirAtB = vecNorm({ x: other.x - nodeB.x, y: other.y - nodeB.y });
807
+ }
808
+ const path = wallSegmentPath(
809
+ nodeA.x,
810
+ nodeA.y,
811
+ nodeB.x,
812
+ nodeB.y,
813
+ wall.thickness,
814
+ adjDirAtA,
815
+ adjDirAtB
816
+ );
817
+ return { wall, path };
818
+ }).filter((x) => x !== null);
819
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
820
+ items.map(({ wall, path }) => /* @__PURE__ */ jsxRuntime.jsxs("g", { children: [
821
+ /* @__PURE__ */ jsxRuntime.jsx(
822
+ "path",
823
+ {
824
+ d: path,
825
+ fill: "#475569",
826
+ stroke: "#1e293b",
827
+ strokeWidth: sw,
828
+ strokeLinejoin: "miter",
829
+ style: { pointerEvents: "none" }
830
+ }
831
+ ),
832
+ isErase && /* @__PURE__ */ jsxRuntime.jsx(
833
+ "path",
834
+ {
835
+ d: path,
836
+ fill: "transparent",
837
+ stroke: "transparent",
838
+ strokeWidth: Math.max(wall.thickness, 16) / zoom,
839
+ style: { cursor: "crosshair", pointerEvents: "all" },
840
+ onClick: () => onDeleteWall?.(wall.id)
841
+ }
842
+ )
843
+ ] }, wall.id)),
844
+ tool === "WALL" && nodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
845
+ "circle",
846
+ {
847
+ cx: node.x,
848
+ cy: node.y,
849
+ r: 4 / zoom,
850
+ fill: "#3b82f6",
851
+ stroke: "white",
852
+ strokeWidth: 1.5 / zoom,
853
+ style: { pointerEvents: "none" }
854
+ },
855
+ node.id
856
+ ))
857
+ ] });
858
+ }
859
+ function arrowPath(x, y, w, h) {
860
+ const headW = Math.min(w * 0.4, h * 0.9);
861
+ const tailH = h * 0.45;
862
+ const yt = y + (h - tailH) / 2;
863
+ const yb = y + (h + tailH) / 2;
864
+ return [
865
+ `M ${x} ${yt}`,
866
+ `L ${x + w - headW} ${yt}`,
867
+ `L ${x + w - headW} ${y}`,
868
+ `L ${x + w} ${y + h / 2}`,
869
+ `L ${x + w - headW} ${y + h}`,
870
+ `L ${x + w - headW} ${yb}`,
871
+ `L ${x} ${yb}`,
872
+ "Z"
873
+ ].join(" ");
874
+ }
875
+ var HANDLE_CURSORS2 = {
876
+ nw: "nwse-resize",
877
+ ne: "nesw-resize",
878
+ se: "nwse-resize",
879
+ sw: "nesw-resize",
880
+ n: "ns-resize",
881
+ s: "ns-resize",
882
+ e: "ew-resize",
883
+ w: "ew-resize"
884
+ };
885
+ var MIN_SIZE2 = 10;
886
+ function rotateDelta(dx, dy, deg) {
887
+ const r = -deg * (Math.PI / 180);
888
+ return [dx * Math.cos(r) - dy * Math.sin(r), dx * Math.sin(r) + dy * Math.cos(r)];
889
+ }
890
+ function ElementNode({
891
+ element,
892
+ typeDef,
893
+ isSelected,
894
+ tool,
895
+ zoom,
896
+ svgRef,
897
+ panZoomRef,
898
+ snapEnabled,
899
+ gridSize,
900
+ statusFill,
901
+ onSelect,
902
+ onMove,
903
+ onMoveCommit,
904
+ onResize,
905
+ onResizeCommit,
906
+ onRotate,
907
+ onRotateCommit,
908
+ onDelete,
909
+ onViewerClick
910
+ }) {
911
+ const { x, y, width: w, height: h, rotation } = element;
912
+ const cx = x + w / 2;
913
+ const cy = y + h / 2;
914
+ const sw = 1.5 / zoom;
915
+ const hs = 7 / zoom;
916
+ const rotOffset = 22 / zoom;
917
+ const fontSize = Math.max(9 / zoom, Math.min(13 / zoom, h * 0.35));
918
+ const startPos = react.useRef({ elX: 0, elY: 0, mouseX: 0, mouseY: 0 });
919
+ const lastMovePos = react.useRef({ x: 0, y: 0 });
920
+ const { handleMouseDown: handleBodyDown } = useDrag(svgRef, panZoomRef, {
921
+ onDragStart: (mx, my) => {
922
+ startPos.current = { elX: element.x, elY: element.y, mouseX: mx, mouseY: my };
923
+ lastMovePos.current = { x: element.x, y: element.y };
924
+ },
925
+ onDragMove: (_dx, _dy, canvasX, canvasY) => {
926
+ let nx = startPos.current.elX + (canvasX - startPos.current.mouseX);
927
+ let ny = startPos.current.elY + (canvasY - startPos.current.mouseY);
928
+ if (snapEnabled) {
929
+ nx = snapToGrid(nx, gridSize);
930
+ ny = snapToGrid(ny, gridSize);
931
+ }
932
+ lastMovePos.current = { x: nx, y: ny };
933
+ onMove(nx, ny);
934
+ },
935
+ onDragEnd: () => {
936
+ onMoveCommit(lastMovePos.current.x, lastMovePos.current.y);
937
+ }
938
+ });
939
+ const activeHandle = react.useRef(null);
940
+ const startGeom = react.useRef({ x: 0, y: 0, w: 0, h: 0, mouseX: 0, mouseY: 0 });
941
+ const lastResizeGeom = react.useRef({ x: 0, y: 0, w: 0, h: 0 });
942
+ const { handleMouseDown: handleHandleDown } = useDrag(svgRef, panZoomRef, {
943
+ onDragStart: (mx, my) => {
944
+ startGeom.current = { x: element.x, y: element.y, w: element.width, h: element.height, mouseX: mx, mouseY: my };
945
+ lastResizeGeom.current = { x: element.x, y: element.y, w: element.width, h: element.height };
946
+ },
947
+ onDragMove: (_dx, _dy, canvasX, canvasY) => {
948
+ const type = activeHandle.current;
949
+ if (!type) return;
950
+ const totalDx = canvasX - startGeom.current.mouseX;
951
+ const totalDy = canvasY - startGeom.current.mouseY;
952
+ const [ldx, ldy] = rotateDelta(totalDx, totalDy, rotation);
953
+ const { x: sx, y: sy, w: sw_, h: sh } = startGeom.current;
954
+ const right = sx + sw_;
955
+ const bottom = sy + sh;
956
+ let nx = sx, ny = sy, nw = sw_, nh = sh;
957
+ switch (type) {
958
+ case "nw":
959
+ nw = Math.max(MIN_SIZE2, sw_ - ldx);
960
+ nh = Math.max(MIN_SIZE2, sh - ldy);
961
+ nx = right - nw;
962
+ ny = bottom - nh;
963
+ break;
964
+ case "n":
965
+ nh = Math.max(MIN_SIZE2, sh - ldy);
966
+ ny = bottom - nh;
967
+ break;
968
+ case "ne":
969
+ nw = Math.max(MIN_SIZE2, sw_ + ldx);
970
+ nh = Math.max(MIN_SIZE2, sh - ldy);
971
+ ny = bottom - nh;
972
+ break;
973
+ case "e":
974
+ nw = Math.max(MIN_SIZE2, sw_ + ldx);
975
+ break;
976
+ case "se":
977
+ nw = Math.max(MIN_SIZE2, sw_ + ldx);
978
+ nh = Math.max(MIN_SIZE2, sh + ldy);
979
+ break;
980
+ case "s":
981
+ nh = Math.max(MIN_SIZE2, sh + ldy);
982
+ break;
983
+ case "sw":
984
+ nw = Math.max(MIN_SIZE2, sw_ - ldx);
985
+ nh = Math.max(MIN_SIZE2, sh + ldy);
986
+ nx = right - nw;
987
+ break;
988
+ case "w":
989
+ nw = Math.max(MIN_SIZE2, sw_ - ldx);
990
+ nx = right - nw;
991
+ break;
992
+ }
993
+ if (snapEnabled) {
994
+ nw = Math.max(MIN_SIZE2, snapToGrid(nw, gridSize));
995
+ nh = Math.max(MIN_SIZE2, snapToGrid(nh, gridSize));
996
+ }
997
+ lastResizeGeom.current = { x: nx, y: ny, w: nw, h: nh };
998
+ onResize(nx, ny, nw, nh);
999
+ },
1000
+ onDragEnd: () => {
1001
+ const { x: rx, y: ry, w: rw, h: rh } = lastResizeGeom.current;
1002
+ onResizeCommit(rx, ry, rw, rh);
1003
+ activeHandle.current = null;
1004
+ }
1005
+ });
1006
+ const startHandleDrag = react.useCallback(
1007
+ (e, type) => {
1008
+ activeHandle.current = type;
1009
+ handleHandleDown(e);
1010
+ },
1011
+ [handleHandleDown]
1012
+ );
1013
+ const rotStart = react.useRef({ angleOffset: 0 });
1014
+ const handleRotateDown = react.useCallback(
1015
+ (e) => {
1016
+ e.stopPropagation();
1017
+ e.preventDefault();
1018
+ const svgRect = svgRef.current?.getBoundingClientRect();
1019
+ if (!svgRect) return;
1020
+ const { panX, panY, zoom: z } = panZoomRef.current;
1021
+ const mcx = (e.clientX - svgRect.left - panX) / z;
1022
+ const mcy = (e.clientY - svgRect.top - panY) / z;
1023
+ const initAngle = Math.atan2(mcy - cy, mcx - cx) * (180 / Math.PI);
1024
+ rotStart.current.angleOffset = element.rotation - initAngle;
1025
+ let currentRot = element.rotation;
1026
+ const onMove2 = (ev) => {
1027
+ const rect = svgRef.current?.getBoundingClientRect();
1028
+ if (!rect) return;
1029
+ const { panX: px, panY: py, zoom: z2 } = panZoomRef.current;
1030
+ const mx2 = (ev.clientX - rect.left - px) / z2;
1031
+ const my2 = (ev.clientY - rect.top - py) / z2;
1032
+ let newRot = Math.atan2(my2 - cy, mx2 - cx) * (180 / Math.PI) + rotStart.current.angleOffset;
1033
+ if (ev.shiftKey) newRot = Math.round(newRot / 15) * 15;
1034
+ currentRot = newRot;
1035
+ onRotate(newRot);
1036
+ };
1037
+ const onUp = () => {
1038
+ onRotateCommit(currentRot);
1039
+ window.removeEventListener("mousemove", onMove2);
1040
+ window.removeEventListener("mouseup", onUp);
1041
+ };
1042
+ window.addEventListener("mousemove", onMove2);
1043
+ window.addEventListener("mouseup", onUp);
1044
+ },
1045
+ [cx, cy, element.rotation, panZoomRef, svgRef, onRotate, onRotateCommit]
1046
+ );
1047
+ const handleBodyClick = react.useCallback(
1048
+ (e) => {
1049
+ e.stopPropagation();
1050
+ if (onViewerClick) {
1051
+ onViewerClick();
1052
+ return;
1053
+ }
1054
+ if (tool === "ERASE") {
1055
+ onDelete();
1056
+ return;
1057
+ }
1058
+ if (tool === "SELECT") {
1059
+ onSelect(e.ctrlKey || e.metaKey || e.shiftKey);
1060
+ }
1061
+ },
1062
+ [tool, onDelete, onSelect, onViewerClick]
1063
+ );
1064
+ const fillColor = statusFill ?? typeDef.color;
1065
+ const bodyCursor = onViewerClick ? "pointer" : tool === "ERASE" ? "crosshair" : tool === "SELECT" ? "move" : "default";
1066
+ const customPath = typeDef.shape === "path" && typeDef.svgPath ? (() => {
1067
+ const parts = (typeDef.viewBox ?? "0 0 100 100").split(/[\s,]+/).map(Number);
1068
+ const vw = parts[2] ?? 100;
1069
+ const vh = parts[3] ?? 100;
1070
+ const scaleX = vw > 0 ? w / vw : 1;
1071
+ const scaleY = vh > 0 ? h / vh : 1;
1072
+ const avgScale = Math.sqrt(Math.abs(scaleX * scaleY)) || 1;
1073
+ return { scaleX, scaleY, strokeWidth: sw / avgScale };
1074
+ })() : null;
1075
+ const handles = [
1076
+ { type: "nw", hx: x, hy: y },
1077
+ { type: "n", hx: x + w / 2, hy: y },
1078
+ { type: "ne", hx: x + w, hy: y },
1079
+ { type: "e", hx: x + w, hy: y + h / 2 },
1080
+ { type: "se", hx: x + w, hy: y + h },
1081
+ { type: "s", hx: x + w / 2, hy: y + h },
1082
+ { type: "sw", hx: x, hy: y + h },
1083
+ { type: "w", hx: x, hy: y + h / 2 }
1084
+ ];
1085
+ return /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `rotate(${rotation}, ${cx}, ${cy})`, children: [
1086
+ typeDef.shape === "rect" && /* @__PURE__ */ jsxRuntime.jsx(
1087
+ "rect",
1088
+ {
1089
+ x,
1090
+ y,
1091
+ width: w,
1092
+ height: h,
1093
+ fill: fillColor,
1094
+ stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
1095
+ strokeWidth: isSelected ? sw * 1.5 : sw,
1096
+ style: { cursor: bodyCursor },
1097
+ onMouseDown: tool === "SELECT" && !onViewerClick ? handleBodyDown : void 0,
1098
+ onClick: handleBodyClick
1099
+ }
1100
+ ),
1101
+ typeDef.shape === "circle" && /* @__PURE__ */ jsxRuntime.jsx(
1102
+ "ellipse",
1103
+ {
1104
+ cx,
1105
+ cy,
1106
+ rx: w / 2,
1107
+ ry: h / 2,
1108
+ fill: fillColor,
1109
+ stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
1110
+ strokeWidth: isSelected ? sw * 1.5 : sw,
1111
+ style: { cursor: bodyCursor },
1112
+ onMouseDown: tool === "SELECT" && !onViewerClick ? handleBodyDown : void 0,
1113
+ onClick: handleBodyClick
1114
+ }
1115
+ ),
1116
+ typeDef.shape === "arrow" && /* @__PURE__ */ jsxRuntime.jsx(
1117
+ "path",
1118
+ {
1119
+ d: arrowPath(x, y, w, h),
1120
+ fill: fillColor,
1121
+ stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
1122
+ strokeWidth: isSelected ? sw * 1.5 : sw,
1123
+ style: { cursor: bodyCursor },
1124
+ onMouseDown: tool === "SELECT" && !onViewerClick ? handleBodyDown : void 0,
1125
+ onClick: handleBodyClick
1126
+ }
1127
+ ),
1128
+ typeDef.shape === "path" && customPath && typeDef.svgPath && /* @__PURE__ */ jsxRuntime.jsx("g", { transform: `translate(${x}, ${y}) scale(${customPath.scaleX}, ${customPath.scaleY})`, children: /* @__PURE__ */ jsxRuntime.jsx(
1129
+ "path",
1130
+ {
1131
+ d: typeDef.svgPath,
1132
+ fill: fillColor,
1133
+ stroke: isSelected ? "#3b82f6" : typeDef.strokeColor,
1134
+ strokeWidth: isSelected ? customPath.strokeWidth * 1.5 : customPath.strokeWidth,
1135
+ style: { cursor: bodyCursor },
1136
+ onMouseDown: tool === "SELECT" && !onViewerClick ? handleBodyDown : void 0,
1137
+ onClick: handleBodyClick
1138
+ }
1139
+ ) }),
1140
+ (element.label ?? typeDef.label) && /* @__PURE__ */ jsxRuntime.jsx(
1141
+ "text",
1142
+ {
1143
+ x: cx,
1144
+ y: cy,
1145
+ textAnchor: "middle",
1146
+ dominantBaseline: "central",
1147
+ fontSize,
1148
+ fill: typeDef.strokeColor,
1149
+ style: { pointerEvents: "none", userSelect: "none" },
1150
+ children: element.label ?? typeDef.label
1151
+ }
1152
+ ),
1153
+ isSelected && tool === "SELECT" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1154
+ /* @__PURE__ */ jsxRuntime.jsx(
1155
+ "line",
1156
+ {
1157
+ x1: cx,
1158
+ y1: y,
1159
+ x2: cx,
1160
+ y2: y - rotOffset,
1161
+ stroke: "#3b82f6",
1162
+ strokeWidth: sw,
1163
+ style: { pointerEvents: "none" }
1164
+ }
1165
+ ),
1166
+ /* @__PURE__ */ jsxRuntime.jsx(
1167
+ "circle",
1168
+ {
1169
+ cx,
1170
+ cy: y - rotOffset,
1171
+ r: hs * 0.8,
1172
+ fill: "white",
1173
+ stroke: "#3b82f6",
1174
+ strokeWidth: sw,
1175
+ style: { cursor: "grab" },
1176
+ onMouseDown: handleRotateDown
1177
+ }
1178
+ ),
1179
+ handles.map(({ type, hx, hy }) => /* @__PURE__ */ jsxRuntime.jsx(
1180
+ "rect",
1181
+ {
1182
+ x: hx - hs,
1183
+ y: hy - hs,
1184
+ width: hs * 2,
1185
+ height: hs * 2,
1186
+ rx: 1 / zoom,
1187
+ fill: "white",
1188
+ stroke: "#3b82f6",
1189
+ strokeWidth: sw,
1190
+ style: { cursor: HANDLE_CURSORS2[type] },
1191
+ onMouseDown: (e) => startHandleDrag(e, type)
1192
+ },
1193
+ type
1194
+ ))
1195
+ ] })
1196
+ ] });
1197
+ }
1198
+ var SNAP_PX = 10;
1199
+ var DEFAULT_THICKNESS = 8;
1200
+ function insideFloor(x, y, floor) {
1201
+ const { area } = floor;
1202
+ if (area.shape === "rect") {
1203
+ const ax = area.x ?? 0, ay = area.y ?? 0, aw = area.width ?? 0, ah = area.height ?? 0;
1204
+ return x >= ax && x <= ax + aw && y >= ay && y <= ay + ah;
1205
+ }
1206
+ if (area.shape === "polygon") {
1207
+ const pts = area.points ?? [];
1208
+ let inside = false;
1209
+ for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
1210
+ const [xi, yi] = pts[i], [xj, yj] = pts[j];
1211
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
1212
+ }
1213
+ return inside;
1214
+ }
1215
+ return true;
1216
+ }
1217
+ function rectsIntersect(a, b) {
1218
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
1219
+ }
1220
+ function EditorCanvas({
1221
+ floor,
1222
+ tool,
1223
+ gridSize,
1224
+ showGrid,
1225
+ readOnly,
1226
+ snapEnabled,
1227
+ elementTypeDefs,
1228
+ selectedIds,
1229
+ statusMap,
1230
+ onAreaResize,
1231
+ onAreaMove,
1232
+ onAreaResizeCommit,
1233
+ onSelectElement,
1234
+ onSelectSet,
1235
+ onClearSelection,
1236
+ onMoveElement,
1237
+ onMoveCommit,
1238
+ onResizeElement,
1239
+ onResizeCommit,
1240
+ onRotateElement,
1241
+ onRotateCommit,
1242
+ onDeleteElement,
1243
+ onPlaceElement,
1244
+ onAddWall,
1245
+ onDeleteWall,
1246
+ onViewerElementClick,
1247
+ onZoomChange,
1248
+ onRegisterZoomBy,
1249
+ onRegisterResetView
1250
+ }) {
1251
+ const svgRef = react.useRef(null);
1252
+ const {
1253
+ state: panZoom,
1254
+ isPanning,
1255
+ handleWheel,
1256
+ handleMouseDown: handlePanMouseDown,
1257
+ handleMouseMove: handlePanMouseMove,
1258
+ handleMouseUp: handlePanMouseUp,
1259
+ handleMouseLeave,
1260
+ zoomBy,
1261
+ resetView
1262
+ } = usePanZoom(1, tool === "PAN");
1263
+ const panZoomRef = react.useRef(panZoom);
1264
+ panZoomRef.current = panZoom;
1265
+ const [lasso, setLasso] = react.useState(null);
1266
+ const lassoStart = react.useRef(null);
1267
+ const [wallDraw, setWallDraw] = react.useState(null);
1268
+ react.useEffect(() => {
1269
+ if (tool !== "WALL") setWallDraw(null);
1270
+ }, [tool]);
1271
+ react.useEffect(() => {
1272
+ onRegisterZoomBy?.(zoomBy);
1273
+ onRegisterResetView?.(resetView);
1274
+ }, []);
1275
+ react.useEffect(() => {
1276
+ onZoomChange?.(panZoom.zoom);
1277
+ }, [panZoom.zoom, onZoomChange]);
1278
+ react.useEffect(() => {
1279
+ const el = svgRef.current;
1280
+ if (!el) return;
1281
+ const prevent = (e) => e.preventDefault();
1282
+ el.addEventListener("wheel", prevent, { passive: false });
1283
+ return () => el.removeEventListener("wheel", prevent);
1284
+ }, []);
1285
+ const toCanvas = react.useCallback((clientX, clientY) => {
1286
+ const rect = svgRef.current?.getBoundingClientRect() ?? { left: 0, top: 0 };
1287
+ const { panX: panX2, panY: panY2, zoom: zoom2 } = panZoomRef.current;
1288
+ return { x: (clientX - rect.left - panX2) / zoom2, y: (clientY - rect.top - panY2) / zoom2 };
1289
+ }, []);
1290
+ const findSnapNode = react.useCallback((cx, cy, excludeId) => {
1291
+ const threshold = SNAP_PX / panZoomRef.current.zoom;
1292
+ const candidates = excludeId ? floor.wallNodes.filter((n) => n.id !== excludeId) : floor.wallNodes;
1293
+ return findNearestNode(cx, cy, candidates, threshold);
1294
+ }, [floor.wallNodes]);
1295
+ const handleSvgMouseDown = react.useCallback((e) => {
1296
+ if (e.button === 1 || e.button === 0 && tool === "PAN") {
1297
+ handlePanMouseDown(e);
1298
+ return;
1299
+ }
1300
+ if (e.button !== 0) return;
1301
+ const raw = toCanvas(e.clientX, e.clientY);
1302
+ const { x: cx, y: cy } = snapPoint(raw.x, raw.y, gridSize, snapEnabled);
1303
+ if (tool === "WALL") {
1304
+ if (!wallDraw) {
1305
+ if (!insideFloor(cx, cy, floor)) return;
1306
+ const snapNode = findSnapNode(cx, cy, null);
1307
+ const sx = snapNode ? snapNode.x : cx;
1308
+ const sy = snapNode ? snapNode.y : cy;
1309
+ setWallDraw({
1310
+ startX: sx,
1311
+ startY: sy,
1312
+ snapStartNode: snapNode,
1313
+ previewX: cx,
1314
+ previewY: cy,
1315
+ snapEndNode: null
1316
+ });
1317
+ } else {
1318
+ const snapNode = findSnapNode(cx, cy, wallDraw.snapStartNode?.id ?? null);
1319
+ let ex, ey;
1320
+ if (snapNode) {
1321
+ ex = snapNode.x;
1322
+ ey = snapNode.y;
1323
+ } else {
1324
+ const { area } = floor;
1325
+ if (area.shape === "rect") {
1326
+ const ax = area.x ?? 0, ay = area.y ?? 0;
1327
+ const aw = area.width ?? 0, ah = area.height ?? 0;
1328
+ ex = Math.max(ax, Math.min(ax + aw, cx));
1329
+ ey = Math.max(ay, Math.min(ay + ah, cy));
1330
+ } else if (area.shape === "polygon") {
1331
+ const pts = area.points ?? [];
1332
+ if (insideFloor(cx, cy, floor)) {
1333
+ ex = cx;
1334
+ ey = cy;
1335
+ } else {
1336
+ let bestDist = Infinity;
1337
+ ex = cx;
1338
+ ey = cy;
1339
+ for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
1340
+ const [ax2, ay2] = pts[j], [bx2, by2] = pts[i];
1341
+ const dx = bx2 - ax2, dy = by2 - ay2;
1342
+ const len2 = dx * dx + dy * dy;
1343
+ const t = len2 > 0 ? Math.max(0, Math.min(1, ((cx - ax2) * dx + (cy - ay2) * dy) / len2)) : 0;
1344
+ const nx = ax2 + t * dx, ny = ay2 + t * dy;
1345
+ const dist2 = (cx - nx) ** 2 + (cy - ny) ** 2;
1346
+ if (dist2 < bestDist) {
1347
+ bestDist = dist2;
1348
+ ex = nx;
1349
+ ey = ny;
1350
+ }
1351
+ }
1352
+ }
1353
+ } else {
1354
+ ex = cx;
1355
+ ey = cy;
1356
+ }
1357
+ }
1358
+ const dist = Math.hypot(ex - wallDraw.startX, ey - wallDraw.startY);
1359
+ if (dist > 2) {
1360
+ onAddWall?.(
1361
+ wallDraw.startX,
1362
+ wallDraw.startY,
1363
+ ex,
1364
+ ey,
1365
+ wallDraw.snapStartNode?.id ?? null,
1366
+ snapNode?.id ?? null
1367
+ );
1368
+ }
1369
+ setWallDraw({
1370
+ startX: ex,
1371
+ startY: ey,
1372
+ snapStartNode: snapNode,
1373
+ previewX: cx,
1374
+ previewY: cy,
1375
+ snapEndNode: null
1376
+ });
1377
+ }
1378
+ return;
1379
+ }
1380
+ if (tool === "PLACE") {
1381
+ onPlaceElement(cx, cy);
1382
+ return;
1383
+ }
1384
+ if (tool === "SELECT") {
1385
+ lassoStart.current = { cx, cy };
1386
+ setLasso({ x: cx, y: cy, w: 0, h: 0 });
1387
+ }
1388
+ }, [
1389
+ handlePanMouseDown,
1390
+ tool,
1391
+ toCanvas,
1392
+ gridSize,
1393
+ snapEnabled,
1394
+ wallDraw,
1395
+ findSnapNode,
1396
+ onAddWall,
1397
+ onPlaceElement
1398
+ ]);
1399
+ const handleSvgMouseMove = react.useCallback((e) => {
1400
+ handlePanMouseMove(e);
1401
+ const raw = toCanvas(e.clientX, e.clientY);
1402
+ const { x: cx, y: cy } = snapPoint(raw.x, raw.y, gridSize, snapEnabled);
1403
+ if (tool === "WALL" && wallDraw) {
1404
+ const snapNode = findSnapNode(cx, cy, wallDraw.snapStartNode?.id ?? null);
1405
+ setWallDraw(
1406
+ (prev) => prev ? { ...prev, previewX: cx, previewY: cy, snapEndNode: snapNode } : null
1407
+ );
1408
+ return;
1409
+ }
1410
+ if (tool === "SELECT" && lassoStart.current) {
1411
+ const lx = Math.min(cx, lassoStart.current.cx);
1412
+ const ly = Math.min(cy, lassoStart.current.cy);
1413
+ setLasso({ x: lx, y: ly, w: Math.abs(cx - lassoStart.current.cx), h: Math.abs(cy - lassoStart.current.cy) });
1414
+ }
1415
+ }, [handlePanMouseMove, tool, toCanvas, gridSize, snapEnabled, wallDraw, findSnapNode]);
1416
+ const handleSvgMouseUp = react.useCallback((e) => {
1417
+ handlePanMouseUp(e);
1418
+ if (lassoStart.current && lasso) {
1419
+ if (lasso.w > 4 / panZoom.zoom || lasso.h > 4 / panZoom.zoom) {
1420
+ const ids = floor.elements.filter((el) => rectsIntersect(lasso, { x: el.x, y: el.y, w: el.width, h: el.height })).map((el) => el.id);
1421
+ if (ids.length > 0) onSelectSet(ids);
1422
+ else if (!e.ctrlKey && !e.metaKey) onClearSelection();
1423
+ } else {
1424
+ if (!e.ctrlKey && !e.metaKey) onClearSelection();
1425
+ }
1426
+ lassoStart.current = null;
1427
+ setLasso(null);
1428
+ }
1429
+ }, [handlePanMouseUp, lasso, panZoom.zoom, floor.elements, onSelectSet, onClearSelection]);
1430
+ const handleContextMenu = react.useCallback((e) => {
1431
+ e.preventDefault();
1432
+ if (tool === "WALL") setWallDraw(null);
1433
+ }, [tool]);
1434
+ const cursor = isPanning ? "grabbing" : tool === "PAN" ? "grab" : tool === "WALL" ? "crosshair" : tool === "PLACE" ? "crosshair" : tool === "ERASE" ? "crosshair" : "default";
1435
+ const { panX, panY, zoom } = panZoom;
1436
+ const previewPath = wallDraw && (() => {
1437
+ const ex = wallDraw.snapEndNode?.x ?? wallDraw.previewX;
1438
+ const ey = wallDraw.snapEndNode?.y ?? wallDraw.previewY;
1439
+ const dist = Math.hypot(ex - wallDraw.startX, ey - wallDraw.startY);
1440
+ return dist > 2 ? wallSegmentPath(wallDraw.startX, wallDraw.startY, ex, ey, DEFAULT_THICKNESS, null, null) : null;
1441
+ })();
1442
+ return /* @__PURE__ */ jsxRuntime.jsx(
1443
+ "svg",
1444
+ {
1445
+ ref: svgRef,
1446
+ className: "w-full h-full select-none outline-none",
1447
+ style: { cursor, display: "block" },
1448
+ onWheel: handleWheel,
1449
+ onMouseDown: handleSvgMouseDown,
1450
+ onMouseMove: handleSvgMouseMove,
1451
+ onMouseUp: handleSvgMouseUp,
1452
+ onMouseLeave: handleMouseLeave,
1453
+ onContextMenu: handleContextMenu,
1454
+ children: /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${panX}, ${panY}) scale(${zoom})`, children: [
1455
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: -5e4, y: -5e4, width: 1e5, height: 1e5, fill: "#f1f5f9" }),
1456
+ showGrid && /* @__PURE__ */ jsxRuntime.jsx(GridOverlay, { gridSize }),
1457
+ /* @__PURE__ */ jsxRuntime.jsx(
1458
+ Artboard,
1459
+ {
1460
+ area: floor.area,
1461
+ onResize: (area) => onAreaResize({ ...floor, area }),
1462
+ onMove: onAreaMove,
1463
+ onResizeCommit: (area) => onAreaResizeCommit?.({ ...floor, area }),
1464
+ svgRef,
1465
+ panZoomRef,
1466
+ zoom,
1467
+ readOnly: readOnly || tool !== "SELECT"
1468
+ }
1469
+ ),
1470
+ /* @__PURE__ */ jsxRuntime.jsx(
1471
+ WallLayer,
1472
+ {
1473
+ nodes: floor.wallNodes,
1474
+ walls: floor.walls,
1475
+ zoom,
1476
+ tool,
1477
+ onDeleteWall
1478
+ }
1479
+ ),
1480
+ floor.elements.map((el) => {
1481
+ const typeDef = elementTypeDefs.get(el.type);
1482
+ if (!typeDef) return null;
1483
+ return /* @__PURE__ */ jsxRuntime.jsx(
1484
+ ElementNode,
1485
+ {
1486
+ element: el,
1487
+ typeDef,
1488
+ isSelected: selectedIds.has(el.id),
1489
+ tool,
1490
+ zoom,
1491
+ svgRef,
1492
+ panZoomRef,
1493
+ snapEnabled,
1494
+ gridSize,
1495
+ statusFill: statusMap?.get(el.id),
1496
+ onSelect: (multi) => onSelectElement(el.id, multi),
1497
+ onMove: (x, y) => onMoveElement(el.id, x, y),
1498
+ onMoveCommit: (x, y) => onMoveCommit(el.id, x, y),
1499
+ onResize: (x, y, w, h) => onResizeElement(el.id, x, y, w, h),
1500
+ onResizeCommit: (x, y, w, h) => onResizeCommit(el.id, x, y, w, h),
1501
+ onRotate: (r) => onRotateElement(el.id, r),
1502
+ onRotateCommit: (r) => onRotateCommit(el.id, r),
1503
+ onDelete: () => onDeleteElement(el.id),
1504
+ onViewerClick: onViewerElementClick ? () => onViewerElementClick(el.id) : void 0
1505
+ },
1506
+ el.id
1507
+ );
1508
+ }),
1509
+ lasso && lasso.w > 0 && lasso.h > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1510
+ "rect",
1511
+ {
1512
+ x: lasso.x,
1513
+ y: lasso.y,
1514
+ width: lasso.w,
1515
+ height: lasso.h,
1516
+ fill: "rgba(59,130,246,0.06)",
1517
+ stroke: "#3b82f6",
1518
+ strokeWidth: 1 / zoom,
1519
+ strokeDasharray: `${4 / zoom},${2 / zoom}`,
1520
+ style: { pointerEvents: "none" }
1521
+ }
1522
+ ),
1523
+ wallDraw && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1524
+ previewPath && /* @__PURE__ */ jsxRuntime.jsx(
1525
+ "path",
1526
+ {
1527
+ d: previewPath,
1528
+ fill: "#94a3b8",
1529
+ fillOpacity: 0.45,
1530
+ stroke: "#3b82f6",
1531
+ strokeWidth: 1 / zoom,
1532
+ strokeDasharray: `${5 / zoom},${3 / zoom}`,
1533
+ style: { pointerEvents: "none" }
1534
+ }
1535
+ ),
1536
+ /* @__PURE__ */ jsxRuntime.jsx(
1537
+ "circle",
1538
+ {
1539
+ cx: wallDraw.startX,
1540
+ cy: wallDraw.startY,
1541
+ r: 5 / zoom,
1542
+ fill: "#3b82f6",
1543
+ stroke: "white",
1544
+ strokeWidth: 1.5 / zoom,
1545
+ style: { pointerEvents: "none" }
1546
+ }
1547
+ ),
1548
+ wallDraw.snapEndNode && /* @__PURE__ */ jsxRuntime.jsx(
1549
+ "circle",
1550
+ {
1551
+ cx: wallDraw.snapEndNode.x,
1552
+ cy: wallDraw.snapEndNode.y,
1553
+ r: 9 / zoom,
1554
+ fill: "none",
1555
+ stroke: "#3b82f6",
1556
+ strokeWidth: 2 / zoom,
1557
+ style: { pointerEvents: "none" }
1558
+ }
1559
+ )
1560
+ ] })
1561
+ ] })
1562
+ }
1563
+ );
1564
+ }
1565
+ function NumField({ label, value, onChange, step = 1 }) {
1566
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col gap-0.5", children: [
1567
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium text-slate-400 uppercase tracking-wide", children: label }),
1568
+ /* @__PURE__ */ jsxRuntime.jsx(
1569
+ "input",
1570
+ {
1571
+ type: "number",
1572
+ value: Math.round(value),
1573
+ step,
1574
+ onChange: (e) => onChange(Number(e.target.value)),
1575
+ className: "w-full border border-slate-200 rounded px-1.5 py-1 text-xs text-slate-700 focus:outline-none focus:ring-1 focus:ring-blue-400 bg-white"
1576
+ }
1577
+ )
1578
+ ] });
1579
+ }
1580
+ function PropertiesPanel({
1581
+ elements,
1582
+ typeDefs,
1583
+ onChangeLabel,
1584
+ onChangeGeometry,
1585
+ onDelete,
1586
+ onDuplicate
1587
+ }) {
1588
+ const count = elements.length;
1589
+ const handleLabelChange = react.useCallback(
1590
+ (id, e) => onChangeLabel(id, e.target.value),
1591
+ [onChangeLabel]
1592
+ );
1593
+ if (count === 0) return null;
1594
+ if (count > 1) {
1595
+ const ids = elements.map((el2) => el2.id);
1596
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-56 shrink-0 border-l border-slate-200 bg-white flex flex-col", children: [
1597
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 border-b border-slate-100 text-xs font-semibold text-slate-500 uppercase tracking-wide", children: "Propiedades" }),
1598
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-3 p-4 text-center", children: [
1599
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-2xl font-bold text-slate-700", children: count }),
1600
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-400", children: "elementos seleccionados" })
1601
+ ] }),
1602
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 pb-3 flex flex-col gap-2", children: [
1603
+ /* @__PURE__ */ jsxRuntime.jsx(
1604
+ "button",
1605
+ {
1606
+ onClick: () => onDuplicate(ids),
1607
+ className: "w-full text-xs px-3 py-1.5 rounded border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors",
1608
+ children: "Duplicar selecci\xF3n"
1609
+ }
1610
+ ),
1611
+ /* @__PURE__ */ jsxRuntime.jsx(
1612
+ "button",
1613
+ {
1614
+ onClick: () => onDelete(ids),
1615
+ className: "w-full text-xs px-3 py-1.5 rounded bg-red-50 border border-red-200 text-red-600 hover:bg-red-100 transition-colors",
1616
+ children: "Eliminar selecci\xF3n"
1617
+ }
1618
+ )
1619
+ ] })
1620
+ ] });
1621
+ }
1622
+ const el = elements[0];
1623
+ const typeDef = typeDefs.get(el.type);
1624
+ const setGeom = (patch) => {
1625
+ onChangeGeometry(
1626
+ el.id,
1627
+ patch.x ?? el.x,
1628
+ patch.y ?? el.y,
1629
+ patch.w ?? el.width,
1630
+ patch.h ?? el.height,
1631
+ patch.r ?? el.rotation
1632
+ );
1633
+ };
1634
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-56 shrink-0 border-l border-slate-200 bg-white flex flex-col overflow-y-auto", children: [
1635
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 border-b border-slate-100 text-xs font-semibold text-slate-500 uppercase tracking-wide", children: "Propiedades" }),
1636
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col gap-4 p-3", children: [
1637
+ typeDef && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1638
+ /* @__PURE__ */ jsxRuntime.jsx(
1639
+ "span",
1640
+ {
1641
+ className: "w-3.5 h-3.5 rounded-sm shrink-0 border",
1642
+ style: { background: typeDef.color, borderColor: typeDef.strokeColor }
1643
+ }
1644
+ ),
1645
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-slate-700 truncate", children: typeDef.label })
1646
+ ] }),
1647
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col gap-0.5", children: [
1648
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium text-slate-400 uppercase tracking-wide", children: "Etiqueta" }),
1649
+ /* @__PURE__ */ jsxRuntime.jsx(
1650
+ "input",
1651
+ {
1652
+ type: "text",
1653
+ value: el.label ?? "",
1654
+ placeholder: typeDef?.label ?? "",
1655
+ onChange: (e) => handleLabelChange(el.id, e),
1656
+ className: "w-full border border-slate-200 rounded px-1.5 py-1 text-xs text-slate-700 focus:outline-none focus:ring-1 focus:ring-blue-400 bg-white"
1657
+ }
1658
+ )
1659
+ ] }),
1660
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1661
+ /* @__PURE__ */ jsxRuntime.jsx(NumField, { label: "X", value: el.x, onChange: (v) => setGeom({ x: v }) }),
1662
+ /* @__PURE__ */ jsxRuntime.jsx(NumField, { label: "Y", value: el.y, onChange: (v) => setGeom({ y: v }) })
1663
+ ] }),
1664
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1665
+ /* @__PURE__ */ jsxRuntime.jsx(NumField, { label: "Ancho", value: el.width, onChange: (v) => setGeom({ w: Math.max(1, v) }) }),
1666
+ /* @__PURE__ */ jsxRuntime.jsx(NumField, { label: "Alto", value: el.height, onChange: (v) => setGeom({ h: Math.max(1, v) }) })
1667
+ ] }),
1668
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1669
+ /* @__PURE__ */ jsxRuntime.jsx(NumField, { label: "Rotaci\xF3n \xB0", value: el.rotation, onChange: (v) => setGeom({ r: v }), step: 15 }),
1670
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col gap-0.5", children: [
1671
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium text-slate-400 uppercase tracking-wide", children: "\xA0" }),
1672
+ /* @__PURE__ */ jsxRuntime.jsx(
1673
+ "button",
1674
+ {
1675
+ onClick: () => setGeom({ r: 0 }),
1676
+ className: "border border-slate-200 rounded px-1.5 py-1 text-xs text-slate-500 hover:bg-slate-50 transition-colors",
1677
+ children: "Resetear"
1678
+ }
1679
+ )
1680
+ ] })
1681
+ ] }),
1682
+ el.metadata && Object.keys(el.metadata).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
1683
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-medium text-slate-400 uppercase tracking-wide", children: "Metadata" }),
1684
+ Object.entries(el.metadata).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between text-xs text-slate-500", children: [
1685
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: k }),
1686
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-2 text-slate-400 truncate", children: String(v) })
1687
+ ] }, k))
1688
+ ] })
1689
+ ] }),
1690
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 pb-3 flex flex-col gap-2 border-t border-slate-100 pt-3", children: [
1691
+ /* @__PURE__ */ jsxRuntime.jsx(
1692
+ "button",
1693
+ {
1694
+ onClick: () => onDuplicate([el.id]),
1695
+ className: "w-full text-xs px-3 py-1.5 rounded border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors",
1696
+ children: "Duplicar (Ctrl+D)"
1697
+ }
1698
+ ),
1699
+ /* @__PURE__ */ jsxRuntime.jsx(
1700
+ "button",
1701
+ {
1702
+ onClick: () => onDelete([el.id]),
1703
+ className: "w-full text-xs px-3 py-1.5 rounded bg-red-50 border border-red-200 text-red-600 hover:bg-red-100 transition-colors",
1704
+ children: "Eliminar"
1705
+ }
1706
+ )
1707
+ ] })
1708
+ ] });
1709
+ }
1710
+ function FloorTabs({
1711
+ floors,
1712
+ activeFloorId,
1713
+ readOnly,
1714
+ onSelect,
1715
+ onAdd,
1716
+ onRename,
1717
+ onDelete,
1718
+ onReorder
1719
+ }) {
1720
+ const [editingId, setEditingId] = react.useState(null);
1721
+ const [editValue, setEditValue] = react.useState("");
1722
+ const inputRef = react.useRef(null);
1723
+ const sorted = floors.slice().sort((a, b) => a.order - b.order);
1724
+ const startEditing = react.useCallback((floor) => {
1725
+ if (readOnly) return;
1726
+ setEditingId(floor.id);
1727
+ setEditValue(floor.name);
1728
+ setTimeout(() => inputRef.current?.select(), 0);
1729
+ }, [readOnly]);
1730
+ const commitEdit = react.useCallback(() => {
1731
+ if (editingId && editValue.trim()) {
1732
+ onRename(editingId, editValue.trim());
1733
+ }
1734
+ setEditingId(null);
1735
+ }, [editingId, editValue, onRename]);
1736
+ const cancelEdit = react.useCallback(() => {
1737
+ setEditingId(null);
1738
+ }, []);
1739
+ const handleKeyDown = react.useCallback((e) => {
1740
+ if (e.key === "Enter") {
1741
+ e.preventDefault();
1742
+ commitEdit();
1743
+ }
1744
+ if (e.key === "Escape") {
1745
+ e.preventDefault();
1746
+ cancelEdit();
1747
+ }
1748
+ }, [commitEdit, cancelEdit]);
1749
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 px-2 py-1 border-b border-slate-200 bg-slate-50 text-xs overflow-x-auto", children: [
1750
+ sorted.map((floor) => {
1751
+ const isActive = floor.id === activeFloorId;
1752
+ const idx = sorted.indexOf(floor);
1753
+ const canMoveLeft = idx > 0;
1754
+ const canMoveRight = idx < sorted.length - 1;
1755
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1756
+ "div",
1757
+ {
1758
+ className: [
1759
+ "flex items-center gap-0.5 px-2 py-1 rounded-t border transition-colors shrink-0",
1760
+ isActive ? "bg-white border-slate-300 text-slate-800 font-medium" : "border-transparent text-slate-500 hover:text-slate-700 cursor-pointer"
1761
+ ].join(" "),
1762
+ onClick: () => !isActive && onSelect(floor.id),
1763
+ children: [
1764
+ !readOnly && isActive && canMoveLeft && /* @__PURE__ */ jsxRuntime.jsx(
1765
+ "button",
1766
+ {
1767
+ className: "text-slate-400 hover:text-slate-700 px-0.5 leading-none",
1768
+ onClick: (e) => {
1769
+ e.stopPropagation();
1770
+ onReorder(floor.id, "left");
1771
+ },
1772
+ title: "Mover a la izquierda",
1773
+ children: "\u25C0"
1774
+ }
1775
+ ),
1776
+ editingId === floor.id ? /* @__PURE__ */ jsxRuntime.jsx(
1777
+ "input",
1778
+ {
1779
+ ref: inputRef,
1780
+ value: editValue,
1781
+ onChange: (e) => setEditValue(e.target.value),
1782
+ onBlur: commitEdit,
1783
+ onKeyDown: handleKeyDown,
1784
+ onClick: (e) => e.stopPropagation(),
1785
+ className: "w-24 border border-blue-400 rounded px-1 text-xs outline-none"
1786
+ }
1787
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1788
+ "span",
1789
+ {
1790
+ onDoubleClick: (e) => {
1791
+ e.stopPropagation();
1792
+ startEditing(floor);
1793
+ },
1794
+ className: "select-none",
1795
+ children: floor.name
1796
+ }
1797
+ ),
1798
+ !readOnly && isActive && canMoveRight && /* @__PURE__ */ jsxRuntime.jsx(
1799
+ "button",
1800
+ {
1801
+ className: "text-slate-400 hover:text-slate-700 px-0.5 leading-none",
1802
+ onClick: (e) => {
1803
+ e.stopPropagation();
1804
+ onReorder(floor.id, "right");
1805
+ },
1806
+ title: "Mover a la derecha",
1807
+ children: "\u25B6"
1808
+ }
1809
+ ),
1810
+ !readOnly && floors.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
1811
+ "button",
1812
+ {
1813
+ className: "text-slate-400 hover:text-red-500 px-0.5 leading-none",
1814
+ onClick: (e) => {
1815
+ e.stopPropagation();
1816
+ onDelete(floor.id);
1817
+ },
1818
+ title: "Eliminar planta",
1819
+ children: "\xD7"
1820
+ }
1821
+ )
1822
+ ]
1823
+ },
1824
+ floor.id
1825
+ );
1826
+ }),
1827
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
1828
+ "button",
1829
+ {
1830
+ className: "flex items-center justify-center w-6 h-6 rounded border border-dashed border-slate-300 text-slate-400 hover:border-blue-400 hover:text-blue-500 transition-colors shrink-0",
1831
+ onClick: onAdd,
1832
+ title: "A\xF1adir planta",
1833
+ children: "+"
1834
+ }
1835
+ )
1836
+ ] });
1837
+ }
1838
+ function useHistory(initial) {
1839
+ const [history, setHistory] = react.useState({
1840
+ past: [],
1841
+ present: initial,
1842
+ future: []
1843
+ });
1844
+ const push = react.useCallback((next) => {
1845
+ setHistory((h) => ({
1846
+ past: [...h.past.slice(-49), h.present],
1847
+ present: next,
1848
+ future: []
1849
+ }));
1850
+ }, []);
1851
+ const replace = react.useCallback((next) => {
1852
+ setHistory((h) => ({ ...h, present: next }));
1853
+ }, []);
1854
+ const undo = react.useCallback(() => {
1855
+ setHistory((h) => {
1856
+ if (h.past.length === 0) return h;
1857
+ const previous = h.past[h.past.length - 1];
1858
+ return {
1859
+ past: h.past.slice(0, -1),
1860
+ present: previous,
1861
+ future: [h.present, ...h.future]
1862
+ };
1863
+ });
1864
+ }, []);
1865
+ const redo = react.useCallback(() => {
1866
+ setHistory((h) => {
1867
+ if (h.future.length === 0) return h;
1868
+ const next = h.future[0];
1869
+ return {
1870
+ past: [...h.past, h.present],
1871
+ present: next,
1872
+ future: h.future.slice(1)
1873
+ };
1874
+ });
1875
+ }, []);
1876
+ return {
1877
+ map: history.present,
1878
+ canUndo: history.past.length > 0,
1879
+ canRedo: history.future.length > 0,
1880
+ push,
1881
+ replace,
1882
+ undo,
1883
+ redo
1884
+ };
1885
+ }
1886
+ function useSelection() {
1887
+ const [selectedIds, setSelectedIds] = react.useState(/* @__PURE__ */ new Set());
1888
+ const select = react.useCallback((id, multi = false) => {
1889
+ setSelectedIds((prev) => {
1890
+ if (multi) {
1891
+ const next = new Set(prev);
1892
+ if (next.has(id)) next.delete(id);
1893
+ else next.add(id);
1894
+ return next;
1895
+ }
1896
+ if (prev.size === 1 && prev.has(id)) return prev;
1897
+ return /* @__PURE__ */ new Set([id]);
1898
+ });
1899
+ }, []);
1900
+ const selectSet = react.useCallback((ids) => {
1901
+ setSelectedIds(new Set(ids));
1902
+ }, []);
1903
+ const clear = react.useCallback(() => {
1904
+ setSelectedIds((prev) => prev.size === 0 ? prev : /* @__PURE__ */ new Set());
1905
+ }, []);
1906
+ const isSelected = react.useCallback(
1907
+ (id) => selectedIds.has(id),
1908
+ [selectedIds]
1909
+ );
1910
+ return { selectedIds, select, selectSet, clear, isSelected };
1911
+ }
1912
+
1913
+ // src/components/VenueMapEditor/utils/idGen.ts
1914
+ var genId = () => crypto.randomUUID();
1915
+ function pointInPolygon(px, py, pts) {
1916
+ let inside = false;
1917
+ for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
1918
+ const [xi, yi] = pts[i], [xj, yj] = pts[j];
1919
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi) inside = !inside;
1920
+ }
1921
+ return inside;
1922
+ }
1923
+ function clampPointToPolygon(px, py, pts) {
1924
+ let bestDist = Infinity, bx = pts[0][0], by = pts[0][1];
1925
+ for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
1926
+ const [ax, ay] = pts[j], [bex, bey] = pts[i];
1927
+ const dx = bex - ax, dy = bey - ay;
1928
+ const len2 = dx * dx + dy * dy;
1929
+ const t = len2 > 0 ? Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2)) : 0;
1930
+ const nx = ax + t * dx, ny = ay + t * dy;
1931
+ const dist = (px - nx) ** 2 + (py - ny) ** 2;
1932
+ if (dist < bestDist) {
1933
+ bestDist = dist;
1934
+ bx = nx;
1935
+ by = ny;
1936
+ }
1937
+ }
1938
+ return { x: bx, y: by };
1939
+ }
1940
+ function clampToFloor(x, y, w, h, area) {
1941
+ const s = Math.min(w, h);
1942
+ const hs = s / 2;
1943
+ const cx = x + w / 2;
1944
+ const cy = y + h / 2;
1945
+ if (area.shape === "rect") {
1946
+ const ax = area.x ?? 0;
1947
+ const ay = area.y ?? 0;
1948
+ const aw = area.width ?? 0;
1949
+ const ah = area.height ?? 0;
1950
+ const ncx = aw >= s ? Math.max(ax + hs, Math.min(ax + aw - hs, cx)) : ax + aw / 2;
1951
+ const ncy = ah >= s ? Math.max(ay + hs, Math.min(ay + ah - hs, cy)) : ay + ah / 2;
1952
+ return { x: ncx - w / 2, y: ncy - h / 2 };
1953
+ }
1954
+ if (area.shape === "polygon") {
1955
+ const pts = area.points ?? [];
1956
+ if (pts.length < 3) return { x, y };
1957
+ if (pointInPolygon(cx, cy, pts)) return { x, y };
1958
+ const clamped = clampPointToPolygon(cx, cy, pts);
1959
+ return { x: clamped.x - w / 2, y: clamped.y - h / 2 };
1960
+ }
1961
+ return { x, y };
1962
+ }
1963
+ function createDefaultMap() {
1964
+ return {
1965
+ id: genId(),
1966
+ name: "Nuevo mapa",
1967
+ floors: [
1968
+ {
1969
+ id: genId(),
1970
+ name: "Planta 1",
1971
+ order: 0,
1972
+ area: { shape: "rect", x: 60, y: 60, width: 600, height: 400 },
1973
+ wallNodes: [],
1974
+ walls: [],
1975
+ elements: []
1976
+ }
1977
+ ]
1978
+ };
1979
+ }
1980
+ var EMPTY_DOMAIN_CONFIG = { id: "__empty__", name: "", elementTypes: [] };
1981
+ function updateFloor(map, updatedFloor) {
1982
+ return {
1983
+ ...map,
1984
+ floors: map.floors.map((f) => f.id === updatedFloor.id ? updatedFloor : f)
1985
+ };
1986
+ }
1987
+ function rectToPolygon(area) {
1988
+ const ax = area.x ?? 0;
1989
+ const ay = area.y ?? 0;
1990
+ const aw = area.width ?? 400;
1991
+ const ah = area.height ?? 300;
1992
+ return {
1993
+ shape: "polygon",
1994
+ points: [
1995
+ [ax, ay],
1996
+ [ax + aw, ay],
1997
+ [ax + aw, ay + ah],
1998
+ [ax, ay + ah]
1999
+ ]
2000
+ };
2001
+ }
2002
+ function polygonToRect(area) {
2003
+ const pts = area.points ?? [];
2004
+ if (pts.length === 0) return { shape: "rect", x: 60, y: 60, width: 400, height: 300 };
2005
+ const xs = pts.map((p) => p[0]);
2006
+ const ys = pts.map((p) => p[1]);
2007
+ const minX = Math.min(...xs);
2008
+ const minY = Math.min(...ys);
2009
+ const maxX = Math.max(...xs);
2010
+ const maxY = Math.max(...ys);
2011
+ return {
2012
+ shape: "rect",
2013
+ x: minX,
2014
+ y: minY,
2015
+ width: maxX - minX,
2016
+ height: maxY - minY
2017
+ };
2018
+ }
2019
+ function VenueMapEditor({
2020
+ domainConfig = EMPTY_DOMAIN_CONFIG,
2021
+ initialMap,
2022
+ onChange,
2023
+ width = "100%",
2024
+ height = "600px",
2025
+ gridSize = 20,
2026
+ showGrid: showGridProp = true,
2027
+ snapToGrid: snapEnabled = false,
2028
+ readOnly = false,
2029
+ fixed = false,
2030
+ elementStatus,
2031
+ onElementClick,
2032
+ onElementTypeClick
2033
+ }) {
2034
+ const initialMapRef = react.useRef(initialMap ?? createDefaultMap());
2035
+ const { map, canUndo, canRedo, push, replace, undo, redo } = useHistory(
2036
+ initialMapRef.current
2037
+ );
2038
+ const { selectedIds, select, selectSet, clear: clearSelection } = useSelection();
2039
+ const [activeFloorId, setActiveFloorId] = react.useState(
2040
+ () => initialMapRef.current.floors[0]?.id ?? ""
2041
+ );
2042
+ const [tool, setTool] = react.useState("SELECT");
2043
+ const [showGrid, setShowGrid] = react.useState(showGridProp);
2044
+ const [zoom, setZoom] = react.useState(1);
2045
+ const [activePlaceTypeId, setActivePlaceTypeId] = react.useState(null);
2046
+ const zoomByRef = react.useRef(() => void 0);
2047
+ const resetViewRef = react.useRef(() => void 0);
2048
+ const importInputRef = react.useRef(null);
2049
+ const libraryInputRef = react.useRef(null);
2050
+ const buildTypeDefs = react.useCallback(() => {
2051
+ const m = new Map(domainConfig.elementTypes.map((t) => [t.id, t]));
2052
+ const libs = map.libraries ?? {};
2053
+ for (const group of Object.values(libs)) {
2054
+ for (const t of group.objects) {
2055
+ if (!m.has(t.id)) m.set(t.id, t);
2056
+ }
2057
+ }
2058
+ return m;
2059
+ }, [domainConfig, map.libraries]);
2060
+ const elementTypeDefs = react.useRef(buildTypeDefs());
2061
+ react.useEffect(() => {
2062
+ elementTypeDefs.current = buildTypeDefs();
2063
+ }, [buildTypeDefs]);
2064
+ const paletteGroups = react.useMemo(() => {
2065
+ const groups = [];
2066
+ if (domainConfig.elementTypes.length > 0) {
2067
+ groups.push({ id: domainConfig.id, name: domainConfig.name, types: domainConfig.elementTypes, isBase: true });
2068
+ }
2069
+ const libs = map.libraries ?? {};
2070
+ for (const [gid, group] of Object.entries(libs)) {
2071
+ groups.push({ id: gid, name: group.name, types: group.objects, isBase: false });
2072
+ }
2073
+ return groups;
2074
+ }, [domainConfig, map.libraries]);
2075
+ react.useEffect(() => {
2076
+ if (activePlaceTypeId) return;
2077
+ const firstType = paletteGroups[0]?.types[0];
2078
+ if (firstType) setActivePlaceTypeId(firstType.id);
2079
+ }, [paletteGroups, activePlaceTypeId]);
2080
+ const lastEmittedMap = react.useRef(void 0);
2081
+ const prevInitial = react.useRef(initialMap);
2082
+ react.useEffect(() => {
2083
+ lastEmittedMap.current = map;
2084
+ onChange?.(map);
2085
+ }, [map, onChange]);
2086
+ react.useEffect(() => {
2087
+ if (!initialMap) return;
2088
+ if (initialMap === prevInitial.current) return;
2089
+ prevInitial.current = initialMap;
2090
+ if (initialMap === lastEmittedMap.current) return;
2091
+ push(initialMap);
2092
+ setActiveFloorId(initialMap.floors[0]?.id ?? "");
2093
+ }, [initialMap, push]);
2094
+ const activeFloor = map.floors.find((f) => f.id === activeFloorId) ?? map.floors[0];
2095
+ const replaceFloor = react.useCallback(
2096
+ (floor) => replace(updateFloor(map, floor)),
2097
+ [map, replace]
2098
+ );
2099
+ const pushFloor = react.useCallback(
2100
+ (floor) => push(updateFloor(map, floor)),
2101
+ [map, push]
2102
+ );
2103
+ const handleAreaResize = react.useCallback(
2104
+ (updatedFloor) => replaceFloor(updatedFloor),
2105
+ [replaceFloor]
2106
+ );
2107
+ const handleAreaResizeCommit = react.useCallback(
2108
+ (updatedFloor) => pushFloor(updatedFloor),
2109
+ [pushFloor]
2110
+ );
2111
+ const handleAreaMove = react.useCallback(
2112
+ (dx, dy) => {
2113
+ if (!activeFloor) return;
2114
+ const area = activeFloor.area;
2115
+ const newArea = area.shape === "polygon" ? { ...area, points: (area.points ?? []).map(([x, y]) => [x + dx, y + dy]) } : { ...area, x: (area.x ?? 0) + dx, y: (area.y ?? 0) + dy };
2116
+ replaceFloor({
2117
+ ...activeFloor,
2118
+ area: newArea,
2119
+ wallNodes: activeFloor.wallNodes.map((n) => ({ ...n, x: n.x + dx, y: n.y + dy })),
2120
+ elements: activeFloor.elements.map((el) => ({ ...el, x: el.x + dx, y: el.y + dy }))
2121
+ });
2122
+ },
2123
+ [activeFloor, replaceFloor]
2124
+ );
2125
+ const handleToggleAreaShape = react.useCallback(() => {
2126
+ if (!activeFloor) return;
2127
+ const { area } = activeFloor;
2128
+ const newArea = area.shape === "polygon" ? polygonToRect(area) : rectToPolygon(area);
2129
+ pushFloor({ ...activeFloor, area: newArea });
2130
+ }, [activeFloor, pushFloor]);
2131
+ const handleAddFloor = react.useCallback(() => {
2132
+ const maxOrder = map.floors.reduce((m, f) => Math.max(m, f.order), -1);
2133
+ const newFloor = {
2134
+ id: genId(),
2135
+ name: `Planta ${map.floors.length + 1}`,
2136
+ order: maxOrder + 1,
2137
+ area: { shape: "rect", x: 60, y: 60, width: 600, height: 400 },
2138
+ wallNodes: [],
2139
+ walls: [],
2140
+ elements: []
2141
+ };
2142
+ const newMap = { ...map, floors: [...map.floors, newFloor] };
2143
+ push(newMap);
2144
+ setActiveFloorId(newFloor.id);
2145
+ }, [map, push]);
2146
+ const handleRenameFloor = react.useCallback(
2147
+ (id, name) => {
2148
+ const floor = map.floors.find((f) => f.id === id);
2149
+ if (!floor) return;
2150
+ push(updateFloor(map, { ...floor, name }));
2151
+ },
2152
+ [map, push]
2153
+ );
2154
+ const handleDeleteFloor = react.useCallback(
2155
+ (id) => {
2156
+ if (map.floors.length <= 1) return;
2157
+ const remaining = map.floors.filter((f) => f.id !== id);
2158
+ const newMap = { ...map, floors: remaining };
2159
+ push(newMap);
2160
+ if (activeFloorId === id) {
2161
+ setActiveFloorId(remaining[0]?.id ?? "");
2162
+ }
2163
+ },
2164
+ [map, push, activeFloorId]
2165
+ );
2166
+ const handleReorderFloor = react.useCallback(
2167
+ (id, direction) => {
2168
+ const sorted = map.floors.slice().sort((a2, b2) => a2.order - b2.order);
2169
+ const idx = sorted.findIndex((f) => f.id === id);
2170
+ if (idx < 0) return;
2171
+ const swapIdx = direction === "left" ? idx - 1 : idx + 1;
2172
+ if (swapIdx < 0 || swapIdx >= sorted.length) return;
2173
+ const a = sorted[idx];
2174
+ const b = sorted[swapIdx];
2175
+ const updatedFloors = map.floors.map((f) => {
2176
+ if (f.id === a.id) return { ...f, order: b.order };
2177
+ if (f.id === b.id) return { ...f, order: a.order };
2178
+ return f;
2179
+ });
2180
+ push({ ...map, floors: updatedFloors });
2181
+ },
2182
+ [map, push]
2183
+ );
2184
+ const handleExportMap = react.useCallback(() => {
2185
+ const blob = new Blob([JSON.stringify(map, null, 2)], { type: "application/json" });
2186
+ const url = URL.createObjectURL(blob);
2187
+ const a = document.createElement("a");
2188
+ a.href = url;
2189
+ a.download = `${map.name || "mapa"}.json`;
2190
+ a.click();
2191
+ URL.revokeObjectURL(url);
2192
+ }, [map]);
2193
+ const handleImportMap = react.useCallback(
2194
+ (file) => {
2195
+ const reader = new FileReader();
2196
+ reader.onload = (e) => {
2197
+ try {
2198
+ const parsed = JSON.parse(e.target?.result);
2199
+ push(parsed);
2200
+ setActiveFloorId(parsed.floors[0]?.id ?? "");
2201
+ } catch {
2202
+ }
2203
+ };
2204
+ reader.readAsText(file);
2205
+ },
2206
+ [push]
2207
+ );
2208
+ const handleLoadLibrary = react.useCallback(
2209
+ (file) => {
2210
+ const reader = new FileReader();
2211
+ reader.onload = (e) => {
2212
+ try {
2213
+ const parsed = JSON.parse(e.target?.result);
2214
+ const merged = { ...map.libraries ?? {}, ...parsed };
2215
+ push({ ...map, libraries: merged });
2216
+ } catch {
2217
+ }
2218
+ };
2219
+ reader.readAsText(file);
2220
+ },
2221
+ [map, push]
2222
+ );
2223
+ const handleRemoveLibraryGroup = react.useCallback(
2224
+ (groupId) => {
2225
+ const libs = { ...map.libraries ?? {} };
2226
+ delete libs[groupId];
2227
+ push({ ...map, libraries: Object.keys(libs).length > 0 ? libs : void 0 });
2228
+ },
2229
+ [map, push]
2230
+ );
2231
+ const DEFAULT_WALL_THICKNESS = 8;
2232
+ const handleAddWall = react.useCallback(
2233
+ (x1, y1, x2, y2, snapStartId, snapEndId) => {
2234
+ if (!activeFloor) return;
2235
+ const nodes = [...activeFloor.wallNodes];
2236
+ let nodeAId;
2237
+ if (snapStartId) {
2238
+ nodeAId = snapStartId;
2239
+ } else {
2240
+ const n = { id: genId(), x: x1, y: y1 };
2241
+ nodes.push(n);
2242
+ nodeAId = n.id;
2243
+ }
2244
+ let nodeBId;
2245
+ if (snapEndId) {
2246
+ nodeBId = snapEndId;
2247
+ } else {
2248
+ const n = { id: genId(), x: x2, y: y2 };
2249
+ nodes.push(n);
2250
+ nodeBId = n.id;
2251
+ }
2252
+ const newWall = {
2253
+ id: genId(),
2254
+ nodeAId,
2255
+ nodeBId,
2256
+ thickness: DEFAULT_WALL_THICKNESS,
2257
+ material: "concrete"
2258
+ };
2259
+ pushFloor({ ...activeFloor, wallNodes: nodes, walls: [...activeFloor.walls, newWall] });
2260
+ },
2261
+ [activeFloor, pushFloor, DEFAULT_WALL_THICKNESS]
2262
+ );
2263
+ const handleDeleteWall = react.useCallback(
2264
+ (wallId) => {
2265
+ if (!activeFloor) return;
2266
+ const remainingWalls = activeFloor.walls.filter((w) => w.id !== wallId);
2267
+ const usedNodeIds = new Set(remainingWalls.flatMap((w) => [w.nodeAId, w.nodeBId]));
2268
+ const remainingNodes = activeFloor.wallNodes.filter((n) => usedNodeIds.has(n.id));
2269
+ pushFloor({ ...activeFloor, walls: remainingWalls, wallNodes: remainingNodes });
2270
+ },
2271
+ [activeFloor, pushFloor]
2272
+ );
2273
+ const handleMoveElement = react.useCallback(
2274
+ (id, x, y) => {
2275
+ if (!activeFloor) return;
2276
+ const el = activeFloor.elements.find((e) => e.id === id);
2277
+ if (!el) return;
2278
+ const { x: cx, y: cy } = clampToFloor(x, y, el.width, el.height, activeFloor.area);
2279
+ replaceFloor({
2280
+ ...activeFloor,
2281
+ elements: activeFloor.elements.map((e) => e.id === id ? { ...e, x: cx, y: cy } : e)
2282
+ });
2283
+ },
2284
+ [activeFloor, replaceFloor]
2285
+ );
2286
+ const handleMoveCommit = react.useCallback(
2287
+ (id, x, y) => {
2288
+ if (!activeFloor) return;
2289
+ const el = activeFloor.elements.find((e) => e.id === id);
2290
+ if (!el) return;
2291
+ const { x: cx, y: cy } = clampToFloor(x, y, el.width, el.height, activeFloor.area);
2292
+ pushFloor({
2293
+ ...activeFloor,
2294
+ elements: activeFloor.elements.map((e) => e.id === id ? { ...e, x: cx, y: cy } : e)
2295
+ });
2296
+ },
2297
+ [activeFloor, pushFloor]
2298
+ );
2299
+ const handleResizeElement = react.useCallback(
2300
+ (id, x, y, w, h) => {
2301
+ if (!activeFloor) return;
2302
+ replaceFloor({
2303
+ ...activeFloor,
2304
+ elements: activeFloor.elements.map(
2305
+ (el) => el.id === id ? { ...el, x, y, width: w, height: h } : el
2306
+ )
2307
+ });
2308
+ },
2309
+ [activeFloor, replaceFloor]
2310
+ );
2311
+ const handleResizeCommit = react.useCallback(
2312
+ (id, x, y, w, h) => {
2313
+ if (!activeFloor) return;
2314
+ pushFloor({
2315
+ ...activeFloor,
2316
+ elements: activeFloor.elements.map(
2317
+ (el) => el.id === id ? { ...el, x, y, width: w, height: h } : el
2318
+ )
2319
+ });
2320
+ },
2321
+ [activeFloor, pushFloor]
2322
+ );
2323
+ const handleRotateElement = react.useCallback(
2324
+ (id, rotation) => {
2325
+ if (!activeFloor) return;
2326
+ replaceFloor({
2327
+ ...activeFloor,
2328
+ elements: activeFloor.elements.map(
2329
+ (el) => el.id === id ? { ...el, rotation } : el
2330
+ )
2331
+ });
2332
+ },
2333
+ [activeFloor, replaceFloor]
2334
+ );
2335
+ const handleRotateCommit = react.useCallback(
2336
+ (id, rotation) => {
2337
+ if (!activeFloor) return;
2338
+ pushFloor({
2339
+ ...activeFloor,
2340
+ elements: activeFloor.elements.map(
2341
+ (el) => el.id === id ? { ...el, rotation } : el
2342
+ )
2343
+ });
2344
+ },
2345
+ [activeFloor, pushFloor]
2346
+ );
2347
+ const handleDeleteElement = react.useCallback(
2348
+ (id) => {
2349
+ if (!activeFloor) return;
2350
+ clearSelection();
2351
+ pushFloor({
2352
+ ...activeFloor,
2353
+ elements: activeFloor.elements.filter((el) => el.id !== id)
2354
+ });
2355
+ },
2356
+ [activeFloor, pushFloor, clearSelection]
2357
+ );
2358
+ const handleDeleteElements = react.useCallback(
2359
+ (ids) => {
2360
+ if (!activeFloor) return;
2361
+ const idSet = new Set(ids);
2362
+ clearSelection();
2363
+ pushFloor({
2364
+ ...activeFloor,
2365
+ elements: activeFloor.elements.filter((el) => !idSet.has(el.id))
2366
+ });
2367
+ },
2368
+ [activeFloor, pushFloor, clearSelection]
2369
+ );
2370
+ const handleDuplicateElements = react.useCallback(
2371
+ (ids) => {
2372
+ if (!activeFloor) return;
2373
+ const idSet = new Set(ids);
2374
+ const copies = activeFloor.elements.filter((el) => idSet.has(el.id)).map((el) => ({ ...el, id: genId(), x: el.x + 20, y: el.y + 20 }));
2375
+ const newFloor = { ...activeFloor, elements: [...activeFloor.elements, ...copies] };
2376
+ pushFloor(newFloor);
2377
+ selectSet(copies.map((c) => c.id));
2378
+ },
2379
+ [activeFloor, pushFloor, selectSet]
2380
+ );
2381
+ const handlePlaceElement = react.useCallback(
2382
+ (canvasX, canvasY) => {
2383
+ if (!activeFloor || !activePlaceTypeId) return;
2384
+ const typeDef = elementTypeDefs.current.get(activePlaceTypeId);
2385
+ if (!typeDef) return;
2386
+ const { area } = activeFloor;
2387
+ if (area.shape === "rect") {
2388
+ const ax = area.x ?? 0, ay = area.y ?? 0;
2389
+ const aw = area.width ?? 0, ah = area.height ?? 0;
2390
+ if (canvasX < ax || canvasX > ax + aw || canvasY < ay || canvasY > ay + ah) return;
2391
+ } else if (area.shape === "polygon") {
2392
+ const pts = area.points ?? [];
2393
+ if (pts.length >= 3 && !pointInPolygon(canvasX, canvasY, pts)) return;
2394
+ }
2395
+ const { x, y } = clampToFloor(
2396
+ canvasX - typeDef.defaultWidth / 2,
2397
+ canvasY - typeDef.defaultHeight / 2,
2398
+ typeDef.defaultWidth,
2399
+ typeDef.defaultHeight,
2400
+ area
2401
+ );
2402
+ const newEl = {
2403
+ id: genId(),
2404
+ type: activePlaceTypeId,
2405
+ x,
2406
+ y,
2407
+ width: typeDef.defaultWidth,
2408
+ height: typeDef.defaultHeight,
2409
+ rotation: 0
2410
+ };
2411
+ pushFloor({ ...activeFloor, elements: [...activeFloor.elements, newEl] });
2412
+ select(newEl.id, false);
2413
+ },
2414
+ [activeFloor, activePlaceTypeId, pushFloor, select]
2415
+ );
2416
+ const handleChangeLabel = react.useCallback(
2417
+ (id, label) => {
2418
+ if (!activeFloor) return;
2419
+ pushFloor({
2420
+ ...activeFloor,
2421
+ elements: activeFloor.elements.map(
2422
+ (el) => el.id === id ? { ...el, label } : el
2423
+ )
2424
+ });
2425
+ },
2426
+ [activeFloor, pushFloor]
2427
+ );
2428
+ const handleChangeGeometry = react.useCallback(
2429
+ (id, x, y, w, h, r) => {
2430
+ if (!activeFloor) return;
2431
+ pushFloor({
2432
+ ...activeFloor,
2433
+ elements: activeFloor.elements.map(
2434
+ (el) => el.id === id ? { ...el, x, y, width: w, height: h, rotation: r } : el
2435
+ )
2436
+ });
2437
+ },
2438
+ [activeFloor, pushFloor]
2439
+ );
2440
+ const handleViewerElementClick = react.useCallback(
2441
+ (id) => {
2442
+ const el = activeFloor?.elements.find((e) => e.id === id);
2443
+ if (!el) return;
2444
+ const typeHandler = onElementTypeClick?.[el.type];
2445
+ if (typeHandler) {
2446
+ typeHandler(el);
2447
+ } else {
2448
+ onElementClick?.(el);
2449
+ }
2450
+ },
2451
+ [activeFloor, onElementClick, onElementTypeClick]
2452
+ );
2453
+ const hasViewerHandlers = !!(onElementClick || onElementTypeClick);
2454
+ const statusMap = react.useMemo(() => {
2455
+ const m = /* @__PURE__ */ new Map();
2456
+ (elementStatus ?? []).forEach((s) => {
2457
+ if (s.status === "occupied") m.set(s.elementId, "#fca5a5");
2458
+ else if (s.status === "reserved") m.set(s.elementId, "#fde68a");
2459
+ else if (s.status === "disabled") m.set(s.elementId, "#d1d5db");
2460
+ });
2461
+ return m;
2462
+ }, [elementStatus]);
2463
+ const selectedElements = activeFloor ? activeFloor.elements.filter((el) => selectedIds.has(el.id)) : [];
2464
+ react.useEffect(() => {
2465
+ const onKey = (e) => {
2466
+ const tag = e.target.tagName;
2467
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
2468
+ const ctrl = e.ctrlKey || e.metaKey;
2469
+ if (ctrl && e.key === "z") {
2470
+ e.preventDefault();
2471
+ undo();
2472
+ return;
2473
+ }
2474
+ if (ctrl && (e.key === "y" || e.key === "Y")) {
2475
+ e.preventDefault();
2476
+ redo();
2477
+ return;
2478
+ }
2479
+ if (ctrl && (e.key === "d" || e.key === "D")) {
2480
+ e.preventDefault();
2481
+ if (selectedIds.size > 0) handleDuplicateElements([...selectedIds]);
2482
+ return;
2483
+ }
2484
+ if (e.key === "Delete" || e.key === "Backspace") {
2485
+ if (selectedIds.size > 0) handleDeleteElements([...selectedIds]);
2486
+ return;
2487
+ }
2488
+ switch (e.key) {
2489
+ case "v":
2490
+ case "V":
2491
+ setTool("SELECT");
2492
+ break;
2493
+ case "h":
2494
+ case "H":
2495
+ setTool("PAN");
2496
+ break;
2497
+ case "w":
2498
+ case "W":
2499
+ setTool("WALL");
2500
+ break;
2501
+ case "p":
2502
+ case "P":
2503
+ setTool("PLACE");
2504
+ break;
2505
+ case "e":
2506
+ case "E":
2507
+ setTool("ERASE");
2508
+ break;
2509
+ case "Escape":
2510
+ setTool("SELECT");
2511
+ break;
2512
+ case "+":
2513
+ case "=":
2514
+ zoomByRef.current(1.2);
2515
+ break;
2516
+ case "-":
2517
+ case "_":
2518
+ zoomByRef.current(1 / 1.2);
2519
+ break;
2520
+ }
2521
+ };
2522
+ window.addEventListener("keydown", onKey);
2523
+ return () => window.removeEventListener("keydown", onKey);
2524
+ }, [undo, redo, selectedIds, handleDuplicateElements, handleDeleteElements]);
2525
+ const effectiveReadOnly = readOnly || fixed;
2526
+ const effectiveTool = fixed ? "PAN" : tool;
2527
+ const containerStyle = {
2528
+ width,
2529
+ height,
2530
+ display: "flex",
2531
+ flexDirection: "column",
2532
+ overflow: "hidden",
2533
+ border: "1px solid #e2e8f0",
2534
+ borderRadius: "0.5rem",
2535
+ background: "#fff",
2536
+ fontFamily: "system-ui, sans-serif"
2537
+ };
2538
+ const activeAreaShape = activeFloor?.area.shape;
2539
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: containerStyle, children: [
2540
+ /* @__PURE__ */ jsxRuntime.jsx(
2541
+ "input",
2542
+ {
2543
+ ref: importInputRef,
2544
+ type: "file",
2545
+ accept: ".json",
2546
+ className: "hidden",
2547
+ onChange: (e) => {
2548
+ const f = e.target.files?.[0];
2549
+ if (f) handleImportMap(f);
2550
+ e.target.value = "";
2551
+ }
2552
+ }
2553
+ ),
2554
+ /* @__PURE__ */ jsxRuntime.jsx(
2555
+ "input",
2556
+ {
2557
+ ref: libraryInputRef,
2558
+ type: "file",
2559
+ accept: ".json",
2560
+ className: "hidden",
2561
+ onChange: (e) => {
2562
+ const f = e.target.files?.[0];
2563
+ if (f) handleLoadLibrary(f);
2564
+ e.target.value = "";
2565
+ }
2566
+ }
2567
+ ),
2568
+ !effectiveReadOnly && /* @__PURE__ */ jsxRuntime.jsx(
2569
+ Toolbar,
2570
+ {
2571
+ tool,
2572
+ onToolChange: (t) => {
2573
+ setTool(t);
2574
+ if (t !== "PLACE") clearSelection();
2575
+ },
2576
+ showGrid,
2577
+ onToggleGrid: () => setShowGrid((g) => !g),
2578
+ zoom,
2579
+ onZoomIn: () => zoomByRef.current(1.2),
2580
+ onZoomOut: () => zoomByRef.current(1 / 1.2),
2581
+ onResetView: () => resetViewRef.current(),
2582
+ canUndo,
2583
+ canRedo,
2584
+ onUndo: undo,
2585
+ onRedo: redo,
2586
+ paletteGroups,
2587
+ activePlaceTypeId,
2588
+ onActivePlaceTypeChange: setActivePlaceTypeId,
2589
+ areaShape: activeAreaShape,
2590
+ onToggleAreaShape: handleToggleAreaShape,
2591
+ onExportMap: handleExportMap,
2592
+ onImportMap: () => importInputRef.current?.click(),
2593
+ onLoadLibrary: () => libraryInputRef.current?.click(),
2594
+ onRemoveLibraryGroup: handleRemoveLibraryGroup
2595
+ }
2596
+ ),
2597
+ /* @__PURE__ */ jsxRuntime.jsx(
2598
+ FloorTabs,
2599
+ {
2600
+ floors: map.floors,
2601
+ activeFloorId,
2602
+ readOnly: effectiveReadOnly,
2603
+ onSelect: setActiveFloorId,
2604
+ onAdd: handleAddFloor,
2605
+ onRename: handleRenameFloor,
2606
+ onDelete: handleDeleteFloor,
2607
+ onReorder: handleReorderFloor
2608
+ }
2609
+ ),
2610
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [
2611
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-0 relative", children: activeFloor && /* @__PURE__ */ jsxRuntime.jsx(
2612
+ EditorCanvas,
2613
+ {
2614
+ floor: activeFloor,
2615
+ tool: effectiveTool,
2616
+ gridSize,
2617
+ showGrid,
2618
+ readOnly: effectiveReadOnly,
2619
+ snapEnabled,
2620
+ elementTypeDefs: elementTypeDefs.current,
2621
+ selectedIds,
2622
+ statusMap,
2623
+ onAreaResize: handleAreaResize,
2624
+ onAreaMove: handleAreaMove,
2625
+ onAreaResizeCommit: handleAreaResizeCommit,
2626
+ onSelectElement: select,
2627
+ onSelectSet: selectSet,
2628
+ onClearSelection: clearSelection,
2629
+ onMoveElement: handleMoveElement,
2630
+ onMoveCommit: handleMoveCommit,
2631
+ onResizeElement: handleResizeElement,
2632
+ onResizeCommit: handleResizeCommit,
2633
+ onRotateElement: handleRotateElement,
2634
+ onRotateCommit: handleRotateCommit,
2635
+ onDeleteElement: handleDeleteElement,
2636
+ onPlaceElement: handlePlaceElement,
2637
+ onAddWall: handleAddWall,
2638
+ onDeleteWall: handleDeleteWall,
2639
+ onViewerElementClick: hasViewerHandlers ? handleViewerElementClick : void 0,
2640
+ onZoomChange: setZoom,
2641
+ onRegisterZoomBy: (fn) => {
2642
+ zoomByRef.current = fn;
2643
+ },
2644
+ onRegisterResetView: (fn) => {
2645
+ resetViewRef.current = fn;
2646
+ }
2647
+ },
2648
+ activeFloor.id
2649
+ ) }),
2650
+ !effectiveReadOnly && selectedElements.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2651
+ PropertiesPanel,
2652
+ {
2653
+ elements: selectedElements,
2654
+ typeDefs: elementTypeDefs.current,
2655
+ onChangeLabel: handleChangeLabel,
2656
+ onChangeGeometry: handleChangeGeometry,
2657
+ onDelete: handleDeleteElements,
2658
+ onDuplicate: handleDuplicateElements
2659
+ }
2660
+ )
2661
+ ] })
2662
+ ] });
2663
+ }
2664
+ function VenueMapViewer({ elementStatus, onElementClick, ...rest }) {
2665
+ return /* @__PURE__ */ jsxRuntime.jsx(
2666
+ VenueMapEditor,
2667
+ {
2668
+ ...rest,
2669
+ fixed: true,
2670
+ elementStatus,
2671
+ onElementClick
2672
+ }
2673
+ );
2674
+ }
2675
+
2676
+ exports.VenueMapEditor = VenueMapEditor;
2677
+ exports.VenueMapViewer = VenueMapViewer;
2678
+ exports.findNearestNode = findNearestNode;
2679
+ exports.genId = genId;
2680
+ exports.snapPoint = snapPoint;
2681
+ exports.snapToGrid = snapToGrid;
2682
+ exports.usePanZoom = usePanZoom;
2683
+ //# sourceMappingURL=index.js.map
2684
+ //# sourceMappingURL=index.js.map