@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -1,333 +0,0 @@
1
- 'use client'
2
-
3
- import { type AnyNodeId, emitter, useScene, type ZoneNode } from '@pascal-app/core'
4
- import { useViewer } from '@pascal-app/viewer'
5
- import { Check, Pencil } from 'lucide-react'
6
- import { useCallback, useEffect, useRef, useState } from 'react'
7
- import { createPortal } from 'react-dom'
8
- import { useShallow } from 'zustand/react/shallow'
9
- import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
10
- import { sfxEmitter } from '../../../lib/sfx-bus'
11
- import useEditor from '../../../store/use-editor'
12
- import useInteractionScope from '../../../store/use-interaction-scope'
13
-
14
- // ─── Per-zone label editor ────────────────────────────────────────────────────
15
-
16
- function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
17
- const zone = useScene((s) => s.nodes[zoneId] as ZoneNode | undefined)
18
- const updateNode = useScene((s) => s.updateNode)
19
- const deleteNode = useScene((s) => s.deleteNode)
20
- const setSelection = useViewer((s) => s.setSelection)
21
- const selectedZoneId = useViewer((s) => s.selection.zoneId)
22
- const hoveredId = useViewer((s) => s.hoveredId)
23
- const mode = useEditor((s) => s.mode)
24
- // During an active interaction the zone label is a context badge that steps
25
- // back: faded + non-interactive so it can't be hovered/clicked mid-action.
26
- const scope = useInteractionScope((s) => s.scope)
27
- const labelStepBack = resolveOverlayPolicy(scope).contextBadges === 'faded'
28
- const isSelected = selectedZoneId === zoneId
29
- const isDeleteHovered = mode === 'delete' && hoveredId === zoneId
30
- const [editing, setEditing] = useState(false)
31
- const [value, setValue] = useState('')
32
- const inputRef = useRef<HTMLInputElement>(null)
33
- const [labelEl, setLabelEl] = useState<HTMLElement | null>(null)
34
-
35
- // Keep a ref so the click handler never has a stale zone name
36
- const zoneNameRef = useRef(zone?.name ?? '')
37
- useEffect(() => {
38
- zoneNameRef.current = zone?.name ?? ''
39
- }, [zone?.name])
40
-
41
- // Setup: find the label element, enable pointer events, and hide the
42
- // zone-renderer's own text node (children[0]) — we replace it via portal.
43
- // Retries via rAF because the <Html> element from drei may not exist yet at mount time.
44
- useEffect(() => {
45
- let cancelled = false
46
- let textEl: HTMLElement | undefined
47
-
48
- const tryFind = () => {
49
- const el = document.getElementById(`${zoneId}-label`)
50
- if (!el) {
51
- if (!cancelled) requestAnimationFrame(tryFind)
52
- return
53
- }
54
- setLabelEl(el)
55
- textEl = el.children[0] as HTMLElement | undefined
56
- if (textEl) textEl.style.display = 'none'
57
- }
58
-
59
- tryFind()
60
-
61
- return () => {
62
- cancelled = true
63
- if (textEl) textEl.style.display = ''
64
- }
65
- }, [zoneId])
66
-
67
- // Focus + select-all when entering edit mode
68
- useEffect(() => {
69
- if (editing) {
70
- inputRef.current?.focus()
71
- inputRef.current?.select()
72
- }
73
- }, [editing])
74
-
75
- // Tint the label pin red when delete-hovered
76
- useEffect(() => {
77
- if (!labelEl) return
78
- const pin = labelEl.querySelector('.label-pin') as HTMLElement | null
79
- if (!pin) return
80
- const line = pin.children[0] as HTMLElement | undefined
81
- const circle = pin.children[1] as HTMLElement | undefined
82
- const color = isDeleteHovered ? '#dc2626' : (zone?.color ?? '#6366f1')
83
- if (line) line.style.backgroundColor = color
84
- if (circle) {
85
- circle.style.backgroundColor = color
86
- }
87
- if (isDeleteHovered) {
88
- pin.style.opacity = '1'
89
- }
90
- return () => {
91
- // Restore zone color
92
- const originalColor = zone?.color ?? '#6366f1'
93
- if (line) line.style.backgroundColor = originalColor
94
- if (circle) circle.style.backgroundColor = originalColor
95
- }
96
- }, [isDeleteHovered, labelEl, zone?.color])
97
-
98
- const save = useCallback(() => {
99
- const trimmed = value.trim()
100
- if (trimmed !== (zone?.name ?? '')) {
101
- updateNode(zoneId, { name: trimmed || undefined })
102
- }
103
- setEditing(false)
104
- }, [value, zone?.name, updateNode, zoneId])
105
-
106
- const cancel = useCallback(() => {
107
- setValue(zone?.name ?? '')
108
- setEditing(false)
109
- }, [zone?.name])
110
-
111
- // Select zone + switch to zone mode from any mode
112
- const selectZone = useCallback(() => {
113
- useEditor.getState().setPhase('structure')
114
- useEditor.getState().setStructureLayer('zones')
115
- useEditor.getState().setMode('select')
116
- setSelection({ zoneId })
117
- }, [zoneId, setSelection])
118
-
119
- // Enter text editing
120
- const enterTextEditing = useCallback(() => {
121
- selectZone()
122
- setValue(zoneNameRef.current)
123
- setEditing(true)
124
- }, [selectZone])
125
-
126
- // Listen for edit-label events from the 2D floorplan (double-click on zone label)
127
- useEffect(() => {
128
- const handler = (event: { zoneId: string }) => {
129
- if (event.zoneId === zoneId) {
130
- setValue(zoneNameRef.current)
131
- setEditing(true)
132
- }
133
- }
134
- emitter.on('zone:edit-label' as any, handler as any)
135
- return () => {
136
- emitter.off('zone:edit-label' as any, handler as any)
137
- }
138
- }, [zoneId])
139
-
140
- if (!labelEl) return null
141
-
142
- const shadowColor = isDeleteHovered ? '#dc2626' : (zone?.color ?? '#6366f1')
143
- const textShadow = [
144
- `-1px -1px 0 ${shadowColor}`,
145
- ` 1px -1px 0 ${shadowColor}`,
146
- `-1px 1px 0 ${shadowColor}`,
147
- ` 1px 1px 0 ${shadowColor}`,
148
- ].join(',')
149
-
150
- // order: -1 puts this flex item before children[0] (hidden) and children[1] (pin)
151
- const sharedStyle: React.CSSProperties = {
152
- order: -1,
153
- color: 'white',
154
- textShadow,
155
- fontSize: 14,
156
- fontFamily: 'sans-serif',
157
- userSelect: 'none',
158
- pointerEvents: labelStepBack ? 'none' : 'auto',
159
- opacity: labelStepBack ? 0.4 : undefined,
160
- display: 'inline-flex',
161
- alignItems: 'center',
162
- gap: 4,
163
- whiteSpace: 'nowrap',
164
- }
165
-
166
- return createPortal(
167
- editing ? (
168
- <div
169
- onMouseDown={(e) => e.stopPropagation()}
170
- onPointerDown={(e) => e.stopPropagation()}
171
- style={sharedStyle}
172
- >
173
- <input
174
- onBlur={save}
175
- onChange={(e) => setValue(e.target.value)}
176
- onClick={(e) => e.stopPropagation()}
177
- onKeyDown={(e) => {
178
- e.stopPropagation()
179
- if (e.key === 'Enter') {
180
- e.preventDefault()
181
- save()
182
- }
183
- if (e.key === 'Escape') {
184
- e.preventDefault()
185
- cancel()
186
- }
187
- }}
188
- ref={inputRef}
189
- style={{
190
- width: `${Math.max((value || zone?.name || '').length + 1, 4)}ch`,
191
- border: 'none',
192
- borderBottom: `1px solid ${shadowColor}`,
193
- background: 'transparent',
194
- color: 'white',
195
- textShadow,
196
- outline: 'none',
197
- padding: 0,
198
- margin: 0,
199
- fontSize: 'inherit',
200
- lineHeight: 'inherit',
201
- fontFamily: 'inherit',
202
- textAlign: 'center',
203
- }}
204
- type="text"
205
- value={value}
206
- />
207
- <button
208
- onClick={(e) => {
209
- e.stopPropagation()
210
- save()
211
- }}
212
- onMouseDown={(e) => e.stopPropagation()}
213
- style={{
214
- background: 'none',
215
- border: 'none',
216
- color: 'white',
217
- cursor: 'pointer',
218
- padding: 0,
219
- display: 'inline-flex',
220
- alignItems: 'center',
221
- }}
222
- type="button"
223
- >
224
- <Check size={12} />
225
- </button>
226
- </div>
227
- ) : (
228
- <button
229
- onClick={(e) => {
230
- e.stopPropagation()
231
- if (mode === 'delete') {
232
- sfxEmitter.emit('sfx:structure-delete')
233
- deleteNode(zoneId as AnyNodeId)
234
- setSelection({ zoneId: null })
235
- return
236
- }
237
- if (isSelected) {
238
- // Already selected → enter text editing
239
- enterTextEditing()
240
- } else {
241
- // Not selected → select zone + switch to zone mode
242
- selectZone()
243
- }
244
- }}
245
- onMouseDown={(e) => e.stopPropagation()}
246
- onPointerEnter={(e) => {
247
- if (mode === 'delete') {
248
- useViewer.setState({ hoveredId: zoneId })
249
- }
250
- }}
251
- onPointerLeave={() => {
252
- if (mode === 'delete' && useViewer.getState().hoveredId === zoneId) {
253
- useViewer.setState({ hoveredId: null })
254
- }
255
- }}
256
- onPointerMove={
257
- mode === 'delete'
258
- ? (e) => {
259
- // Re-dispatch pointermove to the viewer container so DeleteCursorBadge tracks the cursor.
260
- const viewerDiv = (e.currentTarget as HTMLElement).closest(
261
- '.relative.overflow-hidden',
262
- )
263
- if (viewerDiv) {
264
- viewerDiv.dispatchEvent(
265
- new PointerEvent('pointermove', {
266
- clientX: e.clientX,
267
- clientY: e.clientY,
268
- bubbles: true,
269
- }),
270
- )
271
- }
272
- }
273
- : undefined
274
- }
275
- style={{
276
- ...sharedStyle,
277
- background: 'none',
278
- border: 'none',
279
- cursor: 'pointer',
280
- padding: 0,
281
- }}
282
- type="button"
283
- >
284
- <span>{zone?.name}</span>
285
- {isSelected && (
286
- <span
287
- onClick={(e) => {
288
- e.stopPropagation()
289
- enterTextEditing()
290
- }}
291
- role="button"
292
- style={{
293
- display: 'inline-flex',
294
- alignItems: 'center',
295
- cursor: 'text',
296
- filter: `drop-shadow(0 0 2px ${shadowColor})`,
297
- }}
298
- tabIndex={0}
299
- >
300
- <Pencil size={12} />
301
- </span>
302
- )}
303
- </button>
304
- ),
305
- labelEl,
306
- )
307
- }
308
-
309
- // ─── System: rendered in the main React tree (outside Canvas) ─────────────────
310
-
311
- export function ZoneLabelEditorSystem() {
312
- const zoneIds = useScene(
313
- useShallow((s) =>
314
- Object.values(s.nodes)
315
- .filter((n) => n.type === 'zone')
316
- .map((n) => n.id as ZoneNode['id']),
317
- ),
318
- )
319
- // The zone renderer unmounts its <Html> label when zones are hidden —
320
- // unmount the portals with it, or each editor would hold (and rAF-poll for)
321
- // a detached label element.
322
- const showZones = useViewer((s) => s.showZones)
323
-
324
- if (!showZones) return null
325
-
326
- return (
327
- <>
328
- {zoneIds.map((id) => (
329
- <ZoneLabelEditor key={id} zoneId={id} />
330
- ))}
331
- </>
332
- )
333
- }
@@ -1,100 +0,0 @@
1
- import { beforeEach, describe, expect, test } from 'bun:test'
2
- import {
3
- type AnyNode,
4
- type AnyNodeId,
5
- GROUND_SUPPORT_ID,
6
- type SlabNode,
7
- spatialGridManager,
8
- useScene,
9
- } from '@pascal-app/core'
10
- import { useViewer } from '@pascal-app/viewer'
11
- import useEditor from '../../../store/use-editor'
12
- import { createFenceOnCurrentLevel } from './fence-drafting'
13
-
14
- const LEVEL_ID = 'level_test' as AnyNodeId
15
-
16
- function seedLevel(extraNodes: AnyNode[] = []) {
17
- useScene.setState({
18
- nodes: Object.fromEntries([
19
- [
20
- LEVEL_ID,
21
- {
22
- id: LEVEL_ID,
23
- type: 'level',
24
- object: 'node',
25
- parentId: null,
26
- visible: true,
27
- metadata: {},
28
- children: extraNodes.map((node) => node.id),
29
- level: 0,
30
- } as AnyNode,
31
- ],
32
- ...extraNodes.map((node) => [node.id, node] as const),
33
- ]),
34
- rootNodeIds: [LEVEL_ID],
35
- dirtyNodes: new Set(),
36
- collections: {},
37
- } as never)
38
- }
39
-
40
- describe('createFenceOnCurrentLevel', () => {
41
- beforeEach(() => {
42
- spatialGridManager.clear()
43
- useViewer.setState({
44
- selection: {
45
- buildingId: null,
46
- levelId: LEVEL_ID,
47
- zoneId: null,
48
- selectedIds: [],
49
- },
50
- } as never)
51
- useEditor.getState().setToolDefaults('fence', null)
52
- seedLevel()
53
- })
54
-
55
- test('freezes a block top above its underlying slab', () => {
56
- const slab = {
57
- id: 'slab_low',
58
- type: 'slab',
59
- object: 'node',
60
- parentId: LEVEL_ID,
61
- visible: true,
62
- metadata: {},
63
- children: [],
64
- polygon: [
65
- [-2, -2],
66
- [2, -2],
67
- [2, 2],
68
- [-2, 2],
69
- ],
70
- holes: [],
71
- holeMetadata: [],
72
- elevation: 0.25,
73
- thickness: 0.25,
74
- recessed: false,
75
- autoFromWalls: false,
76
- } as SlabNode
77
- seedLevel([slab as AnyNode])
78
- spatialGridManager.handleNodeCreated(slab as AnyNode, LEVEL_ID)
79
-
80
- const fence = createFenceOnCurrentLevel([-1, 0], [1, 0], {
81
- supportCap: 2,
82
- preferredSupportSlabId: slab.id,
83
- constructionElevation: 2,
84
- })
85
-
86
- expect(fence?.supportSlabId).toBe(slab.id)
87
- expect(fence?.supportOffset).toBeCloseTo(1.75)
88
- })
89
-
90
- test('pins ground beneath a block top when no slab exists', () => {
91
- const fence = createFenceOnCurrentLevel([-1, 0], [1, 0], {
92
- supportCap: 2,
93
- preferredSupportSlabId: null,
94
- constructionElevation: 2,
95
- })
96
-
97
- expect(fence?.supportSlabId).toBe(GROUND_SUPPORT_ID)
98
- expect(fence?.supportOffset).toBeCloseTo(2)
99
- })
100
- })
@@ -1,266 +0,0 @@
1
- import {
2
- DEFAULT_ANGLE_STEP,
3
- type FenceConstructionOptions as FenceCommitOptions,
4
- FenceNode,
5
- getTwoPointFenceCurveTangents,
6
- getWallCurveFrameAt,
7
- getWallCurveLength,
8
- isCurvedWall,
9
- resolveFenceConstructionSupport,
10
- snapPointAlongAngleRay,
11
- useScene,
12
- type WallNode,
13
- } from '@pascal-app/core'
14
- import { useViewer } from '@pascal-app/viewer'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import useEditor from '../../../store/use-editor'
17
- import {
18
- findWallSnapTarget,
19
- getSegmentGridStep,
20
- isSegmentLongEnough,
21
- snapPointToGrid,
22
- type WallPlanPoint,
23
- } from '../wall/wall-drafting'
24
-
25
- export type FencePlanPoint = WallPlanPoint
26
-
27
- const FENCE_CORNER_SNAP_RADIUS = 0.28
28
- const FENCE_SPAN_SNAP_RADIUS = 0.16
29
-
30
- type SegmentNode = {
31
- start: FencePlanPoint
32
- end: FencePlanPoint
33
- }
34
-
35
- function distanceSquared(a: FencePlanPoint, b: FencePlanPoint): number {
36
- const dx = a[0] - b[0]
37
- const dz = a[1] - b[1]
38
- return dx * dx + dz * dz
39
- }
40
-
41
- function projectPointOntoSegment(
42
- point: FencePlanPoint,
43
- segment: SegmentNode,
44
- ): FencePlanPoint | null {
45
- const [x1, z1] = segment.start
46
- const [x2, z2] = segment.end
47
- const dx = x2 - x1
48
- const dz = z2 - z1
49
- const lengthSquared = dx * dx + dz * dz
50
- if (lengthSquared < 1e-9) {
51
- return null
52
- }
53
-
54
- const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
55
- if (t <= 0 || t >= 1) {
56
- return null
57
- }
58
-
59
- return [x1 + dx * t, z1 + dz * t]
60
- }
61
-
62
- function findFenceSnapTarget(
63
- point: FencePlanPoint,
64
- fences: FenceNode[],
65
- ignoreFenceIds: string[] = [],
66
- ): FencePlanPoint | null {
67
- const cornerRadiusSquared = FENCE_CORNER_SNAP_RADIUS ** 2
68
- const spanRadiusSquared = FENCE_SPAN_SNAP_RADIUS ** 2
69
- const ignoredFenceIds = new Set(ignoreFenceIds)
70
- let bestCornerTarget: FencePlanPoint | null = null
71
- let bestCornerDistanceSquared = Number.POSITIVE_INFINITY
72
- let bestSpanTarget: FencePlanPoint | null = null
73
- let bestSpanDistanceSquared = Number.POSITIVE_INFINITY
74
-
75
- for (const fence of fences) {
76
- if (ignoredFenceIds.has(fence.id)) {
77
- continue
78
- }
79
-
80
- for (const candidate of [fence.start, fence.end]) {
81
- const candidateDistanceSquared = distanceSquared(point, candidate)
82
- if (
83
- candidateDistanceSquared > cornerRadiusSquared ||
84
- candidateDistanceSquared >= bestCornerDistanceSquared
85
- ) {
86
- continue
87
- }
88
-
89
- bestCornerTarget = candidate
90
- bestCornerDistanceSquared = candidateDistanceSquared
91
- }
92
-
93
- if (isCurvedWall(fence)) {
94
- const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(fence) / 0.3))
95
- for (let index = 1; index < sampleCount; index += 1) {
96
- const frame = getWallCurveFrameAt(fence, index / sampleCount)
97
- const candidate: FencePlanPoint = [frame.point.x, frame.point.y]
98
- const candidateDistanceSquared = distanceSquared(point, candidate)
99
- if (
100
- candidateDistanceSquared > spanRadiusSquared ||
101
- candidateDistanceSquared >= bestSpanDistanceSquared
102
- ) {
103
- continue
104
- }
105
-
106
- bestSpanTarget = candidate
107
- bestSpanDistanceSquared = candidateDistanceSquared
108
- }
109
- } else {
110
- const candidate = projectPointOntoSegment(point, fence)
111
- if (!candidate) {
112
- continue
113
- }
114
-
115
- const candidateDistanceSquared = distanceSquared(point, candidate)
116
- if (
117
- candidateDistanceSquared > spanRadiusSquared ||
118
- candidateDistanceSquared >= bestSpanDistanceSquared
119
- ) {
120
- continue
121
- }
122
-
123
- bestSpanTarget = candidate
124
- bestSpanDistanceSquared = candidateDistanceSquared
125
- }
126
- }
127
-
128
- return bestCornerTarget ?? bestSpanTarget
129
- }
130
-
131
- export function snapFenceDraftPoint(args: {
132
- point: FencePlanPoint
133
- walls: WallNode[]
134
- fences: FenceNode[]
135
- start?: FencePlanPoint
136
- angleSnap?: boolean
137
- ignoreFenceIds?: string[]
138
- bypassSnap?: boolean
139
- magnetic?: boolean
140
- /** Override the grid step. */
141
- step?: number
142
- /**
143
- * Optional grid-snap function. When provided, replaces the default
144
- * local-axis snap — lets the 2D floor-plan keep snapping to the
145
- * world XZ grid even when the building is rotated. Wall / fence
146
- * endpoint snap precedence is preserved.
147
- */
148
- gridSnap?: (point: FencePlanPoint) => FencePlanPoint
149
- }): FencePlanPoint {
150
- const {
151
- point,
152
- walls,
153
- fences,
154
- start,
155
- angleSnap = false,
156
- ignoreFenceIds,
157
- bypassSnap = false,
158
- magnetic = true,
159
- step,
160
- gridSnap,
161
- } = args
162
- if (bypassSnap) return point
163
-
164
- const gridStep = step ?? getSegmentGridStep()
165
-
166
- // Magnetic endpoint snap must beat the angle lock, and the lock can pull
167
- // the cursor far enough off an endpoint that probing the locked point
168
- // would never engage — so under the lock, probe from the RAW cursor
169
- // first (mirrors `snapWallDraftPointDetailed`'s special-point pre-pass).
170
- if (start && angleSnap) {
171
- const rawTarget =
172
- magnetic &&
173
- (findFenceSnapTarget(point, fences, ignoreFenceIds) ?? findWallSnapTarget(point, walls))
174
- if (rawTarget) return rawTarget
175
- }
176
-
177
- // The angle path snaps the distance ALONG the 15° ray — a scalar, the
178
- // same in world and local frames — so the `gridSnap` world-grid override
179
- // only applies when the angle lock is off.
180
- const basePoint: FencePlanPoint =
181
- start && angleSnap
182
- ? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, gridStep)]
183
- : gridSnap
184
- ? gridSnap(point)
185
- : snapPointToGrid(point, gridStep)
186
- if (!magnetic) return basePoint
187
-
188
- const fenceSnapTarget = findFenceSnapTarget(basePoint, fences, ignoreFenceIds)
189
- return fenceSnapTarget ?? findWallSnapTarget(basePoint, walls) ?? basePoint
190
- }
191
-
192
- export function createFenceOnCurrentLevel(
193
- start: FencePlanPoint,
194
- end: FencePlanPoint,
195
- options?: FenceCommitOptions,
196
- ): FenceNode | null {
197
- const currentLevelId = useViewer.getState().selection.levelId
198
- const { createNode, nodes } = useScene.getState()
199
-
200
- if (!(currentLevelId && isSegmentLongEnough(start, end))) {
201
- return null
202
- }
203
-
204
- const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
205
- // Build parameters seeded by a placed preset (height, style, post
206
- // spacing, …) merge in first; `name`/`start`/`end` always win. The
207
- // schema parse validates and drops anything unexpected.
208
- const defaults = useEditor.getState().toolDefaults.fence ?? {}
209
- const authoredFence = FenceNode.parse({
210
- ...defaults,
211
- name: `Fence ${fenceCount + 1}`,
212
- start,
213
- end,
214
- })
215
- // Fences run no per-frame support election — the persisted host IS the
216
- // lift (absent = level floor), so elect it at commit, pointer-capped.
217
- const fence = resolveFenceConstructionSupport(authoredFence, currentLevelId, nodes, options)
218
-
219
- createNode(fence, currentLevelId)
220
- sfxEmitter.emit('sfx:structure-build')
221
-
222
- return fence
223
- }
224
-
225
- /**
226
- * Commit a smooth spline fence from a list of drawn control points. The
227
- * centerline becomes a Catmull-Rom curve through `path`; `start`/`end` are
228
- * pinned to the first/last point so endpoint handles, bbox, and miter
229
- * references stay valid. Requires >= 2 points spanning a usable distance.
230
- */
231
- export function createSplineFenceOnCurrentLevel(
232
- path: FencePlanPoint[],
233
- tangents = getTwoPointFenceCurveTangents(path),
234
- options?: FenceCommitOptions,
235
- ): FenceNode | null {
236
- const currentLevelId = useViewer.getState().selection.levelId
237
- const { createNode, nodes } = useScene.getState()
238
-
239
- if (!currentLevelId || path.length < 2) {
240
- return null
241
- }
242
- const start = path[0]!
243
- const end = path[path.length - 1]!
244
- // A degenerate single-point-ish path (all clicks on one spot) is rejected
245
- // the same way a too-short straight segment is.
246
- if (!isSegmentLongEnough(start, end) && path.length < 3) {
247
- return null
248
- }
249
-
250
- const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
251
- const defaults = useEditor.getState().toolDefaults.fence ?? {}
252
- const authoredFence = FenceNode.parse({
253
- ...defaults,
254
- name: `Fence ${fenceCount + 1}`,
255
- start,
256
- end,
257
- path,
258
- tangents,
259
- })
260
- const fence = resolveFenceConstructionSupport(authoredFence, currentLevelId, nodes, options)
261
-
262
- createNode(fence, currentLevelId)
263
- sfxEmitter.emit('sfx:structure-build')
264
-
265
- return fence
266
- }