@pascal-app/editor 0.7.0 → 0.9.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 (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -38,7 +38,7 @@ export const FenceTreeNode = memo(function FenceTreeNode({
38
38
 
39
39
  return (
40
40
  <TreeNodeWrapper
41
- actions={<TreeNodeActions node={node} />}
41
+ actions={<TreeNodeActions nodeId={node.id} />}
42
42
  depth={depth}
43
43
  expanded={false}
44
44
  hasChildren={false}
@@ -53,7 +53,7 @@ export const FenceTreeNode = memo(function FenceTreeNode({
53
53
  <InlineRenameInput
54
54
  defaultName="Fence"
55
55
  isEditing={isEditing}
56
- node={node}
56
+ nodeId={node.id}
57
57
  onStartEditing={() => setIsEditing(true)}
58
58
  onStopEditing={() => setIsEditing(false)}
59
59
  />
@@ -0,0 +1,83 @@
1
+ import { type AnyNodeId, type GutterNode, useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import Image from 'next/image'
4
+ import { memo, useCallback, useState } from 'react'
5
+ import useEditor from './../../../../../store/use-editor'
6
+ import { InlineRenameInput } from './inline-rename-input'
7
+ import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
8
+ import { TreeNodeActions } from './tree-node-actions'
9
+
10
+ interface GutterTreeNodeProps {
11
+ nodeId: AnyNodeId
12
+ depth: number
13
+ isLast?: boolean
14
+ }
15
+
16
+ export const GutterTreeNode = memo(function GutterTreeNode({
17
+ nodeId,
18
+ depth,
19
+ isLast,
20
+ }: GutterTreeNodeProps) {
21
+ const [isEditing, setIsEditing] = useState(false)
22
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
23
+ const node = useScene((s) => s.nodes[nodeId] as GutterNode | undefined)
24
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
25
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
26
+ const setSelection = useViewer((state) => state.setSelection)
27
+ const setHoveredId = useViewer((state) => state.setHoveredId)
28
+
29
+ const handleClick = useCallback(
30
+ (e: React.MouseEvent) => {
31
+ e.stopPropagation()
32
+ const handled = handleTreeSelection(
33
+ e,
34
+ nodeId,
35
+ useViewer.getState().selection.selectedIds,
36
+ setSelection,
37
+ )
38
+ if (!handled && useEditor.getState().phase === 'furnish') {
39
+ useEditor.getState().setPhase('structure')
40
+ }
41
+ },
42
+ [nodeId, setSelection],
43
+ )
44
+
45
+ const defaultName = node?.name || 'Gutter'
46
+
47
+ return (
48
+ <TreeNodeWrapper
49
+ actions={<TreeNodeActions nodeId={nodeId} />}
50
+ depth={depth}
51
+ expanded={false}
52
+ hasChildren={false}
53
+ icon={
54
+ <Image
55
+ alt=""
56
+ className="object-contain opacity-60"
57
+ height={14}
58
+ src="/icons/roof.png"
59
+ width={14}
60
+ />
61
+ }
62
+ isHovered={isHovered}
63
+ isLast={isLast}
64
+ isSelected={isSelected}
65
+ isVisible={isVisible}
66
+ label={
67
+ <InlineRenameInput
68
+ defaultName={defaultName}
69
+ isEditing={isEditing}
70
+ nodeId={nodeId}
71
+ onStartEditing={() => setIsEditing(true)}
72
+ onStopEditing={() => setIsEditing(false)}
73
+ />
74
+ }
75
+ nodeId={nodeId}
76
+ onClick={handleClick}
77
+ onDoubleClick={() => focusTreeNode(nodeId)}
78
+ onMouseEnter={() => setHoveredId(nodeId)}
79
+ onMouseLeave={() => setHoveredId(null)}
80
+ onToggle={() => {}}
81
+ />
82
+ )
83
+ })
@@ -3,9 +3,9 @@ import {
3
3
  type AnyNodeId,
4
4
  type BuildingNode,
5
5
  emitter,
6
- GuideNode,
6
+ type GuideNode,
7
7
  LevelNode,
8
- ScanNode,
8
+ type ScanNode,
9
9
  type SiteNode,
10
10
  useScene,
11
11
  type ZoneNode,
@@ -32,14 +32,13 @@ import {
32
32
  PopoverContent,
33
33
  PopoverTrigger,
34
34
  } from './../../../../../components/ui/primitives/popover'
35
- import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
36
- import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
37
-
38
35
  import {
39
36
  buildLevelDuplicateCreateOps,
40
37
  type LevelDuplicatePreset,
41
38
  } from './../../../../../lib/level-duplication'
42
-
39
+ import { getDefaultLevelName } from './../../../../../lib/level-name'
40
+ import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
41
+ import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
43
42
  import { cn } from './../../../../../lib/utils'
44
43
  import useEditor from './../../../../../store/use-editor'
45
44
  import { useUploadStore } from '../../../../../store/use-upload'
@@ -725,7 +724,7 @@ const LevelItem = memo(function LevelItem({
725
724
  src="/icons/level.png"
726
725
  />
727
726
  <InlineRenameInput
728
- defaultName={`Level ${level.level}`}
727
+ defaultName={getDefaultLevelName(level.level)}
729
728
  isEditing={isEditing}
730
729
  nodeId={level.id}
731
730
  onStartEditing={() => setIsEditing(true)}
@@ -1543,10 +1542,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1543
1542
  useShallow((s) => {
1544
1543
  if (!siteNode) return []
1545
1544
  return siteNode.children
1546
- .map((child) => {
1547
- const id = typeof child === 'string' ? child : child.id
1548
- return s.nodes[id] as BuildingNode | undefined
1549
- })
1545
+ .map((childId) => s.nodes[childId as AnyNodeId] as BuildingNode | undefined)
1550
1546
  .filter((node): node is BuildingNode => node?.type === 'building')
1551
1547
  }),
1552
1548
  )
@@ -1573,7 +1569,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1573
1569
  'h-5 w-5 object-contain transition-all',
1574
1570
  phase !== 'site' && 'opacity-60 grayscale',
1575
1571
  )}
1576
- src="/icons/site.png"
1572
+ src="/icons/site-flag.png"
1577
1573
  />
1578
1574
  <span className="font-medium text-sm">{siteNode.name || 'Site'}</span>
1579
1575
  </div>
@@ -3,6 +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
7
  import { InlineRenameInput } from './inline-rename-input'
7
8
  import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
8
9
  import { TreeNodeActions } from './tree-node-actions'
@@ -35,7 +36,7 @@ export const LevelTreeNode = memo(function LevelTreeNode({
35
36
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
36
37
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
37
38
 
38
- const defaultName = `Level ${level}`
39
+ const defaultName = getDefaultLevelName(level)
39
40
 
40
41
  return (
41
42
  <TreeNodeWrapper
@@ -0,0 +1,93 @@
1
+ import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import Image from 'next/image'
4
+ import { memo, useCallback, useState } from 'react'
5
+ import useEditor from './../../../../../store/use-editor'
6
+ import { InlineRenameInput } from './inline-rename-input'
7
+ import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
8
+ import { TreeNodeActions } from './tree-node-actions'
9
+
10
+ interface RegistryTreeNodeProps {
11
+ nodeId: AnyNodeId
12
+ depth: number
13
+ isLast?: boolean
14
+ }
15
+
16
+ /**
17
+ * Generic, leaf tree-node row driven entirely by the kind's
18
+ * `def.presentation` (icon + label). Replaces the per-kind boilerplate
19
+ * 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.
22
+ */
23
+ export const RegistryTreeNode = memo(function RegistryTreeNode({
24
+ nodeId,
25
+ depth,
26
+ isLast,
27
+ }: RegistryTreeNodeProps) {
28
+ const [isEditing, setIsEditing] = useState(false)
29
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
30
+ const node = useScene((s) => s.nodes[nodeId])
31
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
32
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
33
+ const setSelection = useViewer((state) => state.setSelection)
34
+ const setHoveredId = useViewer((state) => state.setHoveredId)
35
+
36
+ const presentation = node ? nodeRegistry.get(node.type)?.presentation : undefined
37
+ const icon = presentation?.icon
38
+ const iconSrc = icon?.kind === 'url' ? icon.src : '/icons/roof.png'
39
+ const defaultName = node?.name || presentation?.label || 'Node'
40
+
41
+ const handleClick = useCallback(
42
+ (e: React.MouseEvent) => {
43
+ e.stopPropagation()
44
+ const handled = handleTreeSelection(
45
+ e,
46
+ nodeId,
47
+ useViewer.getState().selection.selectedIds,
48
+ setSelection,
49
+ )
50
+ if (!handled && useEditor.getState().phase === 'furnish') {
51
+ useEditor.getState().setPhase('structure')
52
+ }
53
+ },
54
+ [nodeId, setSelection],
55
+ )
56
+
57
+ return (
58
+ <TreeNodeWrapper
59
+ actions={<TreeNodeActions nodeId={nodeId} />}
60
+ depth={depth}
61
+ expanded={false}
62
+ hasChildren={false}
63
+ icon={
64
+ <Image
65
+ alt=""
66
+ className="object-contain opacity-60"
67
+ height={14}
68
+ src={iconSrc}
69
+ width={14}
70
+ />
71
+ }
72
+ isHovered={isHovered}
73
+ isLast={isLast}
74
+ isSelected={isSelected}
75
+ isVisible={isVisible}
76
+ label={
77
+ <InlineRenameInput
78
+ defaultName={defaultName}
79
+ isEditing={isEditing}
80
+ nodeId={nodeId}
81
+ onStartEditing={() => setIsEditing(true)}
82
+ onStopEditing={() => setIsEditing(false)}
83
+ />
84
+ }
85
+ nodeId={nodeId}
86
+ onClick={handleClick}
87
+ onDoubleClick={() => focusTreeNode(nodeId)}
88
+ onMouseEnter={() => setHoveredId(nodeId)}
89
+ onMouseLeave={() => setHoveredId(null)}
90
+ onToggle={() => {}}
91
+ />
92
+ )
93
+ })
@@ -6,7 +6,7 @@ import { memo, useCallback, useEffect, useState } from 'react'
6
6
  import { useShallow } from 'zustand/react/shallow'
7
7
  import useEditor from '../../../../../store/use-editor'
8
8
  import { InlineRenameInput } from './inline-rename-input'
9
- import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
+ import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
10
10
  import { TreeNodeActions } from './tree-node-actions'
11
11
  import { DropIndicatorLine, useTreeNodeDrag } from './tree-node-drag'
12
12
 
@@ -163,12 +163,36 @@ function RoofSegmentTreeNode({
163
163
  isLast?: boolean
164
164
  }) {
165
165
  const [isEditing, setIsEditing] = useState(false)
166
+ const [expanded, setExpanded] = useState(true)
167
+ const accessoryIds = useScene(
168
+ useShallow(
169
+ (s) => (s.nodes[node.id] as RoofSegmentNode | undefined)?.children ?? ([] as string[]),
170
+ ),
171
+ )
166
172
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
167
173
  const isHovered = useViewer((state) => state.hoveredId === node.id)
168
174
  const setSelection = useViewer((state) => state.setSelection)
169
175
  const setHoveredId = useViewer((state) => state.setHoveredId)
170
176
  const { startDrag, isDragging } = useTreeNodeDrag()
171
177
 
178
+ useEffect(() => {
179
+ return useViewer.subscribe((state) => {
180
+ const { selectedIds } = state.selection
181
+ if (selectedIds.length === 0) return
182
+ const nodes = useScene.getState().nodes
183
+ for (const id of selectedIds) {
184
+ let current = nodes[id as AnyNodeId]
185
+ while (current?.parentId) {
186
+ if (current.parentId === node.id) {
187
+ setExpanded(true)
188
+ return
189
+ }
190
+ current = nodes[current.parentId as AnyNodeId]
191
+ }
192
+ }
193
+ })
194
+ }, [node.id])
195
+
172
196
  const handleClick = useCallback(
173
197
  (e: React.MouseEvent) => {
174
198
  if (isDragging) return
@@ -192,13 +216,15 @@ function RoofSegmentTreeNode({
192
216
 
193
217
  const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
194
218
 
219
+ const hasAccessories = accessoryIds.length > 0
220
+
195
221
  return (
196
222
  <div data-drop-child={node.id}>
197
223
  <TreeNodeWrapper
198
224
  actions={<TreeNodeActions nodeId={node.id} />}
199
225
  depth={depth}
200
- expanded={false}
201
- hasChildren={false}
226
+ expanded={expanded}
227
+ hasChildren={hasAccessories}
202
228
  icon={
203
229
  <Image
204
230
  alt=""
@@ -228,8 +254,18 @@ function RoofSegmentTreeNode({
228
254
  onMouseEnter={() => setHoveredId(node.id)}
229
255
  onMouseLeave={() => setHoveredId(null)}
230
256
  onPointerDown={handlePointerDown}
231
- onToggle={() => {}}
232
- />
257
+ onToggle={() => setExpanded((prev) => !prev)}
258
+ >
259
+ {hasAccessories &&
260
+ accessoryIds.map((childId, index) => (
261
+ <TreeNode
262
+ depth={depth + 1}
263
+ isLast={index === accessoryIds.length - 1}
264
+ key={childId}
265
+ nodeId={childId as AnyNodeId}
266
+ />
267
+ ))}
268
+ </TreeNodeWrapper>
233
269
  </div>
234
270
  )
235
271
  }
@@ -0,0 +1,128 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, type ShelfNode, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import Image from 'next/image'
6
+ import { memo, useCallback, useEffect, useState } from 'react'
7
+ import { useShallow } from 'zustand/react/shallow'
8
+ import useEditor from './../../../../../store/use-editor'
9
+ import { InlineRenameInput } from './inline-rename-input'
10
+ import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
11
+ import { TreeNodeActions } from './tree-node-actions'
12
+
13
+ interface ShelfTreeNodeProps {
14
+ nodeId: ShelfNode['id']
15
+ depth: number
16
+ isLast?: boolean
17
+ }
18
+
19
+ /**
20
+ * Sidebar tree entry for shelf. Mirrors `item-tree-node`'s shape so the
21
+ * shelf's hosted items list as collapsible children — same pattern items
22
+ * use for their nested items. The shelf has its own `children: ItemNode[`id`]`
23
+ * field on the schema; items reparent into it via `def.surfaces` + the
24
+ * placement coordinator's shelf strategy.
25
+ */
26
+ export const ShelfTreeNode = memo(function ShelfTreeNode({
27
+ nodeId,
28
+ depth,
29
+ isLast,
30
+ }: ShelfTreeNodeProps) {
31
+ const [isEditing, setIsEditing] = useState(false)
32
+ const [expanded, setExpanded] = useState(true)
33
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
34
+ const children = useScene(
35
+ useShallow((s) => (s.nodes[nodeId] as ShelfNode | undefined)?.children ?? []),
36
+ )
37
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
38
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
39
+ const setSelection = useViewer((state) => state.setSelection)
40
+ const setHoveredId = useViewer((state) => state.setHoveredId)
41
+
42
+ // Expand when a descendant is selected — same imperative subscription
43
+ // the item tree-node uses, so we don't re-render when unrelated
44
+ // selection-state ticks.
45
+ useEffect(() => {
46
+ return useViewer.subscribe((state) => {
47
+ const { selectedIds } = state.selection
48
+ if (selectedIds.length === 0) return
49
+ const nodes = useScene.getState().nodes
50
+ for (const id of selectedIds) {
51
+ let current = nodes[id as AnyNodeId]
52
+ while (current?.parentId) {
53
+ if (current.parentId === nodeId) {
54
+ setExpanded(true)
55
+ return
56
+ }
57
+ current = nodes[current.parentId as AnyNodeId]
58
+ }
59
+ }
60
+ })
61
+ }, [nodeId])
62
+
63
+ const handleClick = useCallback(
64
+ (e: React.MouseEvent) => {
65
+ e.stopPropagation()
66
+ const handled = handleTreeSelection(
67
+ e,
68
+ nodeId,
69
+ useViewer.getState().selection.selectedIds,
70
+ setSelection,
71
+ )
72
+ if (!handled && useEditor.getState().phase === 'furnish') {
73
+ useEditor.getState().setPhase('structure')
74
+ }
75
+ },
76
+ [nodeId, setSelection],
77
+ )
78
+
79
+ const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])
80
+ const handleMouseEnter = useCallback(() => setHoveredId(nodeId), [nodeId, setHoveredId])
81
+ const handleMouseLeave = useCallback(() => setHoveredId(null), [setHoveredId])
82
+ const handleToggle = useCallback(() => setExpanded((prev) => !prev), [])
83
+ const handleStartEditing = useCallback(() => setIsEditing(true), [])
84
+ const handleStopEditing = useCallback(() => setIsEditing(false), [])
85
+
86
+ const hasChildren = children.length > 0
87
+
88
+ return (
89
+ <TreeNodeWrapper
90
+ actions={<TreeNodeActions nodeId={nodeId} />}
91
+ depth={depth}
92
+ expanded={expanded}
93
+ hasChildren={hasChildren}
94
+ icon={
95
+ <Image alt="" className="object-contain" height={14} src="/icons/shelf.png" width={14} />
96
+ }
97
+ isHovered={isHovered}
98
+ isLast={isLast}
99
+ isSelected={isSelected}
100
+ isVisible={isVisible}
101
+ label={
102
+ <InlineRenameInput
103
+ defaultName="Shelf"
104
+ isEditing={isEditing}
105
+ nodeId={nodeId}
106
+ onStartEditing={handleStartEditing}
107
+ onStopEditing={handleStopEditing}
108
+ />
109
+ }
110
+ nodeId={nodeId}
111
+ onClick={handleClick}
112
+ onDoubleClick={handleDoubleClick}
113
+ onMouseEnter={handleMouseEnter}
114
+ onMouseLeave={handleMouseLeave}
115
+ onToggle={handleToggle}
116
+ >
117
+ {hasChildren &&
118
+ children.map((childId, index) => (
119
+ <TreeNode
120
+ depth={depth + 1}
121
+ isLast={index === children.length - 1}
122
+ key={childId}
123
+ nodeId={childId}
124
+ />
125
+ ))}
126
+ </TreeNodeWrapper>
127
+ )
128
+ })
@@ -91,8 +91,11 @@ function calculatePolygonArea(polygon: Array<[number, number]>): number {
91
91
 
92
92
  for (let i = 0; i < n; i++) {
93
93
  const j = (i + 1) % n
94
- area += polygon[i]?.[0] * polygon[j]?.[1]
95
- area -= polygon[j]?.[0] * polygon[i]?.[1]
94
+ const pi = polygon[i]
95
+ const pj = polygon[j]
96
+ if (!(pi && pj)) continue
97
+ area += pi[0] * pj[1]
98
+ area -= pj[0] * pi[1]
96
99
  }
97
100
 
98
101
  return Math.abs(area) / 2
@@ -0,0 +1,83 @@
1
+ import { type AnyNodeId, type SolarPanelNode, useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import Image from 'next/image'
4
+ import { memo, useCallback, useState } from 'react'
5
+ import useEditor from './../../../../../store/use-editor'
6
+ import { InlineRenameInput } from './inline-rename-input'
7
+ import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
8
+ import { TreeNodeActions } from './tree-node-actions'
9
+
10
+ interface SolarPanelTreeNodeProps {
11
+ nodeId: AnyNodeId
12
+ depth: number
13
+ isLast?: boolean
14
+ }
15
+
16
+ export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
17
+ nodeId,
18
+ depth,
19
+ isLast,
20
+ }: SolarPanelTreeNodeProps) {
21
+ const [isEditing, setIsEditing] = useState(false)
22
+ const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
23
+ const node = useScene((s) => s.nodes[nodeId] as SolarPanelNode | undefined)
24
+ const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
25
+ const isHovered = useViewer((state) => state.hoveredId === nodeId)
26
+ const setSelection = useViewer((state) => state.setSelection)
27
+ const setHoveredId = useViewer((state) => state.setHoveredId)
28
+
29
+ const handleClick = useCallback(
30
+ (e: React.MouseEvent) => {
31
+ e.stopPropagation()
32
+ const handled = handleTreeSelection(
33
+ e,
34
+ nodeId,
35
+ useViewer.getState().selection.selectedIds,
36
+ setSelection,
37
+ )
38
+ if (!handled && useEditor.getState().phase === 'furnish') {
39
+ useEditor.getState().setPhase('structure')
40
+ }
41
+ },
42
+ [nodeId, setSelection],
43
+ )
44
+
45
+ const defaultName = node?.name || 'Solar Panel'
46
+
47
+ return (
48
+ <TreeNodeWrapper
49
+ actions={<TreeNodeActions nodeId={nodeId} />}
50
+ depth={depth}
51
+ expanded={false}
52
+ hasChildren={false}
53
+ icon={
54
+ <Image
55
+ alt=""
56
+ className="object-contain opacity-60"
57
+ height={14}
58
+ src="/icons/roof.png"
59
+ width={14}
60
+ />
61
+ }
62
+ isHovered={isHovered}
63
+ isLast={isLast}
64
+ isSelected={isSelected}
65
+ isVisible={isVisible}
66
+ label={
67
+ <InlineRenameInput
68
+ defaultName={defaultName}
69
+ isEditing={isEditing}
70
+ nodeId={nodeId}
71
+ onStartEditing={() => setIsEditing(true)}
72
+ onStopEditing={() => setIsEditing(false)}
73
+ />
74
+ }
75
+ nodeId={nodeId}
76
+ onClick={handleClick}
77
+ onDoubleClick={() => focusTreeNode(nodeId)}
78
+ onMouseEnter={() => setHoveredId(nodeId)}
79
+ onMouseLeave={() => setHoveredId(null)}
80
+ onToggle={() => {}}
81
+ />
82
+ )
83
+ })
@@ -54,7 +54,7 @@ export const SpawnTreeNode = memo(function SpawnTreeNode({
54
54
  alt=""
55
55
  className="object-contain"
56
56
  height={14}
57
- src="/icons/site.png"
57
+ src="/icons/spawn-point.png"
58
58
  width={14}
59
59
  />
60
60
  }