@pascal-app/editor 1.0.0-beta.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- 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 +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- 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/linear-resize-drag.ts +67 -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 +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -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 +284 -237
- 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-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 +180 -32
- 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/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/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 +135 -2
- 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 +6 -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 +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- 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 +45 -8
- 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/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 +9 -12
- 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/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- 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 +22 -5
- 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 +263 -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 +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- 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 +390 -36
- 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 +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- 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/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- 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 +122 -0
- 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 +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -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/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 +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- 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 +1 -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/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- 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-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/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 +366 -0
- package/src/lib/portable-export.ts +969 -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 +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -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 +30 -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.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -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/usdz-export.ts +38 -0
- 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-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/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
|
})
|
|
@@ -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 }
|
|
@@ -49,10 +49,9 @@ export function EditorCommands() {
|
|
|
49
49
|
const { navigateTo, setInputValue, setOpen } = useCommandPalette()
|
|
50
50
|
|
|
51
51
|
const setPhase = useEditor((s) => s.setPhase)
|
|
52
|
-
const
|
|
53
|
-
const
|
|
52
|
+
const armToolMode = useEditor((s) => s.armToolMode)
|
|
53
|
+
const armMaterialPaint = useEditor((s) => s.armMaterialPaint)
|
|
54
54
|
const setStructureLayer = useEditor((s) => s.setStructureLayer)
|
|
55
|
-
const primeMaterialPaintFromSelection = useEditor((s) => s.primeMaterialPaintFromSelection)
|
|
56
55
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
57
56
|
const setPreviewMode = useEditor((s) => s.setPreviewMode)
|
|
58
57
|
|
|
@@ -68,9 +67,9 @@ export function EditorCommands() {
|
|
|
68
67
|
const activateTool = (tool: StructureTool) => {
|
|
69
68
|
run(() => {
|
|
70
69
|
setPhase('structure')
|
|
71
|
-
setMode('build')
|
|
72
70
|
if (tool === 'zone') setStructureLayer('zones')
|
|
73
|
-
|
|
71
|
+
else setStructureLayer('elements')
|
|
72
|
+
armToolMode({ mode: 'build', tool })
|
|
74
73
|
})
|
|
75
74
|
}
|
|
76
75
|
|
|
@@ -163,10 +162,9 @@ export function EditorCommands() {
|
|
|
163
162
|
shortcut: ['P'],
|
|
164
163
|
execute: () =>
|
|
165
164
|
run(() => {
|
|
166
|
-
primeMaterialPaintFromSelection()
|
|
167
165
|
setPhase('structure')
|
|
168
166
|
setStructureLayer('elements')
|
|
169
|
-
|
|
167
|
+
armMaterialPaint()
|
|
170
168
|
}),
|
|
171
169
|
},
|
|
172
170
|
{
|
|
@@ -176,9 +174,8 @@ export function EditorCommands() {
|
|
|
176
174
|
icon: <Mountain className="h-4 w-4" />,
|
|
177
175
|
keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'],
|
|
178
176
|
shortcut: ['G'],
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
execute: () => run(() => setMode('terrain-sculpt')),
|
|
177
|
+
// The ToolMode transition moves to the site phase itself.
|
|
178
|
+
execute: () => run(() => armToolMode({ mode: 'terrain-sculpt' })),
|
|
182
179
|
},
|
|
183
180
|
|
|
184
181
|
// ── Levels ───────────────────────────────────────────────────────────
|
|
@@ -428,8 +425,8 @@ export function EditorCommands() {
|
|
|
428
425
|
setInputValue,
|
|
429
426
|
setOpen,
|
|
430
427
|
setPhase,
|
|
431
|
-
|
|
432
|
-
|
|
428
|
+
armToolMode,
|
|
429
|
+
armMaterialPaint,
|
|
433
430
|
setStructureLayer,
|
|
434
431
|
isPreviewMode,
|
|
435
432
|
setPreviewMode,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AnyNodeId, LevelNode } from '@pascal-app/core'
|
|
4
4
|
import { useScene } from '@pascal-app/core'
|
|
5
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
6
6
|
import { Command, useCommandState } from 'cmdk'
|
|
7
7
|
import { ChevronRight, Search } from 'lucide-react'
|
|
8
8
|
import type { ReactNode } from 'react'
|
|
@@ -408,7 +408,12 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
408
408
|
key={level.id}
|
|
409
409
|
label={getLevelDisplayName(level)}
|
|
410
410
|
onSelect={() =>
|
|
411
|
-
run(() =>
|
|
411
|
+
run(() => {
|
|
412
|
+
if (level.id !== useViewer.getState().selection.levelId) {
|
|
413
|
+
markPerfAction('level-switch', level.id)
|
|
414
|
+
}
|
|
415
|
+
useViewer.getState().setSelection({ levelId: level.id })
|
|
416
|
+
})
|
|
412
417
|
}
|
|
413
418
|
/>
|
|
414
419
|
))}
|