@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. 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
+ }