@pascal-app/editor 1.0.4 → 1.0.6
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/README.md +106 -0
- package/package.json +40 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/bake-thumbnail.tsx +6 -15
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +81 -8
- package/src/components/editor/editor-layout-mobile.tsx +1 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/export-manager.tsx +14 -19
- package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
- package/src/components/editor/first-person/build-collider-world.ts +22 -5
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
- package/src/components/editor/first-person/mechanism-targets.ts +52 -0
- package/src/components/editor/first-person-controls.tsx +19 -11
- package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +72 -23
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/group-actions.ts +2 -0
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/index.tsx +166 -79
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-highlight.tsx +13 -5
- package/src/components/editor/room-labels.test.ts +621 -128
- package/src/components/editor/room-labels.tsx +1090 -190
- package/src/components/editor/selection-manager.tsx +38 -117
- package/src/components/editor/slab-hole-highlights.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
- package/src/components/editor/snapshot-capture.test.ts +69 -58
- package/src/components/editor/snapshot-capture.ts +7 -1
- package/src/components/editor/thumbnail-generator.test.ts +350 -4
- package/src/components/editor/thumbnail-generator.tsx +128 -54
- package/src/components/editor/use-floorplan-background-placement.ts +3 -1
- package/src/components/editor/views/plugins-view.tsx +1 -8
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +140 -17
- package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +38 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +3 -6
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +94 -94
- package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
- package/src/components/tools/tool-manager.tsx +14 -5
- package/src/components/tools/wall/wall-drafting.test.ts +170 -57
- package/src/components/ui/action-menu/action-button.tsx +6 -1
- package/src/components/ui/action-menu/camera-actions.tsx +7 -3
- package/src/components/ui/action-menu/control-modes.tsx +13 -7
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +11 -9
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +10 -3
- package/src/components/ui/command-palette/editor-commands.tsx +3 -12
- package/src/components/ui/command-palette/index.tsx +45 -15
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +150 -45
- package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
- package/src/components/ui/helpers/helper-manager.tsx +7 -1
- package/src/components/ui/helpers/item-helper.tsx +9 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
- package/src/components/ui/panels/panel-wrapper.tsx +53 -27
- package/src/components/ui/panels/parametric-field-control.tsx +2 -2
- package/src/components/ui/panels/parametric-inspector.tsx +3 -3
- package/src/components/ui/panels/room-panel.tsx +65 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/sidebar.tsx +1 -1
- package/src/components/ui/right-stack.tsx +21 -2
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +3 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/tab-bar.tsx +4 -4
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-rooms.tsx +16 -0
- package/src/hooks/use-auto-save.integration.test.tsx +96 -38
- package/src/hooks/use-auto-save.ts +26 -1
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +65 -16
- package/src/hooks/use-keyboard.ts +29 -12
- package/src/hooks/use-placement-notice.test.ts +24 -1
- package/src/hooks/use-placement-notice.ts +37 -6
- package/src/index.tsx +71 -24
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.ts +9 -2
- package/src/lib/editor-views.ts +35 -1
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-foundation-support.test.ts +1 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/sheet-export.test.ts +24 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
- package/src/lib/floorplan/site-plan/coverage.ts +2 -4
- package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
- package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
- package/src/lib/floorplan/site-plan/geometry.ts +1 -416
- package/src/lib/floorplan/site-plan/index.ts +0 -16
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
- package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
- package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +112 -0
- package/src/lib/fresh-planar-placement.ts +57 -3
- package/src/lib/geometry-script/author.ts +6 -1
- package/src/lib/geometry-script/client.ts +8 -1
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +1 -0
- package/src/lib/glb-export.test.ts +187 -162
- package/src/lib/glb-export.ts +4 -1
- package/src/lib/history.test.ts +12 -7
- package/src/lib/interaction/scope.ts +4 -0
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/lot/clean-ring.test.ts +2 -1
- package/src/lib/lot/drop-in.ts +9 -24
- package/src/lib/lot/lot-patch.test.ts +1 -1
- package/src/lib/lot/lot-patch.ts +2 -2
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/orbit-floor-clearance.test.ts +36 -0
- package/src/lib/orbit-floor-clearance.ts +48 -0
- package/src/lib/paint-pass-through.test.ts +11 -0
- package/src/lib/paint-pass-through.ts +4 -2
- package/src/lib/paint-scope.ts +1 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plugin-chat.ts +15 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/room-assembly-overlay.ts +2 -3
- package/src/lib/room-selection.test.ts +45 -1
- package/src/lib/scene-clipboard.test.ts +246 -1
- package/src/lib/scene-clipboard.ts +44 -14
- package/src/lib/selection-routing.ts +1 -1
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.ts +2 -1
- package/src/lib/viewer-selection.ts +22 -11
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-stair-build-preview.ts +25 -5
- package/src/store/view-layout.test.ts +27 -1
- package/src/store/view-layout.ts +4 -1
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -15
- package/src/components/tools/stair/stair-tool.tsx +0 -682
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_STAIR_TYPE = 'straight' as const
|
|
2
|
-
export const DEFAULT_STAIR_WIDTH = 1.0
|
|
3
|
-
export const DEFAULT_STAIR_ATTACHMENT_SIDE = 'front' as const
|
|
4
|
-
export const DEFAULT_STAIR_FILL_TO_FLOOR = true
|
|
5
|
-
export const DEFAULT_STAIR_THICKNESS = 0.25
|
|
6
|
-
export const DEFAULT_STAIR_OPENING_OFFSET = 0
|
|
7
|
-
export const DEFAULT_CURVED_STAIR_INNER_RADIUS = 0.9
|
|
8
|
-
export const DEFAULT_CURVED_STAIR_SWEEP_ANGLE = Math.PI / 2
|
|
9
|
-
export const DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE = (400 * Math.PI) / 180
|
|
10
|
-
export const DEFAULT_SPIRAL_TOP_LANDING_MODE = 'none' as const
|
|
11
|
-
export const DEFAULT_SPIRAL_TOP_LANDING_DEPTH = 0.9
|
|
12
|
-
export const DEFAULT_SPIRAL_SHOW_CENTER_COLUMN = true
|
|
13
|
-
export const DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS = true
|
|
14
|
-
export const DEFAULT_STAIR_RAILING_MODE = 'both' as const
|
|
15
|
-
export const DEFAULT_STAIR_RAILING_HEIGHT = 0.92
|
|
@@ -1,682 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNode,
|
|
3
|
-
collectAlignmentAnchors,
|
|
4
|
-
createSizedStairFlight,
|
|
5
|
-
createSurfaceOpeningPreviewController,
|
|
6
|
-
DEFAULT_LEVEL_HEIGHT,
|
|
7
|
-
emitter,
|
|
8
|
-
type GridEvent,
|
|
9
|
-
getFloorStackedPosition,
|
|
10
|
-
getLevelFloorToFloorHeight,
|
|
11
|
-
type LevelNode,
|
|
12
|
-
movingAlignmentAnchors,
|
|
13
|
-
type NodeEvent,
|
|
14
|
-
planStairCreation,
|
|
15
|
-
planStairSizing,
|
|
16
|
-
resolveAlignment,
|
|
17
|
-
resolveFrozenFloorPlacementPatch,
|
|
18
|
-
resolveSupportSlabPatch,
|
|
19
|
-
StairNode,
|
|
20
|
-
type StairSegmentNode,
|
|
21
|
-
syncAutoStairOpenings,
|
|
22
|
-
useScene,
|
|
23
|
-
} from '@pascal-app/core'
|
|
24
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
25
|
-
import { useThree } from '@react-three/fiber'
|
|
26
|
-
import { useEffect, useMemo, useRef } from 'react'
|
|
27
|
-
import * as THREE from 'three'
|
|
28
|
-
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
29
|
-
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
30
|
-
import {
|
|
31
|
-
resolveStairDestinationLevel,
|
|
32
|
-
resolveStairPlacementLevelId,
|
|
33
|
-
} from '../../../lib/stair-levels'
|
|
34
|
-
|
|
35
|
-
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
36
|
-
import useEditor, {
|
|
37
|
-
isAlignmentGuideActive,
|
|
38
|
-
isGridSnapActive,
|
|
39
|
-
isMagneticSnapActive,
|
|
40
|
-
} from '../../../store/use-editor'
|
|
41
|
-
|
|
42
|
-
import useFacingPose from '../../../store/use-facing-pose'
|
|
43
|
-
import { useStairBuildPreview } from '../../../store/use-stair-build-preview'
|
|
44
|
-
import { CursorSphere } from '../shared/cursor-sphere'
|
|
45
|
-
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
46
|
-
import {
|
|
47
|
-
type PointerSupportSurface,
|
|
48
|
-
resolvePointerSupportSurface,
|
|
49
|
-
} from '../shared/pointer-support-cap'
|
|
50
|
-
import { createStairCommitGate, swallowFollowUpBrowserClick } from './stair-click-guard'
|
|
51
|
-
import {
|
|
52
|
-
DEFAULT_CURVED_STAIR_INNER_RADIUS,
|
|
53
|
-
DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
|
|
54
|
-
DEFAULT_SPIRAL_SHOW_CENTER_COLUMN,
|
|
55
|
-
DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
|
|
56
|
-
DEFAULT_SPIRAL_TOP_LANDING_DEPTH,
|
|
57
|
-
DEFAULT_SPIRAL_TOP_LANDING_MODE,
|
|
58
|
-
DEFAULT_STAIR_ATTACHMENT_SIDE,
|
|
59
|
-
DEFAULT_STAIR_FILL_TO_FLOOR,
|
|
60
|
-
DEFAULT_STAIR_OPENING_OFFSET,
|
|
61
|
-
DEFAULT_STAIR_RAILING_HEIGHT,
|
|
62
|
-
DEFAULT_STAIR_RAILING_MODE,
|
|
63
|
-
DEFAULT_STAIR_THICKNESS,
|
|
64
|
-
DEFAULT_STAIR_TYPE,
|
|
65
|
-
DEFAULT_STAIR_WIDTH,
|
|
66
|
-
} from './stair-defaults'
|
|
67
|
-
|
|
68
|
-
const GRID_OFFSET = 0.02
|
|
69
|
-
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
70
|
-
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
71
|
-
type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
72
|
-
type MoveTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Generates the step-profile geometry for the ghost preview.
|
|
76
|
-
* Same algorithm as StairSystem's generateStairSegmentGeometry.
|
|
77
|
-
*/
|
|
78
|
-
function createStairPreviewGeometry(rise: number): THREE.BufferGeometry {
|
|
79
|
-
const { stepCount, length, riserHeight, going: treadDepth } = planStairSizing(rise)
|
|
80
|
-
|
|
81
|
-
const shape = new THREE.Shape()
|
|
82
|
-
shape.moveTo(0, 0)
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < stepCount; i++) {
|
|
85
|
-
shape.lineTo(i * treadDepth, (i + 1) * riserHeight)
|
|
86
|
-
shape.lineTo((i + 1) * treadDepth, (i + 1) * riserHeight)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Fill to floor (absoluteHeight = 0)
|
|
90
|
-
shape.lineTo(length, 0)
|
|
91
|
-
shape.lineTo(0, 0)
|
|
92
|
-
|
|
93
|
-
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
94
|
-
steps: 1,
|
|
95
|
-
depth: DEFAULT_STAIR_WIDTH,
|
|
96
|
-
bevelEnabled: false,
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Rotate so extrusion is along X (width), shape profile in XZ plane
|
|
100
|
-
const matrix = new THREE.Matrix4()
|
|
101
|
-
matrix.makeRotationY(-Math.PI / 2)
|
|
102
|
-
matrix.setPosition(DEFAULT_STAIR_WIDTH / 2, 0, 0)
|
|
103
|
-
geometry.applyMatrix4(matrix)
|
|
104
|
-
|
|
105
|
-
return geometry
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Creates a default straight stair segment climbing `rise` — the storey it is
|
|
110
|
-
* dropped on, not a constant: the placed stair has no explicit `totalRise`, so
|
|
111
|
-
* this is the height `syncStairRises` immediately converges it to anyway.
|
|
112
|
-
*/
|
|
113
|
-
function resolvePlacedStairBase(
|
|
114
|
-
nodes: Record<string, AnyNode>,
|
|
115
|
-
levelId: LevelNode['id'],
|
|
116
|
-
stair: StairNode,
|
|
117
|
-
supportSurface: PointerSupportSurface | null,
|
|
118
|
-
seed: StairSegmentNode,
|
|
119
|
-
): number {
|
|
120
|
-
// Keep the pointer-selected base when an overlapping deck is above the cursor.
|
|
121
|
-
const base = getFloorStackedPosition({
|
|
122
|
-
node: stair,
|
|
123
|
-
nodes: { ...nodes, [seed.id]: { ...seed, parentId: stair.id } },
|
|
124
|
-
position: stair.position,
|
|
125
|
-
rotation: stair.rotation,
|
|
126
|
-
levelId,
|
|
127
|
-
maxElevation: supportSurface?.elevation ?? null,
|
|
128
|
-
})[1]
|
|
129
|
-
return base
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function createSeedStairSegment(rise: number) {
|
|
133
|
-
return createSizedStairFlight(rise, {
|
|
134
|
-
width: DEFAULT_STAIR_WIDTH,
|
|
135
|
-
attachmentSide: DEFAULT_STAIR_ATTACHMENT_SIDE,
|
|
136
|
-
fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
|
|
137
|
-
thickness: DEFAULT_STAIR_THICKNESS,
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function createDefaultStairNode({
|
|
142
|
-
name,
|
|
143
|
-
levelId,
|
|
144
|
-
nextLevelId,
|
|
145
|
-
position,
|
|
146
|
-
rotation,
|
|
147
|
-
segmentId,
|
|
148
|
-
}: {
|
|
149
|
-
name: string
|
|
150
|
-
levelId: LevelNode['id']
|
|
151
|
-
nextLevelId: LevelNode['id']
|
|
152
|
-
position: [number, number, number]
|
|
153
|
-
rotation: number
|
|
154
|
-
segmentId: StairSegmentNode['id']
|
|
155
|
-
}) {
|
|
156
|
-
return StairNode.parse({
|
|
157
|
-
name,
|
|
158
|
-
parentId: levelId,
|
|
159
|
-
position,
|
|
160
|
-
rotation,
|
|
161
|
-
stairType: DEFAULT_STAIR_TYPE,
|
|
162
|
-
uniformRisers: true,
|
|
163
|
-
fromLevelId: levelId,
|
|
164
|
-
toLevelId: nextLevelId,
|
|
165
|
-
slabOpeningMode: 'destination',
|
|
166
|
-
openingOffset: DEFAULT_STAIR_OPENING_OFFSET,
|
|
167
|
-
width: DEFAULT_STAIR_WIDTH,
|
|
168
|
-
thickness: DEFAULT_STAIR_THICKNESS,
|
|
169
|
-
fillToFloor: DEFAULT_STAIR_FILL_TO_FLOOR,
|
|
170
|
-
innerRadius: DEFAULT_CURVED_STAIR_INNER_RADIUS,
|
|
171
|
-
sweepAngle: DEFAULT_CURVED_STAIR_SWEEP_ANGLE,
|
|
172
|
-
topLandingMode: DEFAULT_SPIRAL_TOP_LANDING_MODE,
|
|
173
|
-
topLandingDepth: DEFAULT_SPIRAL_TOP_LANDING_DEPTH,
|
|
174
|
-
showCenterColumn: DEFAULT_SPIRAL_SHOW_CENTER_COLUMN,
|
|
175
|
-
showStepSupports: DEFAULT_SPIRAL_SHOW_STEP_SUPPORTS,
|
|
176
|
-
railingHeight: DEFAULT_STAIR_RAILING_HEIGHT,
|
|
177
|
-
railingMode: DEFAULT_STAIR_RAILING_MODE,
|
|
178
|
-
children: [segmentId],
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Creates a stair group with one default stair segment at the given position/rotation.
|
|
184
|
-
*/
|
|
185
|
-
function commitStairPlacement(
|
|
186
|
-
levelId: LevelNode['id'],
|
|
187
|
-
position: [number, number, number],
|
|
188
|
-
rotation: number,
|
|
189
|
-
supportSurface: PointerSupportSurface | null,
|
|
190
|
-
): void {
|
|
191
|
-
const { createNodes, nodes } = useScene.getState()
|
|
192
|
-
const placementLevelId = resolveStairPlacementLevelId(
|
|
193
|
-
nodes,
|
|
194
|
-
levelId,
|
|
195
|
-
useViewer.getState().selection.buildingId,
|
|
196
|
-
)
|
|
197
|
-
if (!placementLevelId) return
|
|
198
|
-
|
|
199
|
-
const stairCount = Object.values(nodes).filter((n) => n.type === 'stair').length
|
|
200
|
-
const name = `Staircase ${stairCount + 1}`
|
|
201
|
-
const storeyRise = getLevelFloorToFloorHeight(placementLevelId, nodes)
|
|
202
|
-
if (!(storeyRise > 0)) return
|
|
203
|
-
const seed = createSeedStairSegment(storeyRise)
|
|
204
|
-
|
|
205
|
-
const destinationPlan = resolveStairDestinationLevel({
|
|
206
|
-
createMissing: true,
|
|
207
|
-
fromLevelId: placementLevelId,
|
|
208
|
-
nodes,
|
|
209
|
-
})
|
|
210
|
-
const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
|
|
211
|
-
|
|
212
|
-
const stair = StairNode.parse({
|
|
213
|
-
...createDefaultStairNode({
|
|
214
|
-
name,
|
|
215
|
-
levelId: placementLevelId,
|
|
216
|
-
nextLevelId,
|
|
217
|
-
position,
|
|
218
|
-
rotation,
|
|
219
|
-
segmentId: seed.id,
|
|
220
|
-
}),
|
|
221
|
-
parentId: placementLevelId,
|
|
222
|
-
})
|
|
223
|
-
const base = resolvePlacedStairBase(nodes, placementLevelId, stair, supportSurface, seed)
|
|
224
|
-
let segment: StairSegmentNode
|
|
225
|
-
try {
|
|
226
|
-
segment = {
|
|
227
|
-
...planStairCreation(
|
|
228
|
-
stair,
|
|
229
|
-
nodes,
|
|
230
|
-
{
|
|
231
|
-
width: seed.width,
|
|
232
|
-
attachmentSide: seed.attachmentSide,
|
|
233
|
-
fillToFloor: seed.fillToFloor,
|
|
234
|
-
thickness: seed.thickness,
|
|
235
|
-
},
|
|
236
|
-
base,
|
|
237
|
-
).flight,
|
|
238
|
-
id: seed.id,
|
|
239
|
-
}
|
|
240
|
-
} catch (error) {
|
|
241
|
-
if (error instanceof RangeError) return
|
|
242
|
-
throw error
|
|
243
|
-
}
|
|
244
|
-
stair.stepCount = segment.stepCount
|
|
245
|
-
const prospectiveNodes = {
|
|
246
|
-
...nodes,
|
|
247
|
-
[stair.id]: stair,
|
|
248
|
-
[segment.id]: { ...segment, parentId: stair.id },
|
|
249
|
-
} as Record<string, AnyNode>
|
|
250
|
-
const placementPatch = supportSurface?.sourceNodeId
|
|
251
|
-
? resolveFrozenFloorPlacementPatch(stair, prospectiveNodes, {
|
|
252
|
-
position,
|
|
253
|
-
rotation,
|
|
254
|
-
elevation: supportSurface.elevation,
|
|
255
|
-
preferredSlabId: supportSurface.supportSlabId,
|
|
256
|
-
})
|
|
257
|
-
: {
|
|
258
|
-
position,
|
|
259
|
-
...resolveSupportSlabPatch(stair, prospectiveNodes, {
|
|
260
|
-
maxElevation: supportSurface?.elevation,
|
|
261
|
-
}),
|
|
262
|
-
}
|
|
263
|
-
const committedStair = StairNode.parse({
|
|
264
|
-
...stair,
|
|
265
|
-
...placementPatch,
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
const createdLevel = destinationPlan?.createdLevel
|
|
269
|
-
const levelCreateOps =
|
|
270
|
-
createdLevel && destinationPlan.buildingId
|
|
271
|
-
? [{ node: createdLevel, parentId: destinationPlan.buildingId }]
|
|
272
|
-
: []
|
|
273
|
-
|
|
274
|
-
createNodes([
|
|
275
|
-
...levelCreateOps,
|
|
276
|
-
{ node: committedStair, parentId: placementLevelId },
|
|
277
|
-
{ node: segment, parentId: committedStair.id },
|
|
278
|
-
])
|
|
279
|
-
|
|
280
|
-
sfxEmitter.emit('sfx:structure-build')
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export const StairTool: React.FC = () => {
|
|
284
|
-
const camera = useThree((state) => state.camera)
|
|
285
|
-
const cameraRef = useRef(camera)
|
|
286
|
-
cameraRef.current = camera
|
|
287
|
-
const cursorRef = useRef<THREE.Group>(null)
|
|
288
|
-
const previewRef = useRef<THREE.Group>(null)
|
|
289
|
-
const previewMeshRef = useRef<THREE.Mesh>(null)
|
|
290
|
-
const rotationRef = useRef(0)
|
|
291
|
-
const supportSurfaceRef = useRef<PointerSupportSurface | null>(null)
|
|
292
|
-
const previousGridPosRef = useRef<[number, number] | null>(null)
|
|
293
|
-
const lastCanonicalPositionRef = useRef<[number, number, number] | null>(null)
|
|
294
|
-
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
295
|
-
|
|
296
|
-
const previewRise = useScene((state) =>
|
|
297
|
-
currentLevelId ? getLevelFloorToFloorHeight(currentLevelId, state.nodes) : DEFAULT_LEVEL_HEIGHT,
|
|
298
|
-
)
|
|
299
|
-
const previewRiseRef = useRef(previewRise)
|
|
300
|
-
previewRiseRef.current = previewRise
|
|
301
|
-
const previewGeometry = useMemo(() => createStairPreviewGeometry(DEFAULT_LEVEL_HEIGHT), [])
|
|
302
|
-
const ownedGeometryRef = useRef(previewGeometry)
|
|
303
|
-
const geometryRiseRef = useRef(DEFAULT_LEVEL_HEIGHT)
|
|
304
|
-
useEffect(() => () => ownedGeometryRef.current.dispose(), [])
|
|
305
|
-
|
|
306
|
-
useEffect(() => {
|
|
307
|
-
if (!currentLevelId) return
|
|
308
|
-
|
|
309
|
-
const openingPreview = createSurfaceOpeningPreviewController()
|
|
310
|
-
// Refuses the duplicate commit triggers a single physical click produces
|
|
311
|
-
// — see `stair-click-guard.ts`. Fresh per armed session.
|
|
312
|
-
const commitGate = createStairCommitGate()
|
|
313
|
-
|
|
314
|
-
// Reset rotation when tool activates
|
|
315
|
-
rotationRef.current = 0
|
|
316
|
-
useStairBuildPreview.getState().reset()
|
|
317
|
-
if (previewRef.current) {
|
|
318
|
-
previewRef.current.rotation.y = 0
|
|
319
|
-
previewRef.current.scale.y = 1
|
|
320
|
-
}
|
|
321
|
-
lastCanonicalPositionRef.current = null
|
|
322
|
-
supportSurfaceRef.current = null
|
|
323
|
-
|
|
324
|
-
const buildPreviewScene = (
|
|
325
|
-
position: [number, number, number],
|
|
326
|
-
rotation: number,
|
|
327
|
-
supportSurface: PointerSupportSurface | null,
|
|
328
|
-
) => {
|
|
329
|
-
const nodes = useScene.getState().nodes
|
|
330
|
-
const placementLevelId = resolveStairPlacementLevelId(
|
|
331
|
-
nodes,
|
|
332
|
-
currentLevelId,
|
|
333
|
-
useViewer.getState().selection.buildingId,
|
|
334
|
-
)
|
|
335
|
-
if (!placementLevelId) return null
|
|
336
|
-
|
|
337
|
-
const destinationPlan = resolveStairDestinationLevel({
|
|
338
|
-
createMissing: true,
|
|
339
|
-
fromLevelId: placementLevelId,
|
|
340
|
-
nodes,
|
|
341
|
-
})
|
|
342
|
-
const nextLevelId = destinationPlan?.toLevel.id ?? placementLevelId
|
|
343
|
-
const storeyRise = getLevelFloorToFloorHeight(placementLevelId, nodes)
|
|
344
|
-
if (!(storeyRise > 0)) return null
|
|
345
|
-
const seed = createSeedStairSegment(storeyRise)
|
|
346
|
-
const stair = createDefaultStairNode({
|
|
347
|
-
name: 'Staircase Preview',
|
|
348
|
-
levelId: placementLevelId,
|
|
349
|
-
nextLevelId,
|
|
350
|
-
position,
|
|
351
|
-
rotation,
|
|
352
|
-
segmentId: seed.id,
|
|
353
|
-
})
|
|
354
|
-
const base = resolvePlacedStairBase(nodes, placementLevelId, stair, supportSurface, seed)
|
|
355
|
-
let segment: StairSegmentNode
|
|
356
|
-
try {
|
|
357
|
-
segment = {
|
|
358
|
-
...planStairCreation(
|
|
359
|
-
stair,
|
|
360
|
-
nodes,
|
|
361
|
-
{
|
|
362
|
-
width: seed.width,
|
|
363
|
-
attachmentSide: seed.attachmentSide,
|
|
364
|
-
fillToFloor: seed.fillToFloor,
|
|
365
|
-
thickness: seed.thickness,
|
|
366
|
-
},
|
|
367
|
-
base,
|
|
368
|
-
).flight,
|
|
369
|
-
id: seed.id,
|
|
370
|
-
}
|
|
371
|
-
} catch (error) {
|
|
372
|
-
if (error instanceof RangeError) return null
|
|
373
|
-
throw error
|
|
374
|
-
}
|
|
375
|
-
stair.stepCount = segment.stepCount
|
|
376
|
-
const previewNodes = {
|
|
377
|
-
...nodes,
|
|
378
|
-
...(destinationPlan?.createdLevel
|
|
379
|
-
? { [destinationPlan.createdLevel.id]: destinationPlan.createdLevel }
|
|
380
|
-
: {}),
|
|
381
|
-
[stair.id]: { ...stair, parentId: placementLevelId },
|
|
382
|
-
[segment.id]: { ...segment, parentId: stair.id },
|
|
383
|
-
} as Record<string, AnyNode>
|
|
384
|
-
|
|
385
|
-
return { placementLevelId, previewNodes, stair, rise: segment.height }
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// The preview rebuild (full-scene copy + destination-level resolution +
|
|
389
|
-
// auto-opening CSG) is expensive; `grid:move` fires it every pointer event
|
|
390
|
-
// but the placed position is grid-snapped, so within a cell every rebuild
|
|
391
|
-
// is identical. Dedupe on the snapped position + rotation so we rebuild
|
|
392
|
-
// only when the staircase would actually land somewhere new — this is the
|
|
393
|
-
// difference between a smooth and a stuttering stair tool (the elevator is
|
|
394
|
-
// cheap because it has no opening sync).
|
|
395
|
-
let lastPreviewKey: string | null = null
|
|
396
|
-
|
|
397
|
-
const applyDraftPreview = (
|
|
398
|
-
position: [number, number, number],
|
|
399
|
-
rotation: number,
|
|
400
|
-
supportSurface: PointerSupportSurface | null,
|
|
401
|
-
) => {
|
|
402
|
-
const key = `${position[0].toFixed(3)},${position[2].toFixed(3)},${rotation.toFixed(4)},${supportSurface?.elevation.toFixed(3) ?? 'none'},${supportSurface?.sourceNodeId ?? 'floor'},${previewRiseRef.current}`
|
|
403
|
-
if (key === lastPreviewKey) return
|
|
404
|
-
lastPreviewKey = key
|
|
405
|
-
const preview = buildPreviewScene(position, rotation, supportSurface)
|
|
406
|
-
useStairBuildPreview
|
|
407
|
-
.getState()
|
|
408
|
-
.setPreview(preview ? [position[0], position[2]] : null, rotation, preview?.rise ?? null)
|
|
409
|
-
const frozenPatch =
|
|
410
|
-
preview && supportSurface?.sourceNodeId
|
|
411
|
-
? resolveFrozenFloorPlacementPatch(preview.stair, preview.previewNodes, {
|
|
412
|
-
position,
|
|
413
|
-
rotation,
|
|
414
|
-
elevation: supportSurface.elevation,
|
|
415
|
-
preferredSlabId: supportSurface.supportSlabId,
|
|
416
|
-
})
|
|
417
|
-
: null
|
|
418
|
-
const previewPosition = frozenPatch?.position ?? position
|
|
419
|
-
const previewStair = frozenPatch
|
|
420
|
-
? ({ ...preview?.stair, ...frozenPatch } as AnyNode)
|
|
421
|
-
: preview?.stair
|
|
422
|
-
const visualPosition =
|
|
423
|
-
preview && previewStair
|
|
424
|
-
? getFloorStackPreviewPosition({
|
|
425
|
-
node: previewStair,
|
|
426
|
-
position: previewPosition,
|
|
427
|
-
rotation,
|
|
428
|
-
levelId: preview.placementLevelId,
|
|
429
|
-
nodes: preview.previewNodes,
|
|
430
|
-
maxElevation: supportSurface?.sourceNodeId ? null : supportSurface?.elevation,
|
|
431
|
-
})
|
|
432
|
-
: previewPosition
|
|
433
|
-
if (cursorRef.current) {
|
|
434
|
-
cursorRef.current.position.set(
|
|
435
|
-
visualPosition[0],
|
|
436
|
-
visualPosition[1] + GRID_OFFSET,
|
|
437
|
-
visualPosition[2],
|
|
438
|
-
)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (previewRef.current) {
|
|
442
|
-
previewRef.current.visible = Boolean(preview)
|
|
443
|
-
previewRef.current.position.set(...visualPosition)
|
|
444
|
-
previewRef.current.rotation.y = rotation
|
|
445
|
-
if (preview && previewMeshRef.current && preview.rise !== geometryRiseRef.current) {
|
|
446
|
-
const previous = previewMeshRef.current.geometry
|
|
447
|
-
previewMeshRef.current.geometry = createStairPreviewGeometry(preview.rise)
|
|
448
|
-
previous.dispose()
|
|
449
|
-
ownedGeometryRef.current = previewMeshRef.current.geometry
|
|
450
|
-
geometryRiseRef.current = preview.rise
|
|
451
|
-
}
|
|
452
|
-
previewRef.current.scale.y = 1
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Forward-facing triangle (editor-side overlay). The run ascends along
|
|
456
|
-
// local +Z from the entry at z≈0; the stair's front is the -Z entry side,
|
|
457
|
-
// so `reversed` points the triangle out of the entry (where you approach
|
|
458
|
-
// from), sitting just before it — not inside the footprint or at the
|
|
459
|
-
// elevated far end. Centre is the footprint mid-run (origin is the entry).
|
|
460
|
-
if (!preview) {
|
|
461
|
-
useFacingPose.getState().clear()
|
|
462
|
-
openingPreview.clear()
|
|
463
|
-
return
|
|
464
|
-
}
|
|
465
|
-
const sizing = planStairSizing(preview.rise)
|
|
466
|
-
useFacingPose.getState().set({
|
|
467
|
-
position: visualPosition,
|
|
468
|
-
rotationY: rotation,
|
|
469
|
-
depth: sizing.length,
|
|
470
|
-
center: [0, sizing.length / 2],
|
|
471
|
-
reversed: true,
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
openingPreview.apply(syncAutoStairOpenings(preview.previewNodes))
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Alignment candidates — anchors of every alignable object; refreshed
|
|
478
|
-
// after each placement. The moving stair aligns by its footprint edges so
|
|
479
|
-
// users can snap the run side against walls, slabs, elevators, or another
|
|
480
|
-
// stair instead of only lining up the invisible origin point.
|
|
481
|
-
let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
482
|
-
const resolveStairFootprintAlignment = (
|
|
483
|
-
x: number,
|
|
484
|
-
z: number,
|
|
485
|
-
rotation: number,
|
|
486
|
-
): ReturnType<typeof resolveAlignment> | null => {
|
|
487
|
-
const preview = buildPreviewScene([x, 0, z], rotation, supportSurfaceRef.current)
|
|
488
|
-
const moving = preview
|
|
489
|
-
? movingAlignmentAnchors(preview.stair, preview.previewNodes, x, z, rotation)
|
|
490
|
-
: []
|
|
491
|
-
if (moving.length === 0) return null
|
|
492
|
-
return resolveAlignment({
|
|
493
|
-
moving,
|
|
494
|
-
candidates: alignmentCandidates,
|
|
495
|
-
threshold: ALIGNMENT_THRESHOLD_M,
|
|
496
|
-
})
|
|
497
|
-
}
|
|
498
|
-
// The probe is the RAW cursor, not the grid-snapped point: resolving
|
|
499
|
-
// against the grid point would only catch anchors that happen to sit near
|
|
500
|
-
// a grid line. Matched axes use the raw probe + snap delta; unmatched axes
|
|
501
|
-
// keep the normal grid snap. Guides are published in every snapping mode
|
|
502
|
-
// (including Off); the magnetic pull toward them (applySnap) applies only in
|
|
503
|
-
// 'lines' mode.
|
|
504
|
-
const alignPoint = (
|
|
505
|
-
gridX: number,
|
|
506
|
-
gridZ: number,
|
|
507
|
-
rawX: number,
|
|
508
|
-
rawZ: number,
|
|
509
|
-
bypass: boolean,
|
|
510
|
-
applySnap: boolean,
|
|
511
|
-
): [number, number] => {
|
|
512
|
-
if (bypass || alignmentCandidates.length === 0) {
|
|
513
|
-
useAlignmentGuides.getState().clear()
|
|
514
|
-
return [gridX, gridZ]
|
|
515
|
-
}
|
|
516
|
-
const ar = resolveStairFootprintAlignment(rawX, rawZ, rotationRef.current)
|
|
517
|
-
if (!ar || ar.guides.length === 0) {
|
|
518
|
-
useAlignmentGuides.getState().clear()
|
|
519
|
-
return [gridX, gridZ]
|
|
520
|
-
}
|
|
521
|
-
if (!applySnap) {
|
|
522
|
-
useAlignmentGuides.getState().set(ar.guides)
|
|
523
|
-
return [gridX, gridZ]
|
|
524
|
-
}
|
|
525
|
-
let x = gridX
|
|
526
|
-
let z = gridZ
|
|
527
|
-
if (ar.snap) {
|
|
528
|
-
if (ar.guides.some((guide) => guide.axis === 'x')) x = rawX + ar.snap.dx
|
|
529
|
-
if (ar.guides.some((guide) => guide.axis === 'z')) z = rawZ + ar.snap.dz
|
|
530
|
-
}
|
|
531
|
-
const finalAlignment = resolveStairFootprintAlignment(x, z, rotationRef.current)
|
|
532
|
-
useAlignmentGuides.getState().set(finalAlignment?.guides ?? ar.guides)
|
|
533
|
-
return [x, z]
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const resolveStairPosition = (event: MoveTriggerEvent): [number, number, number] | null => {
|
|
537
|
-
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position, {
|
|
538
|
-
includeNodeTopSurfaces: true,
|
|
539
|
-
})
|
|
540
|
-
supportSurfaceRef.current = pointed
|
|
541
|
-
const fallbackPosition =
|
|
542
|
-
'node' in event ? lastCanonicalPositionRef.current : event.localPosition
|
|
543
|
-
if (!pointed?.localPoint && !fallbackPosition) return null
|
|
544
|
-
const rawX = pointed?.localPoint?.[0] ?? fallbackPosition![0]
|
|
545
|
-
const rawZ = pointed?.localPoint?.[2] ?? fallbackPosition![2]
|
|
546
|
-
// Grid snap follows the global mode (live step so the HUD chip is
|
|
547
|
-
// honest); Off keeps the raw cursor. Shift cycles the mode centrally.
|
|
548
|
-
const step = useEditor.getState().gridSnapStep
|
|
549
|
-
const [gridX, gridZ] = alignPoint(
|
|
550
|
-
isGridSnapActive() ? Math.round(rawX / step) * step : rawX,
|
|
551
|
-
isGridSnapActive() ? Math.round(rawZ / step) * step : rawZ,
|
|
552
|
-
rawX,
|
|
553
|
-
rawZ,
|
|
554
|
-
!isAlignmentGuideActive(),
|
|
555
|
-
isMagneticSnapActive(),
|
|
556
|
-
)
|
|
557
|
-
return [gridX, 0, gridZ]
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const onPointerMove = (event: MoveTriggerEvent) => {
|
|
561
|
-
const position = resolveStairPosition(event)
|
|
562
|
-
if (!position) return
|
|
563
|
-
const [gridX, , gridZ] = position
|
|
564
|
-
lastCanonicalPositionRef.current = position
|
|
565
|
-
applyDraftPreview(position, rotationRef.current, supportSurfaceRef.current)
|
|
566
|
-
|
|
567
|
-
if (
|
|
568
|
-
(isGridSnapActive() || isMagneticSnapActive()) &&
|
|
569
|
-
previousGridPosRef.current &&
|
|
570
|
-
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
571
|
-
) {
|
|
572
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
previousGridPosRef.current = [gridX, gridZ]
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const commitAtCursor = (event: ClickTriggerEvent) => {
|
|
579
|
-
if (!currentLevelId) return
|
|
580
|
-
// One physical click can reach here twice (node click synthesized on
|
|
581
|
-
// pointerup + the native browser click driving `grid:click`) — see
|
|
582
|
-
// `stair-click-guard.ts`. The gate refuses anything after a single-
|
|
583
|
-
// continuation commit; the swallow below eats the same gesture's
|
|
584
|
-
// follow-up click while the tool stays armed (repeat continuation).
|
|
585
|
-
if (!commitGate.shouldCommit()) return
|
|
586
|
-
const nodeEvent = 'node' in event ? (event as NodeEvent<AnyNode>) : null
|
|
587
|
-
if (nodeEvent) {
|
|
588
|
-
nodeEvent.stopPropagation()
|
|
589
|
-
nodeEvent.nativeEvent.stopPropagation()
|
|
590
|
-
// The canvas-level `grid:click` listener is out of stopPropagation's
|
|
591
|
-
// reach — without this, the browser click that follows this
|
|
592
|
-
// pointerup-synthesized node click commits a second stair.
|
|
593
|
-
swallowFollowUpBrowserClick()
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const position = resolveStairPosition(event)
|
|
597
|
-
if (!position) return
|
|
598
|
-
|
|
599
|
-
commitStairPlacement(currentLevelId, position, rotationRef.current, supportSurfaceRef.current)
|
|
600
|
-
openingPreview.clear()
|
|
601
|
-
// Commit cleared the opening preview, so force the next hover (even on the
|
|
602
|
-
// same cell) to rebuild rather than dedupe against the just-placed key.
|
|
603
|
-
lastPreviewKey = null
|
|
604
|
-
useAlignmentGuides.getState().clear()
|
|
605
|
-
|
|
606
|
-
// Single by default; the C-toggle ('point' context, shared with every
|
|
607
|
-
// other placement tool) opts into placing more. On single, drop the tool
|
|
608
|
-
// and the facing triangle so we fall back to select after one stair.
|
|
609
|
-
if (useEditor.getState().getContinuation('point') === 'repeat') {
|
|
610
|
-
alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
|
|
611
|
-
} else {
|
|
612
|
-
commitGate.markExited()
|
|
613
|
-
useFacingPose.getState().clear()
|
|
614
|
-
useEditor.getState().setTool(null)
|
|
615
|
-
// Return to select mode explicitly (matches the spawn tool's exit).
|
|
616
|
-
// The selection managers route node clicks only while
|
|
617
|
-
// `mode === 'select'`; exiting with `mode: 'build'` + a null tool
|
|
618
|
-
// left every click dead until the user pressed Escape.
|
|
619
|
-
useEditor.getState().setMode('select')
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
624
|
-
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
625
|
-
return
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const ROTATION_STEP = Math.PI / 4
|
|
629
|
-
let rotationDelta = 0
|
|
630
|
-
if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
|
|
631
|
-
else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
|
|
632
|
-
|
|
633
|
-
if (rotationDelta !== 0) {
|
|
634
|
-
event.preventDefault()
|
|
635
|
-
sfxEmitter.emit('sfx:item-rotate')
|
|
636
|
-
rotationRef.current += rotationDelta
|
|
637
|
-
if (lastCanonicalPositionRef.current) {
|
|
638
|
-
applyDraftPreview(
|
|
639
|
-
lastCanonicalPositionRef.current,
|
|
640
|
-
rotationRef.current,
|
|
641
|
-
supportSurfaceRef.current,
|
|
642
|
-
)
|
|
643
|
-
} else if (previewRef.current) {
|
|
644
|
-
previewRef.current.rotation.y = rotationRef.current
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
emitter.on('grid:move', onPointerMove)
|
|
650
|
-
emitter.on('grid:click', commitAtCursor)
|
|
651
|
-
emitter.on('node:click', commitAtCursor)
|
|
652
|
-
emitter.on('node:move', onPointerMove)
|
|
653
|
-
window.addEventListener('keydown', onKeyDown)
|
|
654
|
-
|
|
655
|
-
return () => {
|
|
656
|
-
emitter.off('grid:move', onPointerMove)
|
|
657
|
-
emitter.off('grid:click', commitAtCursor)
|
|
658
|
-
emitter.off('node:click', commitAtCursor)
|
|
659
|
-
emitter.off('node:move', onPointerMove)
|
|
660
|
-
window.removeEventListener('keydown', onKeyDown)
|
|
661
|
-
useAlignmentGuides.getState().clear()
|
|
662
|
-
openingPreview.clear()
|
|
663
|
-
useFacingPose.getState().clear()
|
|
664
|
-
useStairBuildPreview.getState().reset()
|
|
665
|
-
}
|
|
666
|
-
}, [currentLevelId])
|
|
667
|
-
|
|
668
|
-
return (
|
|
669
|
-
<group>
|
|
670
|
-
<CursorSphere ref={cursorRef} />
|
|
671
|
-
|
|
672
|
-
{/* 3D ghost preview — position/rotation updated imperatively. The
|
|
673
|
-
forward-facing triangle is drawn by the editor-side overlay from the
|
|
674
|
-
pose published in `applyDraftPreview`. */}
|
|
675
|
-
<group ref={previewRef}>
|
|
676
|
-
<mesh dispose={null} geometry={previewGeometry} layers={EDITOR_LAYER} ref={previewMeshRef}>
|
|
677
|
-
<meshStandardMaterial color="#818cf8" depthWrite={false} opacity={0.35} transparent />
|
|
678
|
-
</mesh>
|
|
679
|
-
</group>
|
|
680
|
-
</group>
|
|
681
|
-
)
|
|
682
|
-
}
|