@pascal-app/editor 0.8.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 (223) hide show
  1. package/package.json +8 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +754 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +58 -51
  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 +201 -83
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
  10. package/src/components/editor/first-person-controls.tsx +825 -20
  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 +5829 -12005
  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 +178 -149
  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 +433 -156
  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 +136 -62
  28. package/src/components/editor/thumbnail-generator.tsx +9 -3
  29. package/src/components/editor/use-floorplan-background-placement.ts +119 -32
  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-geometry-renderer.tsx +266 -0
  45. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  46. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  47. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  48. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  49. package/src/components/editor-2d/svg-paths.ts +10 -2
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
  51. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  52. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  53. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  54. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  55. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  56. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  57. package/src/components/tools/fence/fence-drafting.ts +64 -22
  58. package/src/components/tools/item/move-tool.tsx +41 -105
  59. package/src/components/tools/item/placement-math.test.ts +10 -0
  60. package/src/components/tools/item/placement-math.ts +31 -5
  61. package/src/components/tools/item/placement-strategies.ts +190 -15
  62. package/src/components/tools/item/placement-types.ts +10 -1
  63. package/src/components/tools/item/use-draft-node.ts +20 -1
  64. package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
  65. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  66. package/src/components/tools/roof/roof-tool.tsx +108 -12
  67. package/src/components/tools/select/box-select-state.ts +77 -0
  68. package/src/components/tools/select/box-select-tool.tsx +324 -538
  69. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  70. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  71. package/src/components/tools/select/select-candidates.test.ts +109 -0
  72. package/src/components/tools/select/select-candidates.ts +85 -0
  73. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  74. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  75. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  76. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  77. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  78. package/src/components/tools/shared/placement-box.tsx +123 -0
  79. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  80. package/src/components/tools/shared/segment-angle.ts +56 -0
  81. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  82. package/src/components/tools/site/site-flag-model.ts +18 -0
  83. package/src/components/tools/stair/stair-defaults.ts +1 -1
  84. package/src/components/tools/stair/stair-tool.tsx +269 -35
  85. package/src/components/tools/tool-manager.tsx +172 -56
  86. package/src/components/tools/wall/wall-drafting.ts +98 -98
  87. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  88. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  89. package/src/components/tools/zone/zone-tool.tsx +16 -6
  90. package/src/components/ui/action-menu/action-button.tsx +21 -1
  91. package/src/components/ui/action-menu/control-modes.tsx +5 -82
  92. package/src/components/ui/action-menu/index.tsx +1 -113
  93. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  94. package/src/components/ui/action-menu/view-toggles.tsx +243 -6
  95. package/src/components/ui/command-palette/editor-commands.tsx +18 -13
  96. package/src/components/ui/command-palette/index.tsx +2 -1
  97. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  98. package/src/components/ui/controls/material-picker.tsx +11 -32
  99. package/src/components/ui/controls/metric-control.tsx +33 -11
  100. package/src/components/ui/floating-level-selector.tsx +47 -10
  101. package/src/components/ui/helpers/helper-manager.tsx +16 -17
  102. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  103. package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
  104. package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
  105. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  106. package/src/components/ui/panels/node-display.ts +1 -0
  107. package/src/components/ui/panels/panel-manager.tsx +35 -47
  108. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  109. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  110. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  111. package/src/components/ui/scene-loader.tsx +1 -3
  112. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  113. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  114. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  115. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  116. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  117. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  118. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  119. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  120. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  121. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  122. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  123. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  125. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  126. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  127. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  128. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
  133. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  134. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  135. package/src/components/viewer-overlay.tsx +169 -52
  136. package/src/components/viewer-zone-system.tsx +1 -1
  137. package/src/hooks/use-auto-save.ts +10 -3
  138. package/src/hooks/use-ceiling-events.ts +175 -0
  139. package/src/hooks/use-drag-action.ts +124 -0
  140. package/src/hooks/use-keyboard.ts +115 -62
  141. package/src/hooks/use-selection.ts +64 -0
  142. package/src/index.tsx +237 -5
  143. package/src/lib/constants.ts +6 -2
  144. package/src/lib/editor-api.ts +62 -0
  145. package/src/lib/elevator-support.ts +96 -0
  146. package/src/lib/floorplan/apply-alignment.ts +111 -0
  147. package/src/lib/floorplan/index.ts +7 -0
  148. package/src/lib/floorplan/selection-tool.ts +40 -0
  149. package/src/lib/floorplan/stairs.ts +12 -1
  150. package/src/lib/fresh-planar-placement.test.ts +102 -0
  151. package/src/lib/fresh-planar-placement.ts +63 -0
  152. package/src/lib/history.ts +2 -1
  153. package/src/lib/level-duplication.ts +54 -42
  154. package/src/lib/level-name.ts +11 -0
  155. package/src/lib/material-paint.ts +211 -19
  156. package/src/lib/placement-metadata.ts +29 -0
  157. package/src/lib/planar-cursor-placement.test.ts +43 -0
  158. package/src/lib/planar-cursor-placement.ts +42 -0
  159. package/src/lib/roof-duplication.ts +1 -1
  160. package/src/lib/scene-clipboard.ts +267 -0
  161. package/src/lib/scene.ts +13 -4
  162. package/src/lib/sfx-bus.ts +23 -1
  163. package/src/lib/sfx-player.ts +70 -22
  164. package/src/lib/site-boundary.ts +1 -0
  165. package/src/lib/stair-levels.test.ts +146 -0
  166. package/src/lib/stair-levels.ts +177 -0
  167. package/src/lib/world-grid-snap.ts +199 -0
  168. package/src/lib/zone-content.ts +130 -0
  169. package/src/store/use-alignment-guides.ts +21 -0
  170. package/src/store/use-editor.tsx +320 -37
  171. package/src/store/use-placement-preview.ts +31 -0
  172. package/src/store/use-wall-move-ghosts.ts +36 -0
  173. package/src/store/use-wall-snap-indicator.ts +32 -0
  174. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  175. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  176. package/src/components/tools/building/move-building-tool.tsx +0 -157
  177. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  178. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  179. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  180. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  181. package/src/components/tools/column/column-tool.tsx +0 -97
  182. package/src/components/tools/column/move-column-tool.tsx +0 -105
  183. package/src/components/tools/door/door-math.ts +0 -110
  184. package/src/components/tools/door/door-tool.tsx +0 -324
  185. package/src/components/tools/door/move-door-tool.tsx +0 -412
  186. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  187. package/src/components/tools/fence/fence-tool.tsx +0 -346
  188. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  189. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  190. package/src/components/tools/item/item-tool.tsx +0 -26
  191. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  192. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  193. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  194. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  195. package/src/components/tools/slab/slab-tool.tsx +0 -322
  196. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  197. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  198. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  199. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  200. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  201. package/src/components/tools/wall/wall-tool.tsx +0 -332
  202. package/src/components/tools/window/move-window-tool.tsx +0 -447
  203. package/src/components/tools/window/window-math.ts +0 -117
  204. package/src/components/tools/window/window-tool.tsx +0 -332
  205. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  206. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  207. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  208. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  209. package/src/components/ui/panels/column-panel.tsx +0 -759
  210. package/src/components/ui/panels/door-panel.tsx +0 -1299
  211. package/src/components/ui/panels/fence-panel.tsx +0 -222
  212. package/src/components/ui/panels/item-panel.tsx +0 -306
  213. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  214. package/src/components/ui/panels/roof-panel.tsx +0 -280
  215. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  216. package/src/components/ui/panels/slab-panel.tsx +0 -250
  217. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  218. package/src/components/ui/panels/stair-panel.tsx +0 -577
  219. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  220. package/src/components/ui/panels/wall-panel.tsx +0 -273
  221. package/src/components/ui/panels/window-panel.tsx +0 -968
  222. package/src/contexts/presets-context.tsx +0 -121
  223. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -83,6 +83,14 @@ function getNormalizedFloorplanStairSweepAngle(stair: StairNode) {
83
83
  return baseSweepAngle
84
84
  }
85
85
 
86
+ function clampFloorplanCircularSweepAngle(sweepAngle: number) {
87
+ if (Math.abs(sweepAngle) >= Math.PI * 2) {
88
+ return Math.sign(sweepAngle || 1) * (Math.PI * 2 - 0.001)
89
+ }
90
+
91
+ return sweepAngle
92
+ }
93
+
86
94
  function getFloorplanStairStepCount(stair: StairNode, minimum: number) {
87
95
  return Math.max(minimum, Math.round(stair.stepCount ?? 10))
88
96
  }
@@ -139,7 +147,10 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
139
147
  const sectorStartAngle = -stair.rotation - normalizedSweepAngle / 2
140
148
  const sectorEndAngle = sectorStartAngle + normalizedSweepAngle
141
149
  const spiralLandingSweep = getFloorplanSpiralLandingSweep(stair, normalizedSweepAngle)
142
- const visualSectorEndAngle = sectorEndAngle + spiralLandingSweep
150
+ const visualSweepAngle = clampFloorplanCircularSweepAngle(
151
+ normalizedSweepAngle + spiralLandingSweep,
152
+ )
153
+ const visualSectorEndAngle = sectorStartAngle + visualSweepAngle
143
154
  const stairCenter = {
144
155
  x: stair.position[0],
145
156
  y: stair.position[2],
@@ -273,10 +284,12 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
273
284
  fill={curvedAccent}
274
285
  key={`${stair.id}:spiral-arrow`}
275
286
  pointerEvents="none"
276
- points={buildSvgArrowHeadPoints(
277
- arrowPoint,
278
- tangentAngle,
279
- clamp(stair.width * 0.18, 0.12, 0.18),
287
+ points={formatSvgPolygonPoints(
288
+ buildSvgArrowHeadPoints(
289
+ arrowPoint,
290
+ tangentAngle,
291
+ clamp(stair.width * 0.18, 0.12, 0.18),
292
+ ),
280
293
  )}
281
294
  />
282
295
  )
@@ -361,10 +374,12 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
361
374
  fill={curvedAccent}
362
375
  key={`${stair.id}:curved-arrow`}
363
376
  pointerEvents="none"
364
- points={buildSvgArrowHeadPoints(
365
- arrowPoint,
366
- tangentAngle,
367
- clamp(stair.width * 0.16, 0.1, 0.16),
377
+ points={formatSvgPolygonPoints(
378
+ buildSvgArrowHeadPoints(
379
+ arrowPoint,
380
+ tangentAngle,
381
+ clamp(stair.width * 0.16, 0.1, 0.16),
382
+ ),
368
383
  )}
369
384
  />
370
385
  )
@@ -103,7 +103,15 @@ export function formatSvgPolygonPoints(points: Point2D[]) {
103
103
  return points.map((point) => `${toSvgX(point.x)},${toSvgY(point.y)}`).join(' ')
104
104
  }
105
105
 
106
- export function buildSvgArrowHeadPoints(point: Point2D, angle: number, size: number) {
106
+ /**
107
+ * Three points defining an arrow head — tip + two trailing barbs.
108
+ * Returned as plain `Point2D` objects so consumers can either feed them
109
+ * straight into `formatSvgPolygonPoints` (for SVG `points=""`) or push
110
+ * them onto a `FloorplanGeometry.polygon.points` array. Mixing both
111
+ * downstream paths through a string-returning helper was awkward — see
112
+ * `nodes/src/stair/floorplan.ts` which needs the points as objects.
113
+ */
114
+ export function buildSvgArrowHeadPoints(point: Point2D, angle: number, size: number): Point2D[] {
107
115
  const left = {
108
116
  x: point.x - size * Math.cos(angle - Math.PI / 6),
109
117
  y: point.y - size * Math.sin(angle - Math.PI / 6),
@@ -113,7 +121,7 @@ export function buildSvgArrowHeadPoints(point: Point2D, angle: number, size: num
113
121
  y: point.y - size * Math.sin(angle + Math.PI / 6),
114
122
  }
115
123
 
116
- return formatSvgPolygonPoints([point, left, right])
124
+ return [point, left, right]
117
125
  }
118
126
 
119
127
  export { toSvgPoint, toSvgX, toSvgY }
@@ -18,6 +18,10 @@ const BRACKET_THICKNESS = 0.04
18
18
  const BRACKET_HEIGHT = 0.04
19
19
  const BRACKET_Y_OFFSET = 0.035
20
20
  const HIT_BOX_SIZE: [number, number, number] = [0.28, 0.08, 0.28]
21
+ // Draw the corner handles after everything else and with depth testing
22
+ // off (see materials below) so they stay visible — and clickable — even
23
+ // when a wall, roof, or the ceiling itself would otherwise occlude them.
24
+ const CORNER_RENDER_ORDER = 1000
21
25
 
22
26
  type CornerBracketData = {
23
27
  corner: [number, number]
@@ -149,6 +153,7 @@ const CornerBracket = ({
149
153
  localPosition: [0, 0, 0],
150
154
  position: [corner.corner[0], ceiling.height ?? 2.5, corner.corner[1]],
151
155
  stopPropagation: () => e.stopPropagation(),
156
+ viaHandle: true,
152
157
  })
153
158
  }
154
159
 
@@ -179,9 +184,16 @@ const CornerBracket = ({
179
184
  e.stopPropagation()
180
185
  setIsHovered(false)
181
186
  }}
187
+ renderOrder={CORNER_RENDER_ORDER}
182
188
  >
183
189
  <boxGeometry args={HIT_BOX_SIZE} />
184
- <meshBasicMaterial color={cubeColor} depthWrite={false} opacity={cubeOpacity} transparent />
190
+ <meshBasicMaterial
191
+ color={cubeColor}
192
+ depthTest={false}
193
+ depthWrite={false}
194
+ opacity={cubeOpacity}
195
+ transparent
196
+ />
185
197
  </mesh>
186
198
  </group>
187
199
  )
@@ -208,9 +220,20 @@ const BracketLeg = ({
208
220
  ]
209
221
 
210
222
  return (
211
- <mesh onClick={onClick} position={position} rotation={[0, angle, 0]}>
223
+ <mesh
224
+ onClick={onClick}
225
+ position={position}
226
+ renderOrder={CORNER_RENDER_ORDER}
227
+ rotation={[0, angle, 0]}
228
+ >
212
229
  <boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
213
- <meshBasicMaterial color={color} depthWrite={false} opacity={opacity} transparent />
230
+ <meshBasicMaterial
231
+ color={color}
232
+ depthTest={false}
233
+ depthWrite={false}
234
+ opacity={opacity}
235
+ transparent
236
+ />
214
237
  </mesh>
215
238
  )
216
239
  }
@@ -25,15 +25,19 @@ export const CeilingSystem = () => {
25
25
  }
26
26
 
27
27
  for (const id of selectedIds) {
28
- let currentId: string | null = id
29
- let isCeilingRelated = false
30
- let levelId: string | null = null
28
+ // Only treat a directly-selected ceiling as "reveal the grid"; a
29
+ // selected descendant (e.g. a freshly-placed ceiling light) used to
30
+ // count too, which left the opaque grid overlay covering the room
31
+ // even after the user moved on. With the grid still in front, every
32
+ // subsequent click in 3D hit the grid mesh (its `useNodeEvents`
33
+ // handlers re-selected the ceiling) instead of the items below.
34
+ const selectedNode = nodes[id as AnyNodeId]
35
+ if (selectedNode?.type !== 'ceiling') continue
31
36
 
37
+ let currentId: string | null = selectedNode.parentId as string | null
38
+ let levelId: string | null = null
32
39
  while (currentId && nodes[currentId as AnyNodeId]) {
33
40
  const node = nodes[currentId as AnyNodeId]
34
- if (node?.type === 'ceiling') {
35
- isCeilingRelated = true
36
- }
37
41
  if (node?.type === 'level') {
38
42
  levelId = node.id
39
43
  break
@@ -41,12 +45,12 @@ export const CeilingSystem = () => {
41
45
  currentId = node?.parentId as string | null
42
46
  }
43
47
 
44
- if (isCeilingRelated && levelId) {
48
+ if (levelId) {
45
49
  levelsToShowCeilings.add(levelId)
46
50
  }
47
51
  }
48
52
 
49
- const ceilings = sceneRegistry.byType.ceiling
53
+ const ceilings = sceneRegistry.byType.ceiling!
50
54
  ceilings.forEach((ceiling) => {
51
55
  const mesh = sceneRegistry.nodes.get(ceiling)
52
56
  if (mesh) {
@@ -1,41 +1,96 @@
1
1
  import { type AnyNodeId, type RoofNode, sceneRegistry, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { useEffect, useRef } from 'react'
4
+ import * as THREE from 'three'
5
+
6
+ // Empty placeholder geometry used when we reveal segments-wrapper for
7
+ // accessory editing. The roof's CSG-merged shell is the only thing
8
+ // that should render the roof surface in this mode — the per-segment
9
+ // CSG geometry (if any was left over from a prior edit) would visually
10
+ // double the cut shape, so we strip each segment mesh back to nothing.
11
+ // `RoofSystem` rebuilds CSG on demand if the user later selects a
12
+ // segment, so destroying the cached geometry here only costs one
13
+ // recomputation per segment when the user actually wants it back.
14
+ function makeEmptySegmentGeometry(): THREE.BufferGeometry {
15
+ const g = new THREE.BufferGeometry()
16
+ // Three zero-vertices (one degenerate, invisible triangle), not an empty
17
+ // attribute: in accessory-reveal mode the segments-wrapper is shown, so these
18
+ // meshes are drawn. An empty position (count 0) leaves WebGPU vertex buffer
19
+ // slot 0 unbound and the draw is rejected, poisoning the command encoder.
20
+ g.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(9), 3))
21
+ // Match the four material slots the roof-segment renderer's material
22
+ // array expects (0=top, 1=side, 2=interior, 3=shingle). Without these
23
+ // groups, mesh.material is a single-material lookup that mismatches
24
+ // the array — same crash mode the BoxGeometry workaround in
25
+ // `roof-system.tsx:144` guards against.
26
+ g.addGroup(0, 0, 0)
27
+ g.addGroup(0, 0, 1)
28
+ g.addGroup(0, 0, 2)
29
+ g.addGroup(0, 0, 3)
30
+ return g
31
+ }
4
32
 
5
33
  /**
6
34
  * Imperatively toggles the Three.js visibility of roof objects based on the
7
35
  * editor selection — without causing React re-renders in RoofRenderer.
8
36
  *
9
- * When a roof (or one of its segments) is selected:
37
+ * Full edit-mode (segment selected):
10
38
  * - merged-roof mesh is hidden
11
39
  * - segments-wrapper group is shown (individual segments visible for editing)
12
40
  * - all children are marked dirty so RoofSystem rebuilds their geometry
13
41
  *
14
- * When deselected:
15
- * - merged-roof mesh is shown
16
- * - segments-wrapper group is hidden
42
+ * Accessory-reveal mode (a dormer/chimney/etc. hosted on a segment is selected):
43
+ * - merged-roof mesh stays visible (we don't want the appearance to jump)
44
+ * - segments-wrapper group is shown ANYWAY so anything portaled into a
45
+ * segment's registered mesh (e.g. dormer in-world handle arrows that
46
+ * don't use `portal: 'grandparent'`) is no longer inheriting the
47
+ * wrapper's hidden flag
48
+ * - segment placeholder geometry is empty, so revealing the wrapper has
49
+ * no visible cost beyond letting the handle arrows render
50
+ *
51
+ * When deselected: merged-roof shown, segments-wrapper hidden.
17
52
  */
18
53
  export const RoofEditSystem = () => {
19
54
  const selectedIds = useViewer((s) => s.selection.selectedIds)
20
55
  const prevActiveRoofIds = useRef(new Set<string>())
56
+ const prevRevealRoofIds = useRef(new Set<string>())
21
57
 
22
58
  useEffect(() => {
23
59
  const nodes = useScene.getState().nodes
24
60
 
25
- // Collect which roof nodes should be in "edit mode".
26
- // Selecting the roof itself should keep the merged visual intact so
27
- // material appearance does not jump between merged and per-segment meshes.
61
+ // Roofs where a segment itself is selected -> full edit mode (hide
62
+ // merged, show wrapper).
28
63
  const activeRoofIds = new Set<string>()
64
+ // Roofs where an accessory (dormer/chimney/etc.) is selected -> only
65
+ // reveal the wrapper so handle portals into the segment mesh become
66
+ // visible. Merged stays on.
67
+ const revealRoofIds = new Set<string>()
68
+
29
69
  for (const id of selectedIds) {
30
70
  const node = nodes[id as AnyNodeId]
31
71
  if (!node) continue
32
72
  if (node.type === 'roof-segment' && node.parentId) {
33
73
  activeRoofIds.add(node.parentId)
74
+ continue
75
+ }
76
+ // Walk up one level: if the parent is a roof-segment, this is a
77
+ // hosted accessory and we want to reveal its grandparent roof's
78
+ // wrapper. Two-step lookup keeps it scoped to roof children
79
+ // without enumerating all accessory kinds.
80
+ if (!node.parentId) continue
81
+ const parent = nodes[node.parentId as AnyNodeId]
82
+ if (parent?.type === 'roof-segment' && parent.parentId) {
83
+ revealRoofIds.add(parent.parentId)
34
84
  }
35
85
  }
36
86
 
37
- // Update all roofs that are currently active OR were previously active
38
- const roofIdsToUpdate = new Set([...activeRoofIds, ...prevActiveRoofIds.current])
87
+ // Union of roofs that need ANY state change this tick.
88
+ const roofIdsToUpdate = new Set([
89
+ ...activeRoofIds,
90
+ ...revealRoofIds,
91
+ ...prevActiveRoofIds.current,
92
+ ...prevRevealRoofIds.current,
93
+ ])
39
94
 
40
95
  for (const roofId of roofIdsToUpdate) {
41
96
  const group = sceneRegistry.nodes.get(roofId)
@@ -44,25 +99,44 @@ export const RoofEditSystem = () => {
44
99
  const mergedMesh = group.getObjectByName('merged-roof')
45
100
  const segmentsWrapper = group.getObjectByName('segments-wrapper')
46
101
  const isActive = activeRoofIds.has(roofId)
102
+ const isReveal = revealRoofIds.has(roofId)
47
103
 
48
104
  if (mergedMesh) mergedMesh.visible = !isActive
49
- if (segmentsWrapper) segmentsWrapper.visible = isActive
105
+ if (segmentsWrapper) segmentsWrapper.visible = isActive || isReveal
50
106
 
51
107
  const roofNode = nodes[roofId as AnyNodeId] as RoofNode | undefined
52
108
  if (roofNode?.children?.length) {
53
109
  const wasActive = prevActiveRoofIds.current.has(roofId)
110
+ const wasReveal = prevRevealRoofIds.current.has(roofId)
54
111
  if (isActive !== wasActive) {
55
- // Entering edit mode: rebuild individual segment geometries
56
- // Exiting edit mode: sync transforms + rebuild merged mesh
112
+ // Entering / exiting full edit mode: rebuild segment / merged
113
+ // geometries. Accessory-reveal doesn't need this segments
114
+ // keep their placeholder; only their visibility flips.
57
115
  const { markDirty } = useScene.getState()
58
116
  for (const childId of roofNode.children) {
59
117
  markDirty(childId as AnyNodeId)
60
118
  }
61
119
  }
120
+ // Entering reveal mode (and NOT also full-edit, which already
121
+ // owns its own rebuild path): strip each segment mesh back to
122
+ // an empty placeholder so the wrapper-now-visible doesn't
123
+ // re-show stale CSG geometry from a previous segment edit.
124
+ // Without this, the host segment's CSG cut renders ON TOP of
125
+ // the merged-roof, doubling the dormer's cut shape and
126
+ // bleeding the host wall material through the dormer body.
127
+ if (isReveal && !isActive && !wasReveal && segmentsWrapper) {
128
+ for (const child of segmentsWrapper.children) {
129
+ const mesh = child as THREE.Mesh
130
+ if (!mesh.isMesh) continue
131
+ mesh.geometry?.dispose()
132
+ mesh.geometry = makeEmptySegmentGeometry()
133
+ }
134
+ }
62
135
  }
63
136
  }
64
137
 
65
138
  prevActiveRoofIds.current = activeRoofIds
139
+ prevRevealRoofIds.current = revealRoofIds
66
140
  }, [selectedIds])
67
141
 
68
142
  return null
@@ -85,7 +85,7 @@ export const StairEditSystem = () => {
85
85
  }
86
86
 
87
87
  prevActiveStairIds.current = activeStairIds
88
- }, [selectedIds, selectedStairSignature])
88
+ }, [selectedIds])
89
89
 
90
90
  return null
91
91
  }
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_ELEVATOR_WIDTH = 1.84
2
+ export const DEFAULT_ELEVATOR_DEPTH = 1.84
3
+ export const DEFAULT_ELEVATOR_CAB_HEIGHT = 2.35
4
+ export const DEFAULT_ELEVATOR_DOOR_WIDTH = 0.95
5
+ export const DEFAULT_ELEVATOR_DOOR_HEIGHT = 2.1
6
+ export const DEFAULT_ELEVATOR_SPEED = 2.2
7
+ export const DEFAULT_ELEVATOR_DOOR_DURATION_MS = 900
8
+ export const DEFAULT_ELEVATOR_DWELL_MS = 1400