@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
@@ -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
  )
@@ -396,8 +411,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
396
411
  <>
397
412
  <polyline
398
413
  fill="none"
399
- points={formatSvgPolygonPoints(arrow.polyline)}
400
414
  pointerEvents="none"
415
+ points={formatSvgPolygonPoints(arrow.polyline)}
401
416
  stroke={straightAccent}
402
417
  strokeWidth="1.15"
403
418
  vectorEffect="non-scaling-stroke"
@@ -411,8 +426,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
411
426
  />
412
427
  <polygon
413
428
  fill={straightAccent}
414
- points={formatSvgPolygonPoints(arrow.head)}
415
429
  pointerEvents="none"
430
+ points={formatSvgPolygonPoints(arrow.head)}
416
431
  />
417
432
  </>
418
433
  ) : null}
@@ -438,8 +453,6 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
438
453
  }
439
454
  : undefined
440
455
  }
441
- onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined}
442
- onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined}
443
456
  onPointerDown={
444
457
  canFocusStairs && stairSelected
445
458
  ? (event) => {
@@ -449,6 +462,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
449
462
  }
450
463
  : undefined
451
464
  }
465
+ onPointerEnter={canSelectStairs ? () => onStairHoverEnter(stair.id) : undefined}
466
+ onPointerLeave={canSelectStairs ? () => onStairHoverChange(null) : undefined}
452
467
  pointerEvents={canSelectStairs ? undefined : 'none'}
453
468
  style={canSelectStairs ? { cursor } : undefined}
454
469
  >
@@ -456,8 +471,8 @@ export const FloorplanStairLayer = memo(function FloorplanStairLayer({
456
471
  <polygon
457
472
  fill="transparent"
458
473
  key={`${stair.id}:hit:${polygonIndex}`}
459
- points={formatSvgPolygonPoints(polygon)}
460
474
  pointerEvents={canSelectStairs ? 'all' : 'none'}
475
+ points={formatSvgPolygonPoints(polygon)}
461
476
  stroke="transparent"
462
477
  strokeLinejoin="round"
463
478
  strokeWidth={hitStrokeWidth}
@@ -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]
@@ -75,7 +79,9 @@ const CeilingSelectionAffordance = ({
75
79
  ceiling: CeilingNode
76
80
  levelId: string
77
81
  }) => {
78
- const [levelObject, setLevelObject] = useState<Object3D | null>(() => sceneRegistry.nodes.get(levelId) ?? null)
82
+ const [levelObject, setLevelObject] = useState<Object3D | null>(
83
+ () => sceneRegistry.nodes.get(levelId) ?? null,
84
+ )
79
85
 
80
86
  const corners = useMemo(() => buildCornerBrackets(ceiling.polygon), [ceiling.polygon])
81
87
 
@@ -110,11 +116,7 @@ const CeilingSelectionAffordance = ({
110
116
  return createPortal(
111
117
  <group position={[0, (ceiling.height ?? 2.5) + BRACKET_Y_OFFSET, 0]}>
112
118
  {corners.map((corner, index) => (
113
- <CornerBracket
114
- ceiling={ceiling}
115
- corner={corner}
116
- key={`${ceiling.id}-corner-${index}`}
117
- />
119
+ <CornerBracket ceiling={ceiling} corner={corner} key={`${ceiling.id}-corner-${index}`} />
118
120
  ))}
119
121
  </group>,
120
122
  levelObject,
@@ -151,6 +153,7 @@ const CornerBracket = ({
151
153
  localPosition: [0, 0, 0],
152
154
  position: [corner.corner[0], ceiling.height ?? 2.5, corner.corner[1]],
153
155
  stopPropagation: () => e.stopPropagation(),
156
+ viaHandle: true,
154
157
  })
155
158
  }
156
159
 
@@ -181,9 +184,16 @@ const CornerBracket = ({
181
184
  e.stopPropagation()
182
185
  setIsHovered(false)
183
186
  }}
187
+ renderOrder={CORNER_RENDER_ORDER}
184
188
  >
185
189
  <boxGeometry args={HIT_BOX_SIZE} />
186
- <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
+ />
187
197
  </mesh>
188
198
  </group>
189
199
  )
@@ -213,10 +223,17 @@ const BracketLeg = ({
213
223
  <mesh
214
224
  onClick={onClick}
215
225
  position={position}
226
+ renderOrder={CORNER_RENDER_ORDER}
216
227
  rotation={[0, angle, 0]}
217
228
  >
218
229
  <boxGeometry args={[length, BRACKET_HEIGHT, BRACKET_THICKNESS]} />
219
- <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
+ />
220
237
  </mesh>
221
238
  )
222
239
  }
@@ -234,7 +251,11 @@ function buildCornerBrackets(polygon: Array<[number, number]>): CornerBracketDat
234
251
 
235
252
  const incomingLength = Math.hypot(incomingVector[0], incomingVector[1])
236
253
  const outgoingLength = Math.hypot(outgoingVector[0], outgoingVector[1])
237
- const cornerStrength = 1 - Math.abs(incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1])
254
+ const cornerStrength =
255
+ 1 -
256
+ Math.abs(
257
+ incomingDirection[0] * outgoingDirection[0] + incomingDirection[1] * outgoingDirection[1],
258
+ )
238
259
 
239
260
  return {
240
261
  corner,
@@ -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-segment 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
@@ -68,7 +68,7 @@ export const StairEditSystem = () => {
68
68
  const segmentsWrapper = group.getObjectByName('segments-wrapper')
69
69
  const isActive = activeStairIds.has(stairId)
70
70
 
71
- if (mergedMesh) mergedMesh.visible = !isActive && !isCurved
71
+ if (mergedMesh) mergedMesh.visible = !(isActive || isCurved)
72
72
  if (segmentsWrapper) segmentsWrapper.visible = isActive && !isCurved
73
73
 
74
74
  if (stairNode?.children?.length) {
@@ -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
  }
File without changes
@@ -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