@pascal-app/editor 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -3,12 +3,21 @@ import { SCENE_LAYER } from '@pascal-app/viewer'
|
|
|
3
3
|
import { useGLTF } from '@react-three/drei/core/Gltf'
|
|
4
4
|
import { useFrame } from '@react-three/fiber'
|
|
5
5
|
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Color,
|
|
8
|
+
CylinderGeometry,
|
|
9
|
+
DoubleSide,
|
|
10
|
+
type Group,
|
|
11
|
+
type Mesh,
|
|
12
|
+
type Object3D,
|
|
13
|
+
RingGeometry,
|
|
14
|
+
} from 'three'
|
|
7
15
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
8
16
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
9
17
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
10
18
|
import { SITE_BOUNDARY_DRAG_LABEL } from '../../../lib/site-boundary'
|
|
11
19
|
import useEditor, { selectSiteFloorplanContext } from '../../../store/use-editor'
|
|
20
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
12
21
|
import {
|
|
13
22
|
ARROW_COLOR,
|
|
14
23
|
ARROW_HOVER_COLOR,
|
|
@@ -38,6 +47,7 @@ const SITE_FLAG_HALO_COLOR = '#6366f1'
|
|
|
38
47
|
|
|
39
48
|
type TintableMaterial = {
|
|
40
49
|
color?: Color
|
|
50
|
+
depthTest: boolean
|
|
41
51
|
depthWrite: boolean
|
|
42
52
|
opacity: number
|
|
43
53
|
needsUpdate: boolean
|
|
@@ -69,10 +79,9 @@ function SiteFlagModel({
|
|
|
69
79
|
mesh.frustumCulled = false
|
|
70
80
|
mesh.raycast = NO_RAYCAST
|
|
71
81
|
mesh.receiveShadow = false
|
|
72
|
-
mesh.renderOrder = 1010
|
|
73
82
|
mesh.material = new MeshBasicNodeMaterial({
|
|
74
83
|
color: new Color(ARROW_COLOR),
|
|
75
|
-
depthTest:
|
|
84
|
+
depthTest: true,
|
|
76
85
|
depthWrite: opacity >= 0.999,
|
|
77
86
|
opacity,
|
|
78
87
|
transparent: opacity < 0.999,
|
|
@@ -93,6 +102,7 @@ function SiteFlagModel({
|
|
|
93
102
|
|
|
94
103
|
for (const material of materials as Array<MeshBasicNodeMaterial & TintableMaterial>) {
|
|
95
104
|
material.color?.copy(color)
|
|
105
|
+
material.depthTest = true
|
|
96
106
|
material.opacity = opacity
|
|
97
107
|
material.transparent = opacity < 0.999
|
|
98
108
|
material.depthWrite = opacity >= 0.999
|
|
@@ -217,11 +227,23 @@ function SiteFlagFallback({
|
|
|
217
227
|
<group position={[0, SITE_FLAG_BASE_Y + (active ? SITE_FLAG_ACTIVE_LIFT : 0), 0]}>
|
|
218
228
|
<mesh layers={SCENE_LAYER} position={[0, 0.16, 0]} raycast={NO_RAYCAST}>
|
|
219
229
|
<cylinderGeometry args={[0.11, 0.16, 0.32, 24]} />
|
|
220
|
-
<meshBasicMaterial
|
|
230
|
+
<meshBasicMaterial
|
|
231
|
+
color={color}
|
|
232
|
+
depthTest
|
|
233
|
+
depthWrite={opacity >= 0.999}
|
|
234
|
+
opacity={opacity}
|
|
235
|
+
transparent={opacity < 0.999}
|
|
236
|
+
/>
|
|
221
237
|
</mesh>
|
|
222
238
|
<mesh layers={SCENE_LAYER} position={[0, 0.34, 0]} raycast={NO_RAYCAST}>
|
|
223
239
|
<cylinderGeometry args={[0.04, 0.11, 0.14, 24]} />
|
|
224
|
-
<meshBasicMaterial
|
|
240
|
+
<meshBasicMaterial
|
|
241
|
+
color={color}
|
|
242
|
+
depthTest
|
|
243
|
+
depthWrite={opacity >= 0.999}
|
|
244
|
+
opacity={opacity}
|
|
245
|
+
transparent={opacity < 0.999}
|
|
246
|
+
/>
|
|
225
247
|
</mesh>
|
|
226
248
|
</group>
|
|
227
249
|
)
|
|
@@ -355,14 +377,20 @@ export const SiteBoundaryEditor: React.FC = () => {
|
|
|
355
377
|
|
|
356
378
|
if (!siteId) return
|
|
357
379
|
|
|
358
|
-
const editor = useEditor.getState()
|
|
359
380
|
if (isDragging) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
381
|
+
useInteractionScope
|
|
382
|
+
.getState()
|
|
383
|
+
.begin({ kind: 'handle-drag', nodeId: siteId, handle: SITE_BOUNDARY_DRAG_LABEL })
|
|
384
|
+
} else {
|
|
385
|
+
const scope = useInteractionScope.getState().scope
|
|
386
|
+
const activeHandleDrag =
|
|
387
|
+
scope.kind === 'handle-drag' ? { nodeId: scope.nodeId, label: scope.handle } : null
|
|
388
|
+
if (
|
|
389
|
+
activeHandleDrag?.nodeId === siteId &&
|
|
390
|
+
activeHandleDrag.label === SITE_BOUNDARY_DRAG_LABEL
|
|
391
|
+
) {
|
|
392
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
|
|
393
|
+
}
|
|
366
394
|
}
|
|
367
395
|
|
|
368
396
|
if (!isDragging) {
|
|
@@ -375,12 +403,14 @@ export const SiteBoundaryEditor: React.FC = () => {
|
|
|
375
403
|
useEffect(
|
|
376
404
|
() => () => {
|
|
377
405
|
if (!siteId) return
|
|
378
|
-
const
|
|
406
|
+
const scope = useInteractionScope.getState().scope
|
|
407
|
+
const activeHandleDrag =
|
|
408
|
+
scope.kind === 'handle-drag' ? { nodeId: scope.nodeId, label: scope.handle } : null
|
|
379
409
|
if (
|
|
380
|
-
|
|
381
|
-
|
|
410
|
+
activeHandleDrag?.nodeId === siteId &&
|
|
411
|
+
activeHandleDrag.label === SITE_BOUNDARY_DRAG_LABEL
|
|
382
412
|
) {
|
|
383
|
-
|
|
413
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
|
|
384
414
|
}
|
|
385
415
|
useLiveNodeOverrides.getState().clearFields(siteId, ['polygon'])
|
|
386
416
|
isDraggingSiteBoundaryRef.current = false
|
|
@@ -394,6 +424,26 @@ export const SiteBoundaryEditor: React.FC = () => {
|
|
|
394
424
|
}
|
|
395
425
|
}, [isSiteEditing, siteId])
|
|
396
426
|
|
|
427
|
+
// The flag models render on SCENE_LAYER (scene-depth occlusion), so unlike
|
|
428
|
+
// EDITOR_LAYER affordances the thumbnail camera can't filter them — hide
|
|
429
|
+
// them around captures (preset/snapshot/auto-save thumbnails), same as
|
|
430
|
+
// `handle-arrow.tsx`.
|
|
431
|
+
const handlesRootRef = useRef<Group>(null)
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
const hideForCapture = () => {
|
|
434
|
+
if (handlesRootRef.current) handlesRootRef.current.visible = false
|
|
435
|
+
}
|
|
436
|
+
const restoreAfterCapture = () => {
|
|
437
|
+
if (handlesRootRef.current) handlesRootRef.current.visible = true
|
|
438
|
+
}
|
|
439
|
+
emitter.on('thumbnail:before-capture', hideForCapture)
|
|
440
|
+
emitter.on('thumbnail:after-capture', restoreAfterCapture)
|
|
441
|
+
return () => {
|
|
442
|
+
emitter.off('thumbnail:before-capture', hideForCapture)
|
|
443
|
+
emitter.off('thumbnail:after-capture', restoreAfterCapture)
|
|
444
|
+
}
|
|
445
|
+
}, [])
|
|
446
|
+
|
|
397
447
|
const activateSiteEditing = useCallback(() => {
|
|
398
448
|
isDraggingSiteBoundaryRef.current = true
|
|
399
449
|
setIsDraggingSiteBoundary(true)
|
|
@@ -470,25 +520,27 @@ export const SiteBoundaryEditor: React.FC = () => {
|
|
|
470
520
|
if (!showSiteHandles) return null
|
|
471
521
|
|
|
472
522
|
return (
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
523
|
+
<group ref={handlesRootRef}>
|
|
524
|
+
<PolygonEditor
|
|
525
|
+
color={isSiteBoundaryHighlighted ? ARROW_COLOR : '#10b981'}
|
|
526
|
+
minVertices={3}
|
|
527
|
+
onBeforeVertexDrag={activateSiteEditing}
|
|
528
|
+
onDragCommit={() => {
|
|
529
|
+
sfxEmitter.emit('sfx:item-place')
|
|
530
|
+
exitSiteEditing()
|
|
531
|
+
}}
|
|
532
|
+
onDragStart={() => sfxEmitter.emit('sfx:item-pick')}
|
|
533
|
+
onDragStateChange={handleSiteBoundaryDragChange}
|
|
534
|
+
onMidpointHoverChange={setHoveredMidpoint}
|
|
535
|
+
onPolygonChange={handlePolygonChange}
|
|
536
|
+
onPolygonPreview={handlePolygonPreview}
|
|
537
|
+
onVertexHoverChange={setHoveredVertex}
|
|
538
|
+
polygon={displayPolygon}
|
|
539
|
+
renderMidpointHandle={renderSiteFlagMidpoint}
|
|
540
|
+
renderVertexHandle={renderSiteFlagVertex}
|
|
541
|
+
showBorderLine={isSiteBoundaryHighlighted}
|
|
542
|
+
showMidpointHandles={showSiteHandles}
|
|
543
|
+
/>
|
|
544
|
+
</group>
|
|
493
545
|
)
|
|
494
546
|
}
|
|
@@ -6,6 +6,7 @@ export const DEFAULT_STAIR_STEP_COUNT = 10
|
|
|
6
6
|
export const DEFAULT_STAIR_ATTACHMENT_SIDE = 'front' as const
|
|
7
7
|
export const DEFAULT_STAIR_FILL_TO_FLOOR = true
|
|
8
8
|
export const DEFAULT_STAIR_THICKNESS = 0.25
|
|
9
|
+
export const DEFAULT_STAIR_OPENING_OFFSET = 0
|
|
9
10
|
export const DEFAULT_CURVED_STAIR_INNER_RADIUS = 0.9
|
|
10
11
|
export const DEFAULT_CURVED_STAIR_SWEEP_ANGLE = Math.PI / 2
|
|
11
12
|
export const DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE = (400 * Math.PI) / 180
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
syncAutoStairOpenings,
|
|
15
15
|
useScene,
|
|
16
16
|
} from '@pascal-app/core'
|
|
17
|
-
import { useAlignmentGuides } from '@pascal-app/editor'
|
|
18
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
19
18
|
import { useEffect, useMemo, useRef } from 'react'
|
|
20
19
|
import * as THREE from 'three'
|
|
@@ -23,6 +22,15 @@ import {
|
|
|
23
22
|
resolveStairDestinationLevel,
|
|
24
23
|
resolveStairPlacementLevelId,
|
|
25
24
|
} from '../../../lib/stair-levels'
|
|
25
|
+
|
|
26
|
+
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
27
|
+
import useEditor, {
|
|
28
|
+
isAlignmentGuideActive,
|
|
29
|
+
isGridSnapActive,
|
|
30
|
+
isMagneticSnapActive,
|
|
31
|
+
} from '../../../store/use-editor'
|
|
32
|
+
|
|
33
|
+
import useFacingPose from '../../../store/use-facing-pose'
|
|
26
34
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
27
35
|
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
28
36
|
import {
|
|
@@ -36,6 +44,7 @@ import {
|
|
|
36
44
|
DEFAULT_STAIR_FILL_TO_FLOOR,
|
|
37
45
|
DEFAULT_STAIR_HEIGHT,
|
|
38
46
|
DEFAULT_STAIR_LENGTH,
|
|
47
|
+
DEFAULT_STAIR_OPENING_OFFSET,
|
|
39
48
|
DEFAULT_STAIR_RAILING_HEIGHT,
|
|
40
49
|
DEFAULT_STAIR_RAILING_MODE,
|
|
41
50
|
DEFAULT_STAIR_STEP_COUNT,
|
|
@@ -138,7 +147,7 @@ function createDefaultStairNode({
|
|
|
138
147
|
fromLevelId: levelId,
|
|
139
148
|
toLevelId: nextLevelId,
|
|
140
149
|
slabOpeningMode: 'destination',
|
|
141
|
-
openingOffset:
|
|
150
|
+
openingOffset: DEFAULT_STAIR_OPENING_OFFSET,
|
|
142
151
|
width: DEFAULT_STAIR_WIDTH,
|
|
143
152
|
totalRise: DEFAULT_STAIR_HEIGHT,
|
|
144
153
|
stepCount: DEFAULT_STAIR_STEP_COUNT,
|
|
@@ -263,7 +272,19 @@ export const StairTool: React.FC = () => {
|
|
|
263
272
|
return { placementLevelId, previewNodes, stair }
|
|
264
273
|
}
|
|
265
274
|
|
|
275
|
+
// The preview rebuild (full-scene copy + destination-level resolution +
|
|
276
|
+
// auto-opening CSG) is expensive; `grid:move` fires it every pointer event
|
|
277
|
+
// but the placed position is grid-snapped, so within a cell every rebuild
|
|
278
|
+
// is identical. Dedupe on the snapped position + rotation so we rebuild
|
|
279
|
+
// only when the staircase would actually land somewhere new — this is the
|
|
280
|
+
// difference between a smooth and a stuttering stair tool (the elevator is
|
|
281
|
+
// cheap because it has no opening sync).
|
|
282
|
+
let lastPreviewKey: string | null = null
|
|
283
|
+
|
|
266
284
|
const applyDraftPreview = (position: [number, number, number], rotation: number) => {
|
|
285
|
+
const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)}`
|
|
286
|
+
if (key === lastPreviewKey) return
|
|
287
|
+
lastPreviewKey = key
|
|
267
288
|
const preview = buildPreviewScene(position, rotation)
|
|
268
289
|
const visualPosition = preview
|
|
269
290
|
? getFloorStackPreviewPosition({
|
|
@@ -287,6 +308,19 @@ export const StairTool: React.FC = () => {
|
|
|
287
308
|
previewRef.current.rotation.y = rotation
|
|
288
309
|
}
|
|
289
310
|
|
|
311
|
+
// Forward-facing triangle (editor-side overlay). The run ascends along
|
|
312
|
+
// local +Z from the entry at z≈0; the stair's front is the -Z entry side,
|
|
313
|
+
// so `reversed` points the triangle out of the entry (where you approach
|
|
314
|
+
// from), sitting just before it — not inside the footprint or at the
|
|
315
|
+
// elevated far end. Centre is the footprint mid-run (origin is the entry).
|
|
316
|
+
useFacingPose.getState().set({
|
|
317
|
+
position: visualPosition,
|
|
318
|
+
rotationY: rotation,
|
|
319
|
+
depth: DEFAULT_STAIR_LENGTH,
|
|
320
|
+
center: [0, DEFAULT_STAIR_LENGTH / 2],
|
|
321
|
+
reversed: true,
|
|
322
|
+
})
|
|
323
|
+
|
|
290
324
|
if (!preview) {
|
|
291
325
|
openingPreview.clear()
|
|
292
326
|
return
|
|
@@ -319,13 +353,16 @@ export const StairTool: React.FC = () => {
|
|
|
319
353
|
// The probe is the RAW cursor, not the grid-snapped point: resolving
|
|
320
354
|
// against the grid point would only catch anchors that happen to sit near
|
|
321
355
|
// a grid line. Matched axes use the raw probe + snap delta; unmatched axes
|
|
322
|
-
// keep the normal grid snap.
|
|
356
|
+
// keep the normal grid snap. Guides are published in every snapping mode
|
|
357
|
+
// (including Off); the magnetic pull toward them (applySnap) applies only in
|
|
358
|
+
// 'lines' mode.
|
|
323
359
|
const alignPoint = (
|
|
324
360
|
gridX: number,
|
|
325
361
|
gridZ: number,
|
|
326
362
|
rawX: number,
|
|
327
363
|
rawZ: number,
|
|
328
364
|
bypass: boolean,
|
|
365
|
+
applySnap: boolean,
|
|
329
366
|
): [number, number] => {
|
|
330
367
|
if (bypass || alignmentCandidates.length === 0) {
|
|
331
368
|
useAlignmentGuides.getState().clear()
|
|
@@ -336,6 +373,10 @@ export const StairTool: React.FC = () => {
|
|
|
336
373
|
useAlignmentGuides.getState().clear()
|
|
337
374
|
return [gridX, gridZ]
|
|
338
375
|
}
|
|
376
|
+
if (!applySnap) {
|
|
377
|
+
useAlignmentGuides.getState().set(ar.guides)
|
|
378
|
+
return [gridX, gridZ]
|
|
379
|
+
}
|
|
339
380
|
let x = gridX
|
|
340
381
|
let z = gridZ
|
|
341
382
|
if (ar.snap) {
|
|
@@ -348,18 +389,27 @@ export const StairTool: React.FC = () => {
|
|
|
348
389
|
}
|
|
349
390
|
|
|
350
391
|
const onGridMove = (event: GridEvent) => {
|
|
392
|
+
// Grid snap follows the global mode (live step so the HUD chip is
|
|
393
|
+
// honest); Off keeps the raw cursor. Shift cycles the mode centrally.
|
|
394
|
+
const step = useEditor.getState().gridSnapStep
|
|
351
395
|
const [gridX, gridZ] = alignPoint(
|
|
352
|
-
|
|
353
|
-
|
|
396
|
+
isGridSnapActive()
|
|
397
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
398
|
+
: event.localPosition[0],
|
|
399
|
+
isGridSnapActive()
|
|
400
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
401
|
+
: event.localPosition[2],
|
|
354
402
|
event.localPosition[0],
|
|
355
403
|
event.localPosition[2],
|
|
356
|
-
|
|
404
|
+
!isAlignmentGuideActive(),
|
|
405
|
+
isMagneticSnapActive(),
|
|
357
406
|
)
|
|
358
407
|
const position: [number, number, number] = [gridX, 0, gridZ]
|
|
359
408
|
lastCanonicalPositionRef.current = position
|
|
360
409
|
applyDraftPreview(position, rotationRef.current)
|
|
361
410
|
|
|
362
411
|
if (
|
|
412
|
+
(isGridSnapActive() || isMagneticSnapActive()) &&
|
|
363
413
|
previousGridPosRef.current &&
|
|
364
414
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
365
415
|
) {
|
|
@@ -370,12 +420,18 @@ export const StairTool: React.FC = () => {
|
|
|
370
420
|
}
|
|
371
421
|
|
|
372
422
|
const getAlignedGridPosition = (event: GridEvent): [number, number, number] => {
|
|
423
|
+
const step = useEditor.getState().gridSnapStep
|
|
373
424
|
const [gridX, gridZ] = alignPoint(
|
|
374
|
-
|
|
375
|
-
|
|
425
|
+
isGridSnapActive()
|
|
426
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
427
|
+
: event.localPosition[0],
|
|
428
|
+
isGridSnapActive()
|
|
429
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
430
|
+
: event.localPosition[2],
|
|
376
431
|
event.localPosition[0],
|
|
377
432
|
event.localPosition[2],
|
|
378
|
-
|
|
433
|
+
!isAlignmentGuideActive(),
|
|
434
|
+
isMagneticSnapActive(),
|
|
379
435
|
)
|
|
380
436
|
return [gridX, 0, gridZ]
|
|
381
437
|
}
|
|
@@ -395,8 +451,20 @@ export const StairTool: React.FC = () => {
|
|
|
395
451
|
|
|
396
452
|
commitStairPlacement(currentLevelId, position, rotationRef.current)
|
|
397
453
|
openingPreview.clear()
|
|
398
|
-
|
|
454
|
+
// Commit cleared the opening preview, so force the next hover (even on the
|
|
455
|
+
// same cell) to rebuild rather than dedupe against the just-placed key.
|
|
456
|
+
lastPreviewKey = null
|
|
399
457
|
useAlignmentGuides.getState().clear()
|
|
458
|
+
|
|
459
|
+
// Single by default; the C-toggle ('point' context, shared with every
|
|
460
|
+
// other placement tool) opts into placing more. On single, drop the tool
|
|
461
|
+
// and the facing triangle so we fall back to select after one stair.
|
|
462
|
+
if (useEditor.getState().getContinuation('point') === 'repeat') {
|
|
463
|
+
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
464
|
+
} else {
|
|
465
|
+
useFacingPose.getState().clear()
|
|
466
|
+
useEditor.getState().setTool(null)
|
|
467
|
+
}
|
|
400
468
|
}
|
|
401
469
|
|
|
402
470
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
@@ -441,6 +509,7 @@ export const StairTool: React.FC = () => {
|
|
|
441
509
|
window.removeEventListener('keydown', onKeyDown)
|
|
442
510
|
useAlignmentGuides.getState().clear()
|
|
443
511
|
openingPreview.clear()
|
|
512
|
+
useFacingPose.getState().clear()
|
|
444
513
|
}
|
|
445
514
|
}, [currentLevelId])
|
|
446
515
|
|
|
@@ -448,7 +517,9 @@ export const StairTool: React.FC = () => {
|
|
|
448
517
|
<group>
|
|
449
518
|
<CursorSphere ref={cursorRef} />
|
|
450
519
|
|
|
451
|
-
{/* 3D ghost preview — position/rotation updated imperatively
|
|
520
|
+
{/* 3D ghost preview — position/rotation updated imperatively. The
|
|
521
|
+
forward-facing triangle is drawn by the editor-side overlay from the
|
|
522
|
+
pose published in `applyDraftPreview`. */}
|
|
452
523
|
<group ref={previewRef}>
|
|
453
524
|
<mesh castShadow geometry={previewGeometry}>
|
|
454
525
|
<meshStandardMaterial color="#818cf8" depthWrite={false} opacity={0.35} transparent />
|
|
@@ -2,19 +2,32 @@ import {
|
|
|
2
2
|
type AnyNodeId,
|
|
3
3
|
type BuildingNode,
|
|
4
4
|
type CeilingNode,
|
|
5
|
+
type FenceNode,
|
|
5
6
|
nodeRegistry,
|
|
6
7
|
type SlabNode,
|
|
7
8
|
useScene,
|
|
9
|
+
type WallNode,
|
|
8
10
|
} from '@pascal-app/core'
|
|
9
11
|
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
-
import { type ComponentType, lazy, Suspense } from 'react'
|
|
12
|
+
import { type ComponentType, lazy, Suspense, useMemo } from 'react'
|
|
11
13
|
import useEditor, { type Phase, type Tool } from '../../store/use-editor'
|
|
14
|
+
import {
|
|
15
|
+
useControlPointReshape,
|
|
16
|
+
useEditingHole,
|
|
17
|
+
useEndpointReshape,
|
|
18
|
+
useIsCurveReshape,
|
|
19
|
+
useMovingNode,
|
|
20
|
+
useReshapingNode,
|
|
21
|
+
useTangentReshape,
|
|
22
|
+
} from '../../store/use-interaction-scope'
|
|
12
23
|
import { Alignment3DGuideLayer } from '../editor/alignment-3d-guide-layer'
|
|
24
|
+
import { OpeningGuides3DLayer } from '../editor/opening-guides-3d-layer'
|
|
13
25
|
import { WallSnapBeaconLayer } from '../editor/wall-snap-beacon-layer'
|
|
14
26
|
import { ElevatorTool } from './elevator/elevator-tool'
|
|
15
27
|
import { MoveTool } from './item/move-tool'
|
|
16
28
|
import { RoofTool } from './roof/roof-tool'
|
|
17
29
|
import { getRegistryAffordanceTool } from './shared/affordance-dispatch'
|
|
30
|
+
import { FacingPoseIndicator } from './shared/facing-pose-indicator'
|
|
18
31
|
import { SiteBoundaryEditor } from './site/site-boundary-editor'
|
|
19
32
|
import { StairTool } from './stair/stair-tool'
|
|
20
33
|
import { ZoneBoundaryEditor } from './zone/zone-boundary-editor'
|
|
@@ -54,12 +67,37 @@ export const ToolManager: React.FC = () => {
|
|
|
54
67
|
const phase = useEditor((state) => state.phase)
|
|
55
68
|
const mode = useEditor((state) => state.mode)
|
|
56
69
|
const tool = useEditor((state) => state.tool)
|
|
57
|
-
const movingNode =
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
70
|
+
const movingNode = useMovingNode()
|
|
71
|
+
const movingNodeOrigin = useEditor((state) => state.movingNodeOrigin)
|
|
72
|
+
const endpointReshape = useEndpointReshape()
|
|
73
|
+
const controlPointReshape = useControlPointReshape()
|
|
74
|
+
const tangentReshape = useTangentReshape()
|
|
75
|
+
const isCurveReshape = useIsCurveReshape()
|
|
76
|
+
const reshapingNode = useReshapingNode()
|
|
77
|
+
// The endpoint affordance tool's `target` is kind-specific
|
|
78
|
+
// (`{ wall | fence, endpoint }`); rebuild it from the (frozen) reshaped node +
|
|
79
|
+
// the scope's endpoint. Memoised so it stays referentially stable across the
|
|
80
|
+
// scene-write re-renders during the drag — otherwise a fresh object each frame
|
|
81
|
+
// re-fires the tool's setup effect (endpoint drag would loop / freeze).
|
|
82
|
+
const endpointTarget = useMemo(() => {
|
|
83
|
+
if (!(endpointReshape && reshapingNode)) return null
|
|
84
|
+
return reshapingNode.type === 'fence'
|
|
85
|
+
? { fence: reshapingNode as FenceNode, endpoint: endpointReshape.endpoint }
|
|
86
|
+
: { wall: reshapingNode as WallNode, endpoint: endpointReshape.endpoint }
|
|
87
|
+
}, [endpointReshape, reshapingNode])
|
|
88
|
+
const controlPointTarget = useMemo(() => {
|
|
89
|
+
if (!(controlPointReshape && reshapingNode?.type === 'fence')) return null
|
|
90
|
+
return { fence: reshapingNode as FenceNode, index: controlPointReshape.index }
|
|
91
|
+
}, [controlPointReshape, reshapingNode])
|
|
92
|
+
const tangentTarget = useMemo(() => {
|
|
93
|
+
if (!(tangentReshape && reshapingNode?.type === 'fence')) return null
|
|
94
|
+
return {
|
|
95
|
+
fence: reshapingNode as FenceNode,
|
|
96
|
+
index: tangentReshape.index,
|
|
97
|
+
side: tangentReshape.side,
|
|
98
|
+
}
|
|
99
|
+
}, [reshapingNode, tangentReshape])
|
|
100
|
+
const editingHole = useEditingHole()
|
|
63
101
|
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
64
102
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
65
103
|
const buildingId = useViewer((state) => state.selection.buildingId)
|
|
@@ -94,10 +132,16 @@ export const ToolManager: React.FC = () => {
|
|
|
94
132
|
// switches to full polygon editing only after a flag activates site mode.
|
|
95
133
|
const showSiteBoundaryEditor = phase === 'site' || mode === 'select'
|
|
96
134
|
|
|
135
|
+
// A multi-selection is manipulated as one rigid group (drag / R / T), so
|
|
136
|
+
// per-node reshape chrome — the slab / ceiling boundary editors' vertex and
|
|
137
|
+
// edge handles — mounts only for a sole selection.
|
|
138
|
+
const isSoleSelection = selectedIds.length === 1
|
|
139
|
+
|
|
97
140
|
// Show slab boundary editor when in structure/select mode with a slab selected (but not editing a hole)
|
|
98
141
|
const showSlabBoundaryEditor =
|
|
99
142
|
phase === 'structure' &&
|
|
100
143
|
mode === 'select' &&
|
|
144
|
+
isSoleSelection &&
|
|
101
145
|
selectedSlabId !== undefined &&
|
|
102
146
|
!editingSlabHoleIsManual
|
|
103
147
|
|
|
@@ -112,6 +156,7 @@ export const ToolManager: React.FC = () => {
|
|
|
112
156
|
const showCeilingBoundaryEditor =
|
|
113
157
|
phase === 'structure' &&
|
|
114
158
|
mode === 'select' &&
|
|
159
|
+
isSoleSelection &&
|
|
115
160
|
selectedCeilingId !== undefined &&
|
|
116
161
|
(!editingHole || editingHole.nodeId !== selectedCeilingId)
|
|
117
162
|
|
|
@@ -133,6 +178,16 @@ export const ToolManager: React.FC = () => {
|
|
|
133
178
|
// Show build tools when in build mode
|
|
134
179
|
const showBuildTool = mode === 'build' && tool !== null
|
|
135
180
|
|
|
181
|
+
// A move initiated from the 2D floor-plan (orange move-dot) is owned end-to-
|
|
182
|
+
// end by `FloorplanRegistryMoveOverlay`, which marks the origin `'2d'` at
|
|
183
|
+
// dot-down. Mounting the 3D affordance mover alongside it would adopt the
|
|
184
|
+
// same node and, on its unmount, restore the adopt-time position — snapping
|
|
185
|
+
// the committed 2D move back to its start. Gate the 3D mover off for 2D moves
|
|
186
|
+
// (the scene writes the overlay makes still mirror into the 3D view). A
|
|
187
|
+
// 3D-initiated move leaves the origin null until its own commit, so this only
|
|
188
|
+
// suppresses the 3D tool for genuinely 2D-owned moves.
|
|
189
|
+
const showMover = movingNode != null && movingNodeOrigin !== '2d'
|
|
190
|
+
|
|
136
191
|
// Registry-first: if the active tool's kind has a NodeDefinition with a
|
|
137
192
|
// tool contribution, the registry-driven tool takes over.
|
|
138
193
|
const RegistryToolComponent = showBuildTool ? getRegistryTool(tool) : null
|
|
@@ -162,7 +217,7 @@ export const ToolManager: React.FC = () => {
|
|
|
162
217
|
<>
|
|
163
218
|
{/* World-space tools: site boundary and building movement operate in world coordinates */}
|
|
164
219
|
{showSiteBoundaryEditor && <SiteBoundaryEditor />}
|
|
165
|
-
{movingNode?.type === 'building' && (
|
|
220
|
+
{showMover && movingNode?.type === 'building' && (
|
|
166
221
|
<MoveTool onNodeMoved={handlePlacedNodeSelected} onSpawnMoved={handlePlacedNodeSelected} />
|
|
167
222
|
)}
|
|
168
223
|
|
|
@@ -216,49 +271,48 @@ export const ToolManager: React.FC = () => {
|
|
|
216
271
|
</Suspense>
|
|
217
272
|
) : null
|
|
218
273
|
})()}
|
|
219
|
-
{
|
|
274
|
+
{endpointTarget &&
|
|
275
|
+
reshapingNode &&
|
|
220
276
|
(() => {
|
|
221
277
|
const RegistryAffordance = getRegistryAffordanceTool(
|
|
222
|
-
|
|
278
|
+
reshapingNode.type,
|
|
223
279
|
'move-endpoint',
|
|
224
280
|
)
|
|
225
281
|
return RegistryAffordance ? (
|
|
226
282
|
<Suspense fallback={null}>
|
|
227
|
-
<RegistryAffordance target={
|
|
283
|
+
<RegistryAffordance target={endpointTarget} />
|
|
228
284
|
</Suspense>
|
|
229
285
|
) : null
|
|
230
286
|
})()}
|
|
231
|
-
{
|
|
287
|
+
{isCurveReshape &&
|
|
288
|
+
reshapingNode &&
|
|
232
289
|
(() => {
|
|
233
|
-
const RegistryAffordance = getRegistryAffordanceTool(
|
|
234
|
-
movingFenceEndpoint.fence.type,
|
|
235
|
-
'move-endpoint',
|
|
236
|
-
)
|
|
290
|
+
const RegistryAffordance = getRegistryAffordanceTool(reshapingNode.type, 'curve')
|
|
237
291
|
return RegistryAffordance ? (
|
|
238
292
|
<Suspense fallback={null}>
|
|
239
|
-
<RegistryAffordance
|
|
293
|
+
<RegistryAffordance node={reshapingNode} />
|
|
240
294
|
</Suspense>
|
|
241
295
|
) : null
|
|
242
296
|
})()}
|
|
243
|
-
{
|
|
297
|
+
{controlPointTarget &&
|
|
244
298
|
(() => {
|
|
245
|
-
const
|
|
246
|
-
return
|
|
299
|
+
const RegistryAffordance = getRegistryAffordanceTool('fence', 'move-control-point')
|
|
300
|
+
return RegistryAffordance ? (
|
|
247
301
|
<Suspense fallback={null}>
|
|
248
|
-
<
|
|
302
|
+
<RegistryAffordance target={controlPointTarget} />
|
|
249
303
|
</Suspense>
|
|
250
304
|
) : null
|
|
251
305
|
})()}
|
|
252
|
-
{
|
|
306
|
+
{tangentTarget &&
|
|
253
307
|
(() => {
|
|
254
|
-
const RegistryAffordance = getRegistryAffordanceTool(
|
|
308
|
+
const RegistryAffordance = getRegistryAffordanceTool('fence', 'move-tangent')
|
|
255
309
|
return RegistryAffordance ? (
|
|
256
310
|
<Suspense fallback={null}>
|
|
257
|
-
<RegistryAffordance
|
|
311
|
+
<RegistryAffordance target={tangentTarget} />
|
|
258
312
|
</Suspense>
|
|
259
313
|
) : null
|
|
260
314
|
})()}
|
|
261
|
-
{
|
|
315
|
+
{showMover && movingNode.type !== 'building' && (
|
|
262
316
|
<MoveTool
|
|
263
317
|
onNodeMoved={handlePlacedNodeSelected}
|
|
264
318
|
onSpawnMoved={handlePlacedNodeSelected}
|
|
@@ -283,6 +337,13 @@ export const ToolManager: React.FC = () => {
|
|
|
283
337
|
tools above. Lives inside the building-local group so the
|
|
284
338
|
building-local guide coords render at the right world position. */}
|
|
285
339
|
<Alignment3DGuideLayer />
|
|
340
|
+
{/* The one forward-facing triangle renderer. Placement/move tools
|
|
341
|
+
publish their ghost pose to `useFacingPose`; this draws it. Mounted
|
|
342
|
+
here so it shares the building-local frame the tools publish in. */}
|
|
343
|
+
<FacingPoseIndicator />
|
|
344
|
+
{/* Wall-plane proximity / sill / equal-spacing guides for openings,
|
|
345
|
+
published by the door/window move tools in the same world frame. */}
|
|
346
|
+
<OpeningGuides3DLayer />
|
|
286
347
|
{/* "Magnetic" beacon at the active wall-draft snap point. */}
|
|
287
348
|
<WallSnapBeaconLayer />
|
|
288
349
|
</group>
|