@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.
- package/package.json +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5829 -12005
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +178 -149
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +433 -156
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -538
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +10 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +320 -37
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- 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
|
|
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={
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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={
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
15
|
-
* - merged-roof mesh
|
|
16
|
-
* - segments-wrapper group is
|
|
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
|
-
//
|
|
26
|
-
//
|
|
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
|
-
//
|
|
38
|
-
const roofIdsToUpdate = new Set([
|
|
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
|
|
56
|
-
//
|
|
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
|
|
@@ -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
|