@pascal-app/editor 1.0.4 → 1.0.5
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 +39 -37
- 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
|
@@ -5,17 +5,21 @@ import {
|
|
|
5
5
|
type CeilingNode,
|
|
6
6
|
createSceneApi,
|
|
7
7
|
type FenceNode,
|
|
8
|
+
isStoreyContent,
|
|
9
|
+
type LevelNode,
|
|
8
10
|
nodeRegistry,
|
|
9
11
|
type SlabNode,
|
|
12
|
+
storeyOf,
|
|
10
13
|
useScene,
|
|
11
14
|
type WallNode,
|
|
12
15
|
type ZoneNode,
|
|
13
16
|
} from '@pascal-app/core'
|
|
14
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
-
import { type ComponentType, lazy, Suspense, useMemo } from 'react'
|
|
18
|
+
import { type ComponentType, lazy, Suspense, useEffect, useMemo } from 'react'
|
|
16
19
|
import { useRegisteredToolEnabled } from '../../hooks/use-registered-tool-enabled'
|
|
17
20
|
import { useCeilingEditCeilingId } from '../../lib/ceiling-edit-session'
|
|
18
21
|
import type { ReshapeKind } from '../../lib/interaction/scope'
|
|
22
|
+
import { selectPlacedNode } from '../../lib/level-selection'
|
|
19
23
|
import { paintRegionTargets, usePaintRegionMode } from '../../lib/paint-region-mode'
|
|
20
24
|
import { siteBoundaryHandlesEnabled } from '../../lib/site-boundary'
|
|
21
25
|
import useEditor, { type Tool } from '../../store/use-editor'
|
|
@@ -48,7 +52,6 @@ import {
|
|
|
48
52
|
import { FacingPoseIndicator } from './shared/facing-pose-indicator'
|
|
49
53
|
import { SiteBoundaryEditor } from './site/site-boundary-editor'
|
|
50
54
|
import { TerrainSculptTool } from './site/terrain-sculpt-tool'
|
|
51
|
-
import { StairTool } from './stair/stair-tool'
|
|
52
55
|
import { ZoneBoundaryEditor } from './zone/zone-boundary-editor'
|
|
53
56
|
import { ZoneTool } from './zone/zone-tool'
|
|
54
57
|
|
|
@@ -109,7 +112,6 @@ function getRegistryTool(tool: Tool | null): ComponentType | null {
|
|
|
109
112
|
// item / shelf / spawn now go through the registry path above.
|
|
110
113
|
const tools: Partial<Record<Tool, React.FC>> = {
|
|
111
114
|
'property-line': SiteBoundaryEditor,
|
|
112
|
-
stair: StairTool,
|
|
113
115
|
zone: ZoneTool,
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -178,16 +180,23 @@ export const ToolManager: React.FC = () => {
|
|
|
178
180
|
const unit = useViewer((state) => state.unit)
|
|
179
181
|
const setSelection = useViewer((state) => state.setSelection)
|
|
180
182
|
const nodes = useScene((state) => state.nodes)
|
|
183
|
+
// Placing a roof makes its level active (selectPlacedNode); a wall, room, slab, stair or item tool
|
|
184
|
+
// armed there, then or after a level change, draws on the storey the roof level covers.
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!(tool && activeLevelId && isStoreyContent(tool, undefined))) return
|
|
187
|
+
const storeyId = storeyOf(useScene.getState().nodes, activeLevelId)
|
|
188
|
+
if (storeyId !== activeLevelId) setSelection({ levelId: storeyId as LevelNode['id'] })
|
|
189
|
+
}, [tool, activeLevelId, setSelection])
|
|
181
190
|
const registrySceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
182
191
|
const registryToolContext = useMemo(
|
|
183
192
|
() => ({
|
|
184
193
|
activeLevelId: activeLevelId ?? null,
|
|
185
194
|
isCameraDragging: () => useViewer.getState().cameraDragging,
|
|
186
195
|
sceneApi: registrySceneApi,
|
|
187
|
-
selectNode:
|
|
196
|
+
selectNode: selectPlacedNode,
|
|
188
197
|
unit,
|
|
189
198
|
}),
|
|
190
|
-
[activeLevelId, registrySceneApi,
|
|
199
|
+
[activeLevelId, registrySceneApi, unit],
|
|
191
200
|
)
|
|
192
201
|
|
|
193
202
|
// Building transform for the local group — all building-relative tools live inside this group
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNodeId,
|
|
5
5
|
applyHeightPatch,
|
|
6
6
|
BlockNode,
|
|
7
|
+
BuildingNode,
|
|
7
8
|
CeilingNode,
|
|
8
9
|
createTerrainField,
|
|
9
10
|
DoorNode as DoorSchema,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
GROUND_SUPPORT_ID,
|
|
13
14
|
getFloorPlacedElevation,
|
|
14
15
|
initSpaceDetectionSync,
|
|
16
|
+
LevelNode as LevelSchema,
|
|
15
17
|
nodeRegistry,
|
|
16
18
|
registerNode,
|
|
17
19
|
resolveTerrainWallConstructionOptions,
|
|
@@ -180,6 +182,116 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
180
182
|
useScene.temporal.getState().resume()
|
|
181
183
|
})
|
|
182
184
|
|
|
185
|
+
describe('on a level below grade', () => {
|
|
186
|
+
const seedBasement = (thickness?: number) => {
|
|
187
|
+
const basement = LevelSchema.parse({
|
|
188
|
+
id: 'level_cellar',
|
|
189
|
+
parentId: 'building_house',
|
|
190
|
+
level: -1,
|
|
191
|
+
baseElevation: -2.5,
|
|
192
|
+
height: 2.5,
|
|
193
|
+
})
|
|
194
|
+
const main = LevelSchema.parse({
|
|
195
|
+
id: 'level_main',
|
|
196
|
+
parentId: 'building_house',
|
|
197
|
+
level: 0,
|
|
198
|
+
height: 2.5,
|
|
199
|
+
})
|
|
200
|
+
const building = BuildingNode.parse({
|
|
201
|
+
id: 'building_house',
|
|
202
|
+
children: [basement.id, main.id],
|
|
203
|
+
})
|
|
204
|
+
useScene.setState({
|
|
205
|
+
nodes: Object.fromEntries(
|
|
206
|
+
[building, basement, main].map((node) => [node.id, node as AnyNode]),
|
|
207
|
+
),
|
|
208
|
+
rootNodeIds: [building.id],
|
|
209
|
+
dirtyNodes: new Set(),
|
|
210
|
+
collections: {},
|
|
211
|
+
} as never)
|
|
212
|
+
useEditor.setState({
|
|
213
|
+
toolDefaults: {
|
|
214
|
+
...useEditor.getState().toolDefaults,
|
|
215
|
+
wall: thickness === undefined ? {} : { thickness },
|
|
216
|
+
},
|
|
217
|
+
} as never)
|
|
218
|
+
useScene.temporal.getState().clear()
|
|
219
|
+
useScene.temporal.getState().resume()
|
|
220
|
+
}
|
|
221
|
+
const select = (levelId: string) =>
|
|
222
|
+
useViewer.setState({
|
|
223
|
+
selection: { buildingId: 'building_house', levelId, zoneId: null, selectedIds: [] },
|
|
224
|
+
} as never)
|
|
225
|
+
let previousDefaults: ReturnType<typeof useEditor.getState>['toolDefaults']
|
|
226
|
+
beforeEach(() => {
|
|
227
|
+
previousDefaults = useEditor.getState().toolDefaults
|
|
228
|
+
})
|
|
229
|
+
afterEach(() => {
|
|
230
|
+
useEditor.setState({ toolDefaults: previousDefaults } as never)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('the wall tool draws an 8 in CMU foundation wall with 2x4 furring flat inside, 0.241 m thick', () => {
|
|
234
|
+
seedBasement()
|
|
235
|
+
select('level_cellar')
|
|
236
|
+
const created = createWallOnCurrentLevel([0, 0], [4, 0])
|
|
237
|
+
expect(created?.thickness).toBe(0.241)
|
|
238
|
+
expect(created?.assembly?.layers).toMatchObject([
|
|
239
|
+
{ material: 'cmu', core: true, thickness: 0.203 },
|
|
240
|
+
{ role: 'furring', thickness: 0.038 },
|
|
241
|
+
])
|
|
242
|
+
const stored = useScene.getState().nodes[created!.id] as WallNode
|
|
243
|
+
expect(stored.assembly).toEqual(created?.assembly)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('only the walls against the ground are foundation walls: the perimeter is, a partition and a wall inside are not', () => {
|
|
247
|
+
seedBasement()
|
|
248
|
+
select('level_cellar')
|
|
249
|
+
const sides: Array<[[number, number], [number, number]]> = [
|
|
250
|
+
[
|
|
251
|
+
[0, 0],
|
|
252
|
+
[8, 0],
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
[8, 0],
|
|
256
|
+
[8, 6],
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
[8, 6],
|
|
260
|
+
[0, 6],
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
[0, 6],
|
|
264
|
+
[0, 0],
|
|
265
|
+
],
|
|
266
|
+
]
|
|
267
|
+
for (const [start, end] of sides) {
|
|
268
|
+
expect(createWallOnCurrentLevel(start, end)?.thickness).toBe(0.241)
|
|
269
|
+
}
|
|
270
|
+
const partition = createWallOnCurrentLevel([4, 0], [4, 6])
|
|
271
|
+
expect(partition?.assembly).toBeUndefined()
|
|
272
|
+
const dangling = createWallOnCurrentLevel([0, 3], [3, 3])
|
|
273
|
+
expect(dangling?.assembly).toBeUndefined()
|
|
274
|
+
// a bay on the outside of the east wall is on the outline again
|
|
275
|
+
const bay = createWallOnCurrentLevel([8, 1], [11, 1])
|
|
276
|
+
expect(bay?.thickness).toBe(0.241)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('the ground floor over it keeps the plain wall', () => {
|
|
280
|
+
seedBasement()
|
|
281
|
+
select('level_main')
|
|
282
|
+
const created = createWallOnCurrentLevel([0, 0], [4, 0])
|
|
283
|
+
expect(created?.assembly).toBeUndefined()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('a thickness set in the tool defaults is kept, with no stack', () => {
|
|
287
|
+
seedBasement(0.3)
|
|
288
|
+
select('level_cellar')
|
|
289
|
+
const created = createWallOnCurrentLevel([0, 0], [4, 0])
|
|
290
|
+
expect(created?.thickness).toBe(0.3)
|
|
291
|
+
expect(created?.assembly).toBeUndefined()
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
183
295
|
test('endpoint near an existing corner attaches to the corner instead of splitting', () => {
|
|
184
296
|
const created = createWallOnCurrentLevel([2, 2], [3.99, 0])
|
|
185
297
|
|
|
@@ -726,63 +838,64 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
726
838
|
expect(useScene.temporal.getState().pastStates.length - before).toBe(1)
|
|
727
839
|
})
|
|
728
840
|
|
|
729
|
-
test.each([
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
841
|
+
test.each([0.05, 0.18])(
|
|
842
|
+
'a room divider preserves the footprint floor at %s and splits ceilings',
|
|
843
|
+
(elevation) => {
|
|
844
|
+
const walls = [
|
|
845
|
+
makeWall([0, 0], [8, 0], 'wall_bottom'),
|
|
846
|
+
makeWall([8, 0], [8, 6], 'wall_right'),
|
|
847
|
+
makeWall([8, 6], [0, 6], 'wall_top'),
|
|
848
|
+
makeWall([0, 6], [0, 0], 'wall_left'),
|
|
849
|
+
]
|
|
850
|
+
const slab = SlabNode.parse({
|
|
851
|
+
id: 'slab_auto',
|
|
852
|
+
parentId: LEVEL_ID,
|
|
853
|
+
polygon: [
|
|
854
|
+
[0, 0],
|
|
855
|
+
[8, 0],
|
|
856
|
+
[8, 6],
|
|
857
|
+
[0, 6],
|
|
858
|
+
],
|
|
859
|
+
elevation,
|
|
860
|
+
floorHeight: elevation,
|
|
861
|
+
thickness: 0.32,
|
|
862
|
+
autoFromWalls: true,
|
|
863
|
+
})
|
|
864
|
+
const ceiling = CeilingNode.parse({
|
|
865
|
+
id: 'ceiling_auto',
|
|
866
|
+
parentId: LEVEL_ID,
|
|
867
|
+
polygon: slab.polygon,
|
|
868
|
+
autoFromWalls: true,
|
|
869
|
+
})
|
|
870
|
+
seedLevel(walls, [slab, ceiling])
|
|
871
|
+
useScene.setState({
|
|
872
|
+
nodes: migrateFloorPlates(
|
|
873
|
+
migrateCeilingRoomLinks(migrateRoomZones(useScene.getState().nodes).nodes).nodes,
|
|
874
|
+
).nodes as Record<AnyNodeId, AnyNode>,
|
|
875
|
+
})
|
|
876
|
+
const stopDetection = initSpaceDetectionSync(useScene, useEditor)
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
expect(createWallOnCurrentLevel([4, 0], [4, 6])).not.toBeNull()
|
|
880
|
+
|
|
881
|
+
const nodes = Object.values(useScene.getState().nodes)
|
|
882
|
+
const slabs = nodes.filter((node) => node.type === 'slab')
|
|
883
|
+
const ceilings = nodes.filter((node) => node.type === 'ceiling')
|
|
884
|
+
expect(slabs).toHaveLength(1)
|
|
885
|
+
expect(slabs[0]).toMatchObject({ plateRole: 'base', floorHeight: elevation })
|
|
886
|
+
expect(slabs[0]!.zoneIds).toHaveLength(2)
|
|
887
|
+
expect(ceilings).toHaveLength(2)
|
|
888
|
+
expect(
|
|
889
|
+
slabs.every(
|
|
890
|
+
(node) => node.autoFromWalls && node.elevation === elevation && node.thickness === 0.32,
|
|
891
|
+
),
|
|
892
|
+
).toBe(true)
|
|
893
|
+
expect(ceilings.every((node) => node.autoFromWalls)).toBe(true)
|
|
894
|
+
} finally {
|
|
895
|
+
stopDetection()
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
)
|
|
786
899
|
|
|
787
900
|
test('close crossings reject the whole insertion without mutating the scene', () => {
|
|
788
901
|
seedLevel([
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './../../../components/ui/primitives/tooltip'
|
|
8
8
|
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
9
9
|
import { cn } from './../../../lib/utils'
|
|
10
|
+
import { useActionMenuPopupSide } from './placement'
|
|
10
11
|
|
|
11
12
|
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
12
13
|
label: string
|
|
@@ -14,6 +15,8 @@ interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
|
14
15
|
isActive?: boolean
|
|
15
16
|
tooltipContent?: React.ReactNode
|
|
16
17
|
tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
|
|
18
|
+
/** The tooltip portals to `<body>`, so a button in a themed scope passes the scope here. */
|
|
19
|
+
tooltipClassName?: string
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
|
|
@@ -26,12 +29,14 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
|
|
|
26
29
|
isActive,
|
|
27
30
|
tooltipContent,
|
|
28
31
|
tooltipSide,
|
|
32
|
+
tooltipClassName,
|
|
29
33
|
onClick,
|
|
30
34
|
onMouseEnter,
|
|
31
35
|
...props
|
|
32
36
|
},
|
|
33
37
|
ref,
|
|
34
38
|
) => {
|
|
39
|
+
const popupSide = useActionMenuPopupSide()
|
|
35
40
|
return (
|
|
36
41
|
<Tooltip>
|
|
37
42
|
<TooltipTrigger asChild>
|
|
@@ -68,7 +73,7 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
|
|
|
68
73
|
)}
|
|
69
74
|
</Button>
|
|
70
75
|
</TooltipTrigger>
|
|
71
|
-
<TooltipContent side={tooltipSide}>
|
|
76
|
+
<TooltipContent className={tooltipClassName} side={tooltipSide ?? popupSide}>
|
|
72
77
|
{tooltipContent || (
|
|
73
78
|
<p>
|
|
74
79
|
{label} {shortcut && `(${shortcut})`}
|
|
@@ -5,6 +5,7 @@ import Image from 'next/image'
|
|
|
5
5
|
import useEditor from '../../../store/use-editor'
|
|
6
6
|
import { visibleScene } from '../../../store/view-layout'
|
|
7
7
|
import { ActionButton } from './action-button'
|
|
8
|
+
import { FollowPascalToggle } from './follow-pascal-toggle'
|
|
8
9
|
|
|
9
10
|
export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
10
11
|
// Orbit stays useful in 2D-only (it spins the synced floorplan view), but
|
|
@@ -29,7 +30,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
29
30
|
<>
|
|
30
31
|
{/* Orbit CCW */}
|
|
31
32
|
<ActionButton
|
|
32
|
-
className="group hover:bg-
|
|
33
|
+
className="group hover:bg-black/4"
|
|
33
34
|
label="Orbit Left"
|
|
34
35
|
onClick={orbitCCW}
|
|
35
36
|
size="icon"
|
|
@@ -46,7 +47,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
46
47
|
|
|
47
48
|
{/* Orbit CW */}
|
|
48
49
|
<ActionButton
|
|
49
|
-
className="group hover:bg-
|
|
50
|
+
className="group hover:bg-black/4"
|
|
50
51
|
label="Orbit Right"
|
|
51
52
|
onClick={orbitCW}
|
|
52
53
|
size="icon"
|
|
@@ -63,10 +64,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
|
|
|
63
64
|
</>
|
|
64
65
|
)}
|
|
65
66
|
|
|
67
|
+
{/* Follow Pascal */}
|
|
68
|
+
{!is2dOnly && <FollowPascalToggle />}
|
|
69
|
+
|
|
66
70
|
{/* Top View */}
|
|
67
71
|
{!is2dOnly && (
|
|
68
72
|
<ActionButton
|
|
69
|
-
className="group hover:bg-
|
|
73
|
+
className="group hover:bg-black/4"
|
|
70
74
|
label="Top View"
|
|
71
75
|
onClick={goToTopView}
|
|
72
76
|
size="icon"
|
|
@@ -8,6 +8,7 @@ import { cn } from './../../../lib/utils'
|
|
|
8
8
|
import useEditor from './../../../store/use-editor'
|
|
9
9
|
import { ActionButton } from './action-button'
|
|
10
10
|
import { MeasurementControl } from './measurement-control'
|
|
11
|
+
import { PointAskButton } from './point-ask-button'
|
|
11
12
|
|
|
12
13
|
type ControlId = 'select' | 'box-select' | 'delete'
|
|
13
14
|
|
|
@@ -29,16 +30,16 @@ const controls: ControlConfig[] = [
|
|
|
29
30
|
imageSrc: '/icons/select.webp',
|
|
30
31
|
label: 'Select',
|
|
31
32
|
shortcut: 'V',
|
|
32
|
-
color: 'hover:bg-blue-500/20 hover:text-blue-400',
|
|
33
|
-
activeColor: 'bg-blue-500/20 text-blue-400',
|
|
33
|
+
color: 'hover:bg-blue-500/20 hover:text-blue-600 dark:hover:text-blue-400',
|
|
34
|
+
activeColor: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 dark:hover:text-accent-foreground',
|
|
34
35
|
},
|
|
35
36
|
{
|
|
36
37
|
id: 'delete',
|
|
37
38
|
icon: Trash2,
|
|
38
39
|
label: 'Delete',
|
|
39
40
|
shortcut: 'X',
|
|
40
|
-
color: 'hover:bg-red-500/20 hover:text-red-400',
|
|
41
|
-
activeColor: 'bg-red-500/20 text-red-400',
|
|
41
|
+
color: 'hover:bg-red-500/20 hover:text-red-600 dark:hover:text-red-400',
|
|
42
|
+
activeColor: 'bg-red-500/20 text-red-600 dark:text-red-400 dark:hover:text-accent-foreground',
|
|
42
43
|
},
|
|
43
44
|
]
|
|
44
45
|
|
|
@@ -75,14 +76,19 @@ export function ControlModes() {
|
|
|
75
76
|
|
|
76
77
|
return (
|
|
77
78
|
<Fragment key={c.id}>
|
|
78
|
-
{c.id === 'delete' ?
|
|
79
|
+
{c.id === 'delete' ? (
|
|
80
|
+
<>
|
|
81
|
+
<PointAskButton />
|
|
82
|
+
<MeasurementControl />
|
|
83
|
+
</>
|
|
84
|
+
) : null}
|
|
79
85
|
<ActionButton
|
|
80
86
|
className={cn(
|
|
81
87
|
'group text-muted-foreground',
|
|
82
88
|
!(isImageMode || isActive) && c.color,
|
|
83
89
|
!isImageMode && isActive && c.activeColor,
|
|
84
|
-
isImageMode && isActive && 'bg-
|
|
85
|
-
isImageMode && !isActive && 'hover:bg-
|
|
90
|
+
isImageMode && isActive && 'bg-black/6 hover:bg-black/6 dark:bg-white/10',
|
|
91
|
+
isImageMode && !isActive && 'hover:bg-black/4',
|
|
86
92
|
)}
|
|
87
93
|
// A static hook for a host app that wants to point a first-run
|
|
88
94
|
// tour at this button. Nothing here reads it.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AnimatePresence, motion } from 'motion/react'
|
|
4
|
+
import { useReducedMotion } from '../../../hooks/use-reduced-motion'
|
|
5
|
+
import useFollowPascal from '../../../store/use-follow-pascal'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Offered while a build plays and the person has the camera: one press and the camera goes back to
|
|
9
|
+
* following. It comes up from where the toolbar is (origin-aware) and goes the same way.
|
|
10
|
+
*/
|
|
11
|
+
export function FollowPascalPill() {
|
|
12
|
+
const shows = useFollowPascal((state) => state.on && state.paused && state.building)
|
|
13
|
+
const resume = useFollowPascal((state) => state.resume)
|
|
14
|
+
const reduced = useReducedMotion()
|
|
15
|
+
return (
|
|
16
|
+
<AnimatePresence>
|
|
17
|
+
{shows ? (
|
|
18
|
+
<motion.button
|
|
19
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
20
|
+
className="-translate-x-1/2 fixed bottom-24 left-1/2 z-50 rounded-full border border-violet-400/40 bg-background/90 px-4 py-2 font-medium text-sm text-violet-700 shadow-xl dark:text-violet-200 backdrop-blur-md active:scale-[0.97]"
|
|
21
|
+
exit={{ opacity: 0, y: reduced ? 0 : 8, scale: reduced ? 1 : 0.96 }}
|
|
22
|
+
initial={{ opacity: 0, y: reduced ? 0 : 8, scale: reduced ? 1 : 0.96 }}
|
|
23
|
+
onClick={resume}
|
|
24
|
+
transition={reduced ? { duration: 0.12 } : { type: 'spring', bounce: 0, duration: 0.3 }}
|
|
25
|
+
type="button"
|
|
26
|
+
>
|
|
27
|
+
Resume following
|
|
28
|
+
</motion.button>
|
|
29
|
+
) : null}
|
|
30
|
+
</AnimatePresence>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Video } from 'lucide-react'
|
|
4
|
+
import { cn } from '../../../lib/utils'
|
|
5
|
+
import useFollowPascal from '../../../store/use-follow-pascal'
|
|
6
|
+
import { ActionButton } from './action-button'
|
|
7
|
+
|
|
8
|
+
/** The toolbar's switch for Follow Pascal: the camera follows what the agent builds. */
|
|
9
|
+
export function FollowPascalToggle({
|
|
10
|
+
className,
|
|
11
|
+
tooltipClassName,
|
|
12
|
+
}: {
|
|
13
|
+
className?: string
|
|
14
|
+
tooltipClassName?: string
|
|
15
|
+
}) {
|
|
16
|
+
const on = useFollowPascal((state) => state.on)
|
|
17
|
+
const setOn = useFollowPascal((state) => state.setOn)
|
|
18
|
+
return (
|
|
19
|
+
<ActionButton
|
|
20
|
+
aria-pressed={on}
|
|
21
|
+
className={cn(
|
|
22
|
+
on
|
|
23
|
+
? 'bg-violet-500/20 text-violet-600 dark:text-violet-300 dark:hover:text-accent-foreground'
|
|
24
|
+
: 'hover:bg-black/4 hover:text-violet-600 dark:hover:text-violet-300',
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
label={`Follow Pascal: ${on ? 'On' : 'Off'}`}
|
|
28
|
+
onClick={() => setOn(!on)}
|
|
29
|
+
size="icon"
|
|
30
|
+
tooltipClassName={tooltipClassName}
|
|
31
|
+
variant="ghost"
|
|
32
|
+
>
|
|
33
|
+
<Video className="h-6 w-6" />
|
|
34
|
+
</ActionButton>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -11,6 +11,8 @@ import { cn } from './../../../lib/utils'
|
|
|
11
11
|
import useEditor from './../../../store/use-editor'
|
|
12
12
|
import { CameraActions } from './camera-actions'
|
|
13
13
|
import { ControlModes } from './control-modes'
|
|
14
|
+
import { FollowPascalPill } from './follow-pascal-pill'
|
|
15
|
+
import { type ActionMenuPlacement, ActionMenuPlacementProvider } from './placement'
|
|
14
16
|
import { SecondaryToggles } from './view-toggles'
|
|
15
17
|
|
|
16
18
|
// Mobile bottom offset matches the viewer's overlap behind the sheet's
|
|
@@ -18,7 +20,16 @@ import { SecondaryToggles } from './view-toggles'
|
|
|
18
20
|
// just above that strip instead of inside it.
|
|
19
21
|
const MOBILE_BOTTOM_OFFSET = 24
|
|
20
22
|
|
|
21
|
-
export function ActionMenu({
|
|
23
|
+
export function ActionMenu({
|
|
24
|
+
className,
|
|
25
|
+
inline = false,
|
|
26
|
+
placement = 'bottom',
|
|
27
|
+
}: {
|
|
28
|
+
className?: string
|
|
29
|
+
// Rendered in the flow of a layout slot instead of floating over the viewer.
|
|
30
|
+
inline?: boolean
|
|
31
|
+
placement?: ActionMenuPlacement
|
|
32
|
+
}) {
|
|
22
33
|
const isMobile = useIsMobile()
|
|
23
34
|
const readOnly = useScene((s) => s.readOnly)
|
|
24
35
|
const hasSelectionOnMobile = useViewer((s) => isMobile && s.selection.selectedIds.length > 0)
|
|
@@ -47,41 +58,53 @@ export function ActionMenu({ className }: { className?: string }) {
|
|
|
47
58
|
? { duration: 0 }
|
|
48
59
|
: { type: 'spring' as const, bounce: 0.2, duration: 0.4 }
|
|
49
60
|
|
|
61
|
+
// Mobile keeps the bottom rail: the sheet owns the rest of the screen.
|
|
62
|
+
const effectivePlacement = isMobile ? 'bottom' : placement
|
|
63
|
+
|
|
50
64
|
return (
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
<ActionMenuPlacementProvider value={effectivePlacement}>
|
|
66
|
+
<TooltipProvider>
|
|
67
|
+
<FollowPascalPill />
|
|
68
|
+
<motion.div
|
|
69
|
+
className={cn(
|
|
70
|
+
'z-50',
|
|
71
|
+
isMobile
|
|
72
|
+
? 'absolute left-1/2 origin-bottom -translate-x-1/2 scale-90'
|
|
73
|
+
: inline
|
|
74
|
+
? 'relative'
|
|
75
|
+
: effectivePlacement === 'top'
|
|
76
|
+
? 'fixed top-3 left-1/2 -translate-x-1/2'
|
|
77
|
+
: 'fixed bottom-6 left-1/2 -translate-x-1/2',
|
|
78
|
+
'rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md',
|
|
79
|
+
'transition-colors duration-200 ease-out',
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
layout
|
|
83
|
+
style={isMobile ? { bottom: MOBILE_BOTTOM_OFFSET } : undefined}
|
|
84
|
+
transition={transition}
|
|
85
|
+
>
|
|
86
|
+
{isMobile ? (
|
|
87
|
+
<div className="flex flex-col items-stretch gap-0.5 px-2 py-1.5">
|
|
88
|
+
{/* Row 1: control modes only */}
|
|
89
|
+
<div className="flex items-center justify-center gap-1">
|
|
90
|
+
<ControlModes />
|
|
91
|
+
</div>
|
|
92
|
+
{/* Row 2: secondary toggles (orbit + top view hidden) */}
|
|
93
|
+
<div className="flex items-center justify-center gap-1 border-border/50 border-t pt-1">
|
|
94
|
+
<SecondaryToggles />
|
|
95
|
+
</div>
|
|
69
96
|
</div>
|
|
70
|
-
|
|
71
|
-
<div className="flex items-center justify-center gap-1
|
|
97
|
+
) : (
|
|
98
|
+
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
|
|
99
|
+
<ControlModes />
|
|
100
|
+
<div className="mx-1 h-5 w-px bg-border" />
|
|
72
101
|
<SecondaryToggles />
|
|
102
|
+
<div className="mx-1 h-5 w-px bg-border" />
|
|
103
|
+
<CameraActions />
|
|
73
104
|
</div>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<div className="mx-1 h-5 w-px bg-border" />
|
|
79
|
-
<SecondaryToggles />
|
|
80
|
-
<div className="mx-1 h-5 w-px bg-border" />
|
|
81
|
-
<CameraActions />
|
|
82
|
-
</div>
|
|
83
|
-
)}
|
|
84
|
-
</motion.div>
|
|
85
|
-
</TooltipProvider>
|
|
105
|
+
)}
|
|
106
|
+
</motion.div>
|
|
107
|
+
</TooltipProvider>
|
|
108
|
+
</ActionMenuPlacementProvider>
|
|
86
109
|
)
|
|
87
110
|
}
|