@pascal-app/editor 0.9.1 → 0.9.2

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 (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. package/src/lib/level-name.ts +0 -11
@@ -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}
@@ -36,8 +36,16 @@ import {
36
36
  buildLevelDuplicateCreateOps,
37
37
  type LevelDuplicatePreset,
38
38
  } from './../../../../../lib/level-duplication'
39
- import { getDefaultLevelName } from './../../../../../lib/level-name'
39
+ import { getDefaultLevelName } from '@pascal-app/core'
40
40
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
41
+ import {
42
+ formatAreaLabel,
43
+ getAreaUnitLabel,
44
+ getLinearUnitLabel,
45
+ linearUnitToMeters,
46
+ metersToLinearUnit,
47
+ squareMetersToAreaUnit,
48
+ } from './../../../../../lib/measurements'
41
49
  import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
42
50
  import { cn } from './../../../../../lib/utils'
43
51
  import useEditor from './../../../../../store/use-editor'
@@ -93,6 +101,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
93
101
  const updateNode = useScene((state) => state.updateNode)
94
102
  const mode = useEditor((state) => state.mode)
95
103
  const setMode = useEditor((state) => state.setMode)
104
+ const viewerUnit = useViewer((state) => state.unit)
96
105
 
97
106
  if (!siteNode) return null
98
107
 
@@ -101,6 +110,14 @@ const PropertyLineSection = memo(function PropertyLineSection() {
101
110
  const perimeter = calculatePerimeter(points)
102
111
  const isEditing = mode === 'edit'
103
112
 
113
+ // Property-line coordinates and readouts follow the metric/imperial toggle.
114
+ const isImperial = viewerUnit === 'imperial'
115
+ const linearLabel = getLinearUnitLabel(viewerUnit)
116
+ const toDisplayLinear = (meters: number) => metersToLinearUnit(meters, viewerUnit)
117
+ const toStoredLinear = (display: number) => linearUnitToMeters(display, viewerUnit)
118
+ const displayArea = squareMetersToAreaUnit(area, viewerUnit)
119
+ const displayPerimeter = toDisplayLinear(perimeter)
120
+
104
121
  const handleToggleEdit = () => {
105
122
  setMode(isEditing ? 'select' : 'edit')
106
123
  }
@@ -166,10 +183,16 @@ const PropertyLineSection = memo(function PropertyLineSection() {
166
183
  {/* Measurements */}
167
184
  <div className="relative flex gap-3 pr-3 pb-2 pl-10">
168
185
  <div className="text-muted-foreground text-xs">
169
- Area: <span className="text-foreground">{area.toFixed(1)} m²</span>
186
+ Area:{' '}
187
+ <span className="text-foreground">
188
+ {displayArea.toFixed(1)} {getAreaUnitLabel(viewerUnit)}
189
+ </span>
170
190
  </div>
171
191
  <div className="text-muted-foreground text-xs">
172
- Perimeter: <span className="text-foreground">{perimeter.toFixed(1)} m</span>
192
+ Perimeter:{' '}
193
+ <span className="text-foreground">
194
+ {displayPerimeter.toFixed(1)} {linearLabel}
195
+ </span>
173
196
  </div>
174
197
  </div>
175
198
 
@@ -184,21 +207,21 @@ const PropertyLineSection = memo(function PropertyLineSection() {
184
207
  <input
185
208
  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
209
  onChange={(e) =>
187
- handlePointChange(index, 0, Number.parseFloat(e.target.value) || 0)
210
+ handlePointChange(index, 0, toStoredLinear(Number.parseFloat(e.target.value) || 0))
188
211
  }
189
212
  step={0.5}
190
213
  type="number"
191
- value={point[0]}
214
+ value={Number(toDisplayLinear(point[0]).toFixed(2))}
192
215
  />
193
216
  <label className="shrink-0 text-muted-foreground">Z</label>
194
217
  <input
195
218
  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
219
  onChange={(e) =>
197
- handlePointChange(index, 1, Number.parseFloat(e.target.value) || 0)
220
+ handlePointChange(index, 1, toStoredLinear(Number.parseFloat(e.target.value) || 0))
198
221
  }
199
222
  step={0.5}
200
223
  type="number"
201
- value={point[1]}
224
+ value={Number(toDisplayLinear(point[1]).toFixed(2))}
202
225
  />
203
226
  <button
204
227
  className={cn(
@@ -356,13 +379,13 @@ const ReferenceItem = memo(function ReferenceItem({
356
379
  <img
357
380
  alt="Scan"
358
381
  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"
382
+ src="/icons/mesh.webp"
360
383
  />
361
384
  ) : (
362
385
  <img
363
386
  alt="Guide"
364
387
  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"
388
+ src="/icons/floorplan.webp"
366
389
  />
367
390
  )}
368
391
  <InlineRenameInput
@@ -721,7 +744,7 @@ const LevelItem = memo(function LevelItem({
721
744
  'h-4 w-4 shrink-0 object-contain transition-all duration-200',
722
745
  !isSelected && 'opacity-60 grayscale',
723
746
  )}
724
- src="/icons/level.png"
747
+ src="/icons/level.webp"
725
748
  />
726
749
  <InlineRenameInput
727
750
  defaultName={getDefaultLevelName(level.level)}
@@ -999,7 +1022,7 @@ const LayerToggle = memo(function LayerToggle() {
999
1022
  'mb-1 h-6 w-6 transition-all',
1000
1023
  activeTab !== 'structure' && 'opacity-50 grayscale',
1001
1024
  )}
1002
- src="/icons/room.png"
1025
+ src="/icons/room.webp"
1003
1026
  />
1004
1027
  Structure
1005
1028
  </div>
@@ -1035,7 +1058,7 @@ const LayerToggle = memo(function LayerToggle() {
1035
1058
  'mb-1 h-6 w-6 transition-all',
1036
1059
  activeTab !== 'furnish' && 'opacity-50 grayscale',
1037
1060
  )}
1038
- src="/icons/couch.png"
1061
+ src="/icons/couch.webp"
1039
1062
  />
1040
1063
  Furnish
1041
1064
  </div>
@@ -1072,7 +1095,7 @@ const LayerToggle = memo(function LayerToggle() {
1072
1095
  'mb-1 h-6 w-6 transition-all',
1073
1096
  activeTab !== 'zones' && 'opacity-50 grayscale',
1074
1097
  )}
1075
- src="/icons/kitchen.png"
1098
+ src="/icons/kitchen.webp"
1076
1099
  />
1077
1100
  Zones
1078
1101
  </div>
@@ -1097,6 +1120,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1097
1120
  const setHoveredId = useViewer((state) => state.setHoveredId)
1098
1121
  const setPhase = useEditor((state) => state.setPhase)
1099
1122
  const setMode = useEditor((state) => state.setMode)
1123
+ const unit = useViewer((state) => state.unit)
1100
1124
 
1101
1125
  const isSelected = selectedZoneId === zone.id
1102
1126
  const isHovered = hoveredId === zone.id
@@ -1109,8 +1133,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1109
1133
  }
1110
1134
  }, [isSelected])
1111
1135
 
1112
- const area = calculatePolygonArea(zone.polygon).toFixed(1)
1113
- const defaultName = `Zone (${area}m²)`
1136
+ const defaultName = `Zone (${formatAreaLabel(calculatePolygonArea(zone.polygon), unit)})`
1114
1137
 
1115
1138
  const handleClick = () => {
1116
1139
  setSelection({ zoneId: zone.id })
@@ -1413,7 +1436,7 @@ const BuildingItem = memo(function BuildingItem({
1413
1436
  'h-5 w-5 object-contain transition-all',
1414
1437
  !isBuildingActive && 'opacity-60 grayscale',
1415
1438
  )}
1416
- src="/icons/building.png"
1439
+ src="/icons/building.webp"
1417
1440
  />
1418
1441
  <span className="truncate font-medium text-sm">{building.name || 'Building'}</span>
1419
1442
  </div>
@@ -1569,7 +1592,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1569
1592
  'h-5 w-5 object-contain transition-all',
1570
1593
  phase !== 'site' && 'opacity-60 grayscale',
1571
1594
  )}
1572
- src="/icons/site-flag.png"
1595
+ src="/icons/site-flag.webp"
1573
1596
  />
1574
1597
  <span className="font-medium text-sm">{siteNode.name || 'Site'}</span>
1575
1598
  </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'
@@ -1,11 +1,20 @@
1
+ import { Icon as IconifyIcon } from '@iconify/react'
1
2
  import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
2
3
  import { useViewer } from '@pascal-app/viewer'
3
4
  import Image from 'next/image'
4
- import { memo, useCallback, useState } from 'react'
5
- import useEditor from './../../../../../store/use-editor'
5
+ import { memo, useCallback, useEffect, useState } from 'react'
6
+ import { useShallow } from 'zustand/react/shallow'
7
+ import { resolveNodeSnapTarget, SnapTargetIcon } from '../../../snap-target-badge'
6
8
  import { InlineRenameInput } from './inline-rename-input'
7
- import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
+ import {
10
+ focusTreeNode,
11
+ handleTreeSelection,
12
+ routeTreeSelectionToNode,
13
+ TreeNode,
14
+ TreeNodeWrapper,
15
+ } from './tree-node'
8
16
  import { TreeNodeActions } from './tree-node-actions'
17
+ import { resolveTreeChildIds, treeContainsDescendant } from './tree-structure'
9
18
 
10
19
  interface RegistryTreeNodeProps {
11
20
  nodeId: AnyNodeId
@@ -14,11 +23,11 @@ interface RegistryTreeNodeProps {
14
23
  }
15
24
 
16
25
  /**
17
- * Generic, leaf tree-node row driven entirely by the kind's
18
- * `def.presentation` (icon + label). Replaces the per-kind boilerplate
26
+ * Generic, registry-driven tree-node row powered by `def.presentation` and
27
+ * `def.tree`. Replaces the per-kind boilerplate
19
28
  * components that differed only in their default name and icon — today the
20
- * roof vents (box / ridge / turbine / cupola / eyebrow). Register a kind in
21
- * `treeNodeByType` against this component instead of authoring another copy.
29
+ * roof vents plus cabinet rows. Register a kind in `treeNodeByType` against
30
+ * this component instead of authoring another copy.
22
31
  */
23
32
  export const RegistryTreeNode = memo(function RegistryTreeNode({
24
33
  nodeId,
@@ -26,48 +35,76 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
26
35
  isLast,
27
36
  }: RegistryTreeNodeProps) {
28
37
  const [isEditing, setIsEditing] = useState(false)
38
+ const [expanded, setExpanded] = useState(true)
29
39
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
30
40
  const node = useScene((s) => s.nodes[nodeId])
41
+ const children = useScene(useShallow((s) => resolveTreeChildIds(nodeId, s.nodes)))
31
42
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
32
43
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
33
44
  const setSelection = useViewer((state) => state.setSelection)
34
45
  const setHoveredId = useViewer((state) => state.setHoveredId)
35
46
 
36
47
  const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
48
+ const tree = node ? nodeRegistry.get(node.type)?.tree : undefined
37
49
  const icon = presentation?.icon
38
- const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.png'
39
- const defaultName = node?.name || presentation?.label || 'Node'
50
+ const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.webp'
51
+ const iconElement =
52
+ icon?.kind === 'iconify' ? (
53
+ <IconifyIcon className="opacity-60" height={14} icon={icon.name} width={14} />
54
+ ) : (
55
+ <Image
56
+ alt=""
57
+ className="object-contain opacity-60"
58
+ height={14}
59
+ src={iconSrc}
60
+ width={14}
61
+ />
62
+ )
63
+ const snapTarget = resolveNodeSnapTarget(node)
64
+ const defaultName =
65
+ node ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentation?.label || 'Node' : 'Node'
66
+ const hasChildren = children.length > 0
67
+
68
+ useEffect(() => {
69
+ return useViewer.subscribe((state) => {
70
+ const { selectedIds } = state.selection
71
+ if (selectedIds.length === 0) return
72
+ const nodes = useScene.getState().nodes
73
+ for (const id of selectedIds) {
74
+ if (treeContainsDescendant(nodeId, id as AnyNodeId, nodes)) {
75
+ setExpanded(true)
76
+ return
77
+ }
78
+ }
79
+ })
80
+ }, [nodeId])
40
81
 
41
82
  const handleClick = useCallback(
42
83
  (e: React.MouseEvent) => {
43
84
  e.stopPropagation()
44
- const handled = handleTreeSelection(
85
+ handleTreeSelection(
45
86
  e,
46
87
  nodeId,
47
88
  useViewer.getState().selection.selectedIds,
48
89
  setSelection,
49
90
  )
50
- if (!handled && useEditor.getState().phase === 'furnish') {
51
- useEditor.getState().setPhase('structure')
52
- }
91
+ routeTreeSelectionToNode(node)
53
92
  },
54
- [nodeId, setSelection],
93
+ [node, nodeId, setSelection],
55
94
  )
56
95
 
57
96
  return (
58
97
  <TreeNodeWrapper
59
98
  actions={<TreeNodeActions nodeId={nodeId} />}
60
99
  depth={depth}
61
- expanded={false}
62
- hasChildren={false}
100
+ expanded={expanded}
101
+ hasChildren={hasChildren}
63
102
  icon={
64
- <Image
65
- alt=""
66
- className="object-contain opacity-60"
67
- height={14}
68
- src={iconSrc}
69
- width={14}
70
- />
103
+ snapTarget ? (
104
+ <SnapTargetIcon target={snapTarget}>{iconElement}</SnapTargetIcon>
105
+ ) : (
106
+ iconElement
107
+ )
71
108
  }
72
109
  isHovered={isHovered}
73
110
  isLast={isLast}
@@ -87,7 +124,17 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
87
124
  onDoubleClick={() => focusTreeNode(nodeId)}
88
125
  onMouseEnter={() => setHoveredId(nodeId)}
89
126
  onMouseLeave={() => setHoveredId(null)}
90
- onToggle={() => {}}
91
- />
127
+ onToggle={() => setExpanded((prev) => !prev)}
128
+ >
129
+ {hasChildren &&
130
+ children.map((childId, index) => (
131
+ <TreeNode
132
+ depth={depth + 1}
133
+ isLast={index === children.length - 1}
134
+ key={childId}
135
+ nodeId={childId as AnyNodeId}
136
+ />
137
+ ))}
138
+ </TreeNodeWrapper>
92
139
  )
93
140
  })
@@ -28,6 +28,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
28
28
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
29
29
  const setSelection = useViewer((state) => state.setSelection)
30
30
  const setHoveredId = useViewer((state) => state.setHoveredId)
31
+ const setRoofHostDragArmedId = useEditor((state) => state.setRoofHostDragArmedId)
31
32
  const { drag, dropTarget } = useTreeNodeDrag()
32
33
 
33
34
  const segments = useScene(
@@ -57,8 +58,11 @@ export const RoofTreeNode = memo(function RoofTreeNode({
57
58
  if (!handled && useEditor.getState().phase === 'furnish') {
58
59
  useEditor.getState().setPhase('structure')
59
60
  }
61
+ if (!e.metaKey && !e.ctrlKey && !e.shiftKey) {
62
+ setRoofHostDragArmedId(nodeId)
63
+ }
60
64
  },
61
- [nodeId, setSelection],
65
+ [nodeId, setRoofHostDragArmedId, setSelection],
62
66
  )
63
67
 
64
68
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
@@ -98,7 +102,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
98
102
  expanded={expanded}
99
103
  hasChildren={segments.length > 0}
100
104
  icon={
101
- <Image alt="" className="object-contain" height={14} src="/icons/roof.png" width={14} />
105
+ <Image alt="" className="object-contain" height={14} src="/icons/roof.webp" width={14} />
102
106
  }
103
107
  isDropTarget={isValidDropTarget && isDropTarget}
104
108
  isHovered={isHovered || isDropTarget}
@@ -230,7 +234,7 @@ function RoofSegmentTreeNode({
230
234
  alt=""
231
235
  className="object-contain opacity-60"
232
236
  height={14}
233
- src="/icons/roof.png"
237
+ src="/icons/roof.webp"
234
238
  width={14}
235
239
  />
236
240
  }
@@ -5,9 +5,14 @@ import { useViewer } from '@pascal-app/viewer'
5
5
  import Image from 'next/image'
6
6
  import { memo, useCallback, useEffect, useState } from 'react'
7
7
  import { useShallow } from 'zustand/react/shallow'
8
- import useEditor from './../../../../../store/use-editor'
9
8
  import { InlineRenameInput } from './inline-rename-input'
10
- import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
9
+ import {
10
+ focusTreeNode,
11
+ handleTreeSelection,
12
+ routeTreeSelectionToNode,
13
+ TreeNode,
14
+ TreeNodeWrapper,
15
+ } from './tree-node'
11
16
  import { TreeNodeActions } from './tree-node-actions'
12
17
 
13
18
  interface ShelfTreeNodeProps {
@@ -34,6 +39,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
34
39
  const children = useScene(
35
40
  useShallow((s) => (s.nodes[nodeId] as ShelfNode | undefined)?.children ?? []),
36
41
  )
42
+ const node = useScene((s) => s.nodes[nodeId] as ShelfNode | undefined)
37
43
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
38
44
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
39
45
  const setSelection = useViewer((state) => state.setSelection)
@@ -63,17 +69,15 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
63
69
  const handleClick = useCallback(
64
70
  (e: React.MouseEvent) => {
65
71
  e.stopPropagation()
66
- const handled = handleTreeSelection(
72
+ handleTreeSelection(
67
73
  e,
68
74
  nodeId,
69
75
  useViewer.getState().selection.selectedIds,
70
76
  setSelection,
71
77
  )
72
- if (!handled && useEditor.getState().phase === 'furnish') {
73
- useEditor.getState().setPhase('structure')
74
- }
78
+ routeTreeSelectionToNode(node)
75
79
  },
76
- [nodeId, setSelection],
80
+ [node, nodeId, setSelection],
77
81
  )
78
82
 
79
83
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
@@ -92,7 +96,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
92
96
  expanded={expanded}
93
97
  hasChildren={hasChildren}
94
98
  icon={
95
- <Image alt="" className="object-contain" height={14} src="/icons/shelf.png" width={14} />
99
+ <Image alt="" className="object-contain" height={14} src="/icons/shelf.webp" width={14} />
96
100
  }
97
101
  isHovered={isHovered}
98
102
  isLast={isLast}
@@ -2,6 +2,7 @@ import { type AnyNodeId, type SlabNode, 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 { formatAreaLabel } from './../../../../../lib/measurements'
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'
@@ -25,6 +26,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
25
26
  const isHovered = useViewer((state) => state.hoveredId === nodeId)
26
27
  const setSelection = useViewer((state) => state.setSelection)
27
28
  const setHoveredId = useViewer((state) => state.setHoveredId)
29
+ const unit = useViewer((state) => state.unit)
28
30
 
29
31
  const handleClick = useCallback(
30
32
  (e: React.MouseEvent) => {
@@ -45,8 +47,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
45
47
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
46
48
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
47
49
 
48
- const area = calculatePolygonArea(polygon).toFixed(1)
49
- const defaultName = `Slab (${area}m²)`
50
+ const defaultName = `Slab (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
50
51
 
51
52
  return (
52
53
  <TreeNodeWrapper
@@ -55,7 +56,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
55
56
  expanded={false}
56
57
  hasChildren={false}
57
58
  icon={
58
- <Image alt="" className="object-contain" height={14} src="/icons/floor.png" width={14} />
59
+ <Image alt="" className="object-contain" height={14} src="/icons/floor.webp" width={14} />
59
60
  }
60
61
  isHovered={isHovered}
61
62
  isLast={isLast}