@pascal-app/editor 0.9.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -15,6 +15,8 @@ export type WallPlanPoint = [number, number]
15
15
  /** Which kind of existing-geometry snap produced a drafted point. */
16
16
  export type WallDraftSnapKind = 'endpoint' | 'midpoint' | 'intersection' | 'wall'
17
17
 
18
+ export type WallSnapRadii = Partial<Record<WallDraftSnapKind, number>>
19
+
18
20
  export type WallDraftSnapResult = {
19
21
  point: WallPlanPoint
20
22
  /**
@@ -23,9 +25,23 @@ export type WallDraftSnapResult = {
23
25
  * the "magnetic" snap the beacon visualises; `null` for grid/angle-only.
24
26
  */
25
27
  snap: WallDraftSnapKind | null
28
+ /**
29
+ * Walls whose geometry produced the snap. Kept separate from `snap` so a
30
+ * caller can use geometry for XZ alignment while independently deciding
31
+ * whether the target is allowed to transfer its construction plane.
32
+ */
33
+ targetWallIds: string[]
26
34
  }
27
35
 
28
36
  export const WALL_JOIN_SNAP_RADIUS = 0.35
37
+ // Tight capture radius for the connectivity snap that runs in NON-magnetic modes
38
+ // (grid / off / angles). Joining an existing wall is treated as connectivity —
39
+ // separate from the 'lines' magnetic alignment — so a room can still close in
40
+ // those modes: within this distance of a wall the endpoint sticks onto it (and
41
+ // the beacon shows); beyond it, positioning is left to the mode. Kept small so
42
+ // only the last few cm near a wall stick, well above the room-detection junction
43
+ // tolerance so a captured endpoint always registers as connected.
44
+ export const WALL_CONNECT_SNAP_RADIUS = 0.05
29
45
  // Generous radius for snapping to an *existing* wall's endpoint while
30
46
  // drafting. Larger than `WALL_JOIN_SNAP_RADIUS` because endpoint snap
31
47
  // is the strongest user intent (closing a polygon, attaching to a
@@ -108,6 +124,53 @@ export function findWallSnapTarget(
108
124
  return bestTarget
109
125
  }
110
126
 
127
+ /**
128
+ * Wall ids that contain an already-resolved snap point.
129
+ *
130
+ * This is provenance, not another snap pass: the tolerance only absorbs float
131
+ * drift around a point the snap pipeline already chose.
132
+ */
133
+ export function wallIdsAtSnapPoint(
134
+ point: WallPlanPoint,
135
+ walls: WallNode[],
136
+ ignoreWallIds?: string[],
137
+ tolerance = 1e-6,
138
+ ): string[] {
139
+ const ignored = new Set(ignoreWallIds ?? [])
140
+ const toleranceSquared = tolerance * tolerance
141
+ const ids: string[] = []
142
+
143
+ for (const wall of walls) {
144
+ if (ignored.has(wall.id)) continue
145
+ if (
146
+ distanceSquared(point, wall.start) <= toleranceSquared ||
147
+ distanceSquared(point, wall.end) <= toleranceSquared
148
+ ) {
149
+ ids.push(wall.id)
150
+ continue
151
+ }
152
+
153
+ if (isCurvedWall(wall)) {
154
+ const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
155
+ for (let index = 1; index < sampleCount; index += 1) {
156
+ const frame = getWallCurveFrameAt(wall, index / sampleCount)
157
+ if (distanceSquared(point, [frame.point.x, frame.point.y]) <= toleranceSquared) {
158
+ ids.push(wall.id)
159
+ break
160
+ }
161
+ }
162
+ continue
163
+ }
164
+
165
+ const projected = projectPointOntoWall(point, wall)
166
+ if (projected && distanceSquared(point, projected) <= toleranceSquared) {
167
+ ids.push(wall.id)
168
+ }
169
+ }
170
+
171
+ return ids
172
+ }
173
+
111
174
  /**
112
175
  * Endpoint-only snap from the *raw* cursor (no grid pre-snap), with a
113
176
  * generous radius. Use this before `findWallSnapTarget` so the strong
@@ -119,9 +182,10 @@ export function findWallEndpointFromRaw(
119
182
  point: WallPlanPoint,
120
183
  walls: WallNode[],
121
184
  ignoreWallIds?: string[],
185
+ radius = WALL_ENDPOINT_SNAP_RADIUS,
122
186
  ): WallPlanPoint | null {
123
187
  const ignored = new Set(ignoreWallIds ?? [])
124
- const radiusSquared = WALL_ENDPOINT_SNAP_RADIUS ** 2
188
+ const radiusSquared = radius ** 2
125
189
  let best: WallPlanPoint | null = null
126
190
  let bestDistSq = Number.POSITIVE_INFINITY
127
191
 
@@ -152,9 +216,10 @@ export function findWallMidpointFromRaw(
152
216
  point: WallPlanPoint,
153
217
  walls: WallNode[],
154
218
  ignoreWallIds?: string[],
219
+ radius = WALL_MIDPOINT_SNAP_RADIUS,
155
220
  ): WallPlanPoint | null {
156
221
  const ignored = new Set(ignoreWallIds ?? [])
157
- const radiusSquared = WALL_MIDPOINT_SNAP_RADIUS ** 2
222
+ const radiusSquared = radius ** 2
158
223
  let best: WallPlanPoint | null = null
159
224
  let bestDistSq = Number.POSITIVE_INFINITY
160
225
 
@@ -202,10 +267,11 @@ export function findWallIntersectionFromRaw(
202
267
  point: WallPlanPoint,
203
268
  walls: WallNode[],
204
269
  ignoreWallIds?: string[],
270
+ radius = WALL_INTERSECTION_SNAP_RADIUS,
205
271
  ): WallPlanPoint | null {
206
272
  const ignored = new Set(ignoreWallIds ?? [])
207
273
  const straight = walls.filter((wall) => !ignored.has(wall.id) && !isCurvedWall(wall))
208
- const radiusSquared = WALL_INTERSECTION_SNAP_RADIUS ** 2
274
+ const radiusSquared = radius ** 2
209
275
  let best: WallPlanPoint | null = null
210
276
  let bestDistSq = Number.POSITIVE_INFINITY
211
277
 
@@ -246,6 +312,53 @@ function nearestCandidate(
246
312
  return best
247
313
  }
248
314
 
315
+ // Tolerance for "the committed endpoint actually lies on existing wall
316
+ // geometry". Commit-time resolution (corner join, connect snap, split) puts
317
+ // the endpoint exactly on the geometry, so this only needs to absorb float
318
+ // drift — it is NOT a snap radius.
319
+ export const WALL_CHAIN_JOIN_TOLERANCE = 1e-3
320
+
321
+ /**
322
+ * True when a committed chain segment's resolved `end` lies on wall geometry
323
+ * (an endpoint, or a straight wall's interior) of a wall outside the current
324
+ * draft chain. The wall tools stop chaining there: a segment that tees into
325
+ * the existing network is a termination — continuing would draft the next
326
+ * segment on top of existing walls. `chainWallIds` excludes the chain's own
327
+ * segments (including the just-committed one) so edge/midpoint snaps onto a
328
+ * previous own segment don't read as a join. Curved wall interiors are
329
+ * skipped (their endpoints still count) — resolving an end onto a curve body
330
+ * is rare and continuing there matches the previous behaviour.
331
+ */
332
+ export function chainEndJoinsExistingWall(
333
+ end: WallPlanPoint,
334
+ walls: WallNode[],
335
+ chainWallIds: string[],
336
+ tolerance = WALL_CHAIN_JOIN_TOLERANCE,
337
+ ): boolean {
338
+ const ignored = new Set(chainWallIds)
339
+ const toleranceSquared = tolerance * tolerance
340
+
341
+ for (const wall of walls) {
342
+ if (ignored.has(wall.id)) continue
343
+
344
+ if (
345
+ distanceSquared(end, wall.start) <= toleranceSquared ||
346
+ distanceSquared(end, wall.end) <= toleranceSquared
347
+ ) {
348
+ return true
349
+ }
350
+
351
+ if (isCurvedWall(wall)) continue
352
+
353
+ const projected = projectPointOntoWall(end, wall)
354
+ if (projected && distanceSquared(end, projected) <= toleranceSquared) {
355
+ return true
356
+ }
357
+ }
358
+
359
+ return false
360
+ }
361
+
249
362
  /**
250
363
  * Discrete "special point" snap from the raw cursor, in priority order:
251
364
  * 1. corners (endpoints) — strongest intent, largest radius
@@ -257,14 +370,29 @@ export function findWallSpecialPointSnap(
257
370
  point: WallPlanPoint,
258
371
  walls: WallNode[],
259
372
  ignoreWallIds?: string[],
373
+ radii?: WallSnapRadii,
260
374
  ): WallDraftSnapResult | null {
261
- const endpoint = findWallEndpointFromRaw(point, walls, ignoreWallIds)
262
- if (endpoint) return { point: endpoint, snap: 'endpoint' }
375
+ const endpoint = findWallEndpointFromRaw(point, walls, ignoreWallIds, radii?.endpoint)
376
+ if (endpoint) {
377
+ return {
378
+ point: endpoint,
379
+ snap: 'endpoint',
380
+ targetWallIds: wallIdsAtSnapPoint(endpoint, walls, ignoreWallIds),
381
+ }
382
+ }
263
383
 
264
- const midpoint = findWallMidpointFromRaw(point, walls, ignoreWallIds)
265
- const intersection = findWallIntersectionFromRaw(point, walls, ignoreWallIds)
384
+ const midpoint = findWallMidpointFromRaw(point, walls, ignoreWallIds, radii?.midpoint)
385
+ const intersection = findWallIntersectionFromRaw(point, walls, ignoreWallIds, radii?.intersection)
266
386
  return nearestCandidate(point, [
267
- midpoint && { point: midpoint, snap: 'midpoint' },
268
- intersection && { point: intersection, snap: 'intersection' },
387
+ midpoint && {
388
+ point: midpoint,
389
+ snap: 'midpoint',
390
+ targetWallIds: wallIdsAtSnapPoint(midpoint, walls, ignoreWallIds),
391
+ },
392
+ intersection && {
393
+ point: intersection,
394
+ snap: 'intersection',
395
+ targetWallIds: wallIdsAtSnapPoint(intersection, walls, ignoreWallIds),
396
+ },
269
397
  ])
270
398
  }
@@ -18,7 +18,11 @@ export const ZoneBoundaryEditor: React.FC<ZoneBoundaryEditorProps> = ({ zoneId }
18
18
 
19
19
  const handlePolygonChange = useCallback(
20
20
  (newPolygon: Array<[number, number]>) => {
21
- updateNode(zoneId, { polygon: newPolygon })
21
+ updateNode(zoneId, {
22
+ polygon: newPolygon,
23
+ autoFromWalls: false,
24
+ boundaryWallIds: [],
25
+ })
22
26
  },
23
27
  [zoneId, updateNode],
24
28
  )
@@ -1,51 +1,28 @@
1
- import { emitter, type GridEvent, type LevelNode, useScene, ZoneNode } from '@pascal-app/core'
1
+ import {
2
+ DEFAULT_ANGLE_STEP,
3
+ emitter,
4
+ type GridEvent,
5
+ type LevelNode,
6
+ snapPointAlongAngleRay,
7
+ useScene,
8
+ ZoneNode,
9
+ } from '@pascal-app/core'
2
10
  import { useViewer } from '@pascal-app/viewer'
3
11
  import { useEffect, useMemo, useRef, useState } from 'react'
4
12
  import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
5
13
  import { EDITOR_LAYER } from './../../../lib/constants'
6
14
  import { sfxEmitter } from './../../../lib/sfx-bus'
15
+ import {
16
+ clearSurfacePlanSnapFeedback,
17
+ resolveSurfacePlanPointSnap,
18
+ } from './../../../lib/surface-plan-snap'
7
19
  import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
8
- import useEditor from './../../../store/use-editor'
20
+ import useEditor, { isAngleSnapActive, isGridSnapActive } from './../../../store/use-editor'
21
+ import { useFloorplanDraftPreview } from './../../../store/use-floorplan-draft-preview'
9
22
  import { CursorSphere } from '../shared/cursor-sphere'
10
23
 
11
24
  const Y_OFFSET = 0.02
12
25
 
13
- /**
14
- * Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
15
- */
16
- const calculateSnapPoint = (
17
- lastPoint: [number, number],
18
- currentPoint: [number, number],
19
- ): [number, number] => {
20
- const [x1, y1] = lastPoint
21
- const [x, y] = currentPoint
22
-
23
- const dx = x - x1
24
- const dy = y - y1
25
- const absDx = Math.abs(dx)
26
- const absDy = Math.abs(dy)
27
-
28
- // Calculate distances to horizontal, vertical, and diagonal lines
29
- const horizontalDist = absDy
30
- const verticalDist = absDx
31
- const diagonalDist = Math.abs(absDx - absDy)
32
-
33
- // Find the minimum distance to determine which axis to snap to
34
- const minDist = Math.min(horizontalDist, verticalDist, diagonalDist)
35
-
36
- if (minDist === diagonalDist) {
37
- // Snap to 45° diagonal
38
- const diagonalLength = Math.min(absDx, absDy)
39
- return [x1 + Math.sign(dx) * diagonalLength, y1 + Math.sign(dy) * diagonalLength]
40
- }
41
- if (minDist === horizontalDist) {
42
- // Snap to horizontal
43
- return [x, y1]
44
- }
45
- // Snap to vertical
46
- return [x1, y]
47
- }
48
-
49
26
  /**
50
27
  * Creates a zone with the given polygon points
51
28
  */
@@ -103,15 +80,53 @@ export const ZoneTool: React.FC = () => {
103
80
  levelY: 0,
104
81
  })
105
82
 
83
+ useEffect(() => {
84
+ useFloorplanDraftPreview.getState().setPolygonDraft('zone', preview.points)
85
+ }, [preview.points])
86
+ useEffect(
87
+ () => () => {
88
+ const draftPreview = useFloorplanDraftPreview.getState()
89
+ if (draftPreview.polygonDraftType === 'zone') {
90
+ draftPreview.setPolygonDraft(null, [])
91
+ }
92
+ draftPreview.setCursorPoint(null)
93
+ },
94
+ [],
95
+ )
96
+
106
97
  useEffect(() => {
107
98
  if (!currentLevelId) return
108
99
 
109
100
  let cursorPosition: [number, number] = [0, 0]
101
+ let snappedCursorPosition: [number, number] | null = null
110
102
 
111
103
  // Initialize line geometries
112
104
  mainLineRef.current.geometry = new BufferGeometry()
113
105
  closingLineRef.current.geometry = new BufferGeometry()
114
106
 
107
+ // Snapping follows the active mode: `angles` locks the ray to 15° from the
108
+ // last vertex (grid quantizes the distance along it), otherwise `grid`
109
+ // quantizes to the world-aligned grid; the shared surface snap then layers
110
+ // wall-corner/midpoint/crossing magnetics and alignment guides on top —
111
+ // the same pipeline slab/ceiling drafting uses. No held-Shift bypass —
112
+ // Shift cycles the mode (see interaction-scope.md).
113
+ const snapDraftPoint = (
114
+ lastPoint: [number, number] | undefined,
115
+ gridPoint: [number, number],
116
+ rawPoint: [number, number],
117
+ ): [number, number] => {
118
+ const gridStep = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
119
+ const orthoPoint: [number, number] =
120
+ isAngleSnapActive() && lastPoint
121
+ ? [...snapPointAlongAngleRay(lastPoint, rawPoint, DEFAULT_ANGLE_STEP, gridStep)]
122
+ : gridPoint
123
+ return resolveSurfacePlanPointSnap({
124
+ rawPoint,
125
+ fallbackPoint: orthoPoint,
126
+ levelId: currentLevelId,
127
+ }).point
128
+ }
129
+
115
130
  const updateLines = () => {
116
131
  const points = pointsRef.current
117
132
  const y = levelYRef.current + Y_OFFSET
@@ -127,11 +142,9 @@ export const ZoneTool: React.FC = () => {
127
142
 
128
143
  // Add cursor point
129
144
  const lastPoint = points[points.length - 1]
130
- if (lastPoint) {
131
- const snapped = calculateSnapPoint(lastPoint, cursorPosition)
132
- if (isValidPoint(snapped)) {
133
- linePoints.push(new Vector3(snapped[0], y, snapped[1]))
134
- }
145
+ const snapped = snappedCursorPosition ?? cursorPosition
146
+ if (lastPoint && isValidPoint(snapped)) {
147
+ linePoints.push(new Vector3(snapped[0], y, snapped[1]))
135
148
  }
136
149
 
137
150
  // Update main line geometry
@@ -145,17 +158,14 @@ export const ZoneTool: React.FC = () => {
145
158
 
146
159
  // Update closing line (from cursor back to first point)
147
160
  const firstPoint = points[0]
148
- if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) {
149
- const snapped = calculateSnapPoint(lastPoint, cursorPosition)
150
- if (isValidPoint(snapped)) {
151
- const closingPoints = [
152
- new Vector3(snapped[0], y, snapped[1]),
153
- new Vector3(firstPoint[0], y, firstPoint[1]),
154
- ]
155
- closingLineRef.current.geometry.dispose()
156
- closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
157
- closingLineRef.current.visible = true
158
- }
161
+ if (points.length >= 2 && lastPoint && isValidPoint(firstPoint) && isValidPoint(snapped)) {
162
+ const closingPoints = [
163
+ new Vector3(snapped[0], y, snapped[1]),
164
+ new Vector3(firstPoint[0], y, firstPoint[1]),
165
+ ]
166
+ closingLineRef.current.geometry.dispose()
167
+ closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
168
+ closingLineRef.current.visible = true
159
169
  } else {
160
170
  closingLineRef.current.visible = false
161
171
  }
@@ -163,40 +173,46 @@ export const ZoneTool: React.FC = () => {
163
173
 
164
174
  const updatePreview = () => {
165
175
  const points = pointsRef.current
166
- const lastPoint = points[points.length - 1]
167
-
168
- let cursorPt: [number, number] | null = null
169
- if (lastPoint) {
170
- cursorPt = calculateSnapPoint(lastPoint, cursorPosition)
171
- } else if (points.length === 0) {
172
- cursorPt = cursorPosition
173
- }
176
+ const cursorPt = snappedCursorPosition ?? cursorPosition
174
177
 
175
- setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current })
178
+ setPreview({
179
+ points: [...points],
180
+ cursorPoint: isValidPoint(cursorPt) ? cursorPt : null,
181
+ levelY: levelYRef.current,
182
+ })
176
183
  updateLines()
177
184
  }
178
185
 
186
+ // World-grid snap projected into building-local; rotated buildings
187
+ // used to pull the snap off the visible grid lines. Grid quantize only
188
+ // in grid mode; off / lines / angles leave the raw cursor.
189
+ const gridPointOf = (event: GridEvent): [number, number] =>
190
+ isGridSnapActive()
191
+ ? snapWorldXZForActiveBuilding(
192
+ event.position[0],
193
+ event.position[2],
194
+ useEditor.getState().gridSnapStep,
195
+ ).local
196
+ : [event.localPosition[0], event.localPosition[2]]
197
+
179
198
  const onGridMove = (event: GridEvent) => {
180
199
  if (!cursorRef.current) return
181
200
 
182
- // World-grid snap projected into building-local; rotated buildings
183
- // used to pull the snap off the visible grid lines.
184
- const [gridX, gridZ] = snapWorldXZForActiveBuilding(
185
- event.position[0],
186
- event.position[2],
187
- 0.5,
188
- ).local
189
- cursorPosition = [gridX, gridZ]
201
+ cursorPosition = gridPointOf(event)
190
202
  levelYRef.current = event.localPosition[1]
191
203
 
192
- // If we have points, snap to axis from last point
193
204
  const lastPoint = pointsRef.current[pointsRef.current.length - 1]
194
- const displayPoint = lastPoint
195
- ? calculateSnapPoint(lastPoint, cursorPosition)
196
- : cursorPosition
197
-
198
- // Play snap sound when the snapped position changes during drawing
205
+ const displayPoint = snapDraftPoint(lastPoint, cursorPosition, [
206
+ event.localPosition[0],
207
+ event.localPosition[2],
208
+ ])
209
+ snappedCursorPosition = displayPoint
210
+ useFloorplanDraftPreview.getState().setCursorPoint(displayPoint)
211
+
212
+ // Play snap sound when the snapped position changes during drawing — only
213
+ // when a quantizing mode is active (off / lines move continuously).
199
214
  if (
215
+ (isGridSnapActive() || isAngleSnapActive()) &&
200
216
  pointsRef.current.length > 0 &&
201
217
  previousSnappedPointRef.current &&
202
218
  (displayPoint[0] !== previousSnappedPointRef.current[0] ||
@@ -214,18 +230,11 @@ export const ZoneTool: React.FC = () => {
214
230
  const onGridClick = (event: GridEvent) => {
215
231
  if (!currentLevelId) return
216
232
 
217
- const [gridX, gridZ] = snapWorldXZForActiveBuilding(
218
- event.position[0],
219
- event.position[2],
220
- 0.5,
221
- ).local
222
- let clickPoint: [number, number] = [gridX, gridZ]
223
-
224
- // Snap to axis from last point
225
233
  const lastPoint = pointsRef.current[pointsRef.current.length - 1]
226
- if (lastPoint) {
227
- clickPoint = calculateSnapPoint(lastPoint, clickPoint)
228
- }
234
+ const clickPoint = snapDraftPoint(lastPoint, gridPointOf(event), [
235
+ event.localPosition[0],
236
+ event.localPosition[2],
237
+ ])
229
238
 
230
239
  // Check if clicking on the first point to close the shape
231
240
  const firstPoint = pointsRef.current[0]
@@ -279,6 +288,7 @@ export const ZoneTool: React.FC = () => {
279
288
 
280
289
  // Reset state on unmount
281
290
  pointsRef.current = []
291
+ clearSurfacePlanSnapFeedback()
282
292
  }
283
293
  }, [currentLevelId])
284
294
 
@@ -2,9 +2,14 @@
2
2
 
3
3
  import { emitter } from '@pascal-app/core'
4
4
  import Image from 'next/image'
5
+ import useEditor from '../../../store/use-editor'
5
6
  import { ActionButton } from './action-button'
6
7
 
7
8
  export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
9
+ // Orbit stays useful in 2D-only (it spins the synced floorplan view), but
10
+ // top view only tilts the hidden 3D camera — pointless without the canvas.
11
+ const is2dOnly = useEditor((s) => s.viewMode === '2d')
12
+
8
13
  const goToTopView = () => {
9
14
  emitter.emit('camera-controls:top-view')
10
15
  }
@@ -33,7 +38,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
33
38
  alt="Orbit Left"
34
39
  className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
35
40
  height={28}
36
- src="/icons/rotate.png"
41
+ src="/icons/rotate.webp"
37
42
  width={28}
38
43
  />
39
44
  </ActionButton>
@@ -50,7 +55,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
50
55
  alt="Orbit Right"
51
56
  className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
52
57
  height={28}
53
- src="/icons/rotate.png"
58
+ src="/icons/rotate.webp"
54
59
  width={28}
55
60
  />
56
61
  </ActionButton>
@@ -58,21 +63,23 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
58
63
  )}
59
64
 
60
65
  {/* Top View */}
61
- <ActionButton
62
- className="group hover:bg-white/5"
63
- label="Top View"
64
- onClick={goToTopView}
65
- size="icon"
66
- variant="ghost"
67
- >
68
- <Image
69
- alt="Top View"
70
- className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
71
- height={28}
72
- src="/icons/topview.png"
73
- width={28}
74
- />
75
- </ActionButton>
66
+ {!is2dOnly && (
67
+ <ActionButton
68
+ className="group hover:bg-white/5"
69
+ label="Top View"
70
+ onClick={goToTopView}
71
+ size="icon"
72
+ variant="ghost"
73
+ >
74
+ <Image
75
+ alt="Top View"
76
+ className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
77
+ height={28}
78
+ src="/icons/topview.webp"
79
+ width={28}
80
+ />
81
+ </ActionButton>
82
+ )}
76
83
  </div>
77
84
  )
78
85
  }