@pascal-app/editor 1.0.0-beta.5 → 1.0.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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
type AnyNode,
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
applyHeightPatch,
|
|
6
|
+
BlockNode,
|
|
6
7
|
CeilingNode,
|
|
7
8
|
createTerrainField,
|
|
8
9
|
DoorNode as DoorSchema,
|
|
9
10
|
encodeTerrainField,
|
|
10
11
|
flattenPatch,
|
|
11
12
|
GROUND_SUPPORT_ID,
|
|
13
|
+
getFloorPlacedElevation,
|
|
12
14
|
initSpaceDetectionSync,
|
|
15
|
+
nodeRegistry,
|
|
16
|
+
registerNode,
|
|
17
|
+
resolveTerrainWallConstructionOptions,
|
|
13
18
|
runAsSingleSceneHistoryStep,
|
|
14
19
|
SlabNode,
|
|
15
20
|
spatialGridManager,
|
|
@@ -23,7 +28,6 @@ import useInteractionScope from '../../../store/use-interaction-scope'
|
|
|
23
28
|
import {
|
|
24
29
|
createWallOnCurrentLevel,
|
|
25
30
|
resolveEndpointWallSplit,
|
|
26
|
-
resolveTerrainWallConstructionOptions,
|
|
27
31
|
snapWallDraftPointDetailed,
|
|
28
32
|
} from './wall-drafting'
|
|
29
33
|
import type { WallPlanPoint } from './wall-snap-geometry'
|
|
@@ -139,6 +143,15 @@ function levelWalls(): WallNode[] {
|
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
describe('createWallOnCurrentLevel', () => {
|
|
146
|
+
// Set by tests that mutate the process-wide node registry; restored here
|
|
147
|
+
// so the mutation can't leak into later test files (order-dependent flakes).
|
|
148
|
+
let restoreRegistry: (() => void) | undefined
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
restoreRegistry?.()
|
|
152
|
+
restoreRegistry = undefined
|
|
153
|
+
})
|
|
154
|
+
|
|
142
155
|
beforeEach(() => {
|
|
143
156
|
useViewer.setState({
|
|
144
157
|
selection: {
|
|
@@ -198,6 +211,181 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
198
211
|
expect(support.elevation).toBe(1.75)
|
|
199
212
|
})
|
|
200
213
|
|
|
214
|
+
test('never exposes a raised wall at floor elevation during commit', () => {
|
|
215
|
+
const observed: WallNode[] = []
|
|
216
|
+
const unsubscribe = useScene.subscribe((state) => {
|
|
217
|
+
const wall = Object.values(state.nodes).find(
|
|
218
|
+
(node): node is WallNode => node?.type === 'wall' && node.id !== 'wall_a',
|
|
219
|
+
)
|
|
220
|
+
if (wall) observed.push(wall)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const created = createWallOnCurrentLevel([2, 2], [3, 2], {
|
|
224
|
+
supportCap: 1.75,
|
|
225
|
+
preferredSupportSlabId: GROUND_SUPPORT_ID,
|
|
226
|
+
constructionElevation: 1.75,
|
|
227
|
+
constructionHeight: 2.5,
|
|
228
|
+
flatConstructionBase: true,
|
|
229
|
+
})
|
|
230
|
+
unsubscribe()
|
|
231
|
+
|
|
232
|
+
expect(created).not.toBeNull()
|
|
233
|
+
expect(observed.length).toBeGreaterThan(0)
|
|
234
|
+
expect(observed.every((wall) => wall.supportSlabId === GROUND_SUPPORT_ID)).toBe(true)
|
|
235
|
+
expect(observed.every((wall) => wall.supportOffset === 1.75)).toBe(true)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('keeps a node-top room plane flat across changing terrain', () => {
|
|
239
|
+
const field = createTerrainField({ cols: 5, rows: 5, spacing: 1, origin: [0, 0] })
|
|
240
|
+
const patch = flattenPatch(field, { minX: 3.5, minZ: 0, maxX: 4, maxZ: 4 }, 1)
|
|
241
|
+
if (!patch) throw new Error('Expected terrain patch')
|
|
242
|
+
const terrain = applyHeightPatch(field, patch)
|
|
243
|
+
const site = {
|
|
244
|
+
id: 'site_test',
|
|
245
|
+
type: 'site',
|
|
246
|
+
object: 'node',
|
|
247
|
+
parentId: null,
|
|
248
|
+
visible: true,
|
|
249
|
+
metadata: {},
|
|
250
|
+
children: ['building_test'],
|
|
251
|
+
terrain: encodeTerrainField(terrain),
|
|
252
|
+
} as unknown as AnyNode
|
|
253
|
+
const building = {
|
|
254
|
+
id: 'building_test',
|
|
255
|
+
type: 'building',
|
|
256
|
+
object: 'node',
|
|
257
|
+
parentId: site.id,
|
|
258
|
+
visible: true,
|
|
259
|
+
metadata: {},
|
|
260
|
+
children: [LEVEL_ID],
|
|
261
|
+
position: [0, 0, 0],
|
|
262
|
+
rotation: [0, 0, 0],
|
|
263
|
+
} as AnyNode
|
|
264
|
+
const level = {
|
|
265
|
+
id: LEVEL_ID,
|
|
266
|
+
type: 'level',
|
|
267
|
+
object: 'node',
|
|
268
|
+
parentId: building.id,
|
|
269
|
+
visible: true,
|
|
270
|
+
metadata: {},
|
|
271
|
+
children: [],
|
|
272
|
+
level: 0,
|
|
273
|
+
baseElevation: 0,
|
|
274
|
+
height: 3,
|
|
275
|
+
} as AnyNode
|
|
276
|
+
useScene.setState({
|
|
277
|
+
nodes: Object.fromEntries([site, building, level].map((node) => [node.id, node])),
|
|
278
|
+
rootNodeIds: [site.id],
|
|
279
|
+
dirtyNodes: new Set(),
|
|
280
|
+
} as never)
|
|
281
|
+
|
|
282
|
+
const lowTerrainWall = createWallOnCurrentLevel([0, 0], [1, 0], {
|
|
283
|
+
constructionElevation: 3,
|
|
284
|
+
constructionHeight: 2.5,
|
|
285
|
+
flatConstructionBase: true,
|
|
286
|
+
supportCap: 3,
|
|
287
|
+
})
|
|
288
|
+
const highTerrainWall = createWallOnCurrentLevel([4, 0], [4, 1], {
|
|
289
|
+
constructionElevation: 3,
|
|
290
|
+
constructionHeight: 2.5,
|
|
291
|
+
flatConstructionBase: true,
|
|
292
|
+
supportCap: 3,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
expect(lowTerrainWall?.supportSlabId).toBe(GROUND_SUPPORT_ID)
|
|
296
|
+
expect(highTerrainWall?.supportSlabId).toBe(GROUND_SUPPORT_ID)
|
|
297
|
+
expect(lowTerrainWall?.supportOffset).toBeCloseTo(3)
|
|
298
|
+
expect(highTerrainWall?.supportOffset).toBeCloseTo(2)
|
|
299
|
+
expect(
|
|
300
|
+
spatialGridManager.getSlabSupportForWall(
|
|
301
|
+
LEVEL_ID,
|
|
302
|
+
lowTerrainWall!.start,
|
|
303
|
+
lowTerrainWall!.end,
|
|
304
|
+
lowTerrainWall!.curveOffset,
|
|
305
|
+
lowTerrainWall!.thickness,
|
|
306
|
+
lowTerrainWall!.supportSlabId,
|
|
307
|
+
undefined,
|
|
308
|
+
lowTerrainWall!.supportOffset,
|
|
309
|
+
).elevation,
|
|
310
|
+
).toBeCloseTo(3)
|
|
311
|
+
expect(
|
|
312
|
+
spatialGridManager.getSlabSupportForWall(
|
|
313
|
+
LEVEL_ID,
|
|
314
|
+
highTerrainWall!.start,
|
|
315
|
+
highTerrainWall!.end,
|
|
316
|
+
highTerrainWall!.curveOffset,
|
|
317
|
+
highTerrainWall!.thickness,
|
|
318
|
+
highTerrainWall!.supportSlabId,
|
|
319
|
+
undefined,
|
|
320
|
+
highTerrainWall!.supportOffset,
|
|
321
|
+
).elevation,
|
|
322
|
+
).toBeCloseTo(3)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test('pins an existing construction source before a generated room slab can lift it', () => {
|
|
326
|
+
// The reset + throwaway `block` registration is scoped to this test —
|
|
327
|
+
// the registry is a process-wide singleton, so leaking it would leave
|
|
328
|
+
// later test FILES with a stripped registry (order-dependent flakes).
|
|
329
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
330
|
+
nodeRegistry._reset()
|
|
331
|
+
spatialGridManager.clear()
|
|
332
|
+
registerNode({
|
|
333
|
+
kind: 'block',
|
|
334
|
+
schemaVersion: 2,
|
|
335
|
+
schema: BlockNode,
|
|
336
|
+
category: 'structure',
|
|
337
|
+
defaults: () => BlockNode.parse({ name: 'Block' }),
|
|
338
|
+
capabilities: {
|
|
339
|
+
floorPlaced: {
|
|
340
|
+
footprint: () => ({ dimensions: [4, 2.4, 4], rotation: [0, 0, 0] }),
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
} as never)
|
|
344
|
+
|
|
345
|
+
const slope = BlockNode.parse({
|
|
346
|
+
name: 'Existing slope',
|
|
347
|
+
parentId: LEVEL_ID,
|
|
348
|
+
position: [0, 0, 0],
|
|
349
|
+
})
|
|
350
|
+
seedLevel([makeWall([0, 0], [4, 0], 'wall_a')], [slope as AnyNode])
|
|
351
|
+
|
|
352
|
+
createWallOnCurrentLevel([0, 1], [1, 1], {
|
|
353
|
+
constructionElevation: 2.4,
|
|
354
|
+
constructionHeight: 2.5,
|
|
355
|
+
constructionSourceNodeId: slope.id,
|
|
356
|
+
flatConstructionBase: true,
|
|
357
|
+
supportCap: 2.4,
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const pinnedSlope = useScene.getState().nodes[slope.id] as BlockNode
|
|
361
|
+
expect(pinnedSlope.supportSlabId).toBe(GROUND_SUPPORT_ID)
|
|
362
|
+
|
|
363
|
+
const generatedSlab = SlabNode.parse({
|
|
364
|
+
polygon: [
|
|
365
|
+
[-2, -2],
|
|
366
|
+
[2, -2],
|
|
367
|
+
[2, 2],
|
|
368
|
+
[-2, 2],
|
|
369
|
+
],
|
|
370
|
+
elevation: 2.45,
|
|
371
|
+
autoFromWalls: true,
|
|
372
|
+
parentId: LEVEL_ID,
|
|
373
|
+
})
|
|
374
|
+
spatialGridManager.handleNodeCreated(generatedSlab as AnyNode, LEVEL_ID)
|
|
375
|
+
|
|
376
|
+
expect(
|
|
377
|
+
getFloorPlacedElevation({
|
|
378
|
+
node: pinnedSlope,
|
|
379
|
+
nodes: {
|
|
380
|
+
...useScene.getState().nodes,
|
|
381
|
+
[generatedSlab.id]: generatedSlab as AnyNode,
|
|
382
|
+
},
|
|
383
|
+
position: pinnedSlope.position,
|
|
384
|
+
rotation: [0, pinnedSlope.rotation, 0],
|
|
385
|
+
}),
|
|
386
|
+
).toBe(0)
|
|
387
|
+
})
|
|
388
|
+
|
|
201
389
|
test('a flat-ground draft (no sculpted terrain) commits plane-bound', () => {
|
|
202
390
|
// Pointing at bare ground freezes a GROUND construction plane at 0. With
|
|
203
391
|
// no terrain field in the scene that plane is just the backdrop — none of
|
|
@@ -248,6 +436,136 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
248
436
|
expect(created?.supportSlabId).not.toBe(GROUND_SUPPORT_ID)
|
|
249
437
|
})
|
|
250
438
|
|
|
439
|
+
test('a fresh-scene wall started on a slab node-top elects that slab plane-bound', () => {
|
|
440
|
+
const slab = SlabNode.parse({
|
|
441
|
+
id: 'slab_fresh_floor',
|
|
442
|
+
parentId: LEVEL_ID,
|
|
443
|
+
polygon: [
|
|
444
|
+
[-1, -1],
|
|
445
|
+
[5, -1],
|
|
446
|
+
[5, 5],
|
|
447
|
+
[-1, 5],
|
|
448
|
+
],
|
|
449
|
+
elevation: 0.05,
|
|
450
|
+
thickness: 0.05,
|
|
451
|
+
})
|
|
452
|
+
seedLevel([], [slab])
|
|
453
|
+
spatialGridManager.clear()
|
|
454
|
+
spatialGridManager.handleNodeCreated(slab as AnyNode, LEVEL_ID)
|
|
455
|
+
|
|
456
|
+
const created = createWallOnCurrentLevel([2, 2], [3, 2], {
|
|
457
|
+
supportCap: 0.05,
|
|
458
|
+
preferredSupportSlabId: null,
|
|
459
|
+
constructionElevation: 0.05,
|
|
460
|
+
constructionHeight: 2.5,
|
|
461
|
+
constructionSourceNodeId: slab.id,
|
|
462
|
+
flatConstructionBase: true,
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
expect(created).not.toBeNull()
|
|
466
|
+
expect(created?.supportSlabId).toBeUndefined()
|
|
467
|
+
expect(created?.height).toBeUndefined()
|
|
468
|
+
expect(created?.supportOffset).toBeUndefined()
|
|
469
|
+
const support = spatialGridManager.getSlabSupportForWall(
|
|
470
|
+
LEVEL_ID,
|
|
471
|
+
created!.start,
|
|
472
|
+
created!.end,
|
|
473
|
+
created!.curveOffset,
|
|
474
|
+
created!.thickness,
|
|
475
|
+
created!.supportSlabId,
|
|
476
|
+
)
|
|
477
|
+
expect(support.electedSlabId).toBe(slab.id)
|
|
478
|
+
expect(support.elevation).toBeCloseTo(0.05)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test('a direct slab source does not pin a cross-slab wall away from the higher majority support', () => {
|
|
482
|
+
const sourceSlab = SlabNode.parse({
|
|
483
|
+
id: 'slab_source_low',
|
|
484
|
+
parentId: LEVEL_ID,
|
|
485
|
+
polygon: [
|
|
486
|
+
[-0.1, -1],
|
|
487
|
+
[0.1, -1],
|
|
488
|
+
[0.1, 1],
|
|
489
|
+
[-0.1, 1],
|
|
490
|
+
],
|
|
491
|
+
elevation: 0.1,
|
|
492
|
+
thickness: 0.05,
|
|
493
|
+
})
|
|
494
|
+
const majoritySlab = SlabNode.parse({
|
|
495
|
+
id: 'slab_majority_high',
|
|
496
|
+
parentId: LEVEL_ID,
|
|
497
|
+
polygon: [
|
|
498
|
+
[0.1, -1],
|
|
499
|
+
[4.1, -1],
|
|
500
|
+
[4.1, 1],
|
|
501
|
+
[0.1, 1],
|
|
502
|
+
],
|
|
503
|
+
elevation: 0.6,
|
|
504
|
+
thickness: 0.1,
|
|
505
|
+
})
|
|
506
|
+
seedLevel([], [sourceSlab, majoritySlab])
|
|
507
|
+
spatialGridManager.clear()
|
|
508
|
+
spatialGridManager.handleNodeCreated(sourceSlab as AnyNode, LEVEL_ID)
|
|
509
|
+
spatialGridManager.handleNodeCreated(majoritySlab as AnyNode, LEVEL_ID)
|
|
510
|
+
|
|
511
|
+
const created = createWallOnCurrentLevel([0, 0], [4, 0], {
|
|
512
|
+
supportCap: 0.6,
|
|
513
|
+
preferredSupportSlabId: null,
|
|
514
|
+
constructionElevation: 0.1,
|
|
515
|
+
constructionHeight: 2.5,
|
|
516
|
+
constructionSourceNodeId: sourceSlab.id,
|
|
517
|
+
flatConstructionBase: true,
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
expect(created?.supportSlabId).toBe(majoritySlab.id)
|
|
521
|
+
expect(created?.height).toBeUndefined()
|
|
522
|
+
expect(created?.supportOffset).toBeUndefined()
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
test('a grazing direct slab source does not override the commit elevation cap', () => {
|
|
526
|
+
const sourceSlab = SlabNode.parse({
|
|
527
|
+
id: 'slab_source_high',
|
|
528
|
+
parentId: LEVEL_ID,
|
|
529
|
+
polygon: [
|
|
530
|
+
[-0.1, -1],
|
|
531
|
+
[0.1, -1],
|
|
532
|
+
[0.1, 1],
|
|
533
|
+
[-0.1, 1],
|
|
534
|
+
],
|
|
535
|
+
elevation: 0.6,
|
|
536
|
+
thickness: 0.1,
|
|
537
|
+
})
|
|
538
|
+
const cappedSlab = SlabNode.parse({
|
|
539
|
+
id: 'slab_capped_low',
|
|
540
|
+
parentId: LEVEL_ID,
|
|
541
|
+
polygon: [
|
|
542
|
+
[-0.1, -1],
|
|
543
|
+
[4.1, -1],
|
|
544
|
+
[4.1, 1],
|
|
545
|
+
[-0.1, 1],
|
|
546
|
+
],
|
|
547
|
+
elevation: 0.1,
|
|
548
|
+
thickness: 0.05,
|
|
549
|
+
})
|
|
550
|
+
seedLevel([], [sourceSlab, cappedSlab])
|
|
551
|
+
spatialGridManager.clear()
|
|
552
|
+
spatialGridManager.handleNodeCreated(sourceSlab as AnyNode, LEVEL_ID)
|
|
553
|
+
spatialGridManager.handleNodeCreated(cappedSlab as AnyNode, LEVEL_ID)
|
|
554
|
+
|
|
555
|
+
const created = createWallOnCurrentLevel([0, 0], [4, 0], {
|
|
556
|
+
supportCap: 0.1,
|
|
557
|
+
preferredSupportSlabId: null,
|
|
558
|
+
constructionElevation: 0.6,
|
|
559
|
+
constructionHeight: 2.5,
|
|
560
|
+
constructionSourceNodeId: sourceSlab.id,
|
|
561
|
+
flatConstructionBase: true,
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
expect(created?.supportSlabId).toBe(cappedSlab.id)
|
|
565
|
+
expect(created?.height).toBeUndefined()
|
|
566
|
+
expect(created?.supportOffset).toBeUndefined()
|
|
567
|
+
})
|
|
568
|
+
|
|
251
569
|
test('a non-ground draft never freezes the ghost height, even at a raised plane', () => {
|
|
252
570
|
const created = createWallOnCurrentLevel([2, 2], [3, 2], {
|
|
253
571
|
supportCap: 1.2,
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type AnyNode,
|
|
3
2
|
type AnyNodeId,
|
|
4
3
|
DEFAULT_ANGLE_STEP,
|
|
5
|
-
DEFAULT_LEVEL_HEIGHT,
|
|
6
|
-
GROUND_SUPPORT_ID,
|
|
7
4
|
planWallInsertion,
|
|
8
5
|
planWallSplitAtPoint,
|
|
9
|
-
|
|
6
|
+
resolveWallConstruction,
|
|
10
7
|
runAsSingleSceneHistoryStep,
|
|
11
8
|
snapPointAlongAngleRay,
|
|
12
|
-
spatialGridManager,
|
|
13
|
-
terrainSupportLift,
|
|
14
9
|
useScene,
|
|
10
|
+
type WallConstructionOptions,
|
|
15
11
|
type WallNode,
|
|
16
12
|
} from '@pascal-app/core'
|
|
17
13
|
import { useViewer } from '@pascal-app/viewer'
|
|
@@ -209,41 +205,6 @@ export function isSegmentLongEnough(start: WallPlanPoint, end: WallPlanPoint): b
|
|
|
209
205
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
|
210
206
|
}
|
|
211
207
|
|
|
212
|
-
export type WallConstructionOptions = {
|
|
213
|
-
/** Pointer-decided maximum support elevation in level-local metres. */
|
|
214
|
-
supportCap?: number | null
|
|
215
|
-
/** Support source selected by the first click or inherited from a snapped wall. */
|
|
216
|
-
preferredSupportSlabId?: string | null
|
|
217
|
-
/** Frozen level-local Y shown by the draft ghost. */
|
|
218
|
-
constructionElevation?: number | null
|
|
219
|
-
/** Height shown by the draft ghost. */
|
|
220
|
-
constructionHeight?: number | null
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function resolveTerrainWallConstructionOptions(
|
|
224
|
-
nodes: Record<string, AnyNode>,
|
|
225
|
-
levelId: string,
|
|
226
|
-
point: WallPlanPoint,
|
|
227
|
-
defaults?: Record<string, unknown>,
|
|
228
|
-
): WallConstructionOptions | undefined {
|
|
229
|
-
const constructionElevation = terrainSupportLift(nodes, levelId, point[0], point[1])
|
|
230
|
-
if (constructionElevation == null) return undefined
|
|
231
|
-
|
|
232
|
-
const level = nodes[levelId]
|
|
233
|
-
const constructionHeight =
|
|
234
|
-
typeof defaults?.height === 'number'
|
|
235
|
-
? defaults.height
|
|
236
|
-
: level?.type === 'level'
|
|
237
|
-
? (level.height ?? DEFAULT_LEVEL_HEIGHT)
|
|
238
|
-
: DEFAULT_LEVEL_HEIGHT
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
constructionElevation,
|
|
242
|
-
constructionHeight,
|
|
243
|
-
supportCap: constructionElevation,
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
208
|
export function createWallOnCurrentLevel(
|
|
248
209
|
start: WallPlanPoint,
|
|
249
210
|
end: WallPlanPoint,
|
|
@@ -269,82 +230,30 @@ export function createWallOnCurrentLevel(
|
|
|
269
230
|
if (!result.ok) return null
|
|
270
231
|
const { plan } = result
|
|
271
232
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const preferredSupportSlabId =
|
|
293
|
-
wallOptions?.preferredSupportSlabId ??
|
|
294
|
-
(wallOptions?.constructionElevation != null && terrainBase != null
|
|
295
|
-
? GROUND_SUPPORT_ID
|
|
296
|
-
: null)
|
|
297
|
-
const supportPatch = resolveWallSupportSlabPatch(createdWall, committedNodes, {
|
|
298
|
-
maxElevation: wallOptions?.supportCap ?? null,
|
|
299
|
-
preferredSlabId: preferredSupportSlabId,
|
|
300
|
-
})
|
|
301
|
-
const supportSlabId = supportPatch.supportSlabId
|
|
302
|
-
const sourceSupport = spatialGridManager.getSlabSupportForWall(
|
|
303
|
-
currentLevelId,
|
|
304
|
-
createdWall.start,
|
|
305
|
-
createdWall.end,
|
|
306
|
-
createdWall.curveOffset,
|
|
307
|
-
createdWall.thickness,
|
|
308
|
-
supportSlabId,
|
|
309
|
-
wallOptions?.supportCap ?? null,
|
|
310
|
-
)
|
|
311
|
-
// Freezing the draft plane into the node (explicit height + offset from
|
|
312
|
-
// the elected support) is the terrain exception only: a ground-drafted
|
|
313
|
-
// chain keeps one construction plane and a fixed body height while
|
|
314
|
-
// sculpting moves the ground. Every other wall stays plane-bound —
|
|
315
|
-
// height absent, top at the storey plane, base re-elected from slab
|
|
316
|
-
// support — so a wall merely started on a slab or deck must never have
|
|
317
|
-
// the ghost height stamped onto it.
|
|
318
|
-
const groundDraft = preferredSupportSlabId === GROUND_SUPPORT_ID
|
|
319
|
-
const supportOffset =
|
|
320
|
-
groundDraft && wallOptions?.constructionElevation != null
|
|
321
|
-
? wallOptions.constructionElevation - sourceSupport.elevation
|
|
322
|
-
: undefined
|
|
323
|
-
const preserveDraftHeight =
|
|
324
|
-
groundDraft &&
|
|
325
|
-
createdWall.height == null &&
|
|
326
|
-
wallOptions?.constructionHeight != null &&
|
|
327
|
-
wallOptions.constructionElevation != null
|
|
328
|
-
return [
|
|
329
|
-
{
|
|
330
|
-
id: createdWall.id,
|
|
331
|
-
data: {
|
|
332
|
-
...supportPatch,
|
|
333
|
-
height: preserveDraftHeight
|
|
334
|
-
? (wallOptions?.constructionHeight ?? createdWall.height)
|
|
335
|
-
: createdWall.height,
|
|
336
|
-
supportOffset:
|
|
337
|
-
supportOffset != null && Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
]
|
|
233
|
+
const construction = resolveWallConstruction(nodes, currentLevelId, plan.insertedWalls, options)
|
|
234
|
+
const finalizedWalls = construction.walls
|
|
235
|
+
const finalizedWallsById = new Map(finalizedWalls.map((wall) => [wall.id, wall]))
|
|
236
|
+
const sourceUpdate = construction.sourceSupportUpdate
|
|
237
|
+
const sourceAlreadyUpdated = sourceUpdate
|
|
238
|
+
? plan.changes.update.some((operation) => operation.id === sourceUpdate.id)
|
|
239
|
+
: false
|
|
240
|
+
applyNodeChanges({
|
|
241
|
+
...plan.changes,
|
|
242
|
+
update: plan.changes.update
|
|
243
|
+
.map((operation) =>
|
|
244
|
+
sourceUpdate?.id === operation.id
|
|
245
|
+
? { ...operation, data: { ...operation.data, ...sourceUpdate.data } }
|
|
246
|
+
: operation,
|
|
247
|
+
)
|
|
248
|
+
.concat(sourceUpdate && !sourceAlreadyUpdated ? [sourceUpdate] : []),
|
|
249
|
+
create: plan.changes.create.map((operation) => ({
|
|
250
|
+
...operation,
|
|
251
|
+
node: finalizedWallsById.get(operation.node.id as WallNode['id']) ?? operation.node,
|
|
252
|
+
})),
|
|
341
253
|
})
|
|
342
|
-
if (supportUpdates.length > 0) {
|
|
343
|
-
useScene.getState().updateNodes(supportUpdates)
|
|
344
|
-
}
|
|
345
254
|
sfxEmitter.emit('sfx:structure-build')
|
|
346
255
|
|
|
347
|
-
const terminalWall =
|
|
256
|
+
const terminalWall = finalizedWalls.at(-1)!
|
|
348
257
|
const committedWall = useScene.getState().nodes[plan.terminalWallId]
|
|
349
258
|
return committedWall?.type === 'wall' ? committedWall : terminalWall
|
|
350
259
|
})
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
emitter,
|
|
4
4
|
type GridEvent,
|
|
5
5
|
type LevelNode,
|
|
6
|
+
runAsSingleSceneHistoryStep,
|
|
6
7
|
snapPointAlongAngleRay,
|
|
7
8
|
useScene,
|
|
8
9
|
ZoneNode,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
clearSurfacePlanSnapFeedback,
|
|
17
18
|
resolveSurfacePlanPointSnap,
|
|
18
19
|
} from './../../../lib/surface-plan-snap'
|
|
20
|
+
import { focusedUnitNode } from './../../../lib/units'
|
|
19
21
|
import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
|
|
20
22
|
import useEditor, { isAngleSnapActive, isGridSnapActive } from './../../../store/use-editor'
|
|
21
23
|
import { useFloorplanDraftPreview } from './../../../store/use-floorplan-draft-preview'
|
|
@@ -27,7 +29,7 @@ const Y_OFFSET = 0.02
|
|
|
27
29
|
* Creates a zone with the given polygon points
|
|
28
30
|
*/
|
|
29
31
|
const commitZoneDrawing = (levelId: LevelNode['id'], points: Array<[number, number]>) => {
|
|
30
|
-
const { createNode, nodes } = useScene.getState()
|
|
32
|
+
const { createNode, updateNode, nodes } = useScene.getState()
|
|
31
33
|
|
|
32
34
|
// Count existing zones for naming and color cycling
|
|
33
35
|
const zoneCount = Object.values(nodes).filter((n) => n.type === 'zone').length
|
|
@@ -42,10 +44,17 @@ const commitZoneDrawing = (levelId: LevelNode['id'], points: Array<[number, numb
|
|
|
42
44
|
color,
|
|
43
45
|
})
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
const focusedUnit = focusedUnitNode()
|
|
46
48
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
+
// Joining the focused unit rides in the zone's own undo step.
|
|
50
|
+
runAsSingleSceneHistoryStep(useScene, () => {
|
|
51
|
+
createNode(zone, levelId)
|
|
52
|
+
if (focusedUnit) updateNode(focusedUnit.id, { members: [...focusedUnit.members, zone.id] })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Selecting the zone would end unit focus; while painting a unit the new
|
|
56
|
+
// zone just joins it and drawing continues.
|
|
57
|
+
if (!focusedUnit) useViewer.getState().setSelection({ zoneId: zone.id })
|
|
49
58
|
|
|
50
59
|
// Play structure build sound
|
|
51
60
|
sfxEmitter.emit('sfx:structure-build')
|
|
@@ -54,7 +54,7 @@ export function ControlModes() {
|
|
|
54
54
|
const mode = useEditor((state) => state.mode)
|
|
55
55
|
const phase = useEditor((state) => state.phase)
|
|
56
56
|
const selectionTool = useEditor((state) => state.floorplanSelectionTool)
|
|
57
|
-
const
|
|
57
|
+
const armToolMode = useEditor((state) => state.armToolMode)
|
|
58
58
|
const setPhase = useEditor((state) => state.setPhase)
|
|
59
59
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
60
60
|
const setSelectionTool = useEditor((state) => state.setFloorplanSelectionTool)
|
|
@@ -82,21 +82,21 @@ export function ControlModes() {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (id === 'select') {
|
|
85
|
-
|
|
85
|
+
armToolMode({ mode: 'select' })
|
|
86
86
|
setSelectionTool('click')
|
|
87
87
|
} else if (id === 'box-select') {
|
|
88
|
-
|
|
88
|
+
armToolMode({ mode: 'select' })
|
|
89
89
|
setSelectionTool('marquee')
|
|
90
90
|
} else if (id === 'zone') {
|
|
91
91
|
if (getIsActive('zone')) {
|
|
92
|
-
|
|
92
|
+
armToolMode({ mode: 'select' })
|
|
93
93
|
} else {
|
|
94
94
|
setPhase('structure')
|
|
95
95
|
setStructureLayer('zones')
|
|
96
|
-
|
|
96
|
+
armToolMode({ mode: 'build', tool: 'zone' })
|
|
97
97
|
}
|
|
98
98
|
} else {
|
|
99
|
-
|
|
99
|
+
armToolMode({ mode: id })
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -118,6 +118,9 @@ export function ControlModes() {
|
|
|
118
118
|
isImageMode && isActive && 'bg-white/10 hover:bg-white/10',
|
|
119
119
|
isImageMode && !isActive && 'hover:bg-white/5',
|
|
120
120
|
)}
|
|
121
|
+
// A static hook for a host app that wants to point a first-run
|
|
122
|
+
// tour at this button. Nothing here reads it.
|
|
123
|
+
data-guide-target={c.id === 'select' ? 'mode-select' : undefined}
|
|
121
124
|
label={c.label}
|
|
122
125
|
onClick={() => handleClick(c.id)}
|
|
123
126
|
shortcut={c.shortcut}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useScene } from '@pascal-app/core'
|
|
3
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
5
|
import { motion } from 'motion/react'
|
|
5
6
|
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
|
6
7
|
import { useIsMobile } from './../../../hooks/use-mobile'
|
|
7
8
|
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
|
9
|
+
import { shouldShowEditingControls } from './../../../lib/interaction/overlay-policy'
|
|
8
10
|
import { cn } from './../../../lib/utils'
|
|
9
11
|
import useEditor from './../../../store/use-editor'
|
|
10
12
|
import { CameraActions } from './camera-actions'
|
|
@@ -18,6 +20,7 @@ const MOBILE_BOTTOM_OFFSET = 24
|
|
|
18
20
|
|
|
19
21
|
export function ActionMenu({ className }: { className?: string }) {
|
|
20
22
|
const isMobile = useIsMobile()
|
|
23
|
+
const readOnly = useScene((s) => s.readOnly)
|
|
21
24
|
const hasSelectionOnMobile = useViewer((s) => isMobile && s.selection.selectedIds.length > 0)
|
|
22
25
|
const hasReferenceOnMobile = useEditor((s) => isMobile && Boolean(s.selectedReferenceId))
|
|
23
26
|
const CONTEXTUAL_TABS = new Set(['ai', 'items', 'studio'])
|
|
@@ -31,7 +34,14 @@ export function ActionMenu({ className }: { className?: string }) {
|
|
|
31
34
|
// Also hide on Chat / Items / Studio tabs; those are contextual workflows
|
|
32
35
|
// (composing / picking furniture / generating renders) where the build
|
|
33
36
|
// menu is irrelevant.
|
|
34
|
-
if (
|
|
37
|
+
if (
|
|
38
|
+
!shouldShowEditingControls(readOnly) ||
|
|
39
|
+
hasSelectionOnMobile ||
|
|
40
|
+
hasReferenceOnMobile ||
|
|
41
|
+
isContextualPanelOnMobile
|
|
42
|
+
) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
35
45
|
|
|
36
46
|
const transition = reducedMotion
|
|
37
47
|
? { duration: 0 }
|