mythik-react 0.1.0

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 (244) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +4 -0
  3. package/README.md +83 -0
  4. package/dist/MythikApp.d.ts +61 -0
  5. package/dist/MythikApp.d.ts.map +1 -0
  6. package/dist/MythikApp.js +381 -0
  7. package/dist/MythikApp.js.map +1 -0
  8. package/dist/MythikRenderer.d.ts +31 -0
  9. package/dist/MythikRenderer.d.ts.map +1 -0
  10. package/dist/MythikRenderer.js +900 -0
  11. package/dist/MythikRenderer.js.map +1 -0
  12. package/dist/animation/index.d.ts +7 -0
  13. package/dist/animation/index.d.ts.map +1 -0
  14. package/dist/animation/index.js +5 -0
  15. package/dist/animation/index.js.map +1 -0
  16. package/dist/animation/stylesheet-singleton.d.ts +12 -0
  17. package/dist/animation/stylesheet-singleton.d.ts.map +1 -0
  18. package/dist/animation/stylesheet-singleton.js +107 -0
  19. package/dist/animation/stylesheet-singleton.js.map +1 -0
  20. package/dist/animation/useElementAnimations.d.ts +30 -0
  21. package/dist/animation/useElementAnimations.d.ts.map +1 -0
  22. package/dist/animation/useElementAnimations.js +254 -0
  23. package/dist/animation/useElementAnimations.js.map +1 -0
  24. package/dist/animation/usePrefersReducedMotion.d.ts +2 -0
  25. package/dist/animation/usePrefersReducedMotion.d.ts.map +1 -0
  26. package/dist/animation/usePrefersReducedMotion.js +29 -0
  27. package/dist/animation/usePrefersReducedMotion.js.map +1 -0
  28. package/dist/animation/useShapeAnimations.d.ts +21 -0
  29. package/dist/animation/useShapeAnimations.d.ts.map +1 -0
  30. package/dist/animation/useShapeAnimations.js +119 -0
  31. package/dist/animation/useShapeAnimations.js.map +1 -0
  32. package/dist/app-context.d.ts +15 -0
  33. package/dist/app-context.d.ts.map +1 -0
  34. package/dist/app-context.js +9 -0
  35. package/dist/app-context.js.map +1 -0
  36. package/dist/background/BackgroundLayer.d.ts +7 -0
  37. package/dist/background/BackgroundLayer.d.ts.map +1 -0
  38. package/dist/background/BackgroundLayer.js +50 -0
  39. package/dist/background/BackgroundLayer.js.map +1 -0
  40. package/dist/background/BackgroundStack.d.ts +19 -0
  41. package/dist/background/BackgroundStack.d.ts.map +1 -0
  42. package/dist/background/BackgroundStack.js +59 -0
  43. package/dist/background/BackgroundStack.js.map +1 -0
  44. package/dist/background/BlobLayer.d.ts +12 -0
  45. package/dist/background/BlobLayer.d.ts.map +1 -0
  46. package/dist/background/BlobLayer.js +60 -0
  47. package/dist/background/BlobLayer.js.map +1 -0
  48. package/dist/background/index.d.ts +3 -0
  49. package/dist/background/index.d.ts.map +1 -0
  50. package/dist/background/index.js +3 -0
  51. package/dist/background/index.js.map +1 -0
  52. package/dist/css-hover.d.ts +15 -0
  53. package/dist/css-hover.d.ts.map +1 -0
  54. package/dist/css-hover.js +51 -0
  55. package/dist/css-hover.js.map +1 -0
  56. package/dist/index.d.ts +10 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +11 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/primitives/accordion.d.ts +12 -0
  61. package/dist/primitives/accordion.d.ts.map +1 -0
  62. package/dist/primitives/accordion.js +25 -0
  63. package/dist/primitives/accordion.js.map +1 -0
  64. package/dist/primitives/area-chart.d.ts +14 -0
  65. package/dist/primitives/area-chart.d.ts.map +1 -0
  66. package/dist/primitives/area-chart.js +18 -0
  67. package/dist/primitives/area-chart.js.map +1 -0
  68. package/dist/primitives/audio-player.d.ts +9 -0
  69. package/dist/primitives/audio-player.d.ts.map +1 -0
  70. package/dist/primitives/audio-player.js +5 -0
  71. package/dist/primitives/audio-player.js.map +1 -0
  72. package/dist/primitives/bar-chart.d.ts +14 -0
  73. package/dist/primitives/bar-chart.d.ts.map +1 -0
  74. package/dist/primitives/bar-chart.js +22 -0
  75. package/dist/primitives/bar-chart.js.map +1 -0
  76. package/dist/primitives/box.d.ts +21 -0
  77. package/dist/primitives/box.d.ts.map +1 -0
  78. package/dist/primitives/box.js +54 -0
  79. package/dist/primitives/box.js.map +1 -0
  80. package/dist/primitives/button.d.ts +14 -0
  81. package/dist/primitives/button.d.ts.map +1 -0
  82. package/dist/primitives/button.js +28 -0
  83. package/dist/primitives/button.js.map +1 -0
  84. package/dist/primitives/camera.d.ts +15 -0
  85. package/dist/primitives/camera.d.ts.map +1 -0
  86. package/dist/primitives/camera.js +25 -0
  87. package/dist/primitives/camera.js.map +1 -0
  88. package/dist/primitives/checkbox.d.ts +12 -0
  89. package/dist/primitives/checkbox.d.ts.map +1 -0
  90. package/dist/primitives/checkbox.js +24 -0
  91. package/dist/primitives/checkbox.js.map +1 -0
  92. package/dist/primitives/divider.d.ts +9 -0
  93. package/dist/primitives/divider.d.ts.map +1 -0
  94. package/dist/primitives/divider.js +10 -0
  95. package/dist/primitives/divider.js.map +1 -0
  96. package/dist/primitives/drawer.d.ts +21 -0
  97. package/dist/primitives/drawer.d.ts.map +1 -0
  98. package/dist/primitives/drawer.js +38 -0
  99. package/dist/primitives/drawer.js.map +1 -0
  100. package/dist/primitives/file-upload.d.ts +27 -0
  101. package/dist/primitives/file-upload.d.ts.map +1 -0
  102. package/dist/primitives/file-upload.js +225 -0
  103. package/dist/primitives/file-upload.js.map +1 -0
  104. package/dist/primitives/grid.d.ts +13 -0
  105. package/dist/primitives/grid.d.ts.map +1 -0
  106. package/dist/primitives/grid.js +13 -0
  107. package/dist/primitives/grid.js.map +1 -0
  108. package/dist/primitives/icon.d.ts +22 -0
  109. package/dist/primitives/icon.d.ts.map +1 -0
  110. package/dist/primitives/icon.js +52 -0
  111. package/dist/primitives/icon.js.map +1 -0
  112. package/dist/primitives/image.d.ts +13 -0
  113. package/dist/primitives/image.d.ts.map +1 -0
  114. package/dist/primitives/image.js +38 -0
  115. package/dist/primitives/image.js.map +1 -0
  116. package/dist/primitives/index.d.ts +57 -0
  117. package/dist/primitives/index.d.ts.map +1 -0
  118. package/dist/primitives/index.js +106 -0
  119. package/dist/primitives/index.js.map +1 -0
  120. package/dist/primitives/input.d.ts +32 -0
  121. package/dist/primitives/input.d.ts.map +1 -0
  122. package/dist/primitives/input.js +192 -0
  123. package/dist/primitives/input.js.map +1 -0
  124. package/dist/primitives/kanban-board.d.ts +13 -0
  125. package/dist/primitives/kanban-board.d.ts.map +1 -0
  126. package/dist/primitives/kanban-board.js +5 -0
  127. package/dist/primitives/kanban-board.js.map +1 -0
  128. package/dist/primitives/line-chart.d.ts +14 -0
  129. package/dist/primitives/line-chart.d.ts.map +1 -0
  130. package/dist/primitives/line-chart.js +17 -0
  131. package/dist/primitives/line-chart.js.map +1 -0
  132. package/dist/primitives/list.d.ts +13 -0
  133. package/dist/primitives/list.d.ts.map +1 -0
  134. package/dist/primitives/list.js +10 -0
  135. package/dist/primitives/list.js.map +1 -0
  136. package/dist/primitives/modal.d.ts +20 -0
  137. package/dist/primitives/modal.d.ts.map +1 -0
  138. package/dist/primitives/modal.js +60 -0
  139. package/dist/primitives/modal.js.map +1 -0
  140. package/dist/primitives/pie-chart.d.ts +15 -0
  141. package/dist/primitives/pie-chart.d.ts.map +1 -0
  142. package/dist/primitives/pie-chart.js +36 -0
  143. package/dist/primitives/pie-chart.js.map +1 -0
  144. package/dist/primitives/screen-outlet.d.ts +9 -0
  145. package/dist/primitives/screen-outlet.d.ts.map +1 -0
  146. package/dist/primitives/screen-outlet.js +92 -0
  147. package/dist/primitives/screen-outlet.js.map +1 -0
  148. package/dist/primitives/screen.d.ts +9 -0
  149. package/dist/primitives/screen.d.ts.map +1 -0
  150. package/dist/primitives/screen.js +10 -0
  151. package/dist/primitives/screen.js.map +1 -0
  152. package/dist/primitives/scroll.d.ts +11 -0
  153. package/dist/primitives/scroll.d.ts.map +1 -0
  154. package/dist/primitives/scroll.js +10 -0
  155. package/dist/primitives/scroll.js.map +1 -0
  156. package/dist/primitives/select.d.ts +19 -0
  157. package/dist/primitives/select.d.ts.map +1 -0
  158. package/dist/primitives/select.js +109 -0
  159. package/dist/primitives/select.js.map +1 -0
  160. package/dist/primitives/signature.d.ts +13 -0
  161. package/dist/primitives/signature.d.ts.map +1 -0
  162. package/dist/primitives/signature.js +45 -0
  163. package/dist/primitives/signature.js.map +1 -0
  164. package/dist/primitives/skeleton.d.ts +14 -0
  165. package/dist/primitives/skeleton.d.ts.map +1 -0
  166. package/dist/primitives/skeleton.js +41 -0
  167. package/dist/primitives/skeleton.js.map +1 -0
  168. package/dist/primitives/slider.d.ts +15 -0
  169. package/dist/primitives/slider.d.ts.map +1 -0
  170. package/dist/primitives/slider.js +7 -0
  171. package/dist/primitives/slider.js.map +1 -0
  172. package/dist/primitives/spacer.d.ts +9 -0
  173. package/dist/primitives/spacer.d.ts.map +1 -0
  174. package/dist/primitives/spacer.js +9 -0
  175. package/dist/primitives/spacer.js.map +1 -0
  176. package/dist/primitives/spatial-map-editing.d.ts +472 -0
  177. package/dist/primitives/spatial-map-editing.d.ts.map +1 -0
  178. package/dist/primitives/spatial-map-editing.js +886 -0
  179. package/dist/primitives/spatial-map-editing.js.map +1 -0
  180. package/dist/primitives/spatial-map.d.ts +1073 -0
  181. package/dist/primitives/spatial-map.d.ts.map +1 -0
  182. package/dist/primitives/spatial-map.js +1705 -0
  183. package/dist/primitives/spatial-map.js.map +1 -0
  184. package/dist/primitives/stack.d.ts +13 -0
  185. package/dist/primitives/stack.d.ts.map +1 -0
  186. package/dist/primitives/stack.js +12 -0
  187. package/dist/primitives/stack.js.map +1 -0
  188. package/dist/primitives/table.d.ts +115 -0
  189. package/dist/primitives/table.d.ts.map +1 -0
  190. package/dist/primitives/table.js +498 -0
  191. package/dist/primitives/table.js.map +1 -0
  192. package/dist/primitives/tabs.d.ts +17 -0
  193. package/dist/primitives/tabs.d.ts.map +1 -0
  194. package/dist/primitives/tabs.js +13 -0
  195. package/dist/primitives/tabs.js.map +1 -0
  196. package/dist/primitives/text.d.ts +11 -0
  197. package/dist/primitives/text.d.ts.map +1 -0
  198. package/dist/primitives/text.js +69 -0
  199. package/dist/primitives/text.js.map +1 -0
  200. package/dist/primitives/textarea.d.ts +15 -0
  201. package/dist/primitives/textarea.d.ts.map +1 -0
  202. package/dist/primitives/textarea.js +23 -0
  203. package/dist/primitives/textarea.js.map +1 -0
  204. package/dist/primitives/toast-container.d.ts +15 -0
  205. package/dist/primitives/toast-container.d.ts.map +1 -0
  206. package/dist/primitives/toast-container.js +160 -0
  207. package/dist/primitives/toast-container.js.map +1 -0
  208. package/dist/primitives/toggle.d.ts +12 -0
  209. package/dist/primitives/toggle.d.ts.map +1 -0
  210. package/dist/primitives/toggle.js +18 -0
  211. package/dist/primitives/toggle.js.map +1 -0
  212. package/dist/primitives/touchable.d.ts +10 -0
  213. package/dist/primitives/touchable.d.ts.map +1 -0
  214. package/dist/primitives/touchable.js +6 -0
  215. package/dist/primitives/touchable.js.map +1 -0
  216. package/dist/primitives/use-design-tokens.d.ts +127 -0
  217. package/dist/primitives/use-design-tokens.d.ts.map +1 -0
  218. package/dist/primitives/use-design-tokens.js +251 -0
  219. package/dist/primitives/use-design-tokens.js.map +1 -0
  220. package/dist/primitives/use-theme.d.ts +11 -0
  221. package/dist/primitives/use-theme.d.ts.map +1 -0
  222. package/dist/primitives/use-theme.js +17 -0
  223. package/dist/primitives/use-theme.js.map +1 -0
  224. package/dist/primitives/wizard.d.ts +11 -0
  225. package/dist/primitives/wizard.d.ts.map +1 -0
  226. package/dist/primitives/wizard.js +15 -0
  227. package/dist/primitives/wizard.js.map +1 -0
  228. package/dist/runtime/context-dispatcher.d.ts +3 -0
  229. package/dist/runtime/context-dispatcher.d.ts.map +1 -0
  230. package/dist/runtime/context-dispatcher.js +11 -0
  231. package/dist/runtime/context-dispatcher.js.map +1 -0
  232. package/dist/runtime/row-dispatcher.d.ts +19 -0
  233. package/dist/runtime/row-dispatcher.d.ts.map +1 -0
  234. package/dist/runtime/row-dispatcher.js +25 -0
  235. package/dist/runtime/row-dispatcher.js.map +1 -0
  236. package/dist/types.d.ts +10 -0
  237. package/dist/types.d.ts.map +1 -0
  238. package/dist/types.js +2 -0
  239. package/dist/types.js.map +1 -0
  240. package/dist/use-device-context.d.ts +8 -0
  241. package/dist/use-device-context.d.ts.map +1 -0
  242. package/dist/use-device-context.js +54 -0
  243. package/dist/use-device-context.js.map +1 -0
  244. package/package.json +59 -0
@@ -0,0 +1,1705 @@
1
+ import React from 'react';
2
+ import { buildSpatialItemChangeContext, buildSpatialZoneChangeContext, getEditPolicy, getSpatialItemLocalBounds, getSpatialItemWorldHandlePoints, getSpatialZoneMapBounds, getSpatialZonePolygonHandles, getSpatialZonePosition, getSpatialZoneTransformedBounds, getSpatialZoneWorldHandlePoints, hasSpatialItemChangedByType, hasSpatialZoneChangedByType, insertSpatialZonePolygonVertex, deleteSpatialZonePolygonVertex, isMovableSpatialItem, mapPointToItemLocal, mapPointToZoneLocal, moveSpatialZone, moveSpatialItem, moveSpatialZonePolygonVertex, normalizeSpatialTransform, normalizeEditPosition, resizeSpatialItem, resizeSpatialZone, rotateSpatialItem, resolveSpatialEditPoint, samePoint, } from './spatial-map-editing.js';
3
+ export { resolveSpatialEditPoint } from './spatial-map-editing.js';
4
+ const DEFAULT_LAYERS = ['background', 'zones', 'items', 'labels', 'overlays'];
5
+ const DEFAULT_STATUS_STYLE = {
6
+ fill: '#f8fafc',
7
+ stroke: '#94a3b8',
8
+ text: '#1f2937',
9
+ strokeWidth: 2,
10
+ };
11
+ const SELECTED_STYLE = {
12
+ fill: 'transparent',
13
+ stroke: '#2563eb',
14
+ strokeWidth: 5,
15
+ };
16
+ function viewBoxToString(viewBox) {
17
+ if (typeof viewBox === 'string')
18
+ return viewBox;
19
+ return `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`;
20
+ }
21
+ function viewBoxRect(viewBox) {
22
+ if (typeof viewBox !== 'string')
23
+ return viewBox;
24
+ const [x, y, width, height] = viewBox.split(/\s+/).map(Number);
25
+ return { x, y, width, height };
26
+ }
27
+ function svgPointFromPointer(event, svg, rect) {
28
+ const svgWithPoint = svg;
29
+ const pointFactory = svgWithPoint.createSVGPoint;
30
+ const matrix = svgWithPoint.getScreenCTM?.();
31
+ if (pointFactory && matrix) {
32
+ const point = pointFactory.call(svgWithPoint);
33
+ point.x = event.clientX;
34
+ point.y = event.clientY;
35
+ const transformed = point.matrixTransform(matrix.inverse());
36
+ return { x: transformed.x, y: transformed.y };
37
+ }
38
+ const clientRect = svg.getBoundingClientRect();
39
+ const width = clientRect.width || rect.width;
40
+ const height = clientRect.height || rect.height;
41
+ return {
42
+ x: rect.x + ((event.clientX - clientRect.left) / width) * rect.width,
43
+ y: rect.y + ((event.clientY - clientRect.top) / height) * rect.height,
44
+ };
45
+ }
46
+ function pointsToString(points) {
47
+ if (typeof points === 'string')
48
+ return points;
49
+ return points.map((point) => {
50
+ if (Array.isArray(point))
51
+ return `${point[0]},${point[1]}`;
52
+ return `${point.x},${point.y}`;
53
+ }).join(' ');
54
+ }
55
+ function renderItemShape(shape, key, style, extraProps = {}) {
56
+ const common = {
57
+ key,
58
+ fill: style.fill ?? DEFAULT_STATUS_STYLE.fill,
59
+ stroke: style.stroke ?? DEFAULT_STATUS_STYLE.stroke,
60
+ strokeWidth: style.strokeWidth ?? DEFAULT_STATUS_STYLE.strokeWidth,
61
+ opacity: style.opacity,
62
+ vectorEffect: 'non-scaling-stroke',
63
+ ...extraProps,
64
+ };
65
+ if (shape.type === 'rect') {
66
+ return React.createElement('rect', {
67
+ ...common,
68
+ x: -shape.width / 2,
69
+ y: -shape.height / 2,
70
+ width: shape.width,
71
+ height: shape.height,
72
+ rx: shape.radius ?? 0,
73
+ ry: shape.radius ?? 0,
74
+ });
75
+ }
76
+ if (shape.type === 'circle') {
77
+ return React.createElement('circle', { ...common, r: shape.radius });
78
+ }
79
+ if (shape.type === 'ellipse') {
80
+ return React.createElement('ellipse', { ...common, rx: shape.radiusX, ry: shape.radiusY });
81
+ }
82
+ if (shape.type === 'polygon') {
83
+ return React.createElement('polygon', { ...common, points: pointsToString(shape.points) });
84
+ }
85
+ return React.createElement('path', { ...common, d: shape.d });
86
+ }
87
+ function renderZoneShape(zone, key, styleOverride, extraProps) {
88
+ const style = {
89
+ fill: '#ffffff',
90
+ stroke: '#94a3b8',
91
+ strokeWidth: 3,
92
+ ...(zone.style ?? {}),
93
+ ...(styleOverride ?? {}),
94
+ };
95
+ const common = {
96
+ key,
97
+ fill: style.fill ?? '#ffffff',
98
+ stroke: style.stroke ?? '#94a3b8',
99
+ strokeWidth: style.strokeWidth ?? 3,
100
+ opacity: style.opacity,
101
+ vectorEffect: 'non-scaling-stroke',
102
+ 'data-testid': `spatial-zone-shape-${zone.id}`,
103
+ ...(extraProps ?? {}),
104
+ };
105
+ if (zone.shape.type === 'rect') {
106
+ return React.createElement('rect', {
107
+ ...common,
108
+ x: zone.shape.x,
109
+ y: zone.shape.y,
110
+ width: zone.shape.width,
111
+ height: zone.shape.height,
112
+ rx: zone.shape.radius ?? 0,
113
+ ry: zone.shape.radius ?? 0,
114
+ });
115
+ }
116
+ if (zone.shape.type === 'circle') {
117
+ return React.createElement('circle', {
118
+ ...common,
119
+ cx: zone.shape.cx,
120
+ cy: zone.shape.cy,
121
+ r: zone.shape.radius,
122
+ });
123
+ }
124
+ if (zone.shape.type === 'ellipse') {
125
+ return React.createElement('ellipse', {
126
+ ...common,
127
+ cx: zone.shape.cx,
128
+ cy: zone.shape.cy,
129
+ rx: zone.shape.radiusX,
130
+ ry: zone.shape.radiusY,
131
+ });
132
+ }
133
+ if (zone.shape.type === 'polygon') {
134
+ return React.createElement('polygon', { ...common, points: pointsToString(zone.shape.points) });
135
+ }
136
+ return React.createElement('path', { ...common, d: zone.shape.d });
137
+ }
138
+ function buildItemContext(item, zones, mode) {
139
+ const zone = item.zoneId ? zones.find((candidate) => candidate.id === item.zoneId) : undefined;
140
+ return {
141
+ kind: 'item',
142
+ mode,
143
+ itemId: item.id,
144
+ zoneId: item.zoneId,
145
+ status: item.status,
146
+ label: item.label,
147
+ position: item.position,
148
+ rotation: item.rotation ?? 0,
149
+ transform: item.transform,
150
+ localBounds: item.localBounds,
151
+ shape: item.shape,
152
+ metadata: item.metadata,
153
+ item,
154
+ zone,
155
+ };
156
+ }
157
+ function buildZoneContext(zone, mode) {
158
+ return {
159
+ kind: 'zone',
160
+ mode,
161
+ zoneId: zone.id,
162
+ label: zone.label,
163
+ position: getSpatialZonePosition(zone),
164
+ rotation: zone.rotation ?? 0,
165
+ transform: zone.transform,
166
+ localBounds: zone.localBounds,
167
+ shape: zone.shape,
168
+ metadata: zone.metadata,
169
+ zone,
170
+ };
171
+ }
172
+ function zoneFromCanvasEvent(event, zones) {
173
+ const target = event.target;
174
+ if (!(target instanceof Element))
175
+ return undefined;
176
+ const zoneId = target.closest('[data-spatial-zone-id]')?.getAttribute('data-spatial-zone-id');
177
+ if (zoneId === null || zoneId === undefined)
178
+ return undefined;
179
+ return zones.find((candidate) => String(candidate.id) === zoneId);
180
+ }
181
+ function getPolicy(mode, policy) {
182
+ const selectZones = policy?.selectZones ?? false;
183
+ const activateZones = policy?.activateZones ?? false;
184
+ const defaults = {
185
+ selectItems: mode !== 'readonly',
186
+ activateItems: mode === 'operate',
187
+ selectZones,
188
+ activateZones,
189
+ zonePressStopsCanvas: selectZones || activateZones,
190
+ clearSelectionOnCanvasPress: mode !== 'readonly',
191
+ keyboardNavigation: mode !== 'readonly',
192
+ };
193
+ return { ...defaults, ...(policy ?? {}) };
194
+ }
195
+ function itemAriaLabel(item) {
196
+ if (item.ariaLabel)
197
+ return item.ariaLabel;
198
+ const label = item.label ?? String(item.id);
199
+ return item.status ? `${label}, ${item.status}` : label;
200
+ }
201
+ function zoneAriaLabel(zone) {
202
+ return zone.ariaLabel ?? zone.label ?? String(zone.id);
203
+ }
204
+ function sameEditGuide(a, b) {
205
+ return a.axis === b.axis
206
+ && a.source === b.source
207
+ && a.value === b.value
208
+ && a.targetId === b.targetId;
209
+ }
210
+ function sameSnapResolution(a, b) {
211
+ return samePoint(a.point, b.point)
212
+ && samePoint(a.rawPoint, b.rawPoint)
213
+ && a.snapped === b.snapped
214
+ && a.sources.length === b.sources.length
215
+ && a.sources.every((source, index) => source === b.sources[index])
216
+ && a.guides.length === b.guides.length
217
+ && a.guides.every((guide, index) => sameEditGuide(guide, b.guides[index]));
218
+ }
219
+ export function SpatialMap({ viewBox, zones = [], items = [], layers = DEFAULT_LAYERS, mode = 'operate', statusStyles = {}, selectedItemId, selectedZoneId, zoneShapeEditId, interactionPolicy, editPolicy, canvasGuide, ariaLabel = 'Spatial map', style, className, _onItemSelect, _onZoneSelect, onItemPress, onItemChange, onZonePress, onZoneChange, onZoneShapeEditExit, onCanvasPress, _selectedItemContext, _selectedZoneContext, }) {
220
+ const policy = getPolicy(mode, interactionPolicy);
221
+ const rect = React.useMemo(() => viewBoxRect(viewBox), [viewBox]);
222
+ const edit = React.useMemo(() => getEditPolicy(mode, editPolicy), [mode, editPolicy]);
223
+ const svgRef = React.useRef(null);
224
+ const dragStateRef = React.useRef(null);
225
+ const [dragState, setDragState] = React.useState(null);
226
+ const [droppedPreviewItems, setDroppedPreviewItems] = React.useState(() => new Map());
227
+ const [droppedPreviewZones, setDroppedPreviewZones] = React.useState(() => new Map());
228
+ const [canvasGuideResolution, setCanvasGuideResolution] = React.useState(null);
229
+ const canvasGuideRawPointRef = React.useRef(null);
230
+ const droppedPreviewFrameIdsRef = React.useRef(new Map());
231
+ const droppedZonePreviewFrameIdsRef = React.useRef(new Map());
232
+ const suppressClickItemRef = React.useRef(null);
233
+ const suppressCanvasClickRef = React.useRef(false);
234
+ const suppressCanvasClickTokenRef = React.useRef(0);
235
+ const activeSelectedItemId = selectedItemId ?? (policy.selectItems ? _selectedItemContext?.itemId : undefined);
236
+ const activeSelectedZoneId = selectedZoneId ?? (policy.selectZones ? _selectedZoneContext?.zoneId : undefined);
237
+ const canvasGuideVisible = canvasGuide?.visible === true;
238
+ const zonesByLayer = new Map();
239
+ const itemsByLayer = new Map();
240
+ for (const zone of zones) {
241
+ const layer = zone.layer ?? 'zones';
242
+ zonesByLayer.set(layer, [...(zonesByLayer.get(layer) ?? []), zone]);
243
+ }
244
+ for (const item of items) {
245
+ const layer = item.layer ?? 'items';
246
+ itemsByLayer.set(layer, [...(itemsByLayer.get(layer) ?? []), item]);
247
+ }
248
+ function setCurrentDragState(next) {
249
+ dragStateRef.current = next;
250
+ setDragState(next);
251
+ }
252
+ function clearDroppedPreviewItem(itemId) {
253
+ setDroppedPreviewItems((current) => {
254
+ if (!current.has(itemId))
255
+ return current;
256
+ const next = new Map(current);
257
+ next.delete(itemId);
258
+ return next;
259
+ });
260
+ }
261
+ function bridgeDroppedPreviewItem(itemId, item) {
262
+ // Keep the committed drop item visible until the renderer's store-driven frame catches up.
263
+ setDroppedPreviewItems((current) => {
264
+ const next = new Map(current);
265
+ next.set(itemId, item);
266
+ return next;
267
+ });
268
+ const existingFrameId = droppedPreviewFrameIdsRef.current.get(itemId);
269
+ if (existingFrameId !== undefined) {
270
+ window.cancelAnimationFrame(existingFrameId);
271
+ }
272
+ const frameId = window.requestAnimationFrame(() => {
273
+ droppedPreviewFrameIdsRef.current.delete(itemId);
274
+ clearDroppedPreviewItem(itemId);
275
+ });
276
+ droppedPreviewFrameIdsRef.current.set(itemId, frameId);
277
+ }
278
+ function clearDroppedPreviewZone(zoneId) {
279
+ setDroppedPreviewZones((current) => {
280
+ if (!current.has(zoneId))
281
+ return current;
282
+ const next = new Map(current);
283
+ next.delete(zoneId);
284
+ return next;
285
+ });
286
+ }
287
+ function bridgeDroppedPreviewZone(zoneId, zone) {
288
+ setDroppedPreviewZones((current) => {
289
+ const next = new Map(current);
290
+ next.set(zoneId, zone);
291
+ return next;
292
+ });
293
+ const existingFrameId = droppedZonePreviewFrameIdsRef.current.get(zoneId);
294
+ if (existingFrameId !== undefined) {
295
+ window.cancelAnimationFrame(existingFrameId);
296
+ }
297
+ const frameId = window.requestAnimationFrame(() => {
298
+ droppedZonePreviewFrameIdsRef.current.delete(zoneId);
299
+ clearDroppedPreviewZone(zoneId);
300
+ });
301
+ droppedZonePreviewFrameIdsRef.current.set(zoneId, frameId);
302
+ }
303
+ function suppressNextCanvasClick() {
304
+ suppressCanvasClickRef.current = true;
305
+ suppressCanvasClickTokenRef.current += 1;
306
+ const token = suppressCanvasClickTokenRef.current;
307
+ window.setTimeout(() => {
308
+ if (suppressCanvasClickTokenRef.current === token) {
309
+ suppressCanvasClickRef.current = false;
310
+ }
311
+ }, 0);
312
+ }
313
+ function resolveEditPoint(rawPoint, currentItemId, intent = { kind: 'pointer' }) {
314
+ return resolveSpatialEditPoint({
315
+ rawPoint,
316
+ rect,
317
+ policy: edit,
318
+ items,
319
+ currentItemId,
320
+ intent,
321
+ });
322
+ }
323
+ function commitItemUpdate(previousItem, nextItem, changeType) {
324
+ if (!hasSpatialItemChangedByType({
325
+ previousItem,
326
+ nextItem,
327
+ changeType,
328
+ policy: edit,
329
+ rect,
330
+ }))
331
+ return null;
332
+ onItemChange?.(buildSpatialItemChangeContext({
333
+ mode,
334
+ changeType,
335
+ previousItem,
336
+ nextItem,
337
+ zones,
338
+ policy: edit,
339
+ }));
340
+ return nextItem;
341
+ }
342
+ function commitItemMove(previousItem, position, intent = { kind: 'pointer' }) {
343
+ const previousPosition = normalizeEditPosition(previousItem.position, rect, edit);
344
+ const nextPosition = resolveEditPoint(position, previousItem.id, intent).point;
345
+ if (samePoint(previousPosition, nextPosition))
346
+ return null;
347
+ const nextItem = moveSpatialItem(previousItem, nextPosition);
348
+ return commitItemUpdate(previousItem, nextItem, 'move');
349
+ }
350
+ function commitZoneUpdate(previousZone, nextZone, changeType, shapeContext) {
351
+ if (!hasSpatialZoneChangedByType({
352
+ previousZone,
353
+ nextZone,
354
+ changeType,
355
+ policy: edit,
356
+ }))
357
+ return null;
358
+ onZoneChange?.(buildSpatialZoneChangeContext({
359
+ mode,
360
+ changeType,
361
+ previousZone,
362
+ nextZone,
363
+ policy: edit,
364
+ ...(shapeContext ?? {}),
365
+ }));
366
+ return nextZone;
367
+ }
368
+ function commitZoneMove(previousZone, position, intent = { kind: 'pointer' }) {
369
+ const nextZone = resolveZoneMove(previousZone, position, intent).zone;
370
+ return commitZoneUpdate(previousZone, nextZone, 'move');
371
+ }
372
+ function zoneSnapAnchor(zone) {
373
+ const position = getSpatialZonePosition(zone);
374
+ const bounds = getSpatialZoneTransformedBounds(zone, edit);
375
+ if (!bounds)
376
+ return position;
377
+ return {
378
+ x: bounds.x + bounds.width / 2,
379
+ y: bounds.y + bounds.height / 2,
380
+ };
381
+ }
382
+ function resolveZoneMove(previousZone, rawPosition, intent = { kind: 'pointer' }) {
383
+ const previousPosition = getSpatialZonePosition(previousZone);
384
+ const previousAnchor = zoneSnapAnchor(previousZone);
385
+ const rawAnchor = {
386
+ x: previousAnchor.x + rawPosition.x - previousPosition.x,
387
+ y: previousAnchor.y + rawPosition.y - previousPosition.y,
388
+ };
389
+ const resolution = resolveEditPoint(rawAnchor, undefined, intent);
390
+ const snappedPosition = {
391
+ x: rawPosition.x + resolution.point.x - rawAnchor.x,
392
+ y: rawPosition.y + resolution.point.y - rawAnchor.y,
393
+ };
394
+ const zone = moveSpatialZone(previousZone, snappedPosition, rect, edit);
395
+ return {
396
+ position: getSpatialZonePosition(zone),
397
+ zone,
398
+ resolution,
399
+ };
400
+ }
401
+ function startItemDrag(event, item) {
402
+ if (!edit.dragItems || !isMovableSpatialItem(item) || !svgRef.current)
403
+ return;
404
+ event.stopPropagation();
405
+ canvasGuideRawPointRef.current = null;
406
+ setCanvasGuideResolution(null);
407
+ const startPoint = svgPointFromPointer(event, svgRef.current, rect);
408
+ setCurrentDragState({
409
+ kind: 'move',
410
+ itemId: item.id,
411
+ pointerId: event.pointerId,
412
+ previousItem: item,
413
+ startPoint,
414
+ previewPosition: item.position,
415
+ snapResolution: {
416
+ rawPoint: item.position,
417
+ point: item.position,
418
+ snapped: false,
419
+ sources: [],
420
+ guides: [],
421
+ },
422
+ hasMoved: false,
423
+ });
424
+ event.currentTarget.setPointerCapture?.(event.pointerId);
425
+ }
426
+ function startZoneDrag(event, zone) {
427
+ if (!edit.dragZones || zone.disabled || !svgRef.current)
428
+ return;
429
+ event.stopPropagation();
430
+ if (policy.zonePressStopsCanvas) {
431
+ suppressNextCanvasClick();
432
+ }
433
+ canvasGuideRawPointRef.current = null;
434
+ setCanvasGuideResolution(null);
435
+ const startPoint = svgPointFromPointer(event, svgRef.current, rect);
436
+ const startAnchor = zoneSnapAnchor(zone);
437
+ setCurrentDragState({
438
+ kind: 'zone-move',
439
+ zoneId: zone.id,
440
+ pointerId: event.pointerId,
441
+ previousZone: zone,
442
+ startPoint,
443
+ startPosition: getSpatialZonePosition(zone),
444
+ snapResolution: {
445
+ rawPoint: startAnchor,
446
+ point: startAnchor,
447
+ snapped: false,
448
+ sources: [],
449
+ guides: [],
450
+ },
451
+ hasMoved: false,
452
+ });
453
+ event.currentTarget.setPointerCapture?.(event.pointerId);
454
+ }
455
+ function startItemResize(event, item, handle) {
456
+ if (!edit.resizeItems || item.disabled || !svgRef.current)
457
+ return;
458
+ if (!getSpatialItemLocalBounds(item))
459
+ return;
460
+ event.preventDefault();
461
+ event.stopPropagation();
462
+ canvasGuideRawPointRef.current = null;
463
+ setCanvasGuideResolution(null);
464
+ setCurrentDragState({
465
+ kind: 'resize',
466
+ itemId: item.id,
467
+ pointerId: event.pointerId,
468
+ previousItem: item,
469
+ handle,
470
+ previewItem: item,
471
+ snapResolution: null,
472
+ hasChanged: false,
473
+ });
474
+ event.currentTarget.setPointerCapture?.(event.pointerId);
475
+ }
476
+ function updateItemResize(event, currentDrag) {
477
+ if (!svgRef.current)
478
+ return;
479
+ const point = svgPointFromPointer(event, svgRef.current, rect);
480
+ const resolution = resolveEditPoint(point, currentDrag.itemId, { kind: 'pointer' });
481
+ const localPoint = mapPointToItemLocal(currentDrag.previousItem, resolution.point, edit);
482
+ const nextItem = resizeSpatialItem(currentDrag.previousItem, {
483
+ handle: currentDrag.handle,
484
+ localPoint,
485
+ policy: edit,
486
+ preserveAspectRatio: event.shiftKey,
487
+ });
488
+ setCurrentDragState({
489
+ ...currentDrag,
490
+ previewItem: nextItem,
491
+ snapResolution: resolution,
492
+ hasChanged: hasSpatialItemChangedByType({
493
+ previousItem: currentDrag.previousItem,
494
+ nextItem,
495
+ changeType: 'resize',
496
+ policy: edit,
497
+ rect,
498
+ }),
499
+ });
500
+ }
501
+ function startZoneResize(event, zone, handle) {
502
+ if (!edit.resizeZones || zone.disabled || !svgRef.current)
503
+ return;
504
+ if (!getSpatialZoneMapBounds(zone))
505
+ return;
506
+ event.preventDefault();
507
+ event.stopPropagation();
508
+ canvasGuideRawPointRef.current = null;
509
+ setCanvasGuideResolution(null);
510
+ setCurrentDragState({
511
+ kind: 'zone-resize',
512
+ zoneId: zone.id,
513
+ pointerId: event.pointerId,
514
+ previousZone: zone,
515
+ handle,
516
+ previewZone: zone,
517
+ snapResolution: null,
518
+ hasChanged: false,
519
+ });
520
+ event.currentTarget.setPointerCapture?.(event.pointerId);
521
+ }
522
+ function updateZoneResize(event, currentDrag) {
523
+ if (!svgRef.current)
524
+ return;
525
+ const point = svgPointFromPointer(event, svgRef.current, rect);
526
+ const resolution = resolveEditPoint(point, undefined, { kind: 'pointer' });
527
+ const localPoint = mapPointToZoneLocal(currentDrag.previousZone, resolution.point, edit);
528
+ const nextZone = resizeSpatialZone(currentDrag.previousZone, {
529
+ handle: currentDrag.handle,
530
+ localPoint,
531
+ policy: edit,
532
+ preserveAspectRatio: event.shiftKey,
533
+ });
534
+ setCurrentDragState({
535
+ ...currentDrag,
536
+ previewZone: nextZone,
537
+ snapResolution: resolution,
538
+ hasChanged: hasSpatialZoneChangedByType({
539
+ previousZone: currentDrag.previousZone,
540
+ nextZone,
541
+ changeType: 'resize',
542
+ policy: edit,
543
+ }),
544
+ });
545
+ }
546
+ function zoneShapeModeActive(zone) {
547
+ return mode === 'edit'
548
+ && edit.shapeEditZones
549
+ && !zone.disabled
550
+ && String(zoneShapeEditId ?? '') === String(zone.id)
551
+ && String(activeSelectedZoneId ?? '') === String(zone.id);
552
+ }
553
+ function resolveZoneShapeVertexMove(zone, vertexIndex, worldPoint, intent = { kind: 'pointer' }) {
554
+ const resolution = resolveEditPoint(worldPoint, undefined, intent);
555
+ return {
556
+ zone: moveSpatialZonePolygonVertex(zone, {
557
+ vertexIndex,
558
+ worldPoint: resolution.point,
559
+ rect,
560
+ policy: edit,
561
+ }),
562
+ resolution,
563
+ };
564
+ }
565
+ function startZoneShapeVertexDrag(event, zone, vertexIndex) {
566
+ if (!zoneShapeModeActive(zone) || !svgRef.current)
567
+ return;
568
+ event.preventDefault();
569
+ event.stopPropagation();
570
+ if (policy.zonePressStopsCanvas) {
571
+ suppressNextCanvasClick();
572
+ }
573
+ canvasGuideRawPointRef.current = null;
574
+ setCanvasGuideResolution(null);
575
+ setCurrentDragState({
576
+ kind: 'zone-shape',
577
+ zoneId: zone.id,
578
+ pointerId: event.pointerId,
579
+ previousZone: zone,
580
+ vertexIndex,
581
+ previewZone: zone,
582
+ snapResolution: null,
583
+ hasChanged: false,
584
+ });
585
+ event.currentTarget.setPointerCapture?.(event.pointerId);
586
+ }
587
+ function updateZoneShapeVertexDrag(event, currentDrag) {
588
+ if (!svgRef.current)
589
+ return;
590
+ const point = svgPointFromPointer(event, svgRef.current, rect);
591
+ const nextMove = resolveZoneShapeVertexMove(currentDrag.previousZone, currentDrag.vertexIndex, point, { kind: 'pointer' });
592
+ setCurrentDragState({
593
+ ...currentDrag,
594
+ previewZone: nextMove.zone,
595
+ snapResolution: nextMove.resolution,
596
+ hasChanged: hasSpatialZoneChangedByType({
597
+ previousZone: currentDrag.previousZone,
598
+ nextZone: nextMove.zone,
599
+ changeType: 'shape',
600
+ policy: edit,
601
+ }),
602
+ });
603
+ }
604
+ function insertZoneShapeVertex(event, zone, segmentIndex, worldPoint) {
605
+ if (!zoneShapeModeActive(zone))
606
+ return;
607
+ event.preventDefault();
608
+ event.stopPropagation();
609
+ if (policy.zonePressStopsCanvas) {
610
+ suppressNextCanvasClick();
611
+ }
612
+ const resolution = resolveEditPoint(worldPoint, undefined, { kind: 'pointer' });
613
+ const nextZone = insertSpatialZonePolygonVertex(zone, {
614
+ segmentIndex,
615
+ worldPoint: resolution.point,
616
+ rect,
617
+ policy: edit,
618
+ });
619
+ const committedZone = commitZoneUpdate(zone, nextZone, 'shape', {
620
+ shapeAction: 'insert-vertex',
621
+ segmentIndex,
622
+ });
623
+ if (committedZone) {
624
+ bridgeDroppedPreviewZone(zone.id, committedZone);
625
+ if (policy.selectZones) {
626
+ _onZoneSelect?.(buildZoneContext(committedZone, mode));
627
+ }
628
+ }
629
+ }
630
+ function angleFromItemCenter(item, point) {
631
+ return Math.atan2(point.y - item.position.y, point.x - item.position.x) * 180 / Math.PI;
632
+ }
633
+ function startItemRotate(event, item) {
634
+ if (!edit.rotateItems || item.disabled || !svgRef.current)
635
+ return;
636
+ event.preventDefault();
637
+ event.stopPropagation();
638
+ canvasGuideRawPointRef.current = null;
639
+ setCanvasGuideResolution(null);
640
+ const point = svgPointFromPointer(event, svgRef.current, rect);
641
+ setCurrentDragState({
642
+ kind: 'rotate',
643
+ itemId: item.id,
644
+ pointerId: event.pointerId,
645
+ previousItem: item,
646
+ startAngle: angleFromItemCenter(item, point),
647
+ startRotation: item.rotation ?? 0,
648
+ previewItem: item,
649
+ hasChanged: false,
650
+ });
651
+ event.currentTarget.setPointerCapture?.(event.pointerId);
652
+ }
653
+ function updateItemRotate(event, currentDrag) {
654
+ if (!svgRef.current)
655
+ return;
656
+ const point = svgPointFromPointer(event, svgRef.current, rect);
657
+ const currentAngle = angleFromItemCenter(currentDrag.previousItem, point);
658
+ const stepPolicy = event.shiftKey
659
+ ? { ...edit, rotationStep: edit.rotationLargeStep }
660
+ : edit;
661
+ const nextItem = rotateSpatialItem(currentDrag.previousItem, currentDrag.startRotation + currentAngle - currentDrag.startAngle, stepPolicy);
662
+ setCurrentDragState({
663
+ ...currentDrag,
664
+ previewItem: nextItem,
665
+ hasChanged: hasSpatialItemChangedByType({
666
+ previousItem: currentDrag.previousItem,
667
+ nextItem,
668
+ changeType: 'rotate',
669
+ policy: edit,
670
+ rect,
671
+ }),
672
+ });
673
+ }
674
+ function updateItemDrag(event) {
675
+ const currentDrag = dragStateRef.current;
676
+ if (!currentDrag || currentDrag.pointerId !== event.pointerId || !svgRef.current)
677
+ return;
678
+ event.preventDefault();
679
+ if (currentDrag.kind === 'resize') {
680
+ updateItemResize(event, currentDrag);
681
+ return;
682
+ }
683
+ if (currentDrag.kind === 'zone-resize') {
684
+ updateZoneResize(event, currentDrag);
685
+ return;
686
+ }
687
+ if (currentDrag.kind === 'rotate') {
688
+ updateItemRotate(event, currentDrag);
689
+ return;
690
+ }
691
+ if (currentDrag.kind === 'zone-shape') {
692
+ updateZoneShapeVertexDrag(event, currentDrag);
693
+ return;
694
+ }
695
+ if (currentDrag.kind === 'zone-move') {
696
+ const point = svgPointFromPointer(event, svgRef.current, rect);
697
+ const nextPosition = {
698
+ x: currentDrag.startPosition.x + point.x - currentDrag.startPoint.x,
699
+ y: currentDrag.startPosition.y + point.y - currentDrag.startPoint.y,
700
+ };
701
+ const nextMove = resolveZoneMove(currentDrag.previousZone, nextPosition, { kind: 'pointer' });
702
+ const previousPosition = getSpatialZonePosition(currentDrag.previousZone);
703
+ setCurrentDragState({
704
+ ...currentDrag,
705
+ previewZone: nextMove.zone,
706
+ snapResolution: nextMove.resolution,
707
+ hasMoved: currentDrag.hasMoved || !samePoint(nextMove.position, previousPosition),
708
+ });
709
+ return;
710
+ }
711
+ const point = svgPointFromPointer(event, svgRef.current, rect);
712
+ const rawNextPosition = {
713
+ x: currentDrag.previousItem.position.x + point.x - currentDrag.startPoint.x,
714
+ y: currentDrag.previousItem.position.y + point.y - currentDrag.startPoint.y,
715
+ };
716
+ const nextResolution = resolveEditPoint(rawNextPosition, currentDrag.itemId, { kind: 'pointer' });
717
+ const previousPosition = normalizeEditPosition(currentDrag.previousItem.position, rect, edit);
718
+ setCurrentDragState({
719
+ ...currentDrag,
720
+ previewPosition: nextResolution.point,
721
+ snapResolution: nextResolution,
722
+ hasMoved: currentDrag.hasMoved || !samePoint(nextResolution.point, previousPosition),
723
+ });
724
+ }
725
+ function endItemDrag(event) {
726
+ const currentDrag = dragStateRef.current;
727
+ if (!currentDrag || currentDrag.pointerId !== event.pointerId)
728
+ return;
729
+ event.preventDefault();
730
+ if (currentDrag.kind === 'resize') {
731
+ if (currentDrag.hasChanged) {
732
+ suppressClickItemRef.current = currentDrag.itemId;
733
+ suppressNextCanvasClick();
734
+ const nextItem = commitItemUpdate(currentDrag.previousItem, currentDrag.previewItem, 'resize');
735
+ if (nextItem) {
736
+ bridgeDroppedPreviewItem(currentDrag.itemId, nextItem);
737
+ }
738
+ }
739
+ setCurrentDragState(null);
740
+ return;
741
+ }
742
+ if (currentDrag.kind === 'zone-resize') {
743
+ if (currentDrag.hasChanged) {
744
+ suppressNextCanvasClick();
745
+ const nextZone = commitZoneUpdate(currentDrag.previousZone, currentDrag.previewZone, 'resize');
746
+ if (nextZone) {
747
+ bridgeDroppedPreviewZone(currentDrag.zoneId, nextZone);
748
+ if (policy.selectZones) {
749
+ _onZoneSelect?.(buildZoneContext(nextZone, mode));
750
+ }
751
+ }
752
+ }
753
+ setCurrentDragState(null);
754
+ return;
755
+ }
756
+ if (currentDrag.kind === 'rotate') {
757
+ if (currentDrag.hasChanged) {
758
+ suppressClickItemRef.current = currentDrag.itemId;
759
+ suppressNextCanvasClick();
760
+ const nextItem = commitItemUpdate(currentDrag.previousItem, currentDrag.previewItem, 'rotate');
761
+ if (nextItem) {
762
+ bridgeDroppedPreviewItem(currentDrag.itemId, nextItem);
763
+ }
764
+ }
765
+ setCurrentDragState(null);
766
+ return;
767
+ }
768
+ if (currentDrag.kind === 'zone-move') {
769
+ if (currentDrag.hasMoved) {
770
+ suppressNextCanvasClick();
771
+ const nextZone = commitZoneMove(currentDrag.previousZone, getSpatialZonePosition(currentDrag.previewZone ?? currentDrag.previousZone));
772
+ if (nextZone) {
773
+ bridgeDroppedPreviewZone(currentDrag.zoneId, nextZone);
774
+ if (policy.selectZones) {
775
+ _onZoneSelect?.(buildZoneContext(nextZone, mode));
776
+ }
777
+ }
778
+ }
779
+ setCurrentDragState(null);
780
+ return;
781
+ }
782
+ if (currentDrag.kind === 'zone-shape') {
783
+ if (currentDrag.hasChanged) {
784
+ suppressNextCanvasClick();
785
+ const nextZone = commitZoneUpdate(currentDrag.previousZone, currentDrag.previewZone, 'shape', {
786
+ shapeAction: 'move-vertex',
787
+ vertexIndex: currentDrag.vertexIndex,
788
+ });
789
+ if (nextZone) {
790
+ bridgeDroppedPreviewZone(currentDrag.zoneId, nextZone);
791
+ if (policy.selectZones) {
792
+ _onZoneSelect?.(buildZoneContext(nextZone, mode));
793
+ }
794
+ }
795
+ }
796
+ setCurrentDragState(null);
797
+ return;
798
+ }
799
+ if (currentDrag.hasMoved) {
800
+ suppressClickItemRef.current = currentDrag.itemId;
801
+ suppressNextCanvasClick();
802
+ const nextItem = commitItemMove(currentDrag.previousItem, currentDrag.snapResolution.rawPoint, { kind: 'pointer' });
803
+ if (nextItem) {
804
+ bridgeDroppedPreviewItem(currentDrag.itemId, nextItem);
805
+ }
806
+ }
807
+ setCurrentDragState(null);
808
+ }
809
+ function cancelItemDrag(event) {
810
+ const currentDrag = dragStateRef.current;
811
+ if (!currentDrag || currentDrag.pointerId !== event.pointerId)
812
+ return;
813
+ setCurrentDragState(null);
814
+ }
815
+ React.useEffect(() => {
816
+ if (!dragState)
817
+ return;
818
+ document.addEventListener('pointermove', updateItemDrag);
819
+ document.addEventListener('pointerup', endItemDrag);
820
+ document.addEventListener('pointercancel', cancelItemDrag);
821
+ return () => {
822
+ document.removeEventListener('pointermove', updateItemDrag);
823
+ document.removeEventListener('pointerup', endItemDrag);
824
+ document.removeEventListener('pointercancel', cancelItemDrag);
825
+ };
826
+ }, [dragState !== null, edit, items, rect, mode, zones, onItemChange, onZoneChange, _onZoneSelect, policy.selectZones]);
827
+ React.useEffect(() => {
828
+ const currentDrag = dragStateRef.current;
829
+ if (currentDrag?.kind !== 'zone-shape')
830
+ return;
831
+ const stillEditingSameZone = String(zoneShapeEditId ?? '') === String(currentDrag.zoneId)
832
+ && String(activeSelectedZoneId ?? '') === String(currentDrag.zoneId)
833
+ && zones.some((zone) => String(zone.id) === String(currentDrag.zoneId));
834
+ if (!stillEditingSameZone) {
835
+ setCurrentDragState(null);
836
+ }
837
+ }, [zoneShapeEditId, activeSelectedZoneId, zones]);
838
+ React.useEffect(() => {
839
+ return () => {
840
+ for (const frameId of droppedPreviewFrameIdsRef.current.values()) {
841
+ window.cancelAnimationFrame(frameId);
842
+ }
843
+ droppedPreviewFrameIdsRef.current.clear();
844
+ for (const frameId of droppedZonePreviewFrameIdsRef.current.values()) {
845
+ window.cancelAnimationFrame(frameId);
846
+ }
847
+ droppedZonePreviewFrameIdsRef.current.clear();
848
+ };
849
+ }, []);
850
+ React.useEffect(() => {
851
+ if (!canvasGuideVisible) {
852
+ canvasGuideRawPointRef.current = null;
853
+ setCanvasGuideResolution(null);
854
+ return;
855
+ }
856
+ setCanvasGuideResolution((current) => {
857
+ const rawPoint = canvasGuideRawPointRef.current;
858
+ if (!current || !rawPoint)
859
+ return current;
860
+ const next = resolveEditPoint(rawPoint, undefined, { kind: 'canvas' });
861
+ return sameSnapResolution(current, next) ? current : next;
862
+ });
863
+ }, [canvasGuideVisible, edit, items, rect]);
864
+ function keyboardDelta(event) {
865
+ if (event.ctrlKey)
866
+ return null;
867
+ const step = event.shiftKey ? edit.keyboardLargeStep : edit.keyboardStep;
868
+ if (event.key === 'ArrowRight')
869
+ return { x: step, y: 0 };
870
+ if (event.key === 'ArrowLeft')
871
+ return { x: -step, y: 0 };
872
+ if (event.key === 'ArrowDown')
873
+ return { x: 0, y: step };
874
+ if (event.key === 'ArrowUp')
875
+ return { x: 0, y: -step };
876
+ return null;
877
+ }
878
+ function moveItemByKeyboard(event, item) {
879
+ if (!edit.keyboardMoveItems || !isMovableSpatialItem(item))
880
+ return false;
881
+ const delta = keyboardDelta(event);
882
+ if (!delta)
883
+ return false;
884
+ const nextPosition = {
885
+ x: item.position.x + delta.x,
886
+ y: item.position.y + delta.y,
887
+ };
888
+ event.preventDefault();
889
+ event.stopPropagation();
890
+ const nextItem = commitItemMove(item, nextPosition, {
891
+ kind: 'keyboard',
892
+ origin: item.position,
893
+ delta,
894
+ });
895
+ if (!nextItem)
896
+ return true;
897
+ if (policy.selectItems) {
898
+ _onItemSelect?.(buildItemContext(nextItem, zones, mode));
899
+ }
900
+ return true;
901
+ }
902
+ function moveZoneByKeyboard(event, zone) {
903
+ if (!edit.keyboardMoveZones || zone.disabled)
904
+ return false;
905
+ const delta = keyboardDelta(event);
906
+ if (!delta)
907
+ return false;
908
+ const startPosition = getSpatialZonePosition(zone);
909
+ const nextPosition = {
910
+ x: startPosition.x + delta.x,
911
+ y: startPosition.y + delta.y,
912
+ };
913
+ event.preventDefault();
914
+ event.stopPropagation();
915
+ const nextZone = commitZoneMove(zone, nextPosition, {
916
+ kind: 'keyboard',
917
+ origin: zoneSnapAnchor(zone),
918
+ delta,
919
+ });
920
+ if (!nextZone)
921
+ return true;
922
+ if (policy.selectZones) {
923
+ _onZoneSelect?.(buildZoneContext(nextZone, mode));
924
+ }
925
+ return true;
926
+ }
927
+ function resizeZoneByKeyboard(event, zone) {
928
+ if (!edit.keyboardResizeZones || !event.ctrlKey || zone.disabled)
929
+ return false;
930
+ if (!getSpatialZoneMapBounds(zone))
931
+ return false;
932
+ const step = event.shiftKey ? edit.resizeLargeStep : edit.resizeStep;
933
+ let nextTransform = normalizeSpatialTransform(zone.transform, edit);
934
+ if (event.key === 'ArrowRight')
935
+ nextTransform = { ...nextTransform, scaleX: nextTransform.scaleX + step };
936
+ else if (event.key === 'ArrowLeft')
937
+ nextTransform = { ...nextTransform, scaleX: nextTransform.scaleX - step };
938
+ else if (event.key === 'ArrowDown')
939
+ nextTransform = { ...nextTransform, scaleY: nextTransform.scaleY + step };
940
+ else if (event.key === 'ArrowUp')
941
+ nextTransform = { ...nextTransform, scaleY: nextTransform.scaleY - step };
942
+ else
943
+ return false;
944
+ event.preventDefault();
945
+ event.stopPropagation();
946
+ const nextZone = {
947
+ ...zone,
948
+ transform: normalizeSpatialTransform(nextTransform, edit),
949
+ };
950
+ const committedZone = commitZoneUpdate(zone, nextZone, 'resize');
951
+ if (committedZone && policy.selectZones) {
952
+ _onZoneSelect?.(buildZoneContext(committedZone, mode));
953
+ }
954
+ return true;
955
+ }
956
+ function resizeItemByKeyboard(event, item) {
957
+ if (!edit.keyboardResizeItems || !event.ctrlKey || item.disabled)
958
+ return false;
959
+ const step = event.shiftKey ? edit.resizeLargeStep : edit.resizeStep;
960
+ let nextTransform = normalizeSpatialTransform(item.transform, edit);
961
+ if (event.key === 'ArrowRight')
962
+ nextTransform = { ...nextTransform, scaleX: nextTransform.scaleX + step };
963
+ else if (event.key === 'ArrowLeft')
964
+ nextTransform = { ...nextTransform, scaleX: nextTransform.scaleX - step };
965
+ else if (event.key === 'ArrowDown')
966
+ nextTransform = { ...nextTransform, scaleY: nextTransform.scaleY + step };
967
+ else if (event.key === 'ArrowUp')
968
+ nextTransform = { ...nextTransform, scaleY: nextTransform.scaleY - step };
969
+ else
970
+ return false;
971
+ event.preventDefault();
972
+ event.stopPropagation();
973
+ const nextItem = {
974
+ ...item,
975
+ transform: normalizeSpatialTransform(nextTransform, edit),
976
+ };
977
+ const committedItem = commitItemUpdate(item, nextItem, 'resize');
978
+ if (committedItem && policy.selectItems) {
979
+ _onItemSelect?.(buildItemContext(committedItem, zones, mode));
980
+ }
981
+ return true;
982
+ }
983
+ function rotateItemByKeyboard(event, item) {
984
+ if (!edit.keyboardRotateItems || item.disabled)
985
+ return false;
986
+ if (!['[', ']', '{', '}'].includes(event.key))
987
+ return false;
988
+ event.preventDefault();
989
+ event.stopPropagation();
990
+ const step = event.shiftKey ? edit.rotationLargeStep : edit.rotationStep;
991
+ const direction = event.key === ']' || event.key === '}' ? 1 : -1;
992
+ const nextItem = rotateSpatialItem(item, (item.rotation ?? 0) + direction * step, { ...edit, rotationStep: 0 });
993
+ const committedItem = commitItemUpdate(item, nextItem, 'rotate');
994
+ if (committedItem && policy.selectItems) {
995
+ _onItemSelect?.(buildItemContext(committedItem, zones, mode));
996
+ }
997
+ return true;
998
+ }
999
+ function itemKeyboardShortcuts() {
1000
+ const shortcuts = [];
1001
+ if (edit.keyboardMoveItems)
1002
+ shortcuts.push('ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight');
1003
+ if (edit.keyboardResizeItems) {
1004
+ shortcuts.push('Ctrl+ArrowUp', 'Ctrl+ArrowDown', 'Ctrl+ArrowLeft', 'Ctrl+ArrowRight');
1005
+ }
1006
+ if (edit.keyboardRotateItems)
1007
+ shortcuts.push('[', ']');
1008
+ return shortcuts.length > 0 ? shortcuts.join(' ') : undefined;
1009
+ }
1010
+ function zoneKeyboardShortcuts() {
1011
+ const shortcuts = [];
1012
+ if (edit.keyboardMoveZones)
1013
+ shortcuts.push('ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight');
1014
+ if (edit.keyboardResizeZones) {
1015
+ shortcuts.push('Ctrl+ArrowUp', 'Ctrl+ArrowDown', 'Ctrl+ArrowLeft', 'Ctrl+ArrowRight');
1016
+ }
1017
+ return shortcuts.length > 0 ? shortcuts.join(' ') : undefined;
1018
+ }
1019
+ function zoneShapeKeyboardShortcuts() {
1020
+ if (!edit.keyboardShapeEditZones)
1021
+ return undefined;
1022
+ return 'ArrowUp ArrowDown ArrowLeft ArrowRight Delete Backspace Escape';
1023
+ }
1024
+ function zoneShapeInsertKeyboardShortcuts() {
1025
+ if (!edit.keyboardShapeEditZones)
1026
+ return undefined;
1027
+ return 'Enter Space';
1028
+ }
1029
+ function zoneShapeKeyboardDelta(event) {
1030
+ if (event.ctrlKey)
1031
+ return null;
1032
+ const step = event.shiftKey ? edit.keyboardLargeStep : edit.keyboardStep;
1033
+ if (event.key === 'ArrowRight')
1034
+ return { x: step, y: 0 };
1035
+ if (event.key === 'ArrowLeft')
1036
+ return { x: -step, y: 0 };
1037
+ if (event.key === 'ArrowDown')
1038
+ return { x: 0, y: step };
1039
+ if (event.key === 'ArrowUp')
1040
+ return { x: 0, y: -step };
1041
+ return null;
1042
+ }
1043
+ function commitZoneShapeKeyboardChange(previousZone, nextZone, shapeContext) {
1044
+ const committedZone = commitZoneUpdate(previousZone, nextZone, 'shape', shapeContext);
1045
+ if (committedZone && policy.selectZones) {
1046
+ _onZoneSelect?.(buildZoneContext(committedZone, mode));
1047
+ }
1048
+ return committedZone !== null;
1049
+ }
1050
+ function handleZoneShapeVertexKeyDown(event, zone, vertexIndex) {
1051
+ if (event.key === 'Escape') {
1052
+ event.preventDefault();
1053
+ event.stopPropagation();
1054
+ setCurrentDragState(null);
1055
+ onZoneShapeEditExit?.();
1056
+ return;
1057
+ }
1058
+ if (!edit.keyboardShapeEditZones || zone.disabled)
1059
+ return;
1060
+ if (event.key === 'Delete' || event.key === 'Backspace') {
1061
+ event.preventDefault();
1062
+ event.stopPropagation();
1063
+ const nextZone = deleteSpatialZonePolygonVertex(zone, { vertexIndex });
1064
+ commitZoneShapeKeyboardChange(zone, nextZone, {
1065
+ shapeAction: 'delete-vertex',
1066
+ vertexIndex,
1067
+ });
1068
+ return;
1069
+ }
1070
+ const delta = zoneShapeKeyboardDelta(event);
1071
+ if (!delta)
1072
+ return;
1073
+ const handles = getSpatialZonePolygonHandles(zone, edit);
1074
+ const vertex = handles?.vertices.find((candidate) => candidate.index === vertexIndex);
1075
+ if (!vertex)
1076
+ return;
1077
+ event.preventDefault();
1078
+ event.stopPropagation();
1079
+ const rawPoint = {
1080
+ x: vertex.point.x + delta.x,
1081
+ y: vertex.point.y + delta.y,
1082
+ };
1083
+ const nextMove = resolveZoneShapeVertexMove(zone, vertexIndex, rawPoint, {
1084
+ kind: 'keyboard',
1085
+ origin: vertex.point,
1086
+ delta,
1087
+ });
1088
+ commitZoneShapeKeyboardChange(zone, nextMove.zone, {
1089
+ shapeAction: 'move-vertex',
1090
+ vertexIndex,
1091
+ });
1092
+ }
1093
+ function activateZone(event, zone) {
1094
+ if (policy.zonePressStopsCanvas) {
1095
+ event.stopPropagation();
1096
+ }
1097
+ if (zone.disabled)
1098
+ return;
1099
+ const context = buildZoneContext(zone, mode);
1100
+ if (policy.selectZones) {
1101
+ _onZoneSelect?.(context);
1102
+ }
1103
+ if (policy.activateZones) {
1104
+ onZonePress?.(context);
1105
+ }
1106
+ }
1107
+ function activateItem(event, item) {
1108
+ event.stopPropagation();
1109
+ if (suppressClickItemRef.current === item.id) {
1110
+ suppressClickItemRef.current = null;
1111
+ suppressCanvasClickRef.current = false;
1112
+ return;
1113
+ }
1114
+ if (item.disabled)
1115
+ return;
1116
+ if (!policy.selectItems && !policy.activateItems)
1117
+ return;
1118
+ const context = buildItemContext(item, zones, mode);
1119
+ if (policy.selectItems) {
1120
+ _onItemSelect?.(context);
1121
+ }
1122
+ if (policy.activateItems) {
1123
+ onItemPress?.(context);
1124
+ }
1125
+ }
1126
+ function handleCanvasClick(event) {
1127
+ if (suppressCanvasClickRef.current) {
1128
+ suppressCanvasClickRef.current = false;
1129
+ return;
1130
+ }
1131
+ if (!svgRef.current)
1132
+ return;
1133
+ const rawPoint = svgPointFromPointer(event, svgRef.current, rect);
1134
+ const resolution = resolveEditPoint(rawPoint, undefined, { kind: 'canvas' });
1135
+ if (canvasGuideVisible) {
1136
+ canvasGuideRawPointRef.current = rawPoint;
1137
+ setCanvasGuideResolution(resolution);
1138
+ }
1139
+ const zone = zoneFromCanvasEvent(event, zones);
1140
+ onCanvasPress?.({
1141
+ kind: 'canvas',
1142
+ mode,
1143
+ point: resolution.point,
1144
+ rawPoint,
1145
+ ...(edit.snap.enabled ? {
1146
+ snap: {
1147
+ snapped: resolution.snapped,
1148
+ sources: resolution.sources,
1149
+ },
1150
+ } : {}),
1151
+ viewBox: rect,
1152
+ zoneId: zone?.id,
1153
+ zone,
1154
+ });
1155
+ }
1156
+ function handleCanvasPointerMove(event) {
1157
+ if (!canvasGuideVisible || dragStateRef.current || !svgRef.current)
1158
+ return;
1159
+ const rawPoint = svgPointFromPointer(event, svgRef.current, rect);
1160
+ canvasGuideRawPointRef.current = rawPoint;
1161
+ setCanvasGuideResolution(resolveEditPoint(rawPoint, undefined, { kind: 'canvas' }));
1162
+ }
1163
+ function handleCanvasPointerLeave() {
1164
+ if (canvasGuideVisible) {
1165
+ canvasGuideRawPointRef.current = null;
1166
+ setCanvasGuideResolution(null);
1167
+ }
1168
+ }
1169
+ function renderCanvasGuide() {
1170
+ if (!canvasGuideVisible || !canvasGuideResolution)
1171
+ return null;
1172
+ const guidePoint = canvasGuideResolution.point;
1173
+ const stroke = canvasGuide?.stroke ?? '#4f46e5';
1174
+ const strokeWidth = canvasGuide?.strokeWidth ?? 1.5;
1175
+ const strokeDasharray = canvasGuide?.strokeDasharray ?? '6 6';
1176
+ const opacity = canvasGuide?.opacity ?? 0.72;
1177
+ const showPoint = canvasGuide?.showPoint !== false;
1178
+ return React.createElement('g', {
1179
+ key: 'canvas-guide',
1180
+ 'data-testid': 'spatial-canvas-guide',
1181
+ 'aria-hidden': true,
1182
+ pointerEvents: 'none',
1183
+ opacity,
1184
+ }, [
1185
+ React.createElement('line', {
1186
+ key: 'x',
1187
+ 'data-testid': 'spatial-canvas-guide-x',
1188
+ x1: guidePoint.x,
1189
+ x2: guidePoint.x,
1190
+ y1: rect.y,
1191
+ y2: rect.y + rect.height,
1192
+ stroke,
1193
+ strokeWidth,
1194
+ strokeDasharray,
1195
+ strokeLinecap: 'round',
1196
+ vectorEffect: 'non-scaling-stroke',
1197
+ }),
1198
+ React.createElement('line', {
1199
+ key: 'y',
1200
+ 'data-testid': 'spatial-canvas-guide-y',
1201
+ x1: rect.x,
1202
+ x2: rect.x + rect.width,
1203
+ y1: guidePoint.y,
1204
+ y2: guidePoint.y,
1205
+ stroke,
1206
+ strokeWidth,
1207
+ strokeDasharray,
1208
+ strokeLinecap: 'round',
1209
+ vectorEffect: 'non-scaling-stroke',
1210
+ }),
1211
+ showPoint ? React.createElement('circle', {
1212
+ key: 'point',
1213
+ 'data-testid': 'spatial-canvas-guide-point',
1214
+ cx: guidePoint.x,
1215
+ cy: guidePoint.y,
1216
+ r: Math.max(3, strokeWidth * 2),
1217
+ fill: stroke,
1218
+ stroke: '#ffffff',
1219
+ strokeWidth: 1,
1220
+ vectorEffect: 'non-scaling-stroke',
1221
+ }) : null,
1222
+ ]);
1223
+ }
1224
+ function renderEditGuides(resolution) {
1225
+ if (!edit.guides.enabled || !resolution)
1226
+ return null;
1227
+ const stroke = '#2563eb';
1228
+ const strokeWidth = 1.5;
1229
+ const children = [];
1230
+ if (edit.guides.showSnapLines) {
1231
+ for (const guide of resolution.guides) {
1232
+ children.push(renderEditGuideLine(guide, stroke, strokeWidth));
1233
+ }
1234
+ }
1235
+ if (edit.guides.showCoordinates) {
1236
+ children.push(React.createElement('text', {
1237
+ key: 'coordinate',
1238
+ 'data-testid': 'spatial-edit-coordinate',
1239
+ x: resolution.point.x + 10,
1240
+ y: resolution.point.y - 10,
1241
+ fill: stroke,
1242
+ stroke: '#ffffff',
1243
+ strokeWidth: 3,
1244
+ paintOrder: 'stroke',
1245
+ style: {
1246
+ fontSize: 12,
1247
+ fontWeight: 700,
1248
+ userSelect: 'none',
1249
+ },
1250
+ }, `${resolution.point.x}, ${resolution.point.y}`));
1251
+ }
1252
+ if (children.length === 0)
1253
+ return null;
1254
+ return React.createElement('g', {
1255
+ key: 'edit-guides',
1256
+ 'data-testid': 'spatial-edit-guides',
1257
+ 'aria-hidden': true,
1258
+ pointerEvents: 'none',
1259
+ }, children);
1260
+ }
1261
+ function renderEditGuideLine(guide, stroke, strokeWidth) {
1262
+ if (guide.axis === 'x') {
1263
+ return React.createElement('line', {
1264
+ key: `x-${guide.source}-${guide.value}`,
1265
+ 'data-testid': `spatial-edit-guide-x-${guide.source}-${guide.value}`,
1266
+ x1: guide.value,
1267
+ x2: guide.value,
1268
+ y1: rect.y,
1269
+ y2: rect.y + rect.height,
1270
+ stroke,
1271
+ strokeWidth,
1272
+ strokeDasharray: '5 5',
1273
+ strokeLinecap: 'round',
1274
+ vectorEffect: 'non-scaling-stroke',
1275
+ });
1276
+ }
1277
+ return React.createElement('line', {
1278
+ key: `y-${guide.source}-${guide.value}`,
1279
+ 'data-testid': `spatial-edit-guide-y-${guide.source}-${guide.value}`,
1280
+ x1: rect.x,
1281
+ x2: rect.x + rect.width,
1282
+ y1: guide.value,
1283
+ y2: guide.value,
1284
+ stroke,
1285
+ strokeWidth,
1286
+ strokeDasharray: '5 5',
1287
+ strokeLinecap: 'round',
1288
+ vectorEffect: 'non-scaling-stroke',
1289
+ });
1290
+ }
1291
+ function resolveRenderedItem(item) {
1292
+ const droppedPreviewItem = droppedPreviewItems.get(item.id);
1293
+ if (dragState && 'itemId' in dragState && dragState.itemId === item.id) {
1294
+ if (dragState.previewItem)
1295
+ return dragState.previewItem;
1296
+ if (dragState.kind === 'move')
1297
+ return moveSpatialItem(dragState.previousItem, dragState.previewPosition);
1298
+ }
1299
+ return droppedPreviewItem ?? item;
1300
+ }
1301
+ function resolveRenderedZone(zone) {
1302
+ const droppedPreviewZone = droppedPreviewZones.get(zone.id);
1303
+ if ((dragState?.kind === 'zone-move' || dragState?.kind === 'zone-resize' || dragState?.kind === 'zone-shape')
1304
+ && dragState.zoneId === zone.id
1305
+ && dragState.previewZone) {
1306
+ return dragState.previewZone;
1307
+ }
1308
+ return droppedPreviewZone ?? zone;
1309
+ }
1310
+ function zoneTransform(zone) {
1311
+ const position = getSpatialZonePosition(zone);
1312
+ const transform = normalizeSpatialTransform(zone.transform, edit);
1313
+ const parts = [];
1314
+ if (position.x !== 0 || position.y !== 0)
1315
+ parts.push(`translate(${position.x} ${position.y})`);
1316
+ if ((zone.rotation ?? 0) !== 0)
1317
+ parts.push(`rotate(${zone.rotation ?? 0})`);
1318
+ if (transform.scaleX !== 1 || transform.scaleY !== 1)
1319
+ parts.push(`scale(${transform.scaleX} ${transform.scaleY})`);
1320
+ return parts.length > 0 ? parts.join(' ') : undefined;
1321
+ }
1322
+ function labelTransform(item, transform) {
1323
+ const rotation = `rotate(${-(item.rotation ?? 0)})`;
1324
+ if (transform.scaleX === 1 && transform.scaleY === 1)
1325
+ return rotation;
1326
+ return `scale(${1 / transform.scaleX} ${1 / transform.scaleY}) ${rotation}`;
1327
+ }
1328
+ function renderItem(item) {
1329
+ const renderedItem = resolveRenderedItem(item);
1330
+ const disabled = renderedItem.disabled === true;
1331
+ const movable = edit.dragItems
1332
+ || edit.keyboardMoveItems
1333
+ || edit.resizeItems
1334
+ || edit.rotateItems
1335
+ || edit.keyboardResizeItems
1336
+ || edit.keyboardRotateItems;
1337
+ const interactive = !disabled && (policy.selectItems || policy.activateItems || movable);
1338
+ const statusStyle = renderedItem.status ? statusStyles[renderedItem.status] : undefined;
1339
+ const visualStyle = { ...DEFAULT_STATUS_STYLE, ...(statusStyle ?? {}) };
1340
+ const selected = String(activeSelectedItemId ?? '') === String(item.id);
1341
+ const itemTransform = normalizeSpatialTransform(renderedItem.transform, edit);
1342
+ const transform = `translate(${renderedItem.position.x} ${renderedItem.position.y}) rotate(${renderedItem.rotation ?? 0}) scale(${itemTransform.scaleX} ${itemTransform.scaleY})`;
1343
+ return React.createElement('g', {
1344
+ key: String(item.id),
1345
+ role: interactive ? 'button' : undefined,
1346
+ tabIndex: interactive && policy.keyboardNavigation ? 0 : undefined,
1347
+ 'aria-label': interactive ? itemAriaLabel(renderedItem) : undefined,
1348
+ 'aria-disabled': disabled || undefined,
1349
+ 'aria-keyshortcuts': interactive && policy.keyboardNavigation ? itemKeyboardShortcuts() : undefined,
1350
+ 'data-testid': `spatial-item-${item.id}`,
1351
+ 'data-selected': selected ? 'true' : 'false',
1352
+ transform,
1353
+ style: {
1354
+ cursor: interactive ? 'pointer' : 'default',
1355
+ outline: 'none',
1356
+ },
1357
+ onPointerDown: edit.dragItems && isMovableSpatialItem(renderedItem)
1358
+ ? (event) => startItemDrag(event, renderedItem)
1359
+ : undefined,
1360
+ onClick: interactive ? (event) => activateItem(event, renderedItem) : undefined,
1361
+ onKeyDown: interactive ? (event) => {
1362
+ if (resizeItemByKeyboard(event, renderedItem))
1363
+ return;
1364
+ if (rotateItemByKeyboard(event, renderedItem))
1365
+ return;
1366
+ if (moveItemByKeyboard(event, renderedItem))
1367
+ return;
1368
+ if (event.key === 'Enter' || event.key === ' ') {
1369
+ event.preventDefault();
1370
+ activateItem(event, renderedItem);
1371
+ }
1372
+ } : undefined,
1373
+ }, [
1374
+ renderItemShape(renderedItem.shape, 'shape', visualStyle, { 'data-testid': `spatial-shape-${item.id}` }),
1375
+ selected ? renderItemShape(renderedItem.shape, 'selected', SELECTED_STYLE, {
1376
+ pointerEvents: 'none',
1377
+ 'data-testid': `spatial-selection-${item.id}`,
1378
+ }) : null,
1379
+ renderedItem.label ? React.createElement('text', {
1380
+ key: 'label',
1381
+ 'data-testid': `spatial-label-${item.id}`,
1382
+ transform: labelTransform(renderedItem, itemTransform),
1383
+ textAnchor: 'middle',
1384
+ dominantBaseline: 'middle',
1385
+ fill: visualStyle.text ?? DEFAULT_STATUS_STYLE.text,
1386
+ style: {
1387
+ fontWeight: 700,
1388
+ fontSize: 18,
1389
+ pointerEvents: 'none',
1390
+ userSelect: 'none',
1391
+ },
1392
+ }, renderedItem.label) : null,
1393
+ ]);
1394
+ }
1395
+ function renderEditHandles(item, selected) {
1396
+ if (!selected || mode !== 'edit' || !edit.handles.visible || item.disabled)
1397
+ return null;
1398
+ const points = getSpatialItemWorldHandlePoints(item, edit);
1399
+ const children = [];
1400
+ const handleSize = 7;
1401
+ if (points && edit.resizeItems && edit.handles.resize) {
1402
+ for (const handle of ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']) {
1403
+ children.push(React.createElement('rect', {
1404
+ key: handle,
1405
+ 'data-testid': `spatial-edit-handle-${item.id}-${handle}`,
1406
+ x: points[handle].x - handleSize / 2,
1407
+ y: points[handle].y - handleSize / 2,
1408
+ width: handleSize,
1409
+ height: handleSize,
1410
+ rx: 2,
1411
+ ry: 2,
1412
+ fill: '#ffffff',
1413
+ stroke: '#2563eb',
1414
+ strokeWidth: 1.5,
1415
+ vectorEffect: 'non-scaling-stroke',
1416
+ pointerEvents: 'all',
1417
+ onPointerDown: (event) => startItemResize(event, item, handle),
1418
+ 'aria-hidden': true,
1419
+ }));
1420
+ }
1421
+ }
1422
+ if (edit.rotateItems && edit.handles.rotate) {
1423
+ const rotatePoint = points?.rotate ?? { x: item.position.x, y: item.position.y - 48 };
1424
+ children.push(React.createElement('circle', {
1425
+ key: 'rotate',
1426
+ 'data-testid': `spatial-edit-handle-${item.id}-rotate`,
1427
+ cx: rotatePoint.x,
1428
+ cy: rotatePoint.y,
1429
+ r: 6,
1430
+ fill: '#ffffff',
1431
+ stroke: '#2563eb',
1432
+ strokeWidth: 1.5,
1433
+ vectorEffect: 'non-scaling-stroke',
1434
+ pointerEvents: 'all',
1435
+ onPointerDown: (event) => startItemRotate(event, item),
1436
+ 'aria-hidden': true,
1437
+ }));
1438
+ }
1439
+ if (children.length === 0)
1440
+ return null;
1441
+ return React.createElement('g', {
1442
+ key: `handles-${item.id}`,
1443
+ 'data-testid': `spatial-edit-handles-${item.id}`,
1444
+ pointerEvents: 'none',
1445
+ }, children);
1446
+ }
1447
+ function renderZoneShapeEditHandles(zone, selected) {
1448
+ if (!selected || !zoneShapeModeActive(zone) || !edit.handles.visible)
1449
+ return null;
1450
+ const handles = getSpatialZonePolygonHandles(zone, edit);
1451
+ if (!handles)
1452
+ return null;
1453
+ const children = [];
1454
+ const keyboardActive = policy.keyboardNavigation && edit.keyboardShapeEditZones;
1455
+ const zoneLabel = zoneAriaLabel(zone);
1456
+ const vertexCount = handles.vertices.length;
1457
+ for (const segment of handles.segments) {
1458
+ const nextVertexNumber = segment.index + 2 > vertexCount ? 1 : segment.index + 2;
1459
+ const insertLabel = `Insert vertex between points ${segment.index + 1} and ${nextVertexNumber} of ${zoneLabel}`;
1460
+ children.push(React.createElement('g', {
1461
+ key: `segment-${segment.index}`,
1462
+ 'data-testid': `spatial-zone-segment-control-${zone.id}-${segment.index}`,
1463
+ }, [
1464
+ React.createElement('circle', {
1465
+ key: 'handle',
1466
+ 'data-testid': `spatial-zone-segment-handle-${zone.id}-${segment.index}`,
1467
+ 'data-action': 'insert-vertex',
1468
+ cx: segment.point.x,
1469
+ cy: segment.point.y,
1470
+ r: 4.5,
1471
+ fill: '#ffffff',
1472
+ stroke: '#7c3aed',
1473
+ strokeWidth: 1.5,
1474
+ strokeDasharray: '2 2',
1475
+ vectorEffect: 'non-scaling-stroke',
1476
+ pointerEvents: 'all',
1477
+ role: keyboardActive ? 'button' : undefined,
1478
+ tabIndex: keyboardActive ? 0 : undefined,
1479
+ 'aria-label': keyboardActive ? insertLabel : undefined,
1480
+ 'aria-keyshortcuts': keyboardActive ? zoneShapeInsertKeyboardShortcuts() : undefined,
1481
+ 'aria-hidden': keyboardActive ? undefined : true,
1482
+ style: { cursor: 'copy' },
1483
+ onPointerDown: (event) => {
1484
+ event.preventDefault();
1485
+ event.stopPropagation();
1486
+ },
1487
+ onClick: (event) => insertZoneShapeVertex(event, zone, segment.index, segment.point),
1488
+ onKeyDown: keyboardActive
1489
+ ? (event) => {
1490
+ if (event.key !== 'Enter' && event.key !== ' ')
1491
+ return;
1492
+ insertZoneShapeVertex(event, zone, segment.index, segment.point);
1493
+ }
1494
+ : undefined,
1495
+ }, React.createElement('title', { key: 'title' }, insertLabel)),
1496
+ React.createElement('line', {
1497
+ key: 'plus-x',
1498
+ 'data-testid': `spatial-zone-segment-handle-plus-${zone.id}-${segment.index}-x`,
1499
+ x1: segment.point.x - 3,
1500
+ y1: segment.point.y,
1501
+ x2: segment.point.x + 3,
1502
+ y2: segment.point.y,
1503
+ stroke: '#7c3aed',
1504
+ strokeWidth: 1.6,
1505
+ strokeLinecap: 'round',
1506
+ vectorEffect: 'non-scaling-stroke',
1507
+ pointerEvents: 'none',
1508
+ 'aria-hidden': true,
1509
+ }),
1510
+ React.createElement('line', {
1511
+ key: 'plus-y',
1512
+ 'data-testid': `spatial-zone-segment-handle-plus-${zone.id}-${segment.index}-y`,
1513
+ x1: segment.point.x,
1514
+ y1: segment.point.y - 3,
1515
+ x2: segment.point.x,
1516
+ y2: segment.point.y + 3,
1517
+ stroke: '#7c3aed',
1518
+ strokeWidth: 1.6,
1519
+ strokeLinecap: 'round',
1520
+ vectorEffect: 'non-scaling-stroke',
1521
+ pointerEvents: 'none',
1522
+ 'aria-hidden': true,
1523
+ }),
1524
+ ]));
1525
+ }
1526
+ for (const vertex of handles.vertices) {
1527
+ const vertexLabel = `Move vertex ${vertex.index + 1} of ${zoneLabel}`;
1528
+ children.push(React.createElement('circle', {
1529
+ key: `vertex-${vertex.index}`,
1530
+ 'data-testid': `spatial-zone-vertex-handle-${zone.id}-${vertex.index}`,
1531
+ 'data-action': 'move-vertex',
1532
+ cx: vertex.point.x,
1533
+ cy: vertex.point.y,
1534
+ r: 6,
1535
+ fill: '#ffffff',
1536
+ stroke: '#2563eb',
1537
+ strokeWidth: 1.8,
1538
+ vectorEffect: 'non-scaling-stroke',
1539
+ pointerEvents: 'all',
1540
+ role: keyboardActive ? 'button' : undefined,
1541
+ tabIndex: keyboardActive ? 0 : undefined,
1542
+ 'aria-label': keyboardActive ? vertexLabel : undefined,
1543
+ 'aria-keyshortcuts': keyboardActive ? zoneShapeKeyboardShortcuts() : undefined,
1544
+ 'aria-hidden': keyboardActive ? undefined : true,
1545
+ style: { cursor: 'grab' },
1546
+ onPointerDown: (event) => startZoneShapeVertexDrag(event, zone, vertex.index),
1547
+ onKeyDown: keyboardActive
1548
+ ? (event) => handleZoneShapeVertexKeyDown(event, zone, vertex.index)
1549
+ : undefined,
1550
+ }, React.createElement('title', { key: 'title' }, vertexLabel)));
1551
+ }
1552
+ if (children.length === 0)
1553
+ return null;
1554
+ return React.createElement('g', {
1555
+ key: `zone-shape-handles-${zone.id}`,
1556
+ 'data-testid': `spatial-zone-shape-handles-${zone.id}`,
1557
+ pointerEvents: 'none',
1558
+ }, children);
1559
+ }
1560
+ function renderZoneEditHandles(zone, selected) {
1561
+ if (!selected || zoneShapeModeActive(zone) || mode !== 'edit' || !edit.handles.visible || !edit.resizeZones || !edit.handles.resize || zone.disabled)
1562
+ return null;
1563
+ const points = getSpatialZoneWorldHandlePoints(zone, edit);
1564
+ if (!points)
1565
+ return null;
1566
+ const handleSize = 7;
1567
+ const children = [];
1568
+ for (const handle of ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']) {
1569
+ children.push(React.createElement('rect', {
1570
+ key: handle,
1571
+ 'data-testid': `spatial-zone-edit-handle-${zone.id}-${handle}`,
1572
+ x: points[handle].x - handleSize / 2,
1573
+ y: points[handle].y - handleSize / 2,
1574
+ width: handleSize,
1575
+ height: handleSize,
1576
+ rx: 2,
1577
+ ry: 2,
1578
+ fill: '#ffffff',
1579
+ stroke: '#2563eb',
1580
+ strokeWidth: 1.5,
1581
+ vectorEffect: 'non-scaling-stroke',
1582
+ pointerEvents: 'all',
1583
+ onPointerDown: (event) => startZoneResize(event, zone, handle),
1584
+ 'aria-hidden': true,
1585
+ }));
1586
+ }
1587
+ return React.createElement('g', {
1588
+ key: `zone-handles-${zone.id}`,
1589
+ 'data-testid': `spatial-zone-edit-handles-${zone.id}`,
1590
+ pointerEvents: 'none',
1591
+ }, children);
1592
+ }
1593
+ function renderLayer(layer) {
1594
+ const nodes = [];
1595
+ for (const zone of zonesByLayer.get(layer) ?? []) {
1596
+ const renderedZone = resolveRenderedZone(zone);
1597
+ const selected = String(activeSelectedZoneId ?? '') === String(renderedZone.id);
1598
+ const interactive = !renderedZone.disabled && (policy.selectZones
1599
+ || policy.activateZones
1600
+ || edit.dragZones
1601
+ || edit.keyboardMoveZones
1602
+ || edit.resizeZones
1603
+ || edit.keyboardResizeZones);
1604
+ nodes.push(React.createElement('g', {
1605
+ key: `zone-${renderedZone.id}`,
1606
+ 'data-testid': `spatial-zone-${renderedZone.id}`,
1607
+ 'data-spatial-zone-id': String(renderedZone.id),
1608
+ 'data-selected': selected ? 'true' : 'false',
1609
+ transform: zoneTransform(renderedZone),
1610
+ role: interactive ? 'button' : undefined,
1611
+ tabIndex: interactive && policy.keyboardNavigation ? 0 : undefined,
1612
+ 'aria-label': interactive ? zoneAriaLabel(renderedZone) : undefined,
1613
+ 'aria-keyshortcuts': interactive && policy.keyboardNavigation ? zoneKeyboardShortcuts() : undefined,
1614
+ style: interactive ? { cursor: renderedZone.disabled ? 'not-allowed' : 'pointer', outline: 'none' } : undefined,
1615
+ onPointerDown: edit.dragZones && !renderedZone.disabled
1616
+ ? (event) => startZoneDrag(event, renderedZone)
1617
+ : undefined,
1618
+ onClick: interactive
1619
+ ? (event) => activateZone(event, renderedZone)
1620
+ : undefined,
1621
+ onKeyDown: interactive
1622
+ ? (event) => {
1623
+ if (resizeZoneByKeyboard(event, renderedZone))
1624
+ return;
1625
+ if (moveZoneByKeyboard(event, renderedZone))
1626
+ return;
1627
+ if (event.key === 'Enter' || event.key === ' ') {
1628
+ event.preventDefault();
1629
+ activateZone(event, renderedZone);
1630
+ }
1631
+ }
1632
+ : undefined,
1633
+ }, [
1634
+ renderZoneShape(renderedZone, 'shape'),
1635
+ selected ? renderZoneShape(renderedZone, 'selected', SELECTED_STYLE, {
1636
+ pointerEvents: 'none',
1637
+ 'data-testid': `spatial-zone-selection-${renderedZone.id}`,
1638
+ }) : null,
1639
+ renderedZone.label ? React.createElement('title', { key: 'title' }, renderedZone.label) : null,
1640
+ ]));
1641
+ const shapeHandles = renderZoneShapeEditHandles(renderedZone, selected);
1642
+ if (shapeHandles)
1643
+ nodes.push(shapeHandles);
1644
+ const handles = renderZoneEditHandles(renderedZone, selected);
1645
+ if (handles)
1646
+ nodes.push(handles);
1647
+ }
1648
+ for (const item of itemsByLayer.get(layer) ?? []) {
1649
+ nodes.push(renderItem(item));
1650
+ const selected = String(activeSelectedItemId ?? '') === String(item.id);
1651
+ const handles = renderEditHandles(resolveRenderedItem(item), selected);
1652
+ if (handles)
1653
+ nodes.push(handles);
1654
+ }
1655
+ return nodes;
1656
+ }
1657
+ const activeEditGuideResolution = dragState?.kind === 'move'
1658
+ ? dragState.snapResolution
1659
+ : dragState?.kind === 'zone-move'
1660
+ ? dragState.snapResolution
1661
+ : dragState?.kind === 'resize'
1662
+ ? dragState.snapResolution
1663
+ : dragState?.kind === 'zone-resize'
1664
+ ? dragState.snapResolution
1665
+ : dragState?.kind === 'zone-shape'
1666
+ ? dragState.snapResolution
1667
+ : null;
1668
+ return React.createElement('div', {
1669
+ className,
1670
+ style: {
1671
+ width: '100%',
1672
+ ...style,
1673
+ },
1674
+ }, React.createElement('svg', {
1675
+ ref: svgRef,
1676
+ role: 'img',
1677
+ 'aria-label': ariaLabel,
1678
+ viewBox: viewBoxToString(viewBox),
1679
+ preserveAspectRatio: 'xMidYMid meet',
1680
+ style: {
1681
+ width: '100%',
1682
+ height: 'auto',
1683
+ display: 'block',
1684
+ overflow: 'visible',
1685
+ cursor: canvasGuideVisible ? 'crosshair' : undefined,
1686
+ },
1687
+ onClick: policy.clearSelectionOnCanvasPress || onCanvasPress ? handleCanvasClick : undefined,
1688
+ onPointerMove: canvasGuideVisible ? handleCanvasPointerMove : undefined,
1689
+ onPointerLeave: canvasGuideVisible ? handleCanvasPointerLeave : undefined,
1690
+ }, [
1691
+ React.createElement('rect', {
1692
+ key: 'canvas-hit-area',
1693
+ 'data-testid': 'spatial-map-canvas',
1694
+ x: rect.x,
1695
+ y: rect.y,
1696
+ width: rect.width,
1697
+ height: rect.height,
1698
+ fill: 'transparent',
1699
+ }),
1700
+ ...layers.flatMap(renderLayer),
1701
+ renderEditGuides(activeEditGuideResolution ?? canvasGuideResolution),
1702
+ renderCanvasGuide(),
1703
+ ]));
1704
+ }
1705
+ //# sourceMappingURL=spatial-map.js.map