@pascal-app/editor 1.0.0-beta.4 → 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 +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- 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 +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -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 +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- 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 +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- 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 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- 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 +204 -114
- 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 +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- 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 +447 -180
- 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 +63 -8
- 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-brush-cursor.tsx +2 -1
- 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 +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- 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 +12 -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/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 +23 -6
- 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 +78 -26
- 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/panel-wrapper.tsx +228 -12
- 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/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -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 +128 -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 +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- 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 +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- 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 +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -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/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -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.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -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 +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -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/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- 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 +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -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/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- 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/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type AnyNode,
|
|
3
2
|
type AnyNodeId,
|
|
4
3
|
DEFAULT_ANGLE_STEP,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
getScaledDimensions,
|
|
9
|
-
type ItemNode,
|
|
10
|
-
resolveWallSupportSlabPatch,
|
|
4
|
+
planWallInsertion,
|
|
5
|
+
planWallSplitAtPoint,
|
|
6
|
+
resolveWallConstruction,
|
|
11
7
|
runAsSingleSceneHistoryStep,
|
|
12
8
|
snapPointAlongAngleRay,
|
|
13
|
-
spatialGridManager,
|
|
14
|
-
terrainSupportLift,
|
|
15
9
|
useScene,
|
|
10
|
+
type WallConstructionOptions,
|
|
16
11
|
type WallNode,
|
|
17
|
-
WallNode as WallSchema,
|
|
18
|
-
type WindowNode,
|
|
19
12
|
} from '@pascal-app/core'
|
|
20
13
|
import { useViewer } from '@pascal-app/viewer'
|
|
21
14
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
@@ -25,7 +18,6 @@ import {
|
|
|
25
18
|
distanceSquared,
|
|
26
19
|
findWallSnapTarget,
|
|
27
20
|
findWallSpecialPointSnap,
|
|
28
|
-
projectPointOntoWall,
|
|
29
21
|
WALL_CONNECT_SNAP_RADIUS,
|
|
30
22
|
WALL_JOIN_SNAP_RADIUS,
|
|
31
23
|
type WallDraftSnapResult,
|
|
@@ -49,17 +41,6 @@ export {
|
|
|
49
41
|
|
|
50
42
|
export const WALL_GRID_STEP = 0.5
|
|
51
43
|
export const WALL_MIN_LENGTH = 0.01
|
|
52
|
-
// An endpoint projecting within this distance of an existing wall's corner
|
|
53
|
-
// resolves to the corner without splitting — splitting there would mint a
|
|
54
|
-
// sliver segment a hair longer than `WALL_MIN_LENGTH` that no snap radius
|
|
55
|
-
// can ever target again.
|
|
56
|
-
const WALL_SPLIT_ENDPOINT_EPSILON = 0.02
|
|
57
|
-
|
|
58
|
-
type WallSplitIntersection = {
|
|
59
|
-
/** `null` = snap-only outcome: resolve to `point` but split no wall. */
|
|
60
|
-
wallId: WallNode['id'] | null
|
|
61
|
-
point: WallPlanPoint
|
|
62
|
-
}
|
|
63
44
|
|
|
64
45
|
export function getSegmentGridStep(): number {
|
|
65
46
|
// A 0 step means "no grid lattice" — every grid-snap consumer guards on
|
|
@@ -78,283 +59,6 @@ export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): Wa
|
|
|
78
59
|
return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
|
|
79
60
|
}
|
|
80
61
|
|
|
81
|
-
function splitWallAtPoint(
|
|
82
|
-
wall: WallNode,
|
|
83
|
-
splitPoint: WallPlanPoint,
|
|
84
|
-
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
|
85
|
-
): [WallNode, WallNode] {
|
|
86
|
-
const { id: _id, parentId: _parentId, children, ...rest } = wall
|
|
87
|
-
|
|
88
|
-
const first = WallSchema.parse({
|
|
89
|
-
...rest,
|
|
90
|
-
start: wall.start,
|
|
91
|
-
end: splitPoint,
|
|
92
|
-
children: [],
|
|
93
|
-
})
|
|
94
|
-
const second = WallSchema.parse({
|
|
95
|
-
...rest,
|
|
96
|
-
start: splitPoint,
|
|
97
|
-
end: wall.end,
|
|
98
|
-
children: [],
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
if (wall.supportSlabId !== GROUND_SUPPORT_ID || !wall.parentId) {
|
|
102
|
-
return [first, second]
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const levelId = wall.parentId
|
|
106
|
-
const originalElevation =
|
|
107
|
-
(terrainSupportLift(nodes, levelId, wall.start[0], wall.start[1]) ?? 0) +
|
|
108
|
-
(wall.supportOffset ?? 0)
|
|
109
|
-
const rebase = (segment: WallNode): WallNode => {
|
|
110
|
-
const terrainElevation =
|
|
111
|
-
terrainSupportLift(nodes, levelId, segment.start[0], segment.start[1]) ?? 0
|
|
112
|
-
const supportOffset = originalElevation - terrainElevation
|
|
113
|
-
return {
|
|
114
|
-
...segment,
|
|
115
|
-
supportOffset: Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return [rebase(first), rebase(second)]
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function pointsEqual(a: WallPlanPoint, b: WallPlanPoint, tolerance = 1e-6): boolean {
|
|
122
|
-
return distanceSquared(a, b) <= tolerance * tolerance
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function findWallIntersection(
|
|
126
|
-
point: WallPlanPoint,
|
|
127
|
-
walls: WallNode[],
|
|
128
|
-
radius: number,
|
|
129
|
-
ignoreWallIds?: string[],
|
|
130
|
-
): WallSplitIntersection | null {
|
|
131
|
-
const ignore = new Set(ignoreWallIds ?? [])
|
|
132
|
-
let best: WallSplitIntersection | null = null
|
|
133
|
-
let bestDistanceSquared = Number.POSITIVE_INFINITY
|
|
134
|
-
|
|
135
|
-
for (const wall of walls) {
|
|
136
|
-
if (ignore.has(wall.id)) continue
|
|
137
|
-
|
|
138
|
-
const projected = projectPointOntoWall(point, wall)
|
|
139
|
-
if (!projected) continue
|
|
140
|
-
|
|
141
|
-
const candidateDistanceSquared = distanceSquared(point, projected)
|
|
142
|
-
if (
|
|
143
|
-
candidateDistanceSquared > radius * radius ||
|
|
144
|
-
candidateDistanceSquared >= bestDistanceSquared
|
|
145
|
-
) {
|
|
146
|
-
continue
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const nearCorner = ([wall.start, wall.end] as WallPlanPoint[]).find(
|
|
150
|
-
(corner) =>
|
|
151
|
-
distanceSquared(projected, corner) <=
|
|
152
|
-
WALL_SPLIT_ENDPOINT_EPSILON * WALL_SPLIT_ENDPOINT_EPSILON,
|
|
153
|
-
)
|
|
154
|
-
best = nearCorner
|
|
155
|
-
? { wallId: null, point: [nearCorner[0], nearCorner[1]] }
|
|
156
|
-
: { wallId: wall.id, point: projected }
|
|
157
|
-
bestDistanceSquared = candidateDistanceSquared
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return best
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function wallHasAttachments(wall: WallNode, nodes: ReturnType<typeof useScene.getState>['nodes']) {
|
|
164
|
-
if ((wall.children?.length ?? 0) > 0) {
|
|
165
|
-
return true
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return Object.values(nodes).some((node) => {
|
|
169
|
-
if (!node) return false
|
|
170
|
-
if ('parentId' in node && node.parentId === wall.id) return true
|
|
171
|
-
if ('wallId' in node && typeof node.wallId === 'string' && node.wallId === wall.id) return true
|
|
172
|
-
return false
|
|
173
|
-
})
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function wallLength(wall: Pick<WallNode, 'start' | 'end'>) {
|
|
177
|
-
return Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1])
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function getWallAttachmentSpan(node: AnyNode): { min: number; max: number; center: number } | null {
|
|
181
|
-
if (node.type === 'door') {
|
|
182
|
-
const door = node as DoorNode
|
|
183
|
-
return {
|
|
184
|
-
min: door.position[0] - door.width / 2,
|
|
185
|
-
max: door.position[0] + door.width / 2,
|
|
186
|
-
center: door.position[0],
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (node.type === 'window') {
|
|
191
|
-
const win = node as WindowNode
|
|
192
|
-
return {
|
|
193
|
-
min: win.position[0] - win.width / 2,
|
|
194
|
-
max: win.position[0] + win.width / 2,
|
|
195
|
-
center: win.position[0],
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (node.type === 'item') {
|
|
200
|
-
const item = node as ItemNode
|
|
201
|
-
if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') {
|
|
202
|
-
return null
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const [width] = getScaledDimensions(item)
|
|
206
|
-
return {
|
|
207
|
-
min: item.position[0] - width / 2,
|
|
208
|
-
max: item.position[0] + width / 2,
|
|
209
|
-
center: item.position[0],
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return null
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function remapAttachmentToWall(
|
|
217
|
-
node: AnyNode,
|
|
218
|
-
nextWallId: WallNode['id'],
|
|
219
|
-
nextLocalX: number,
|
|
220
|
-
nextWallLength: number,
|
|
221
|
-
): Partial<AnyNode> | null {
|
|
222
|
-
const clampedX = Math.max(0, Math.min(nextWallLength, nextLocalX))
|
|
223
|
-
|
|
224
|
-
if (node.type === 'door' || node.type === 'window' || node.type === 'item') {
|
|
225
|
-
const currentPosition = 'position' in node ? node.position : null
|
|
226
|
-
if (!currentPosition) return null
|
|
227
|
-
|
|
228
|
-
const nextPosition: typeof currentPosition = [
|
|
229
|
-
clampedX,
|
|
230
|
-
currentPosition[1],
|
|
231
|
-
currentPosition[2],
|
|
232
|
-
] as typeof currentPosition
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
parentId: nextWallId,
|
|
236
|
-
position: nextPosition,
|
|
237
|
-
...(node.type === 'item'
|
|
238
|
-
? {
|
|
239
|
-
wallId: nextWallId,
|
|
240
|
-
wallT: nextWallLength > 1e-6 ? clampedX / nextWallLength : 0,
|
|
241
|
-
}
|
|
242
|
-
: {
|
|
243
|
-
wallId: nextWallId,
|
|
244
|
-
}),
|
|
245
|
-
} as Partial<AnyNode>
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return null
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function buildAttachmentMigrationPlan(
|
|
252
|
-
wall: WallNode,
|
|
253
|
-
splitPoint: WallPlanPoint,
|
|
254
|
-
firstWall: WallNode,
|
|
255
|
-
secondWall: WallNode,
|
|
256
|
-
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
|
257
|
-
): { id: AnyNodeId; data: Partial<AnyNode> }[] | null {
|
|
258
|
-
const splitDistance = Math.hypot(splitPoint[0] - wall.start[0], splitPoint[1] - wall.start[1])
|
|
259
|
-
const firstLength = wallLength(firstWall)
|
|
260
|
-
const secondLength = wallLength(secondWall)
|
|
261
|
-
const tolerance = 1e-4
|
|
262
|
-
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = []
|
|
263
|
-
|
|
264
|
-
for (const childId of wall.children ?? []) {
|
|
265
|
-
const childNode = nodes[childId as AnyNodeId]
|
|
266
|
-
if (!childNode) continue
|
|
267
|
-
|
|
268
|
-
const span = getWallAttachmentSpan(childNode)
|
|
269
|
-
if (!span) {
|
|
270
|
-
return null
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (span.max <= splitDistance + tolerance) {
|
|
274
|
-
const nextUpdate = remapAttachmentToWall(childNode, firstWall.id, span.center, firstLength)
|
|
275
|
-
if (!nextUpdate) return null
|
|
276
|
-
updates.push({ id: childNode.id as AnyNodeId, data: nextUpdate })
|
|
277
|
-
continue
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (span.min >= splitDistance - tolerance) {
|
|
281
|
-
const nextUpdate = remapAttachmentToWall(
|
|
282
|
-
childNode,
|
|
283
|
-
secondWall.id,
|
|
284
|
-
span.center - splitDistance,
|
|
285
|
-
secondLength,
|
|
286
|
-
)
|
|
287
|
-
if (!nextUpdate) return null
|
|
288
|
-
updates.push({ id: childNode.id as AnyNodeId, data: nextUpdate })
|
|
289
|
-
continue
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return null
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return updates
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function splitWallIfNeeded(
|
|
299
|
-
intersection: WallSplitIntersection | null,
|
|
300
|
-
walls: WallNode[],
|
|
301
|
-
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
|
302
|
-
createNodes: ReturnType<typeof useScene.getState>['createNodes'],
|
|
303
|
-
updateNodes: ReturnType<typeof useScene.getState>['updateNodes'],
|
|
304
|
-
deleteNode: ReturnType<typeof useScene.getState>['deleteNode'],
|
|
305
|
-
): { walls: WallNode[]; point: WallPlanPoint } | null {
|
|
306
|
-
if (!intersection) return null
|
|
307
|
-
|
|
308
|
-
if (!intersection.wallId) {
|
|
309
|
-
return { walls, point: intersection.point }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const wallToSplit = walls.find((wall) => wall.id === intersection.wallId)
|
|
313
|
-
if (!wallToSplit) {
|
|
314
|
-
return { walls, point: intersection.point }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const [first, second] = splitWallAtPoint(wallToSplit, intersection.point, nodes)
|
|
318
|
-
const attachmentUpdates = buildAttachmentMigrationPlan(
|
|
319
|
-
wallToSplit,
|
|
320
|
-
intersection.point,
|
|
321
|
-
first,
|
|
322
|
-
second,
|
|
323
|
-
nodes,
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
if (wallHasAttachments(wallToSplit, nodes) && !attachmentUpdates) {
|
|
327
|
-
return { walls, point: intersection.point }
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
createNodes([
|
|
331
|
-
{ node: first, parentId: wallToSplit.parentId as AnyNodeId | undefined },
|
|
332
|
-
{ node: second, parentId: wallToSplit.parentId as AnyNodeId | undefined },
|
|
333
|
-
])
|
|
334
|
-
if (attachmentUpdates && attachmentUpdates.length > 0) {
|
|
335
|
-
updateNodes(attachmentUpdates)
|
|
336
|
-
}
|
|
337
|
-
deleteNode(wallToSplit.id as AnyNodeId)
|
|
338
|
-
|
|
339
|
-
return {
|
|
340
|
-
walls: [...walls.filter((wall) => wall.id !== wallToSplit.id), first, second],
|
|
341
|
-
point: intersection.point,
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Commit-time split resolution for an endpoint MOVE — the sibling of the
|
|
347
|
-
* inline resolution in `createWallOnCurrentLevel`: when a moved endpoint is
|
|
348
|
-
* dropped on another wall's interior, split that host exactly like the draw
|
|
349
|
-
* path (duplicate props, migrate attachments by span, skip the split when an
|
|
350
|
-
* opening straddles the point). Mutates the scene store (create halves /
|
|
351
|
-
* migrate attachments / delete host), so callers MUST run it inside the same
|
|
352
|
-
* `runAsSingleSceneHistoryStep` as their endpoint write.
|
|
353
|
-
*
|
|
354
|
-
* Returns the resolved endpoint (projection onto the host, or a nearby corner
|
|
355
|
-
* when the drop is within `WALL_SPLIT_ENDPOINT_EPSILON` of one — corner joins
|
|
356
|
-
* are not splits), or `null` when the point lands on no wall.
|
|
357
|
-
*/
|
|
358
62
|
export function resolveEndpointWallSplit(args: {
|
|
359
63
|
point: WallPlanPoint
|
|
360
64
|
/** Level the moved wall lives on — only its walls are split candidates. */
|
|
@@ -369,14 +73,23 @@ export function resolveEndpointWallSplit(args: {
|
|
|
369
73
|
radius?: number
|
|
370
74
|
}): WallPlanPoint | null {
|
|
371
75
|
const { point, levelId, ignoreWallIds, radius = WALL_CONNECT_SNAP_RADIUS } = args
|
|
372
|
-
const { nodes,
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
76
|
+
const { nodes, applyNodeChanges } = useScene.getState()
|
|
77
|
+
const result = planWallSplitAtPoint(nodes, {
|
|
78
|
+
point,
|
|
79
|
+
levelId: levelId as AnyNodeId | null,
|
|
80
|
+
ignoreWallIds,
|
|
81
|
+
radius,
|
|
82
|
+
})
|
|
83
|
+
if (!result.ok) return null
|
|
84
|
+
const { plan } = result
|
|
85
|
+
if (
|
|
86
|
+
plan.changes.create.length > 0 ||
|
|
87
|
+
plan.changes.update.length > 0 ||
|
|
88
|
+
plan.changes.delete.length > 0
|
|
89
|
+
) {
|
|
90
|
+
applyNodeChanges(plan.changes)
|
|
91
|
+
}
|
|
92
|
+
return plan.point
|
|
380
93
|
}
|
|
381
94
|
|
|
382
95
|
type SnapWallDraftArgs = {
|
|
@@ -492,176 +205,56 @@ export function isSegmentLongEnough(start: WallPlanPoint, end: WallPlanPoint): b
|
|
|
492
205
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
|
493
206
|
}
|
|
494
207
|
|
|
495
|
-
export type WallConstructionOptions = {
|
|
496
|
-
/** Pointer-decided maximum support elevation in level-local metres. */
|
|
497
|
-
supportCap?: number | null
|
|
498
|
-
/** Support source selected by the first click or inherited from a snapped wall. */
|
|
499
|
-
preferredSupportSlabId?: string | null
|
|
500
|
-
/** Frozen level-local Y shown by the draft ghost. */
|
|
501
|
-
constructionElevation?: number | null
|
|
502
|
-
/** Height shown by the draft ghost. */
|
|
503
|
-
constructionHeight?: number | null
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
export function resolveTerrainWallConstructionOptions(
|
|
507
|
-
nodes: Record<string, AnyNode>,
|
|
508
|
-
levelId: string,
|
|
509
|
-
point: WallPlanPoint,
|
|
510
|
-
defaults?: Record<string, unknown>,
|
|
511
|
-
): WallConstructionOptions | undefined {
|
|
512
|
-
const constructionElevation = terrainSupportLift(nodes, levelId, point[0], point[1])
|
|
513
|
-
if (constructionElevation == null) return undefined
|
|
514
|
-
|
|
515
|
-
const level = nodes[levelId]
|
|
516
|
-
const constructionHeight =
|
|
517
|
-
typeof defaults?.height === 'number'
|
|
518
|
-
? defaults.height
|
|
519
|
-
: level?.type === 'level'
|
|
520
|
-
? (level.height ?? DEFAULT_LEVEL_HEIGHT)
|
|
521
|
-
: DEFAULT_LEVEL_HEIGHT
|
|
522
|
-
|
|
523
|
-
return {
|
|
524
|
-
constructionElevation,
|
|
525
|
-
constructionHeight,
|
|
526
|
-
supportCap: constructionElevation,
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
208
|
export function createWallOnCurrentLevel(
|
|
531
209
|
start: WallPlanPoint,
|
|
532
210
|
end: WallPlanPoint,
|
|
533
211
|
options?: WallConstructionOptions,
|
|
534
212
|
): WallNode | null {
|
|
535
213
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
536
|
-
const {
|
|
537
|
-
const { updateNodes } = useScene.getState()
|
|
214
|
+
const { nodes, applyNodeChanges } = useScene.getState()
|
|
538
215
|
|
|
539
216
|
if (!(currentLevelId && isSegmentLongEnough(start, end))) {
|
|
540
217
|
return null
|
|
541
218
|
}
|
|
542
219
|
|
|
543
|
-
let workingWalls = Object.values(nodes).filter(
|
|
544
|
-
(node): node is WallNode => node?.type === 'wall' && node.parentId === currentLevelId,
|
|
545
|
-
)
|
|
546
|
-
|
|
547
|
-
let resolvedStart = start
|
|
548
|
-
let resolvedEnd = end
|
|
549
|
-
|
|
550
|
-
// The corner-join / wall-split resolution follows the snapping mode like the
|
|
551
|
-
// draft preview does: magnetic ('lines') keeps the generous join radius,
|
|
552
|
-
// every other mode uses the same tight connect radius the draft path already
|
|
553
|
-
// sticks endpoints with. So an endpoint the user saw connect to a wall body
|
|
554
|
-
// actually splits that wall (and redistributes its attachments) in every
|
|
555
|
-
// mode, while `'off'` / `'angles'` gain no residual long-range snap.
|
|
556
220
|
const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
|
|
557
221
|
|
|
558
|
-
// One undo step for the whole commit: the split ops (create halves, migrate
|
|
559
|
-
// attachments, delete host) plus the new wall each push their own history
|
|
560
|
-
// entry, and a single Ctrl-Z must not strand a half-split wall network.
|
|
561
222
|
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
pointsEqual(resolvedStart, resolvedEnd)
|
|
593
|
-
) {
|
|
594
|
-
return null
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const duplicateWall = workingWalls.some(
|
|
598
|
-
(wall) =>
|
|
599
|
-
(pointsEqual(wall.start, resolvedStart) && pointsEqual(wall.end, resolvedEnd)) ||
|
|
600
|
-
(pointsEqual(wall.start, resolvedEnd) && pointsEqual(wall.end, resolvedStart)),
|
|
601
|
-
)
|
|
602
|
-
if (duplicateWall) {
|
|
603
|
-
return null
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length
|
|
607
|
-
// A placed wall preset seeds `toolDefaults.wall` (thickness, height,
|
|
608
|
-
// materials, sides) before the tool activates; merge those first so the
|
|
609
|
-
// drawn wall reproduces the preset. Identity + endpoints always win.
|
|
610
|
-
const defaults = useEditor.getState().toolDefaults.wall ?? {}
|
|
611
|
-
const wall = WallSchema.parse({
|
|
612
|
-
...defaults,
|
|
613
|
-
name: `Wall ${wallCount + 1}`,
|
|
614
|
-
start: resolvedStart,
|
|
615
|
-
end: resolvedEnd,
|
|
223
|
+
const result = planWallInsertion(nodes, {
|
|
224
|
+
levelId: currentLevelId as AnyNodeId,
|
|
225
|
+
start,
|
|
226
|
+
end,
|
|
227
|
+
joinRadius,
|
|
228
|
+
wallDefaults: useEditor.getState().toolDefaults.wall ?? {},
|
|
229
|
+
})
|
|
230
|
+
if (!result.ok) return null
|
|
231
|
+
const { plan } = result
|
|
232
|
+
|
|
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
|
+
})),
|
|
616
253
|
})
|
|
617
|
-
|
|
618
|
-
createNode(wall, currentLevelId)
|
|
619
|
-
const createdWall = useScene.getState().nodes[wall.id]
|
|
620
|
-
if (createdWall?.type === 'wall') {
|
|
621
|
-
const terrainBase = terrainSupportLift(
|
|
622
|
-
useScene.getState().nodes,
|
|
623
|
-
currentLevelId,
|
|
624
|
-
createdWall.start[0],
|
|
625
|
-
createdWall.start[1],
|
|
626
|
-
)
|
|
627
|
-
const preferredSupportSlabId =
|
|
628
|
-
options?.preferredSupportSlabId ??
|
|
629
|
-
(options?.constructionElevation != null && terrainBase != null ? GROUND_SUPPORT_ID : null)
|
|
630
|
-
const supportPatch = resolveWallSupportSlabPatch(createdWall, useScene.getState().nodes, {
|
|
631
|
-
maxElevation: options?.supportCap ?? null,
|
|
632
|
-
preferredSlabId: preferredSupportSlabId,
|
|
633
|
-
})
|
|
634
|
-
const supportSlabId = supportPatch.supportSlabId
|
|
635
|
-
const sourceSupport = spatialGridManager.getSlabSupportForWall(
|
|
636
|
-
currentLevelId,
|
|
637
|
-
createdWall.start,
|
|
638
|
-
createdWall.end,
|
|
639
|
-
createdWall.curveOffset,
|
|
640
|
-
createdWall.thickness,
|
|
641
|
-
supportSlabId,
|
|
642
|
-
options?.supportCap ?? null,
|
|
643
|
-
)
|
|
644
|
-
const supportOffset =
|
|
645
|
-
options?.constructionElevation == null
|
|
646
|
-
? undefined
|
|
647
|
-
: options.constructionElevation - sourceSupport.elevation
|
|
648
|
-
const preserveDraftHeight =
|
|
649
|
-
createdWall.height == null &&
|
|
650
|
-
options?.constructionHeight != null &&
|
|
651
|
-
options.constructionElevation != null &&
|
|
652
|
-
(terrainBase != null || Math.abs(options.constructionElevation) > 1e-6)
|
|
653
|
-
useScene.getState().updateNode(createdWall.id, {
|
|
654
|
-
...supportPatch,
|
|
655
|
-
height: preserveDraftHeight
|
|
656
|
-
? (options?.constructionHeight ?? createdWall.height)
|
|
657
|
-
: createdWall.height,
|
|
658
|
-
supportOffset:
|
|
659
|
-
supportOffset != null && Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
|
|
660
|
-
})
|
|
661
|
-
}
|
|
662
254
|
sfxEmitter.emit('sfx:structure-build')
|
|
663
255
|
|
|
664
|
-
const
|
|
665
|
-
|
|
256
|
+
const terminalWall = finalizedWalls.at(-1)!
|
|
257
|
+
const committedWall = useScene.getState().nodes[plan.terminalWallId]
|
|
258
|
+
return committedWall?.type === 'wall' ? committedWall : terminalWall
|
|
666
259
|
})
|
|
667
260
|
}
|
|
@@ -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 }
|