@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
@@ -1,4 +1,5 @@
1
1
  import type { BuildStats, SchemaIssue, ValidateBuildJsonResult } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
2
3
  import {
3
4
  AlertTriangle,
4
5
  AppWindow,
@@ -22,6 +23,11 @@ import {
22
23
  DialogHeader,
23
24
  DialogTitle,
24
25
  } from '../../../../../components/ui/primitives/dialog'
26
+ import {
27
+ getAreaUnitLabel,
28
+ type LinearUnit,
29
+ squareMetersToAreaUnit,
30
+ } from '../../../../../lib/measurements'
25
31
 
26
32
  export type PendingImport = {
27
33
  fileName: string
@@ -80,15 +86,17 @@ function formatFileSize(bytes: number): string {
80
86
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
81
87
  }
82
88
 
83
- function formatFloorArea(m2: number): string {
89
+ function formatFloorArea(m2: number, unit: LinearUnit): string {
84
90
  if (m2 === 0) return '—'
85
- if (m2 < 10) return `${m2.toFixed(2)} m²`
86
- return `${m2.toFixed(1)} m²`
91
+ const value = squareMetersToAreaUnit(m2, unit)
92
+ const label = getAreaUnitLabel(unit)
93
+ return `${value < 10 ? value.toFixed(2) : value.toFixed(1)} ${label}`
87
94
  }
88
95
 
89
96
  export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
90
97
  const [showAllWarnings, setShowAllWarnings] = useState(false)
91
98
  const [showSchemaIssues, setShowSchemaIssues] = useState(false)
99
+ const unit = useViewer((state) => state.unit)
92
100
 
93
101
  if (!pending) return null
94
102
 
@@ -165,7 +173,7 @@ export function LoadBuildDialog({ pending, onCancel, onConfirm }: Props) {
165
173
  <div className="flex items-center justify-between border-t px-3 py-2">
166
174
  <span className="text-muted-foreground text-sm">Floor area</span>
167
175
  <span className="font-medium text-sm">
168
- {formatFloorArea(stats.floorAreaM2)}
176
+ {formatFloorArea(stats.floorAreaM2, unit)}
169
177
  </span>
170
178
  </div>
171
179
  )}
@@ -1,4 +1,4 @@
1
- import { type BuildingNode, LevelNode, useScene } from '@pascal-app/core'
1
+ import { type BuildingNode, DEFAULT_LEVEL_HEIGHT, LevelNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { Building2, Plus } from 'lucide-react'
4
4
  import { memo, useState } from 'react'
@@ -43,6 +43,7 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
43
43
  const levelCount = children.filter((childId) => nodes[childId]?.type === 'level').length
44
44
  const newLevel = LevelNode.parse({
45
45
  level: levelCount,
46
+ height: DEFAULT_LEVEL_HEIGHT,
46
47
  children: [],
47
48
  parentId: nodeId,
48
49
  })
@@ -3,6 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useEffect, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
+ import { formatAreaLabel } from './../../../../../lib/measurements'
6
7
  import useEditor from './../../../../../store/use-editor'
7
8
  import { InlineRenameInput } from './inline-rename-input'
8
9
  import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
@@ -32,6 +33,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
32
33
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
33
34
  const setSelection = useViewer((state) => state.setSelection)
34
35
  const setHoveredId = useViewer((state) => state.setHoveredId)
36
+ const unit = useViewer((state) => state.unit)
35
37
 
36
38
  // Expand when a descendant is selected — imperative to avoid subscribing to the full selectedIds array
37
39
  useEffect(() => {
@@ -75,8 +77,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
75
77
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
76
78
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
77
79
 
78
- const area = calculatePolygonArea(polygon).toFixed(1)
79
- const defaultName = `Ceiling (${area}m²)`
80
+ const defaultName = `Ceiling (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
80
81
 
81
82
  return (
82
83
  <TreeNodeWrapper
@@ -85,7 +86,7 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
85
86
  expanded={expanded}
86
87
  hasChildren={children.length > 0}
87
88
  icon={
88
- <Image alt="" className="object-contain" height={14} src="/icons/ceiling.png" width={14} />
89
+ <Image alt="" className="object-contain" height={14} src="/icons/ceiling.webp" width={14} />
89
90
  }
90
91
  isHovered={isHovered}
91
92
  isLast={isLast}
@@ -2,6 +2,7 @@ import { type AnyNodeId, type ChimneyNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useState } from 'react'
5
+ import { SnapTargetIcon } from '../../../snap-target-badge'
5
6
  import useEditor from './../../../../../store/use-editor'
6
7
  import { InlineRenameInput } from './inline-rename-input'
7
8
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -51,13 +52,15 @@ export const ChimneyTreeNode = memo(function ChimneyTreeNode({
51
52
  expanded={false}
52
53
  hasChildren={false}
53
54
  icon={
54
- <Image
55
- alt=""
56
- className="object-contain opacity-60"
57
- height={14}
58
- src="/icons/roof.png"
59
- width={14}
60
- />
55
+ <SnapTargetIcon target="roof">
56
+ <Image
57
+ alt=""
58
+ className="object-contain opacity-60"
59
+ height={14}
60
+ src="/icons/roof.webp"
61
+ width={14}
62
+ />
63
+ </SnapTargetIcon>
61
64
  }
62
65
  isHovered={isHovered}
63
66
  isLast={isLast}
@@ -51,7 +51,7 @@ export const ColumnTreeNode = memo(function ColumnTreeNode({
51
51
  expanded={false}
52
52
  hasChildren={false}
53
53
  icon={
54
- <Image alt="" className="object-contain" height={14} src="/icons/column.png" width={14} />
54
+ <Image alt="" className="object-contain" height={14} src="/icons/column.webp" width={14} />
55
55
  }
56
56
  isHovered={isHovered}
57
57
  isLast={isLast}
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, useScene } from '@pascal-app/core'
3
+ import { type AnyNodeId, type DoorNode, useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import Image from 'next/image'
6
6
  import { memo, useCallback, useState } from 'react'
7
+ import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
7
8
  import useEditor from './../../../../../store/use-editor'
8
9
  import { InlineRenameInput } from './inline-rename-input'
9
10
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -22,6 +23,7 @@ export const DoorTreeNode = memo(function DoorTreeNode({
22
23
  }: DoorTreeNodeProps) {
23
24
  const [isEditing, setIsEditing] = useState(false)
24
25
  const isVisible = useScene((s) => s.nodes[nodeId as AnyNodeId]?.visible !== false)
26
+ const node = useScene((s) => s.nodes[nodeId] as DoorNode | undefined)
25
27
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
26
28
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
27
29
  const setSelection = useViewer((state) => state.setSelection)
@@ -45,6 +47,7 @@ export const DoorTreeNode = memo(function DoorTreeNode({
45
47
 
46
48
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
47
49
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
50
+ const snapTarget = resolveNodeSnapTarget(node) ?? 'wall'
48
51
 
49
52
  return (
50
53
  <TreeNodeWrapper
@@ -53,7 +56,9 @@ export const DoorTreeNode = memo(function DoorTreeNode({
53
56
  expanded={false}
54
57
  hasChildren={false}
55
58
  icon={
56
- <Image alt="" className="object-contain" height={14} src="/icons/door.png" width={14} />
59
+ <SnapTargetIcon target={snapTarget}>
60
+ <Image alt="" className="object-contain" height={14} src="/icons/door.webp" width={14} />
61
+ </SnapTargetIcon>
57
62
  }
58
63
  isHovered={isHovered}
59
64
  isLast={isLast}
@@ -2,6 +2,7 @@ import { type AnyNodeId, type DormerNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useState } from 'react'
5
+ import { SnapTargetIcon } from '../../../snap-target-badge'
5
6
  import useEditor from './../../../../../store/use-editor'
6
7
  import { InlineRenameInput } from './inline-rename-input'
7
8
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -57,13 +58,15 @@ export const DormerTreeNode = memo(function DormerTreeNode({
57
58
  expanded={false}
58
59
  hasChildren={false}
59
60
  icon={
60
- <Image
61
- alt=""
62
- className="object-contain opacity-60"
63
- height={14}
64
- src="/icons/roof.png"
65
- width={14}
66
- />
61
+ <SnapTargetIcon target="roof">
62
+ <Image
63
+ alt=""
64
+ className="object-contain opacity-60"
65
+ height={14}
66
+ src="/icons/roof.webp"
67
+ width={14}
68
+ />
69
+ </SnapTargetIcon>
67
70
  }
68
71
  isHovered={isHovered}
69
72
  isLast={isLast}
@@ -49,7 +49,7 @@ export const ElevatorTreeNode = memo(function ElevatorTreeNode({
49
49
  expanded={false}
50
50
  hasChildren={false}
51
51
  icon={
52
- <Image alt="" className="object-contain" height={14} src="/icons/elevator.png" width={14} />
52
+ <Image alt="" className="object-contain" height={14} src="/icons/elevator.webp" width={14} />
53
53
  }
54
54
  isHovered={isHovered}
55
55
  isLast={isLast}
@@ -43,7 +43,7 @@ export const FenceTreeNode = memo(function FenceTreeNode({
43
43
  expanded={false}
44
44
  hasChildren={false}
45
45
  icon={
46
- <Image alt="" className="object-contain" height={14} src="/icons/fence.png" width={14} />
46
+ <Image alt="" className="object-contain" height={14} src="/icons/fence.webp" width={14} />
47
47
  }
48
48
  isHovered={isHovered}
49
49
  isLast={isLast}
@@ -2,6 +2,7 @@ import { type AnyNodeId, type GutterNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useState } from 'react'
5
+ import { SnapTargetIcon } from '../../../snap-target-badge'
5
6
  import useEditor from './../../../../../store/use-editor'
6
7
  import { InlineRenameInput } from './inline-rename-input'
7
8
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
@@ -51,13 +52,15 @@ export const GutterTreeNode = memo(function GutterTreeNode({
51
52
  expanded={false}
52
53
  hasChildren={false}
53
54
  icon={
54
- <Image
55
- alt=""
56
- className="object-contain opacity-60"
57
- height={14}
58
- src="/icons/roof.png"
59
- width={14}
60
- />
55
+ <SnapTargetIcon target="roof">
56
+ <Image
57
+ alt=""
58
+ className="object-contain opacity-60"
59
+ height={14}
60
+ src="/icons/roof.webp"
61
+ width={14}
62
+ />
63
+ </SnapTargetIcon>
61
64
  }
62
65
  isHovered={isHovered}
63
66
  isLast={isLast}
@@ -2,6 +2,7 @@ import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
4
  type BuildingNode,
5
+ DEFAULT_LEVEL_HEIGHT,
5
6
  emitter,
6
7
  type GuideNode,
7
8
  LevelNode,
@@ -36,8 +37,16 @@ import {
36
37
  buildLevelDuplicateCreateOps,
37
38
  type LevelDuplicatePreset,
38
39
  } from './../../../../../lib/level-duplication'
39
- import { getDefaultLevelName } from './../../../../../lib/level-name'
40
+ import { getDefaultLevelName } from '@pascal-app/core'
40
41
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
42
+ import {
43
+ formatAreaLabel,
44
+ getAreaUnitLabel,
45
+ getLinearUnitLabel,
46
+ linearUnitToMeters,
47
+ metersToLinearUnit,
48
+ squareMetersToAreaUnit,
49
+ } from './../../../../../lib/measurements'
41
50
  import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
42
51
  import { cn } from './../../../../../lib/utils'
43
52
  import useEditor from './../../../../../store/use-editor'
@@ -93,6 +102,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
93
102
  const updateNode = useScene((state) => state.updateNode)
94
103
  const mode = useEditor((state) => state.mode)
95
104
  const setMode = useEditor((state) => state.setMode)
105
+ const viewerUnit = useViewer((state) => state.unit)
96
106
 
97
107
  if (!siteNode) return null
98
108
 
@@ -101,6 +111,14 @@ const PropertyLineSection = memo(function PropertyLineSection() {
101
111
  const perimeter = calculatePerimeter(points)
102
112
  const isEditing = mode === 'edit'
103
113
 
114
+ // Property-line coordinates and readouts follow the metric/imperial toggle.
115
+ const isImperial = viewerUnit === 'imperial'
116
+ const linearLabel = getLinearUnitLabel(viewerUnit)
117
+ const toDisplayLinear = (meters: number) => metersToLinearUnit(meters, viewerUnit)
118
+ const toStoredLinear = (display: number) => linearUnitToMeters(display, viewerUnit)
119
+ const displayArea = squareMetersToAreaUnit(area, viewerUnit)
120
+ const displayPerimeter = toDisplayLinear(perimeter)
121
+
104
122
  const handleToggleEdit = () => {
105
123
  setMode(isEditing ? 'select' : 'edit')
106
124
  }
@@ -166,10 +184,16 @@ const PropertyLineSection = memo(function PropertyLineSection() {
166
184
  {/* Measurements */}
167
185
  <div className="relative flex gap-3 pr-3 pb-2 pl-10">
168
186
  <div className="text-muted-foreground text-xs">
169
- Area: <span className="text-foreground">{area.toFixed(1)} m²</span>
187
+ Area:{' '}
188
+ <span className="text-foreground">
189
+ {displayArea.toFixed(1)} {getAreaUnitLabel(viewerUnit)}
190
+ </span>
170
191
  </div>
171
192
  <div className="text-muted-foreground text-xs">
172
- Perimeter: <span className="text-foreground">{perimeter.toFixed(1)} m</span>
193
+ Perimeter:{' '}
194
+ <span className="text-foreground">
195
+ {displayPerimeter.toFixed(1)} {linearLabel}
196
+ </span>
173
197
  </div>
174
198
  </div>
175
199
 
@@ -184,21 +208,21 @@ const PropertyLineSection = memo(function PropertyLineSection() {
184
208
  <input
185
209
  className="w-16 rounded border border-border/50 bg-accent/50 px-1.5 py-0.5 text-foreground text-xs focus:border-primary focus:outline-none"
186
210
  onChange={(e) =>
187
- handlePointChange(index, 0, Number.parseFloat(e.target.value) || 0)
211
+ handlePointChange(index, 0, toStoredLinear(Number.parseFloat(e.target.value) || 0))
188
212
  }
189
213
  step={0.5}
190
214
  type="number"
191
- value={point[0]}
215
+ value={Number(toDisplayLinear(point[0]).toFixed(2))}
192
216
  />
193
217
  <label className="shrink-0 text-muted-foreground">Z</label>
194
218
  <input
195
219
  className="w-16 rounded border border-border/50 bg-accent/50 px-1.5 py-0.5 text-foreground text-xs focus:border-primary focus:outline-none"
196
220
  onChange={(e) =>
197
- handlePointChange(index, 1, Number.parseFloat(e.target.value) || 0)
221
+ handlePointChange(index, 1, toStoredLinear(Number.parseFloat(e.target.value) || 0))
198
222
  }
199
223
  step={0.5}
200
224
  type="number"
201
- value={point[1]}
225
+ value={Number(toDisplayLinear(point[1]).toFixed(2))}
202
226
  />
203
227
  <button
204
228
  className={cn(
@@ -356,13 +380,13 @@ const ReferenceItem = memo(function ReferenceItem({
356
380
  <img
357
381
  alt="Scan"
358
382
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
359
- src="/icons/mesh.png"
383
+ src="/icons/mesh.webp"
360
384
  />
361
385
  ) : (
362
386
  <img
363
387
  alt="Guide"
364
388
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
365
- src="/icons/floorplan.png"
389
+ src="/icons/floorplan.webp"
366
390
  />
367
391
  )}
368
392
  <InlineRenameInput
@@ -721,7 +745,7 @@ const LevelItem = memo(function LevelItem({
721
745
  'h-4 w-4 shrink-0 object-contain transition-all duration-200',
722
746
  !isSelected && 'opacity-60 grayscale',
723
747
  )}
724
- src="/icons/level.png"
748
+ src="/icons/level.webp"
725
749
  />
726
750
  <InlineRenameInput
727
751
  defaultName={getDefaultLevelName(level.level)}
@@ -903,6 +927,7 @@ const LevelsSection = memo(function LevelsSection({
903
927
  const handleAddLevel = () => {
904
928
  const newLevel = LevelNode.parse({
905
929
  level: levels.length,
930
+ height: DEFAULT_LEVEL_HEIGHT,
906
931
  children: [],
907
932
  parentId: building.id,
908
933
  })
@@ -999,7 +1024,7 @@ const LayerToggle = memo(function LayerToggle() {
999
1024
  'mb-1 h-6 w-6 transition-all',
1000
1025
  activeTab !== 'structure' && 'opacity-50 grayscale',
1001
1026
  )}
1002
- src="/icons/room.png"
1027
+ src="/icons/room.webp"
1003
1028
  />
1004
1029
  Structure
1005
1030
  </div>
@@ -1035,7 +1060,7 @@ const LayerToggle = memo(function LayerToggle() {
1035
1060
  'mb-1 h-6 w-6 transition-all',
1036
1061
  activeTab !== 'furnish' && 'opacity-50 grayscale',
1037
1062
  )}
1038
- src="/icons/couch.png"
1063
+ src="/icons/couch.webp"
1039
1064
  />
1040
1065
  Furnish
1041
1066
  </div>
@@ -1072,7 +1097,7 @@ const LayerToggle = memo(function LayerToggle() {
1072
1097
  'mb-1 h-6 w-6 transition-all',
1073
1098
  activeTab !== 'zones' && 'opacity-50 grayscale',
1074
1099
  )}
1075
- src="/icons/kitchen.png"
1100
+ src="/icons/kitchen.webp"
1076
1101
  />
1077
1102
  Zones
1078
1103
  </div>
@@ -1097,6 +1122,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1097
1122
  const setHoveredId = useViewer((state) => state.setHoveredId)
1098
1123
  const setPhase = useEditor((state) => state.setPhase)
1099
1124
  const setMode = useEditor((state) => state.setMode)
1125
+ const unit = useViewer((state) => state.unit)
1100
1126
 
1101
1127
  const isSelected = selectedZoneId === zone.id
1102
1128
  const isHovered = hoveredId === zone.id
@@ -1109,8 +1135,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1109
1135
  }
1110
1136
  }, [isSelected])
1111
1137
 
1112
- const area = calculatePolygonArea(zone.polygon).toFixed(1)
1113
- const defaultName = `Zone (${area}m²)`
1138
+ const defaultName = `Zone (${formatAreaLabel(calculatePolygonArea(zone.polygon), unit)})`
1114
1139
 
1115
1140
  const handleClick = () => {
1116
1141
  setSelection({ zoneId: zone.id })
@@ -1413,7 +1438,7 @@ const BuildingItem = memo(function BuildingItem({
1413
1438
  'h-5 w-5 object-contain transition-all',
1414
1439
  !isBuildingActive && 'opacity-60 grayscale',
1415
1440
  )}
1416
- src="/icons/building.png"
1441
+ src="/icons/building.webp"
1417
1442
  />
1418
1443
  <span className="truncate font-medium text-sm">{building.name || 'Building'}</span>
1419
1444
  </div>
@@ -1569,7 +1594,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1569
1594
  'h-5 w-5 object-contain transition-all',
1570
1595
  phase !== 'site' && 'opacity-60 grayscale',
1571
1596
  )}
1572
- src="/icons/site-flag.png"
1597
+ src="/icons/site-flag.webp"
1573
1598
  />
1574
1599
  <span className="font-medium text-sm">{siteNode.name || 'Site'}</span>
1575
1600
  </div>
@@ -3,19 +3,25 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useEffect, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
- import useEditor from './../../../../../store/use-editor'
6
+ import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
7
7
  import { InlineRenameInput } from './inline-rename-input'
8
- import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
8
+ import {
9
+ focusTreeNode,
10
+ handleTreeSelection,
11
+ routeTreeSelectionToNode,
12
+ TreeNode,
13
+ TreeNodeWrapper,
14
+ } from './tree-node'
9
15
  import { TreeNodeActions } from './tree-node-actions'
10
16
 
11
17
  const CATEGORY_ICONS: Record<string, string> = {
12
- door: '/icons/door.png',
13
- window: '/icons/window.png',
14
- furniture: '/icons/couch.png',
15
- appliance: '/icons/appliance.png',
16
- kitchen: '/icons/kitchen.png',
17
- bathroom: '/icons/bathroom.png',
18
- outdoor: '/icons/tree.png',
18
+ door: '/icons/door.webp',
19
+ window: '/icons/window.webp',
20
+ furniture: '/icons/couch.webp',
21
+ appliance: '/icons/appliance.webp',
22
+ kitchen: '/icons/kitchen.webp',
23
+ bathroom: '/icons/bathroom.webp',
24
+ outdoor: '/icons/tree.webp',
19
25
  }
20
26
 
21
27
  interface ItemTreeNodeProps {
@@ -35,7 +41,8 @@ export const ItemTreeNode = memo(function ItemTreeNode({
35
41
  const children = useScene(
36
42
  useShallow((s) => (s.nodes[nodeId] as ItemNode | undefined)?.children ?? []),
37
43
  )
38
- const asset = useScene((s) => (s.nodes[nodeId] as ItemNode | undefined)?.asset)
44
+ const node = useScene((s) => s.nodes[nodeId] as ItemNode | undefined)
45
+ const asset = node?.asset
39
46
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
40
47
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
41
48
  const setSelection = useViewer((state) => state.setSelection)
@@ -63,17 +70,15 @@ export const ItemTreeNode = memo(function ItemTreeNode({
63
70
  const handleClick = useCallback(
64
71
  (e: React.MouseEvent) => {
65
72
  e.stopPropagation()
66
- const handled = handleTreeSelection(
73
+ handleTreeSelection(
67
74
  e,
68
75
  nodeId,
69
76
  useViewer.getState().selection.selectedIds,
70
77
  setSelection,
71
78
  )
72
- if (!handled && useEditor.getState().phase === 'structure') {
73
- useEditor.getState().setPhase('furnish')
74
- }
79
+ routeTreeSelectionToNode(node)
75
80
  },
76
- [nodeId, setSelection],
81
+ [node, nodeId, setSelection],
77
82
  )
78
83
 
79
84
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
@@ -83,7 +88,8 @@ export const ItemTreeNode = memo(function ItemTreeNode({
83
88
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
84
89
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
85
90
 
86
- const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.png'
91
+ const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.webp'
92
+ const snapTarget = resolveNodeSnapTarget(node)
87
93
  const defaultName = asset?.name || 'Item'
88
94
  const hasChildren = children.length > 0
89
95
 
@@ -93,7 +99,15 @@ export const ItemTreeNode = memo(function ItemTreeNode({
93
99
  depth={depth}
94
100
  expanded={expanded}
95
101
  hasChildren={hasChildren}
96
- icon={<Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />}
102
+ icon={
103
+ snapTarget ? (
104
+ <SnapTargetIcon target={snapTarget}>
105
+ <Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />
106
+ </SnapTargetIcon>
107
+ ) : (
108
+ <Image alt="" className="object-contain" height={14} src={iconSrc} width={14} />
109
+ )
110
+ }
97
111
  isHovered={isHovered}
98
112
  isLast={isLast}
99
113
  isSelected={isSelected}
@@ -3,7 +3,7 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import { Layers } from 'lucide-react'
4
4
  import { memo, useCallback, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
- import { getDefaultLevelName } from '../../../../../lib/level-name'
6
+ import { getDefaultLevelName } from '@pascal-app/core'
7
7
  import { InlineRenameInput } from './inline-rename-input'
8
8
  import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
9
9
  import { TreeNodeActions } from './tree-node-actions'