@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,570 @@
1
+ import {
2
+ type AnyNodeId,
3
+ applyZoneTransformPlan,
4
+ duplicateZone,
5
+ generateId,
6
+ type Point,
7
+ rotateZone,
8
+ type StructureNodes,
9
+ transformZone,
10
+ useScene,
11
+ type ZoneTransformPlan,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { Plane, Vector2, Vector3 } from 'three'
15
+ import { create } from 'zustand'
16
+ import { levelFrame } from '../components/editor/group-transform-shared'
17
+ import { swallowNextClick } from '../components/editor/handles/use-handle-drag'
18
+ import { getEditorThreeContext } from '../components/editor/three-context-bridge'
19
+ import useEditor, { isGridSnapActive } from '../store/use-editor'
20
+ import useInteractionScope from '../store/use-interaction-scope'
21
+ import { clientToPlan } from './floorplan/plan-coords'
22
+ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
23
+ import { isHistoryShortcut } from './history'
24
+ import { mezzanineConflictMessage } from './mezzanine-messages'
25
+ import { sfxEmitter } from './sfx-bus'
26
+ import { isTypingTarget } from './typing-target'
27
+
28
+ export const ROOM_MOVE_DRAG_LABEL = 'room-move'
29
+
30
+ export type RoomTransformKind = 'move' | 'duplicate'
31
+
32
+ export type RoomTransformSession = {
33
+ kind: RoomTransformKind
34
+ zoneId: string
35
+ levelId: string
36
+ /** The rotation origin: the room's area centroid, in level plan coordinates. */
37
+ pivot: Point
38
+ /** The room's outline and wall footprints at rest, carried by the ghost. */
39
+ outline: Point[][]
40
+ walls: Point[][]
41
+ translate: Point
42
+ angle: number
43
+ /** Alt: a crossing through a door or window slides the opening along its wall. */
44
+ force: boolean
45
+ valid: boolean
46
+ message?: string
47
+ }
48
+
49
+ /** Why the last Rotate left / right did nothing, shown under the room's pill for a moment. */
50
+ export type RoomActionNotice = { zoneId: string; message: string }
51
+
52
+ export const useRoomTransform = create<{
53
+ session: RoomTransformSession | null
54
+ notice: RoomActionNotice | null
55
+ }>(() => ({
56
+ session: null,
57
+ notice: null,
58
+ }))
59
+
60
+ let noticeTimer: ReturnType<typeof setTimeout> | null = null
61
+ /** Shows (or clears) the short line under a room's pill for a moment. */
62
+ export function showRoomNotice(notice: RoomActionNotice | null) {
63
+ if (noticeTimer) clearTimeout(noticeTimer)
64
+ noticeTimer = notice ? setTimeout(() => showRoomNotice(null), 3500) : null
65
+ useRoomTransform.setState({ notice })
66
+ }
67
+
68
+ /**
69
+ * The planner's transform, mirrored for the ghost: rotation about the pivot
70
+ * (the planner's handedness), then translation.
71
+ */
72
+ export function transformRoomPoint(
73
+ [x, z]: Point,
74
+ pivot: Point,
75
+ angle: number,
76
+ translate: Point,
77
+ ): Point {
78
+ const cos = Math.cos(angle),
79
+ sin = Math.sin(angle)
80
+ return [
81
+ pivot[0] + cos * (x - pivot[0]) + sin * (z - pivot[1]) + translate[0],
82
+ pivot[1] - sin * (x - pivot[0]) + cos * (z - pivot[1]) + translate[1],
83
+ ]
84
+ }
85
+
86
+ /** Area centroid of an outline (outer ring first, holes after). */
87
+ export function roomPivot(rings: Point[][]): Point {
88
+ let weight = 0,
89
+ x = 0,
90
+ z = 0
91
+ for (const [index, ring] of rings.entries()) {
92
+ let cross = 0,
93
+ rx = 0,
94
+ rz = 0
95
+ for (let i = 0; i < ring.length; i++) {
96
+ const a = ring[i]!,
97
+ b = ring[(i + 1) % ring.length]!
98
+ const c = a[0] * b[1] - b[0] * a[1]
99
+ cross += c
100
+ rx += (a[0] + b[0]) * c
101
+ rz += (a[1] + b[1]) * c
102
+ }
103
+ const sign = index === 0 ? 1 : -1
104
+ weight += (sign * cross) / 2
105
+ x += (sign * rx) / 6
106
+ z += (sign * rz) / 6
107
+ }
108
+ if (Math.abs(weight) < 1e-9) {
109
+ const all = rings.flat()
110
+ return all.length
111
+ ? [
112
+ all.reduce((sum, p) => sum + p[0], 0) / all.length,
113
+ all.reduce((sum, p) => sum + p[1], 0) / all.length,
114
+ ]
115
+ : [0, 0]
116
+ }
117
+ return [x / weight, z / weight]
118
+ }
119
+
120
+ // Short cursor-side wording for what the planner still refuses: overlaps
121
+ // resolve (crossings split, collinear walls merge), so only an opening in the
122
+ // way, a deck resting on the room, or an open room can stop a placement.
123
+ const TRANSFORM_MESSAGES: Record<string, string> = {
124
+ 'open-room': 'Room is open',
125
+ 'occupied-split': 'Door or window in the way · Alt slides it',
126
+ 'deck-reference': 'A deck rests on this room',
127
+ }
128
+ export function roomTransformMessage(code?: string, force = false) {
129
+ if (code === 'occupied-split' && force) return 'No room to slide the opening'
130
+ const mezzanine = mezzanineConflictMessage(code)
131
+ if (mezzanine) return mezzanine
132
+ return (code && TRANSFORM_MESSAGES[code]) || "Can't place here"
133
+ }
134
+
135
+ export function planRoomTransform(
136
+ nodes: StructureNodes,
137
+ session: Pick<RoomTransformSession, 'kind' | 'zoneId' | 'pivot' | 'translate' | 'angle'> & {
138
+ force?: boolean
139
+ },
140
+ mintId: (kind: string) => string,
141
+ ): ZoneTransformPlan {
142
+ const input = {
143
+ zoneId: session.zoneId,
144
+ translate: session.translate,
145
+ rotate: { angle: session.angle, pivot: session.pivot },
146
+ force: session.force ?? false,
147
+ mintId: mintId as (kind: string) => never,
148
+ }
149
+ if (session.kind !== 'duplicate') return transformZone(nodes, input)
150
+ const plan = duplicateZone(nodes, input)
151
+ const zone = nodes[session.zoneId]
152
+ if (zone?.type !== 'zone' || zone.floor?.support !== 'open' || plan.conflicts?.length) return plan
153
+ // Core slides a mezzanine copy clear of a conflict on its own; the carry
154
+ // places it where the pointer is or not at all, and says why.
155
+ const created = plan.changes.find(
156
+ (change) => change.op === 'create' && change.node.type === 'zone',
157
+ )
158
+ const first = zone.polygon[0]
159
+ const landed =
160
+ created?.op === 'create' && created.node.type === 'zone' ? created.node.polygon[0] : null
161
+ if (!(first && landed)) return plan
162
+ const expected = transformRoomPoint(first, session.pivot, session.angle, session.translate)
163
+ if (Math.hypot(landed[0] - expected[0], landed[1] - expected[1]) < 1e-6) return plan
164
+ const moved = transformZone(nodes, input).conflicts?.[0]
165
+ return {
166
+ ...plan,
167
+ changes: [],
168
+ conflicts: [
169
+ moved ?? {
170
+ code: 'overlaps-mezzanine',
171
+ nodeIds: [zone.id],
172
+ message: 'Mezzanines must not overlap.',
173
+ },
174
+ ],
175
+ }
176
+ }
177
+
178
+ function previewMint() {
179
+ const used = new Set(Object.keys(useScene.getState().nodes))
180
+ let n = 0
181
+ return (kind: string) => {
182
+ let id = `${kind}_roompreview${n++}`
183
+ while (used.has(id)) id = `${kind}_roompreview${n++}`
184
+ return id
185
+ }
186
+ }
187
+
188
+ // The planner is the expensive step; identical candidates on the same scene
189
+ // reuse its verdict.
190
+ let memo: { nodes: StructureNodes; key: string; code?: string } | null = null
191
+
192
+ /** Moves the ghost and asks the planner whether it can land there. */
193
+ export function previewRoomTransform(translate: Point, angle: number, force?: boolean) {
194
+ const session = useRoomTransform.getState().session
195
+ if (!session) return
196
+ const forced = force ?? session.force
197
+ const nodes = useScene.getState().nodes
198
+ const key = JSON.stringify([session.kind, translate, angle, forced])
199
+ let code: string | undefined
200
+ if (memo?.nodes === nodes && memo.key === key) code = memo.code
201
+ else {
202
+ const idle = session.kind !== 'duplicate' && angle === 0 && !translate[0] && !translate[1]
203
+ try {
204
+ code = idle
205
+ ? undefined
206
+ : planRoomTransform(nodes, { ...session, translate, angle, force: forced }, previewMint())
207
+ .conflicts?.[0]?.code
208
+ } catch {
209
+ code = 'error'
210
+ }
211
+ memo = { nodes, key, code }
212
+ }
213
+ useRoomTransform.setState({
214
+ session: {
215
+ ...session,
216
+ translate,
217
+ angle,
218
+ force: forced,
219
+ valid: !code,
220
+ message: code ? roomTransformMessage(code, forced) : undefined,
221
+ },
222
+ })
223
+ }
224
+
225
+ let teardown: (() => void) | null = null
226
+ let owner: GestureHandle | null = null
227
+
228
+ // Re-entrant safe: the session and its owner are gone before the listeners
229
+ // and the scope are released, so nothing reacting to that release finds
230
+ // anything to cancel.
231
+ function end() {
232
+ const release = teardown
233
+ const current = owner
234
+ teardown = null
235
+ owner = null
236
+ memo = null
237
+ useRoomTransform.setState({ session: null })
238
+ try {
239
+ release?.()
240
+ } finally {
241
+ current?.end()
242
+ }
243
+ }
244
+
245
+ /** The pick-up in flight, for the gesture owner's lifecycle checks. */
246
+ export function getRoomTransformSession() {
247
+ return useRoomTransform.getState().session
248
+ }
249
+
250
+ /** Puts the room back (ghost cleared, scope released). Idempotent. */
251
+ export function cancelRoomTransform() {
252
+ if (!useRoomTransform.getState().session) return
253
+ if (owner?.active) owner.cancel()
254
+ else end()
255
+ }
256
+
257
+ /** Places the room. Refused (the session stays) while the ghost is red. */
258
+ export function commitRoomTransform(): boolean {
259
+ const session = useRoomTransform.getState().session
260
+ if (!session?.valid) return false
261
+ const idle =
262
+ session.kind !== 'duplicate' &&
263
+ session.angle === 0 &&
264
+ !session.translate[0] &&
265
+ !session.translate[1]
266
+ if (idle) {
267
+ end()
268
+ return true
269
+ }
270
+ let plan: ZoneTransformPlan
271
+ try {
272
+ plan = planRoomTransform(useScene.getState().nodes, session, generateId as never)
273
+ } catch {
274
+ useRoomTransform.setState({
275
+ session: { ...session, valid: false, message: roomTransformMessage() },
276
+ })
277
+ return false
278
+ }
279
+ if (plan.conflicts?.length) {
280
+ useRoomTransform.setState({
281
+ session: {
282
+ ...session,
283
+ valid: false,
284
+ message: roomTransformMessage(plan.conflicts[0]?.code, session.force),
285
+ },
286
+ })
287
+ return false
288
+ }
289
+ // Out of the lifecycle before the write, so its scene change cannot cancel it.
290
+ const current = owner
291
+ owner = null
292
+ const apply = () => {
293
+ try {
294
+ applyZoneTransformPlan(plan)
295
+ sfxEmitter.emit('sfx:item-place')
296
+ } finally {
297
+ end()
298
+ }
299
+ }
300
+ if (current) current.finish(apply)
301
+ else apply()
302
+ const zoneId = plan.zoneId
303
+ if (useScene.getState().nodes[zoneId as AnyNodeId])
304
+ useEditor.getState().selectRoom({ levelId: session.levelId, zoneId })
305
+ return true
306
+ }
307
+
308
+ /** Level-frame plan point under the pointer, on the 2D plan or the 3D floor plane. */
309
+ function resolvePlanPoint(event: PointerEvent, levelId: string, planeY: number): Point | null {
310
+ const three = getEditorThreeContext()
311
+ if (three && event.target === three.domElement) {
312
+ const rect = three.domElement.getBoundingClientRect()
313
+ const ndc = new Vector2(
314
+ ((event.clientX - rect.left) / rect.width) * 2 - 1,
315
+ -((event.clientY - rect.top) / rect.height) * 2 + 1,
316
+ )
317
+ three.raycaster.setFromCamera(ndc, three.camera)
318
+ const { matrix, inverse } = levelFrame(levelId)
319
+ const origin = new Vector3(0, planeY, 0).applyMatrix4(matrix)
320
+ const normal = new Vector3(0, 1, 0).transformDirection(matrix)
321
+ const hit = three.raycaster.ray.intersectPlane(
322
+ new Plane().setFromNormalAndCoplanarPoint(normal, origin),
323
+ new Vector3(),
324
+ )
325
+ if (!hit) return null
326
+ const local = hit.applyMatrix4(inverse)
327
+ return [local.x, local.z]
328
+ }
329
+ const svg = (document.querySelector('g[data-floorplan-scene]') as SVGGElement | null)
330
+ ?.ownerSVGElement
331
+ if (!svg) return null
332
+ const rect = svg.getBoundingClientRect()
333
+ if (
334
+ event.clientX < rect.left ||
335
+ event.clientX > rect.right ||
336
+ event.clientY < rect.top ||
337
+ event.clientY > rect.bottom
338
+ )
339
+ return null
340
+ const plan = clientToPlan(event.clientX, event.clientY)
341
+ return plan ? [plan[0], plan[1]] : null
342
+ }
343
+
344
+ /** The carry itself: delta-relative moves; the angle is what R / T carried. */
345
+ export function roomTransformFromPointer(
346
+ anchor: Point,
347
+ pointer: Point,
348
+ carriedAngle: number,
349
+ { gridStep, free }: { gridStep: number; free: boolean },
350
+ ): { translate: Point; angle: number } {
351
+ let dx = pointer[0] - anchor[0],
352
+ dz = pointer[1] - anchor[1]
353
+ if (!free && gridStep > 0) {
354
+ dx = Math.round(dx / gridStep) * gridStep
355
+ dz = Math.round(dz / gridStep) * gridStep
356
+ }
357
+ return { translate: [dx || 0, dz || 0], angle: carriedAngle }
358
+ }
359
+
360
+ export type RoomRotateResult = { ok: true; zoneId: string } | { ok: false; message: string }
361
+
362
+ /**
363
+ * Rotate left / right: the room turns a quarter at once, as one undo step,
364
+ * and stays selected. `rotateZone` keeps its walls on the original grid (the
365
+ * current grid step). Rotate left is counter-clockwise seen from above.
366
+ * `force` (Alt-click) slides a door or window a crossing would cut.
367
+ */
368
+ export function rotateRoom(
369
+ room: { zoneId: string; levelId: string },
370
+ direction: 'left' | 'right',
371
+ { force = false }: { force?: boolean } = {},
372
+ ): RoomRotateResult {
373
+ if (useRoomTransform.getState().session) cancelRoomTransform()
374
+ const refuse = (code?: string): RoomRotateResult => {
375
+ const message = roomTransformMessage(code, force)
376
+ showRoomNotice({ zoneId: room.zoneId, message })
377
+ return { ok: false, message }
378
+ }
379
+ const nodes = useScene.getState().nodes
380
+ if (!nodes[room.zoneId as AnyNodeId]) return refuse()
381
+ let plan: ZoneTransformPlan
382
+ try {
383
+ plan = rotateZone(nodes, {
384
+ zoneId: room.zoneId,
385
+ quarterTurns: direction === 'left' ? 1 : -1,
386
+ gridStep: useEditor.getState().gridSnapStep,
387
+ force,
388
+ mintId: generateId as never,
389
+ })
390
+ } catch {
391
+ return refuse()
392
+ }
393
+ if (plan.conflicts?.length) return refuse(plan.conflicts[0]?.code)
394
+ showRoomNotice(null)
395
+ applyZoneTransformPlan(plan)
396
+ sfxEmitter.emit('sfx:item-rotate')
397
+ if (useScene.getState().nodes[plan.zoneId as AnyNodeId])
398
+ useEditor.getState().selectRoom({ levelId: room.levelId, zoneId: plan.zoneId })
399
+ return { ok: true, zoneId: plan.zoneId }
400
+ }
401
+
402
+ /**
403
+ * Picks the room up — Move and Duplicate carry it with the pointer (R / T
404
+ * turn the carried room ±45°) — across the 2D plan and the 3D view, like the
405
+ * group pick-up. The ghost turns red where the planner refuses;
406
+ * a click places it as one undo step, Escape / right-click / ⌘Z put it back.
407
+ */
408
+ export function startRoomTransform(
409
+ kind: RoomTransformKind,
410
+ room: { zoneId: string; levelId: string; outline: Point[][]; walls: Point[][]; floorY: number },
411
+ ): boolean {
412
+ if (useRoomTransform.getState().session) cancelRoomTransform()
413
+ if (!useScene.getState().nodes[room.zoneId as AnyNodeId] || !room.outline[0]?.length) return false
414
+ const pivot = roomPivot(room.outline)
415
+ useRoomTransform.setState({
416
+ session: {
417
+ kind,
418
+ zoneId: room.zoneId,
419
+ levelId: room.levelId,
420
+ pivot,
421
+ outline: room.outline,
422
+ walls: room.walls,
423
+ translate: [0, 0],
424
+ angle: 0,
425
+ force: false,
426
+ valid: kind !== 'duplicate',
427
+ },
428
+ })
429
+ if (kind === 'duplicate') previewRoomTransform([0, 0], 0)
430
+ sfxEmitter.emit('sfx:item-pick')
431
+ const handle = beginGesture({
432
+ kind: 'room-transform',
433
+ scope: {
434
+ kind: 'handle-drag',
435
+ nodeId: room.zoneId,
436
+ handle: ROOM_MOVE_DRAG_LABEL,
437
+ },
438
+ stale: () => !useScene.getState().nodes[room.zoneId as AnyNodeId],
439
+ onCancel: () => {
440
+ if (owner === handle) owner = null
441
+ end()
442
+ },
443
+ })
444
+ owner = handle
445
+ document.body.style.cursor = 'grabbing'
446
+
447
+ let anchor: Point | null = null
448
+ let carriedAngle = 0
449
+ let frame = 0
450
+ let pending: PointerEvent | null = null
451
+ let pressed: number | null = null
452
+
453
+ const apply = (event: PointerEvent) => {
454
+ const pointer = resolvePlanPoint(event, room.levelId, room.floorY)
455
+ if (!pointer) return
456
+ if (!anchor) {
457
+ anchor = pointer
458
+ return
459
+ }
460
+ const next = roomTransformFromPointer(anchor, pointer, carriedAngle, {
461
+ gridStep: isGridSnapActive() ? useEditor.getState().gridSnapStep : 0,
462
+ free: event.altKey,
463
+ })
464
+ const current = useRoomTransform.getState().session
465
+ if (
466
+ current &&
467
+ current.force === event.altKey &&
468
+ current.angle === next.angle &&
469
+ current.translate[0] === next.translate[0] &&
470
+ current.translate[1] === next.translate[1]
471
+ )
472
+ return
473
+ sfxEmitter.emit('sfx:grid-snap')
474
+ previewRoomTransform(next.translate, next.angle, event.altKey)
475
+ }
476
+ const flush = () => {
477
+ frame = 0
478
+ const event = pending
479
+ pending = null
480
+ if (event) apply(event)
481
+ }
482
+ const onMove = (event: PointerEvent) => {
483
+ pending = event
484
+ if (!frame) frame = requestAnimationFrame(flush) || 0
485
+ }
486
+ const onPointerDown = (event: PointerEvent) => {
487
+ if (event.button === 2) {
488
+ event.preventDefault()
489
+ event.stopPropagation()
490
+ cancelRoomTransform()
491
+ return
492
+ }
493
+ if (event.button !== 0 || !resolvePlanPoint(event, room.levelId, room.floorY)) return
494
+ event.preventDefault()
495
+ event.stopPropagation()
496
+ if (frame) cancelAnimationFrame(frame)
497
+ frame = 0
498
+ pending = null
499
+ apply(event)
500
+ pressed = event.pointerId
501
+ }
502
+ const onPointerUp = (event: PointerEvent) => {
503
+ if (pressed === null || event.pointerId !== pressed) return
504
+ pressed = null
505
+ event.preventDefault()
506
+ event.stopPropagation()
507
+ swallowNextClick()
508
+ useViewer.getState().setInputDragging(true)
509
+ setTimeout(() => useViewer.getState().setInputDragging(false), 0)
510
+ commitRoomTransform()
511
+ }
512
+ const label = ROOM_MOVE_DRAG_LABEL
513
+ const ownsScope = () => {
514
+ const scope = useInteractionScope.getState().scope
515
+ return scope.kind === 'handle-drag' && scope.handle === label && scope.nodeId === room.zoneId
516
+ }
517
+ // Alt re-asks the planner where the room stands: slide an opening or not.
518
+ const onAlt = (event: KeyboardEvent) => {
519
+ if (event.key !== 'Alt' || isTypingTarget(event.target) || !ownsScope()) return
520
+ const session = useRoomTransform.getState().session
521
+ const force = event.type === 'keydown'
522
+ if (session && session.force !== force)
523
+ previewRoomTransform(session.translate, session.angle, force)
524
+ }
525
+ const onKeyDown = (event: KeyboardEvent) => {
526
+ if (event.defaultPrevented || isTypingTarget(event.target) || !ownsScope()) return
527
+ if (event.key === 'Alt') return onAlt(event)
528
+ const key = event.key.toLowerCase()
529
+ if (
530
+ (key === 'r' || key === 't') &&
531
+ !event.metaKey &&
532
+ !event.ctrlKey &&
533
+ !event.altKey &&
534
+ !event.shiftKey
535
+ ) {
536
+ event.preventDefault()
537
+ event.stopPropagation()
538
+ carriedAngle += (key === 'r' ? 1 : -1) * (Math.PI / 4)
539
+ const session = useRoomTransform.getState().session
540
+ if (session) previewRoomTransform(session.translate, carriedAngle)
541
+ sfxEmitter.emit('sfx:item-rotate')
542
+ return
543
+ }
544
+ if (event.key !== 'Escape' && !isHistoryShortcut(event)) return
545
+ event.preventDefault()
546
+ event.stopPropagation()
547
+ cancelRoomTransform()
548
+ }
549
+ const onContextMenu = (event: Event) => {
550
+ event.preventDefault()
551
+ event.stopPropagation()
552
+ }
553
+ window.addEventListener('pointermove', onMove)
554
+ window.addEventListener('pointerdown', onPointerDown, true)
555
+ window.addEventListener('pointerup', onPointerUp, true)
556
+ window.addEventListener('keydown', onKeyDown, true)
557
+ window.addEventListener('keyup', onAlt, true)
558
+ window.addEventListener('contextmenu', onContextMenu, true)
559
+ teardown = () => {
560
+ if (frame) cancelAnimationFrame(frame)
561
+ window.removeEventListener('pointermove', onMove)
562
+ window.removeEventListener('pointerdown', onPointerDown, true)
563
+ window.removeEventListener('pointerup', onPointerUp, true)
564
+ window.removeEventListener('keydown', onKeyDown, true)
565
+ window.removeEventListener('keyup', onAlt, true)
566
+ window.removeEventListener('contextmenu', onContextMenu, true)
567
+ if (document.body.style.cursor === 'grabbing') document.body.style.cursor = ''
568
+ }
569
+ return true
570
+ }