@pascal-app/editor 1.0.0-beta.4 → 1.0.0

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 (283) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
  6. package/src/components/editor/bake-exporter.tsx +1 -0
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +17 -5
  13. package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
  14. package/src/components/editor/export-manager.tsx +175 -33
  15. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  16. package/src/components/editor/first-person-controls.tsx +182 -57
  17. package/src/components/editor/floating-action-menu.tsx +55 -55
  18. package/src/components/editor/floorplan-background-selection.test.ts +31 -4
  19. package/src/components/editor/floorplan-background-selection.ts +27 -7
  20. package/src/components/editor/floorplan-panel.tsx +81 -57
  21. package/src/components/editor/grid.tsx +12 -7
  22. package/src/components/editor/group-actions.ts +34 -50
  23. package/src/components/editor/group-floating-action-menu.tsx +30 -15
  24. package/src/components/editor/group-move-3d.ts +51 -21
  25. package/src/components/editor/group-transform-shared.test.ts +78 -0
  26. package/src/components/editor/group-transform-shared.ts +74 -7
  27. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  28. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  29. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  30. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  31. package/src/components/editor/handles/resize-snap.ts +10 -1
  32. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  33. package/src/components/editor/index.tsx +252 -46
  34. package/src/components/editor/measurement-pill.tsx +12 -12
  35. package/src/components/editor/node-action-menu.tsx +42 -1
  36. package/src/components/editor/node-arrow-handles.tsx +47 -58
  37. package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
  38. package/src/components/editor/selection-manager.tsx +132 -47
  39. package/src/components/editor/site-edge-labels.tsx +2 -1
  40. package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
  41. package/src/components/editor/snapshot-capture.test.ts +422 -0
  42. package/src/components/editor/snapshot-capture.ts +145 -0
  43. package/src/components/editor/thumbnail-generator.tsx +295 -232
  44. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  45. package/src/components/editor/wall-measurement-label.tsx +3 -2
  46. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  47. package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
  48. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  49. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  50. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  51. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  52. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  53. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  54. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
  57. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  59. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  60. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  61. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  62. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  63. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  64. package/src/components/systems/selection-affordance-services.ts +19 -0
  65. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  66. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  67. package/src/components/tools/fence/fence-drafting.ts +6 -25
  68. package/src/components/tools/item/draft-creation.test.ts +12 -0
  69. package/src/components/tools/item/draft-creation.ts +10 -0
  70. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  71. package/src/components/tools/item/face-host-commit.ts +56 -0
  72. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  73. package/src/components/tools/item/face-host-preview.ts +82 -0
  74. package/src/components/tools/item/move-tool.tsx +4 -3
  75. package/src/components/tools/item/placement-math.test.ts +11 -1
  76. package/src/components/tools/item/placement-math.ts +0 -20
  77. package/src/components/tools/item/placement-strategies.test.ts +494 -0
  78. package/src/components/tools/item/placement-strategies.ts +192 -24
  79. package/src/components/tools/item/placement-surface.test.ts +61 -0
  80. package/src/components/tools/item/placement-surface.ts +24 -0
  81. package/src/components/tools/item/placement-types.ts +6 -0
  82. package/src/components/tools/item/test-face-host.ts +121 -0
  83. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  84. package/src/components/tools/item/use-draft-node.ts +26 -4
  85. package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
  86. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  87. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  88. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  89. package/src/components/tools/registry-tool-context.tsx +30 -0
  90. package/src/components/tools/select/box-select-tool.tsx +3 -1
  91. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  92. package/src/components/tools/select/marquee-footprint.ts +29 -0
  93. package/src/components/tools/shared/placement-box.tsx +63 -8
  94. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  95. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  96. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  97. package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
  98. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  99. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  100. package/src/components/tools/stair/stair-tool.tsx +127 -74
  101. package/src/components/tools/tool-manager.tsx +25 -8
  102. package/src/components/tools/wall/wall-drafting.test.ts +527 -4
  103. package/src/components/tools/wall/wall-drafting.ts +55 -462
  104. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  105. package/src/components/ui/action-menu/index.tsx +11 -1
  106. package/src/components/ui/command-palette/editor-commands.tsx +12 -13
  107. package/src/components/ui/command-palette/index.tsx +7 -2
  108. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  109. package/src/components/ui/controls/material-picker.tsx +4 -4
  110. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  111. package/src/components/ui/controls/segmented-control.tsx +8 -1
  112. package/src/components/ui/controls/slider-control.tsx +19 -3
  113. package/src/components/ui/controls/toggle-control.tsx +18 -5
  114. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  115. package/src/components/ui/floating-level-selector.tsx +23 -6
  116. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  117. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  118. package/src/components/ui/helpers/item-helper.tsx +1 -1
  119. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  120. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  121. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  122. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  123. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  124. package/src/components/ui/panels/multi-field-value.ts +163 -0
  125. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  126. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  127. package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
  128. package/src/components/ui/panels/node-display.ts +4 -0
  129. package/src/components/ui/panels/panel-manager.tsx +77 -3
  130. package/src/components/ui/panels/panel-wrapper.tsx +228 -12
  131. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  132. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  133. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  134. package/src/components/ui/panels/reference-panel.tsx +31 -1
  135. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  136. package/src/components/ui/scene-loader.tsx +35 -0
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
  138. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  139. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
  140. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  142. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  143. package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
  144. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  145. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  146. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  147. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
  148. package/src/components/ui/sidebar/tab-bar.tsx +9 -1
  149. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  150. package/src/components/viewer/floorplan-compass-button.tsx +50 -0
  151. package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
  152. package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
  153. package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
  154. package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
  155. package/src/components/viewer/floorplan-preview.tsx +1069 -0
  156. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
  157. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  158. package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
  159. package/src/components/viewer/viewer-stage-modes.ts +49 -0
  160. package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
  161. package/src/components/viewer/viewer-stage.test.tsx +47 -0
  162. package/src/components/viewer/viewer-stage.tsx +221 -0
  163. package/src/components/viewer-overlay.tsx +13 -30
  164. package/src/components/viewer-zone-system.tsx +6 -2
  165. package/src/components/walkthrough-hud.tsx +19 -10
  166. package/src/hooks/use-auto-save.test.ts +122 -1
  167. package/src/hooks/use-auto-save.ts +119 -13
  168. package/src/hooks/use-ceiling-events.test.ts +122 -0
  169. package/src/hooks/use-drag-action.ts +17 -3
  170. package/src/hooks/use-grid-events.test.ts +240 -0
  171. package/src/hooks/use-grid-events.ts +190 -11
  172. package/src/hooks/use-keyboard.test.ts +244 -0
  173. package/src/hooks/use-keyboard.ts +164 -63
  174. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  175. package/src/hooks/use-save-shortcut.ts +33 -0
  176. package/src/index.tsx +109 -4
  177. package/src/lib/active-placement-surface.test.ts +16 -0
  178. package/src/lib/active-placement-surface.ts +14 -0
  179. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  180. package/src/lib/ceiling-surface-raycast.ts +12 -0
  181. package/src/lib/contextual-help-extension.ts +17 -0
  182. package/src/lib/contextual-help.test.ts +8 -0
  183. package/src/lib/contextual-help.ts +8 -0
  184. package/src/lib/continuation.test.ts +11 -0
  185. package/src/lib/continuation.ts +8 -1
  186. package/src/lib/direct-manipulation.test.ts +132 -0
  187. package/src/lib/direct-manipulation.ts +38 -1
  188. package/src/lib/editor-api.ts +6 -3
  189. package/src/lib/elevation-guides.test.ts +28 -1
  190. package/src/lib/elevation-guides.ts +22 -7
  191. package/src/lib/floating-menu-scale.test.ts +29 -0
  192. package/src/lib/floating-menu-scale.ts +14 -0
  193. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  194. package/src/lib/floorplan/apply-alignment.ts +10 -1
  195. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  196. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  197. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  198. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  199. package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
  200. package/src/lib/floorplan/floorplan-readonly.ts +85 -0
  201. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  202. package/src/lib/floorplan-grid-event-point.ts +13 -0
  203. package/src/lib/glb-export.test.ts +1169 -21
  204. package/src/lib/glb-export.ts +629 -101
  205. package/src/lib/grid-event-presentation.ts +16 -0
  206. package/src/lib/history.test.ts +523 -77
  207. package/src/lib/history.ts +133 -14
  208. package/src/lib/host-tree-children.test.ts +27 -0
  209. package/src/lib/host-tree-children.ts +79 -0
  210. package/src/lib/inspector-card-mode.test.ts +92 -0
  211. package/src/lib/inspector-card-mode.ts +60 -0
  212. package/src/lib/interaction/hot-set.test.ts +10 -1
  213. package/src/lib/interaction/hot-set.ts +3 -1
  214. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  215. package/src/lib/interaction/overlay-policy.ts +16 -0
  216. package/src/lib/interaction/registered-drafting.ts +36 -0
  217. package/src/lib/interaction/scope.ts +31 -0
  218. package/src/lib/level-print-export.test.ts +484 -0
  219. package/src/lib/level-print-export.ts +526 -0
  220. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  221. package/src/lib/local-project-presentation-persistence.ts +335 -0
  222. package/src/lib/material-paint.ts +2 -0
  223. package/src/lib/measurement-parser.ts +3 -0
  224. package/src/lib/measurements.test.ts +4 -0
  225. package/src/lib/model-export.ts +29 -0
  226. package/src/lib/paint-preview-owner.ts +71 -0
  227. package/src/lib/paint-scope.test.ts +58 -2
  228. package/src/lib/paint-scope.ts +8 -2
  229. package/src/lib/planar-cursor-placement.test.ts +178 -1
  230. package/src/lib/planar-cursor-placement.ts +61 -5
  231. package/src/lib/plugin-panels.test.ts +92 -1
  232. package/src/lib/plugin-panels.ts +48 -0
  233. package/src/lib/portable-export.test.ts +366 -0
  234. package/src/lib/portable-export.ts +969 -0
  235. package/src/lib/print-3mf.test.ts +86 -0
  236. package/src/lib/print-3mf.ts +161 -0
  237. package/src/lib/print-content-scope.test.ts +107 -0
  238. package/src/lib/print-content-scope.ts +90 -0
  239. package/src/lib/print-export.test.ts +294 -0
  240. package/src/lib/print-export.ts +735 -0
  241. package/src/lib/print-feature-thickness.test.ts +147 -0
  242. package/src/lib/print-feature-thickness.ts +180 -0
  243. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  244. package/src/lib/print-golden-house.test.ts +249 -0
  245. package/src/lib/print-roof-solids.test.ts +133 -0
  246. package/src/lib/print-roof-solids.ts +630 -0
  247. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  248. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  249. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  250. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  251. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  252. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  253. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  254. package/src/lib/print-shell-compiler.ts +273 -0
  255. package/src/lib/print-wall-solids.test.ts +194 -0
  256. package/src/lib/print-wall-solids.ts +328 -0
  257. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  258. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  259. package/src/lib/scene.ts +22 -8
  260. package/src/lib/selection-routing.test.ts +125 -6
  261. package/src/lib/selection-routing.ts +34 -1
  262. package/src/lib/session-groups.test.ts +88 -0
  263. package/src/lib/session-groups.ts +201 -0
  264. package/src/lib/sfx-bus.ts +4 -0
  265. package/src/lib/sfx-player.ts +8 -0
  266. package/src/lib/snapping-mode.test.ts +32 -4
  267. package/src/lib/snapping-mode.ts +11 -7
  268. package/src/lib/structured-clone-fallback.test.ts +45 -0
  269. package/src/lib/structured-clone-fallback.ts +24 -0
  270. package/src/lib/usdz-export.ts +38 -0
  271. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  272. package/src/store/live-draft-preview-stores.test.ts +3 -0
  273. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  274. package/src/store/tool-mode.test.ts +87 -0
  275. package/src/store/use-camera-hint-focus.ts +48 -0
  276. package/src/store/use-editor.tsx +299 -188
  277. package/src/store/use-floorplan-draft-preview.ts +9 -0
  278. package/src/store/use-interaction-scope.test.ts +23 -1
  279. package/src/store/use-interaction-scope.ts +63 -2
  280. package/src/store/use-placement-preview.ts +56 -3
  281. package/src/store/use-session-groups.ts +103 -0
  282. package/src/components/tools/roof/roof-tool.tsx +0 -656
  283. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -1,656 +0,0 @@
1
- import {
2
- type AlignmentAnchor,
3
- type AnyNode,
4
- type AnyNodeId,
5
- collectAlignmentAnchors,
6
- emitter,
7
- type GridEvent,
8
- type LevelNode,
9
- RoofNode,
10
- RoofSegmentNode,
11
- resolveBuildingForLevel,
12
- sceneRegistry,
13
- useScene,
14
- type WallNode,
15
- wallSegmentAnchors,
16
- } from '@pascal-app/core'
17
- import { clearSurfacePlanSnapFeedback, resolveSurfacePlanPointSnap } from '@pascal-app/editor'
18
- import { useViewer } from '@pascal-app/viewer'
19
- import { useEffect, useMemo, useRef, useState } from 'react'
20
- import * as THREE from 'three'
21
- import {
22
- BufferGeometry,
23
- DoubleSide,
24
- Float32BufferAttribute,
25
- type Group,
26
- type Line,
27
- Vector3,
28
- } from 'three'
29
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
30
- import { EDITOR_LAYER } from '../../../lib/constants'
31
- import { sfxEmitter } from '../../../lib/sfx-bus'
32
- import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
33
- import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
34
- import { useFloorplanDraftPreview } from '../../../store/use-floorplan-draft-preview'
35
- import { CursorSphere } from '../shared/cursor-sphere'
36
-
37
- const DEFAULT_WALL_HEIGHT = 0.5
38
- const DEFAULT_PITCH_DEG = 40
39
- const GRID_OFFSET = 0.02
40
-
41
- // Walls that are direct children of a level.
42
- function getLevelWalls(
43
- levelId: string | null,
44
- nodes: Readonly<Record<string, AnyNode>>,
45
- ): WallNode[] {
46
- if (!levelId) return []
47
- const levelNode = nodes[levelId]
48
- if (levelNode?.type !== 'level') return []
49
- return (levelNode as LevelNode).children
50
- .map((childId) => nodes[childId])
51
- .filter((node): node is WallNode => node?.type === 'wall')
52
- }
53
-
54
- // Walls on the level directly beneath the active one. Levels share the same
55
- // local XZ origin (they only differ in world Y), so these walls live in the
56
- // identical coordinate frame and feed straight into both the alignment pool
57
- // and the magnetic wall-snap pipeline — letting a roof drawn on the upper
58
- // floor snap onto the wall corners of the floor below.
59
- function getBelowLevelWalls(
60
- currentLevelId: string | null,
61
- nodes: Readonly<Record<string, AnyNode>>,
62
- ): WallNode[] {
63
- if (!currentLevelId) return []
64
- const currentLevel = nodes[currentLevelId]
65
- if (currentLevel?.type !== 'level') return []
66
- const buildingId = resolveBuildingForLevel(currentLevel.id, nodes)
67
- if (!buildingId) return []
68
- const building = nodes[buildingId]
69
- if (building?.type !== 'building') return []
70
- const currentIndex = (currentLevel as LevelNode).level
71
- const belowLevel = (building.children ?? [])
72
- .map((childId) => nodes[childId])
73
- .filter((node): node is LevelNode => node?.type === 'level' && node.level < currentIndex)
74
- .sort((a, b) => b.level - a.level)[0]
75
- return getLevelWalls(belowLevel?.id ?? null, nodes)
76
- }
77
-
78
- // Current-level + floor-below walls — the magnetic snap targets the roof draft
79
- // locks onto (corners, midpoints, crossings, wall bodies), matching the wall
80
- // tool. Same coordinate frame, so no transform is needed.
81
- function getRoofSnapWalls(
82
- currentLevelId: string | null,
83
- nodes: Readonly<Record<string, AnyNode>>,
84
- ): WallNode[] {
85
- return [...getLevelWalls(currentLevelId, nodes), ...getBelowLevelWalls(currentLevelId, nodes)]
86
- }
87
-
88
- // Current-level alignment anchors plus the floor-below wall corners.
89
- function collectRoofAlignmentAnchors(
90
- nodes: Readonly<Record<string, AnyNode>>,
91
- currentLevelId: string | null,
92
- ): AlignmentAnchor[] {
93
- return [
94
- ...collectAlignmentAnchors(nodes, '', currentLevelId),
95
- ...getBelowLevelWalls(currentLevelId, nodes).flatMap((wall) =>
96
- wallSegmentAnchors(wall.id, wall.start, wall.end, wall.thickness),
97
- ),
98
- ]
99
- }
100
-
101
- /**
102
- * Creates a roof group with one default gable segment
103
- */
104
- const commitRoofPlacement = (
105
- levelId: LevelNode['id'],
106
- corner1: [number, number, number],
107
- corner2: [number, number, number],
108
- selectedIds: string[],
109
- ): AnyNode['id'] => {
110
- const { createNode, createNodes, nodes } = useScene.getState()
111
-
112
- // A placed roof preset seeds `toolDefaults.roof` with the flattened
113
- // subtree params (roofType, pitch, wallHeight, overhang, materials, …)
114
- // before the tool activates. The footprint (width/depth) and placement
115
- // come from the drawn rectangle and always win; the segment carries the
116
- // shape/material params, the roof container picks up the materials.
117
- const defaults = useEditor.getState().toolDefaults.roof ?? {}
118
-
119
- const centerX = (corner1[0] + corner2[0]) / 2
120
- const centerZ = (corner1[2] + corner2[2]) / 2
121
-
122
- const width = Math.max(Math.abs(corner2[0] - corner1[0]), 1)
123
- const depth = Math.max(Math.abs(corner2[2] - corner1[2]), 1)
124
-
125
- // Determine if there is an active roof node we should add to
126
- let targetRoofId: RoofNode['id'] | null = null
127
- const selectedId = selectedIds[0]
128
- if (selectedIds.length === 1 && selectedId) {
129
- const selectedNode = nodes[selectedId as AnyNodeId]
130
- if (selectedNode?.type === 'roof') {
131
- targetRoofId = selectedNode.id
132
- } else if (selectedNode?.type === 'roof-segment' && selectedNode.parentId) {
133
- targetRoofId = selectedNode.parentId as RoofNode['id']
134
- }
135
- }
136
-
137
- if (targetRoofId) {
138
- const targetRoof = nodes[targetRoofId] as RoofNode
139
- let localX = centerX
140
- let localZ = centerZ
141
-
142
- // Convert world coordinates to the local space of the parent roof
143
- const targetObj = sceneRegistry.nodes.get(targetRoofId)
144
- if (targetObj) {
145
- const worldVec = new THREE.Vector3(centerX, 0, centerZ)
146
- targetObj.worldToLocal(worldVec)
147
- localX = worldVec.x
148
- localZ = worldVec.z
149
- } else {
150
- // Math fallback if mesh isn't ready
151
- const dx = centerX - targetRoof.position[0]
152
- const dz = centerZ - targetRoof.position[2]
153
- const angle = -targetRoof.rotation
154
- localX = dx * Math.cos(angle) - dz * Math.sin(angle)
155
- localZ = dx * Math.sin(angle) + dz * Math.cos(angle)
156
- }
157
-
158
- const segment = RoofSegmentNode.parse({
159
- wallHeight: DEFAULT_WALL_HEIGHT,
160
- pitch: DEFAULT_PITCH_DEG,
161
- roofType: 'gable',
162
- ...defaults,
163
- width,
164
- depth,
165
- position: [localX, 0, localZ],
166
- })
167
-
168
- createNode(segment, targetRoofId as AnyNode['id'])
169
- sfxEmitter.emit('sfx:structure-build')
170
- return segment.id // Returns segment ID so it can be selected immediately
171
- }
172
-
173
- // Count existing roofs for naming
174
- const roofCount = Object.values(nodes).filter((n) => n.type === 'roof').length
175
- const name = `Roof ${roofCount + 1}`
176
-
177
- // Create the segment first (centered in its new parent)
178
- const segment = RoofSegmentNode.parse({
179
- wallHeight: DEFAULT_WALL_HEIGHT,
180
- pitch: DEFAULT_PITCH_DEG,
181
- roofType: 'gable',
182
- ...defaults,
183
- width,
184
- depth,
185
- position: [0, 0, 0],
186
- })
187
-
188
- // Create the roof container. Segment-shaped params (roofType, pitch, …) are
189
- // dropped by the RoofNode schema; surface materials in `defaults` carry over.
190
- const roof = RoofNode.parse({
191
- ...defaults,
192
- name,
193
- position: [centerX, 0, centerZ],
194
- children: [segment.id],
195
- })
196
-
197
- // Create roof first (so segment can be parented to it), then segment
198
- createNodes([
199
- { node: roof, parentId: levelId },
200
- { node: segment, parentId: roof.id },
201
- ])
202
-
203
- sfxEmitter.emit('sfx:structure-build')
204
- return roof.id
205
- }
206
-
207
- type PreviewState = {
208
- corner1: [number, number, number] | null
209
- cursorPosition: [number, number, number]
210
- levelY: number
211
- }
212
-
213
- function buildRoofGhostGeometry(
214
- width: number,
215
- depth: number,
216
- wallHeight: number,
217
- pitchDeg: number,
218
- ) {
219
- const safeWidth = Math.max(width, 0.1)
220
- const safeDepth = Math.max(depth, 0.1)
221
- const halfWidth = safeWidth / 2
222
- const halfDepth = safeDepth / 2
223
- const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
224
-
225
- const vertices = [
226
- // Front slope
227
- -halfWidth,
228
- wallHeight,
229
- -halfDepth,
230
- halfWidth,
231
- wallHeight,
232
- -halfDepth,
233
- halfWidth,
234
- ridgeHeight,
235
- 0,
236
-
237
- -halfWidth,
238
- wallHeight,
239
- -halfDepth,
240
- halfWidth,
241
- ridgeHeight,
242
- 0,
243
- -halfWidth,
244
- ridgeHeight,
245
- 0,
246
-
247
- // Back slope
248
- -halfWidth,
249
- ridgeHeight,
250
- 0,
251
- halfWidth,
252
- ridgeHeight,
253
- 0,
254
- halfWidth,
255
- wallHeight,
256
- halfDepth,
257
-
258
- -halfWidth,
259
- ridgeHeight,
260
- 0,
261
- halfWidth,
262
- wallHeight,
263
- halfDepth,
264
- -halfWidth,
265
- wallHeight,
266
- halfDepth,
267
-
268
- // Left gable
269
- -halfWidth,
270
- wallHeight,
271
- -halfDepth,
272
- -halfWidth,
273
- ridgeHeight,
274
- 0,
275
- -halfWidth,
276
- wallHeight,
277
- halfDepth,
278
-
279
- // Right gable
280
- halfWidth,
281
- wallHeight,
282
- -halfDepth,
283
- halfWidth,
284
- wallHeight,
285
- halfDepth,
286
- halfWidth,
287
- ridgeHeight,
288
- 0,
289
- ]
290
-
291
- const geometry = new BufferGeometry()
292
- geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
293
- geometry.computeVertexNormals()
294
- return geometry
295
- }
296
-
297
- function buildRoofGhostEdges(width: number, depth: number, wallHeight: number, pitchDeg: number) {
298
- const safeWidth = Math.max(width, 0.1)
299
- const safeDepth = Math.max(depth, 0.1)
300
- const halfWidth = safeWidth / 2
301
- const halfDepth = safeDepth / 2
302
- const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
303
-
304
- const vertices = [
305
- // Base rectangle
306
- -halfWidth,
307
- wallHeight,
308
- -halfDepth,
309
- halfWidth,
310
- wallHeight,
311
- -halfDepth,
312
- halfWidth,
313
- wallHeight,
314
- -halfDepth,
315
- halfWidth,
316
- wallHeight,
317
- halfDepth,
318
- halfWidth,
319
- wallHeight,
320
- halfDepth,
321
- -halfWidth,
322
- wallHeight,
323
- halfDepth,
324
- -halfWidth,
325
- wallHeight,
326
- halfDepth,
327
- -halfWidth,
328
- wallHeight,
329
- -halfDepth,
330
-
331
- // Ridge + gable edges
332
- -halfWidth,
333
- ridgeHeight,
334
- 0,
335
- halfWidth,
336
- ridgeHeight,
337
- 0,
338
- -halfWidth,
339
- wallHeight,
340
- -halfDepth,
341
- -halfWidth,
342
- ridgeHeight,
343
- 0,
344
- -halfWidth,
345
- ridgeHeight,
346
- 0,
347
- -halfWidth,
348
- wallHeight,
349
- halfDepth,
350
- halfWidth,
351
- wallHeight,
352
- -halfDepth,
353
- halfWidth,
354
- ridgeHeight,
355
- 0,
356
- halfWidth,
357
- ridgeHeight,
358
- 0,
359
- halfWidth,
360
- wallHeight,
361
- halfDepth,
362
- ]
363
-
364
- const geometry = new BufferGeometry()
365
- geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
366
- return geometry
367
- }
368
-
369
- export const RoofTool: React.FC = () => {
370
- const cursorRef = useRef<Group>(null)
371
- const outlineRef = useRef<Line>(null!)
372
- const currentLevelId = useViewer((state) => state.selection.levelId)
373
- const selectedIds = useViewer((state) => state.selection.selectedIds)
374
- const setSelection = useViewer((state) => state.setSelection)
375
-
376
- const selectedIdsRef = useRef(selectedIds)
377
- useEffect(() => {
378
- selectedIdsRef.current = selectedIds
379
- }, [selectedIds])
380
-
381
- // Clear preset-seeded defaults on deactivation so a later manual roof draw
382
- // isn't built with a stale preset's parameters. Unmount-only.
383
- useEffect(() => () => useEditor.getState().setToolDefaults('roof', null), [])
384
-
385
- const corner1Ref = useRef<[number, number, number] | null>(null)
386
- const previousGridPosRef = useRef<[number, number] | null>(null)
387
- const [preview, setPreview] = useState<PreviewState>({
388
- corner1: null,
389
- cursorPosition: [0, 0, 0],
390
- levelY: 0,
391
- })
392
-
393
- useEffect(() => {
394
- if (!currentLevelId) return
395
-
396
- outlineRef.current.geometry = new BufferGeometry()
397
-
398
- // Alignment candidates — anchors of every alignable object on the active
399
- // level plus the wall corners of the floor directly below, so a roof drawn
400
- // on the upper floor aligns to the walls beneath it. Refreshed after each
401
- // roof commits. Both corners of the rectangle align.
402
- let alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
403
-
404
- // Resolve a grid:move/click into the drafted corner via the shared surface
405
- // snap pipeline: magnetic lock onto wall corners / midpoints / crossings /
406
- // bodies on the active level + floor below (raising the green beacon),
407
- // falling back to alignment guides, then to the world-grid snap. The same
408
- // path the slab/ceiling tools use, so the beacon and coloring match. The
409
- // pipeline reads the snapping mode itself (Shift bypass, magnetic on/off),
410
- // so this tool never inspects the flags. `levelId` is intentionally omitted
411
- // so the explicit floor-below `walls` aren't filtered back out.
412
- const resolveDraftPoint = (event: GridEvent): [number, number] => {
413
- const rawPoint: [number, number] = [event.localPosition[0], event.localPosition[2]]
414
- const gridFallback: [number, number] = isGridSnapActive()
415
- ? snapWorldXZForActiveBuilding(
416
- event.position[0],
417
- event.position[2],
418
- useEditor.getState().gridSnapStep,
419
- ).local
420
- : rawPoint
421
- const nodes = useScene.getState().nodes
422
- return resolveSurfacePlanPointSnap({
423
- rawPoint,
424
- fallbackPoint: gridFallback,
425
- walls: getRoofSnapWalls(currentLevelId, nodes),
426
- candidates: alignmentCandidates,
427
- movingId: '__roof-draft__',
428
- highlightWalls: true,
429
- }).point
430
- }
431
-
432
- const updateOutline = (
433
- corner1: [number, number, number],
434
- corner2: [number, number, number],
435
- ) => {
436
- const gridY = corner1[1] + GRID_OFFSET
437
-
438
- const groundPoints = [
439
- new Vector3(corner1[0], gridY, corner1[2]),
440
- new Vector3(corner2[0], gridY, corner1[2]),
441
- new Vector3(corner2[0], gridY, corner2[2]),
442
- new Vector3(corner1[0], gridY, corner2[2]),
443
- new Vector3(corner1[0], gridY, corner1[2]),
444
- ]
445
-
446
- outlineRef.current.geometry.dispose()
447
- outlineRef.current.geometry = new BufferGeometry().setFromPoints(groundPoints)
448
- outlineRef.current.visible = true
449
- }
450
-
451
- const onGridMove = (event: GridEvent) => {
452
- if (!cursorRef.current) return
453
-
454
- const [gridX, gridZ] = resolveDraftPoint(event)
455
- const y = event.localPosition[1]
456
-
457
- const cursorPosition: [number, number, number] = [gridX, y, gridZ]
458
- const gridY = y + GRID_OFFSET
459
-
460
- cursorRef.current.position.set(gridX, gridY, gridZ)
461
-
462
- if (
463
- (isGridSnapActive() || isMagneticSnapActive()) &&
464
- corner1Ref.current &&
465
- previousGridPosRef.current &&
466
- (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
467
- ) {
468
- sfxEmitter.emit('sfx:grid-snap')
469
- }
470
-
471
- previousGridPosRef.current = [gridX, gridZ]
472
-
473
- setPreview({
474
- corner1: corner1Ref.current,
475
- cursorPosition,
476
- levelY: y,
477
- })
478
-
479
- if (corner1Ref.current) {
480
- const draftPreview = useFloorplanDraftPreview.getState()
481
- draftPreview.setRoofDraftStart([corner1Ref.current[0], corner1Ref.current[2]])
482
- draftPreview.setRoofDraftEnd([gridX, gridZ])
483
- updateOutline(corner1Ref.current, cursorPosition)
484
- }
485
- }
486
-
487
- const onGridClick = (event: GridEvent) => {
488
- if (!currentLevelId) return
489
-
490
- const [gridX, gridZ] = resolveDraftPoint(event)
491
- const y = event.localPosition[1]
492
-
493
- if (corner1Ref.current) {
494
- const roofId = commitRoofPlacement(
495
- currentLevelId,
496
- corner1Ref.current,
497
- [gridX, y, gridZ],
498
- selectedIdsRef.current,
499
- )
500
-
501
- setSelection({ selectedIds: [roofId as AnyNode['id']] })
502
-
503
- corner1Ref.current = null
504
- const draftPreview = useFloorplanDraftPreview.getState()
505
- draftPreview.setRoofDraftStart(null)
506
- draftPreview.setRoofDraftEnd(null)
507
- outlineRef.current.visible = false
508
- alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
509
- clearSurfacePlanSnapFeedback()
510
- } else {
511
- corner1Ref.current = [gridX, y, gridZ]
512
- const draftPreview = useFloorplanDraftPreview.getState()
513
- draftPreview.setRoofDraftStart([gridX, gridZ])
514
- draftPreview.setRoofDraftEnd([gridX, gridZ])
515
- sfxEmitter.emit('sfx:structure-build-start')
516
- setPreview((prev) => ({
517
- ...prev,
518
- corner1: corner1Ref.current,
519
- }))
520
- }
521
- }
522
-
523
- const onCancel = () => {
524
- if (corner1Ref.current) {
525
- markToolCancelConsumed()
526
- corner1Ref.current = null
527
- const draftPreview = useFloorplanDraftPreview.getState()
528
- draftPreview.setRoofDraftStart(null)
529
- draftPreview.setRoofDraftEnd(null)
530
- outlineRef.current.visible = false
531
- setPreview((prev) => ({ ...prev, corner1: null }))
532
- }
533
- clearSurfacePlanSnapFeedback()
534
- }
535
-
536
- emitter.on('grid:move', onGridMove)
537
- emitter.on('grid:click', onGridClick)
538
- emitter.on('tool:cancel', onCancel)
539
-
540
- return () => {
541
- emitter.off('grid:move', onGridMove)
542
- emitter.off('grid:click', onGridClick)
543
- emitter.off('tool:cancel', onCancel)
544
- clearSurfacePlanSnapFeedback()
545
-
546
- corner1Ref.current = null
547
- const draftPreview = useFloorplanDraftPreview.getState()
548
- draftPreview.setRoofDraftStart(null)
549
- draftPreview.setRoofDraftEnd(null)
550
- }
551
- }, [currentLevelId, setSelection])
552
-
553
- const { corner1, cursorPosition, levelY } = preview
554
-
555
- const previewDimensions = useMemo(() => {
556
- if (!corner1) return null
557
- const length = Math.abs(cursorPosition[0] - corner1[0])
558
- const width = Math.abs(cursorPosition[2] - corner1[2])
559
- const centerX = (corner1[0] + cursorPosition[0]) / 2
560
- const centerZ = (corner1[2] + cursorPosition[2]) / 2
561
- return { length, width, centerX, centerZ }
562
- }, [corner1, cursorPosition])
563
-
564
- const roofGhostGeometry = useMemo(() => {
565
- if (!previewDimensions) return null
566
- return buildRoofGhostGeometry(
567
- previewDimensions.length,
568
- previewDimensions.width,
569
- DEFAULT_WALL_HEIGHT,
570
- DEFAULT_PITCH_DEG,
571
- )
572
- }, [previewDimensions])
573
-
574
- const roofGhostEdges = useMemo(() => {
575
- if (!previewDimensions) return null
576
- return buildRoofGhostEdges(
577
- previewDimensions.length,
578
- previewDimensions.width,
579
- DEFAULT_WALL_HEIGHT,
580
- DEFAULT_PITCH_DEG,
581
- )
582
- }, [previewDimensions])
583
-
584
- useEffect(
585
- () => () => {
586
- roofGhostGeometry?.dispose()
587
- roofGhostEdges?.dispose()
588
- },
589
- [roofGhostEdges, roofGhostGeometry],
590
- )
591
-
592
- return (
593
- <group>
594
- <CursorSphere ref={cursorRef} />
595
-
596
- {/* @ts-ignore */}
597
- <line
598
- frustumCulled={false}
599
- layers={EDITOR_LAYER}
600
- // @ts-expect-error
601
- ref={outlineRef}
602
- renderOrder={1}
603
- visible={false}
604
- >
605
- <bufferGeometry />
606
- <lineBasicNodeMaterial
607
- color="#818cf8"
608
- depthTest={false}
609
- depthWrite={false}
610
- linewidth={2}
611
- opacity={0.3}
612
- transparent
613
- />
614
- </line>
615
-
616
- {corner1 && (
617
- <CursorSphere
618
- color="#818cf8"
619
- position={[corner1[0], levelY + GRID_OFFSET, corner1[2]]}
620
- showTooltip={false}
621
- />
622
- )}
623
-
624
- {previewDimensions && previewDimensions.length > 0.1 && previewDimensions.width > 0.1 && (
625
- <group
626
- layers={EDITOR_LAYER}
627
- position={[previewDimensions.centerX, levelY + GRID_OFFSET, previewDimensions.centerZ]}
628
- >
629
- {roofGhostGeometry && (
630
- <mesh geometry={roofGhostGeometry} layers={EDITOR_LAYER} renderOrder={1}>
631
- <meshBasicMaterial
632
- color="#818cf8"
633
- depthTest={false}
634
- depthWrite={false}
635
- opacity={0.16}
636
- side={DoubleSide}
637
- transparent
638
- />
639
- </mesh>
640
- )}
641
- {roofGhostEdges && (
642
- <lineSegments geometry={roofGhostEdges} layers={EDITOR_LAYER} renderOrder={2}>
643
- <lineBasicMaterial
644
- color="#818cf8"
645
- depthTest={false}
646
- depthWrite={false}
647
- opacity={0.5}
648
- transparent
649
- />
650
- </lineSegments>
651
- )}
652
- </group>
653
- )}
654
- </group>
655
- )
656
- }
@@ -1,14 +0,0 @@
1
- import type { SnapContext } from '../../../lib/snapping-mode'
2
- import { ContextualHelperPanel } from './contextual-helper-panel'
3
-
4
- export function RoofHelper({ snapContext }: { snapContext?: SnapContext | null }) {
5
- return (
6
- <ContextualHelperPanel
7
- hints={[
8
- { keys: ['Left click'], label: 'Set corner' },
9
- { keys: ['Esc'], label: 'Cancel' },
10
- ]}
11
- snapContext={snapContext}
12
- />
13
- )
14
- }