@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,4 +1,6 @@
|
|
|
1
|
+
import { downloadProjectFile } from '../../../../../lib/download-project-file'
|
|
1
2
|
import {
|
|
3
|
+
getProjectFileSink,
|
|
2
4
|
clearSceneHistory,
|
|
3
5
|
DEFAULT_LEVEL_HEIGHT,
|
|
4
6
|
emitter,
|
|
@@ -26,9 +28,12 @@ import {
|
|
|
26
28
|
Layers,
|
|
27
29
|
ListTree,
|
|
28
30
|
Map as MapIcon,
|
|
31
|
+
Monitor,
|
|
32
|
+
Moon,
|
|
29
33
|
Save,
|
|
30
34
|
ChevronDown,
|
|
31
35
|
Send,
|
|
36
|
+
Sun,
|
|
32
37
|
Trash2,
|
|
33
38
|
Upload,
|
|
34
39
|
} from 'lucide-react'
|
|
@@ -55,6 +60,7 @@ import {
|
|
|
55
60
|
sendGlbToLocalApp,
|
|
56
61
|
waitForLocalImport,
|
|
57
62
|
} from '../../../../../lib/send-to-app'
|
|
63
|
+
import { SegmentedControl } from './../../../../../components/ui/controls/segmented-control'
|
|
58
64
|
import { Button } from './../../../../../components/ui/primitives/button'
|
|
59
65
|
import {
|
|
60
66
|
Dialog,
|
|
@@ -66,12 +72,15 @@ import { Input } from './../../../../../components/ui/primitives/input'
|
|
|
66
72
|
import { Switch } from './../../../../../components/ui/primitives/switch'
|
|
67
73
|
import { cn } from './../../../../../lib/utils'
|
|
68
74
|
import { refusedObjectsNotice } from './../../../../../components/editor/group-actions'
|
|
75
|
+
import { addStoreyBelow, addStoreyOnTop } from './../../../../../lib/add-storey'
|
|
69
76
|
import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
|
|
70
77
|
import { bringBuildArtifacts } from './../../../../../lib/scene-clipboard'
|
|
71
78
|
import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
|
|
72
79
|
import useFloorplanMode from './../../../../../store/use-floorplan-mode'
|
|
73
80
|
import { type SendToAppStep, useSendToApp } from './../../../../../store/use-send-to-app'
|
|
74
81
|
import { AudioSettingsDialog } from './audio-settings-dialog'
|
|
82
|
+
import { AnimationSpeedSetting } from './animation-speed-setting'
|
|
83
|
+
import { ConstructionAnimationSetting } from './construction-animation-setting'
|
|
75
84
|
import { KeyboardShortcutsDialog } from './keyboard-shortcuts-dialog'
|
|
76
85
|
import { LoadBuildDialog, type PendingImport } from './load-build-dialog'
|
|
77
86
|
import { PrintExportButton } from './print-export-button'
|
|
@@ -275,7 +284,8 @@ function SettingsSubheading({ children }: { children: ReactNode }) {
|
|
|
275
284
|
return <h4 className="font-medium text-muted-foreground text-xs">{children}</h4>
|
|
276
285
|
}
|
|
277
286
|
|
|
278
|
-
|
|
287
|
+
/** One on/off preference: a label, a line on what it does, a switch. Hosts use it for their rows. */
|
|
288
|
+
export function SettingsSwitchRow({
|
|
279
289
|
checked,
|
|
280
290
|
description,
|
|
281
291
|
label,
|
|
@@ -300,6 +310,65 @@ function SettingsSwitchRow({
|
|
|
300
310
|
)
|
|
301
311
|
}
|
|
302
312
|
|
|
313
|
+
export type InterfaceTheme = 'system' | 'light' | 'dark'
|
|
314
|
+
|
|
315
|
+
const INTERFACE_THEME_OPTIONS: { label: ReactNode; value: InterfaceTheme }[] = [
|
|
316
|
+
{
|
|
317
|
+
label: (
|
|
318
|
+
<>
|
|
319
|
+
<Monitor className="size-3.5" />
|
|
320
|
+
System
|
|
321
|
+
</>
|
|
322
|
+
),
|
|
323
|
+
value: 'system',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
label: (
|
|
327
|
+
<>
|
|
328
|
+
<Sun className="size-3.5" />
|
|
329
|
+
Light
|
|
330
|
+
</>
|
|
331
|
+
),
|
|
332
|
+
value: 'light',
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
label: (
|
|
336
|
+
<>
|
|
337
|
+
<Moon className="size-3.5" />
|
|
338
|
+
Dark
|
|
339
|
+
</>
|
|
340
|
+
),
|
|
341
|
+
value: 'dark',
|
|
342
|
+
},
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The interface theme as a System / Light / Dark row. The editor applies no theme itself: its
|
|
347
|
+
* chrome follows the host's `.dark` class, so a host that owns the setting passes this row.
|
|
348
|
+
*/
|
|
349
|
+
export function InterfaceThemeRow({
|
|
350
|
+
onChange,
|
|
351
|
+
value,
|
|
352
|
+
}: {
|
|
353
|
+
onChange: (value: InterfaceTheme) => void
|
|
354
|
+
value: InterfaceTheme
|
|
355
|
+
}) {
|
|
356
|
+
const labelId = useId()
|
|
357
|
+
return (
|
|
358
|
+
<div className="flex flex-col gap-2 py-1 @sm:flex-row @sm:items-center @sm:justify-between @sm:gap-4">
|
|
359
|
+
<div className="min-w-0 space-y-0.5">
|
|
360
|
+
<p className="font-medium text-sm" id={labelId}>
|
|
361
|
+
Interface theme
|
|
362
|
+
</p>
|
|
363
|
+
<p className="text-muted-foreground text-xs">System follows your device’s appearance.</p>
|
|
364
|
+
</div>
|
|
365
|
+
<div aria-labelledby={labelId} className="w-full shrink-0 @sm:w-60" role="group">
|
|
366
|
+
<SegmentedControl onChange={onChange} options={INTERFACE_THEME_OPTIONS} value={value} />
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
303
372
|
export interface SettingsPanelProps {
|
|
304
373
|
/** Merged onto the scrolling root so hosts can restyle padding (e.g. inside a dialog). */
|
|
305
374
|
className?: string
|
|
@@ -311,6 +380,11 @@ export interface SettingsPanelProps {
|
|
|
311
380
|
field: 'isPrivate' | 'showScansPublic' | 'showGuidesPublic',
|
|
312
381
|
value: boolean,
|
|
313
382
|
) => Promise<void>
|
|
383
|
+
/**
|
|
384
|
+
* Rows a host adds under Preferences, for its own features and settings (SettingsSwitchRow,
|
|
385
|
+
* InterfaceThemeRow).
|
|
386
|
+
*/
|
|
387
|
+
preferences?: ReactNode
|
|
314
388
|
}
|
|
315
389
|
|
|
316
390
|
function FloorSettingsSection() {
|
|
@@ -346,20 +420,14 @@ function FloorSettingsSection() {
|
|
|
346
420
|
|
|
347
421
|
if (!building) return null
|
|
348
422
|
|
|
349
|
-
const
|
|
350
|
-
const newLevel =
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
})
|
|
356
|
-
createNode(newLevel, building.id)
|
|
423
|
+
const addFloor = () => {
|
|
424
|
+
const newLevel = addStoreyOnTop(building.id)
|
|
425
|
+
setSelection({ buildingId: building.id, levelId: newLevel.id })
|
|
426
|
+
}
|
|
427
|
+
const addBasement = () => {
|
|
428
|
+
const newLevel = addStoreyBelow(building.id)
|
|
357
429
|
setSelection({ buildingId: building.id, levelId: newLevel.id })
|
|
358
430
|
}
|
|
359
|
-
|
|
360
|
-
const addFloor = () =>
|
|
361
|
-
addLevel(levels.length === 0 ? 0 : Math.max(0, ...levels.map((level) => level.level)) + 1)
|
|
362
|
-
const addBasement = () => addLevel(Math.min(0, ...levels.map((level) => level.level)) - 1)
|
|
363
431
|
|
|
364
432
|
return (
|
|
365
433
|
<div className="space-y-3">
|
|
@@ -435,6 +503,7 @@ export function SettingsPanel({
|
|
|
435
503
|
projectName,
|
|
436
504
|
projectVisibility,
|
|
437
505
|
onVisibilityChange,
|
|
506
|
+
preferences,
|
|
438
507
|
}: SettingsPanelProps = {}) {
|
|
439
508
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
440
509
|
const copyResetTimeoutRef = useRef<number | null>(null)
|
|
@@ -556,21 +625,20 @@ export function SettingsPanel({
|
|
|
556
625
|
}
|
|
557
626
|
const json = JSON.stringify(sceneData, null, 2)
|
|
558
627
|
const blob = new Blob([json], { type: 'application/json' })
|
|
559
|
-
const url = URL.createObjectURL(blob)
|
|
560
|
-
const link = document.createElement('a')
|
|
561
|
-
link.href = url
|
|
562
628
|
const date = new Date().toISOString().split('T')[0]
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
629
|
+
setModelExportError(null)
|
|
630
|
+
void downloadProjectFile(blob, `layout_${date}.json`).catch((error: unknown) => {
|
|
631
|
+
setModelExportError(error instanceof Error ? error.message : 'Could not save the file')
|
|
632
|
+
})
|
|
566
633
|
}
|
|
567
634
|
|
|
568
635
|
const handleFileLoad = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
569
636
|
const file = e.target.files?.[0]
|
|
570
637
|
if (!file) return
|
|
571
638
|
|
|
639
|
+
const fileSink = getProjectFileSink()
|
|
572
640
|
const reader = new FileReader()
|
|
573
|
-
reader.onload = (event) => {
|
|
641
|
+
reader.onload = async (event) => {
|
|
574
642
|
const text = event.target?.result as string
|
|
575
643
|
let parsed: unknown
|
|
576
644
|
try {
|
|
@@ -597,10 +665,20 @@ export function SettingsPanel({
|
|
|
597
665
|
})
|
|
598
666
|
return
|
|
599
667
|
}
|
|
668
|
+
const result = validateBuildJson(parsed)
|
|
669
|
+
if (result.ok) {
|
|
670
|
+
try {
|
|
671
|
+
await fileSink?.({ blob: file, name: file.name })
|
|
672
|
+
} catch (error) {
|
|
673
|
+
setModelExportError(error instanceof Error ? error.message : 'Could not save the source file')
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
if (fileSink !== getProjectFileSink()) return
|
|
677
|
+
}
|
|
600
678
|
setPendingImport({
|
|
601
679
|
fileName: file.name,
|
|
602
680
|
fileSizeBytes: file.size,
|
|
603
|
-
result
|
|
681
|
+
result,
|
|
604
682
|
})
|
|
605
683
|
}
|
|
606
684
|
reader.readAsText(file)
|
|
@@ -1184,7 +1262,13 @@ export function SettingsPanel({
|
|
|
1184
1262
|
/>
|
|
1185
1263
|
</SettingsSection>
|
|
1186
1264
|
|
|
1187
|
-
<SettingsSection
|
|
1265
|
+
<SettingsSection
|
|
1266
|
+
description="Sound levels, the construction animation and keyboard shortcuts."
|
|
1267
|
+
title="Preferences"
|
|
1268
|
+
>
|
|
1269
|
+
<ConstructionAnimationSetting />
|
|
1270
|
+
<AnimationSpeedSetting />
|
|
1271
|
+
{preferences}
|
|
1188
1272
|
<div className="grid gap-2 @sm:grid-cols-2">
|
|
1189
1273
|
<AudioSettingsDialog />
|
|
1190
1274
|
<KeyboardShortcutsDialog />
|
|
@@ -1204,7 +1288,7 @@ export function SettingsPanel({
|
|
|
1204
1288
|
</Button>
|
|
1205
1289
|
</div>
|
|
1206
1290
|
</DialogTrigger>
|
|
1207
|
-
<DialogContent className="h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
|
|
1291
|
+
<DialogContent className="dark h-[80vh] max-w-[95vw] gap-0 overflow-hidden border-0 bg-[#1e1e1e] p-0 shadow-none sm:max-w-5xl">
|
|
1208
1292
|
<DialogTitle className="sr-only">Scene Graph</DialogTitle>
|
|
1209
1293
|
<div
|
|
1210
1294
|
className="flex h-full min-h-0 w-full min-w-0 *:h-full *:w-full *:overflow-y-auto"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { InterfaceThemeRow, SettingsPanel } from '.'
|
|
4
|
+
|
|
5
|
+
const pressedLabels = (html: string) =>
|
|
6
|
+
[...html.matchAll(/aria-pressed="true"[^>]*>(?:<[^>]+>)*([^<]+)/g)].map((match) => match[1])
|
|
7
|
+
|
|
8
|
+
describe('Interface theme setting', () => {
|
|
9
|
+
test("the row shows the host's value pressed", () => {
|
|
10
|
+
const html = renderToStaticMarkup(<InterfaceThemeRow onChange={() => {}} value="light" />)
|
|
11
|
+
expect(html).toContain('Interface theme')
|
|
12
|
+
expect(pressedLabels(html)).toEqual(['Light'])
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('Settings shows the row a host passes, and none without it', () => {
|
|
16
|
+
const row = <InterfaceThemeRow onChange={() => {}} value="system" />
|
|
17
|
+
expect(renderToStaticMarkup(<SettingsPanel preferences={row} />)).toContain('Interface theme')
|
|
18
|
+
expect(renderToStaticMarkup(<SettingsPanel />)).not.toContain('Interface theme')
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getProjectFileSink } from '@pascal-app/core'
|
|
2
|
+
import { downloadProjectFile } from '../../../../../lib/download-project-file'
|
|
1
3
|
import { AlertTriangle, Printer } from 'lucide-react'
|
|
2
4
|
import { useState } from 'react'
|
|
3
5
|
import { Button } from '../../../../../components/ui/primitives/button'
|
|
@@ -23,15 +25,6 @@ type PreparedPrintExport = {
|
|
|
23
25
|
|
|
24
26
|
type PrintModelExportFormat = Extract<ModelExportFormat, 'print-3mf' | 'print-stl'>
|
|
25
27
|
|
|
26
|
-
function downloadArtifact(artifact: ModelExportArtifact) {
|
|
27
|
-
const url = URL.createObjectURL(artifact.blob)
|
|
28
|
-
const link = document.createElement('a')
|
|
29
|
-
link.href = url
|
|
30
|
-
link.download = artifact.filename
|
|
31
|
-
link.click()
|
|
32
|
-
URL.revokeObjectURL(url)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
28
|
function firstBlockingMessage(report: PrintExportReport | PrintLevelBundleReport) {
|
|
36
29
|
const bundleDiagnostic = report.diagnostics.find((item) => item.severity === 'error')
|
|
37
30
|
if (bundleDiagnostic || !isPrintLevelBundleReport(report)) return bundleDiagnostic?.message
|
|
@@ -81,11 +74,12 @@ export function PrintExportButton({ onlyVisible }: { onlyVisible: boolean }) {
|
|
|
81
74
|
const handleExport = async (format: PrintModelExportFormat) => {
|
|
82
75
|
if (!modelExport) return
|
|
83
76
|
|
|
77
|
+
const fileSink = getProjectFileSink()
|
|
84
78
|
setExportingFormat(format)
|
|
85
79
|
setError(null)
|
|
86
80
|
try {
|
|
87
81
|
const prepared = await preparePrintExport(modelExport, onlyVisible, format)
|
|
88
|
-
|
|
82
|
+
await downloadProjectFile(prepared.artifact.blob, prepared.artifact.filename, fileSink)
|
|
89
83
|
} catch (reason) {
|
|
90
84
|
setError(reason instanceof Error ? reason.message : '3D print export failed.')
|
|
91
85
|
} finally {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type BuildingNode,
|
|
1
|
+
import { type BuildingNode, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { Building2, Plus } from 'lucide-react'
|
|
4
4
|
import { memo, useState } from 'react'
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow'
|
|
6
|
+
import { addStoreyOnTop } from './../../../../../lib/add-storey'
|
|
6
7
|
import {
|
|
7
8
|
Tooltip,
|
|
8
9
|
TooltipContent,
|
|
@@ -23,7 +24,6 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
|
|
|
23
24
|
isLast,
|
|
24
25
|
}: BuildingTreeNodeProps) {
|
|
25
26
|
const [expanded, setExpanded] = useState(true)
|
|
26
|
-
const createNode = useScene((state) => state.createNode)
|
|
27
27
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
28
28
|
const name = useScene((s) => s.nodes[nodeId]?.name)
|
|
29
29
|
// Levels and elevators first, units after, so the units read as one group.
|
|
@@ -46,15 +46,7 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
|
|
|
46
46
|
|
|
47
47
|
const handleAddLevel = (e: React.MouseEvent) => {
|
|
48
48
|
e.stopPropagation()
|
|
49
|
-
|
|
50
|
-
const levelCount = children.filter((childId) => nodes[childId]?.type === 'level').length
|
|
51
|
-
const newLevel = LevelNode.parse({
|
|
52
|
-
level: levelCount,
|
|
53
|
-
height: DEFAULT_LEVEL_HEIGHT,
|
|
54
|
-
children: [],
|
|
55
|
-
parentId: nodeId,
|
|
56
|
-
})
|
|
57
|
-
createNode(newLevel, nodeId)
|
|
49
|
+
addStoreyOnTop(nodeId)
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
return (
|
|
@@ -2,10 +2,9 @@ import {
|
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
type BuildingNode,
|
|
5
|
-
DEFAULT_LEVEL_HEIGHT,
|
|
6
5
|
emitter,
|
|
7
6
|
type GuideNode,
|
|
8
|
-
LevelNode,
|
|
7
|
+
type LevelNode,
|
|
9
8
|
type ScanNode,
|
|
10
9
|
type SiteNode,
|
|
11
10
|
useScene,
|
|
@@ -55,6 +54,7 @@ import {
|
|
|
55
54
|
type LevelDuplicatePreset,
|
|
56
55
|
} from '@pascal-app/core/building'
|
|
57
56
|
import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
|
|
57
|
+
import { addStoreyOnTop } from './../../../../../lib/add-storey'
|
|
58
58
|
import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
|
|
59
59
|
import {
|
|
60
60
|
formatAreaLabel,
|
|
@@ -200,7 +200,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
|
|
|
200
200
|
className={cn(
|
|
201
201
|
'flex h-6 w-6 cursor-pointer items-center justify-center rounded transition-colors',
|
|
202
202
|
isEditing
|
|
203
|
-
? 'bg-orange-500/20 text-orange-400'
|
|
203
|
+
? 'bg-orange-500/20 text-orange-700 dark:text-orange-400'
|
|
204
204
|
: 'text-muted-foreground hover:bg-accent',
|
|
205
205
|
)}
|
|
206
206
|
onClick={handleToggleEdit}
|
|
@@ -256,7 +256,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
|
|
|
256
256
|
className={cn(
|
|
257
257
|
'flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded',
|
|
258
258
|
points.length > 3
|
|
259
|
-
? 'text-muted-foreground hover:bg-red-500/20 hover:text-red-400'
|
|
259
|
+
? 'text-muted-foreground hover:bg-red-500/20 hover:text-red-600 dark:hover:text-red-400'
|
|
260
260
|
: 'cursor-not-allowed text-muted-foreground/30',
|
|
261
261
|
)}
|
|
262
262
|
disabled={points.length <= 3}
|
|
@@ -1014,7 +1014,6 @@ const LevelsSection = memo(function LevelsSection({
|
|
|
1014
1014
|
onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void
|
|
1015
1015
|
onDeleteAsset?: (projectId: string, url: string) => void
|
|
1016
1016
|
} = {}) {
|
|
1017
|
-
const createNode = useScene((state) => state.createNode)
|
|
1018
1017
|
const updateNode = useScene((state) => state.updateNode)
|
|
1019
1018
|
const selectedBuildingId = useViewer((state) => state.selection.buildingId)
|
|
1020
1019
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
@@ -1037,13 +1036,7 @@ const LevelsSection = memo(function LevelsSection({
|
|
|
1037
1036
|
if (!building) return null
|
|
1038
1037
|
|
|
1039
1038
|
const handleAddLevel = () => {
|
|
1040
|
-
const newLevel =
|
|
1041
|
-
level: levels.length,
|
|
1042
|
-
height: DEFAULT_LEVEL_HEIGHT,
|
|
1043
|
-
children: [],
|
|
1044
|
-
parentId: building.id,
|
|
1045
|
-
})
|
|
1046
|
-
createNode(newLevel, building.id)
|
|
1039
|
+
const newLevel = addStoreyOnTop(building.id)
|
|
1047
1040
|
setSelection({ buildingId: building.id, levelId: newLevel.id })
|
|
1048
1041
|
}
|
|
1049
1042
|
|
|
@@ -283,14 +283,15 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
283
283
|
/>
|
|
284
284
|
)}
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
286
|
+
{/* A row with nothing to fold keeps the space, and a click there selects the row. */}
|
|
287
|
+
{hasChildren ? (
|
|
288
|
+
<button
|
|
289
|
+
className="z-10 flex h-4 w-4 shrink-0 items-center justify-center bg-inherit"
|
|
290
|
+
onClick={(e) => {
|
|
291
|
+
e.stopPropagation()
|
|
292
|
+
onToggle()
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
294
295
|
<motion.div
|
|
295
296
|
animate={{ rotate: expanded ? 90 : 0 }}
|
|
296
297
|
initial={false}
|
|
@@ -298,8 +299,10 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
298
299
|
>
|
|
299
300
|
<ChevronRight className="h-3 w-3" />
|
|
300
301
|
</motion.div>
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
</button>
|
|
303
|
+
) : (
|
|
304
|
+
<span className="h-4 w-4 shrink-0" />
|
|
305
|
+
)}
|
|
303
306
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
304
307
|
<span
|
|
305
308
|
className={cn(
|
|
@@ -195,14 +195,14 @@ export function ZonePanel() {
|
|
|
195
195
|
/>
|
|
196
196
|
<ActionButton
|
|
197
197
|
className="w-full flex-none"
|
|
198
|
-
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
198
|
+
icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
|
|
199
199
|
label="Delete"
|
|
200
200
|
onClick={() => deleteSelectedZone(false)}
|
|
201
201
|
type="button"
|
|
202
202
|
/>
|
|
203
203
|
<ActionButton
|
|
204
204
|
className="w-full flex-none"
|
|
205
|
-
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
205
|
+
icon={<Trash2 className="h-4 w-4 text-red-600 dark:text-red-400" />}
|
|
206
206
|
label="Delete with contents"
|
|
207
207
|
onClick={() => deleteSelectedZone(true)}
|
|
208
208
|
type="button"
|
|
@@ -47,8 +47,8 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
|
|
|
47
47
|
className={cn(
|
|
48
48
|
'relative h-7 rounded-md px-3 font-medium text-sm transition-colors',
|
|
49
49
|
isActive
|
|
50
|
-
? 'bg-accent text-foreground'
|
|
51
|
-
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
|
|
50
|
+
? 'bg-sidebar-accent text-foreground'
|
|
51
|
+
: 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground',
|
|
52
52
|
)}
|
|
53
53
|
key={tab.id}
|
|
54
54
|
onClick={() => {
|
|
@@ -101,8 +101,8 @@ export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailPr
|
|
|
101
101
|
className={cn(
|
|
102
102
|
'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
|
|
103
103
|
showActive
|
|
104
|
-
? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
|
|
105
|
-
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
|
|
104
|
+
? 'bg-sidebar-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
|
|
105
|
+
: 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
|
|
106
106
|
)}
|
|
107
107
|
onClick={() => {
|
|
108
108
|
triggerSFX('sfx:menu-click')
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from 'lucide-react'
|
|
29
29
|
import { cn } from '../../lib/utils'
|
|
30
30
|
import { ActionButton } from '../ui/action-menu/action-button'
|
|
31
|
+
import { FollowPascalToggle } from '../ui/action-menu/follow-pascal-toggle'
|
|
31
32
|
import {
|
|
32
33
|
DropdownMenu,
|
|
33
34
|
DropdownMenuContent,
|
|
@@ -103,13 +104,14 @@ function VisibilityMenu({
|
|
|
103
104
|
className="hover:bg-white/5 hover:text-foreground"
|
|
104
105
|
label="Visibility"
|
|
105
106
|
size="icon"
|
|
107
|
+
tooltipClassName="dark"
|
|
106
108
|
tooltipSide="top"
|
|
107
109
|
variant="ghost"
|
|
108
110
|
>
|
|
109
111
|
<Eye className="h-5 w-5" />
|
|
110
112
|
</ActionButton>
|
|
111
113
|
</DropdownMenuTrigger>
|
|
112
|
-
<DropdownMenuContent align="center" className="min-w-44" side="top">
|
|
114
|
+
<DropdownMenuContent align="center" className="dark min-w-44" side="top">
|
|
113
115
|
{canShowScans && (
|
|
114
116
|
<DropdownMenuItem
|
|
115
117
|
onSelect={(e) => keepOpen(e, () => useViewer.getState().setShowScans(!showScans))}
|
|
@@ -160,13 +162,14 @@ function DisplayMenu() {
|
|
|
160
162
|
className="hover:bg-white/5 hover:text-foreground"
|
|
161
163
|
label="Display settings"
|
|
162
164
|
size="icon"
|
|
165
|
+
tooltipClassName="dark"
|
|
163
166
|
tooltipSide="top"
|
|
164
167
|
variant="ghost"
|
|
165
168
|
>
|
|
166
169
|
<SlidersHorizontal className="h-5 w-5" />
|
|
167
170
|
</ActionButton>
|
|
168
171
|
</DropdownMenuTrigger>
|
|
169
|
-
<DropdownMenuContent align="center" className="min-w-56" side="top">
|
|
172
|
+
<DropdownMenuContent align="center" className="dark min-w-56" side="top">
|
|
170
173
|
<DropdownMenuItem
|
|
171
174
|
onSelect={(e) => keepOpen(e, () => useViewer.getState().setShadows(!shadows))}
|
|
172
175
|
>
|
|
@@ -207,7 +210,7 @@ function DisplayMenu() {
|
|
|
207
210
|
<span>Render</span>
|
|
208
211
|
<span className="ml-auto text-muted-foreground text-xs">{activeShading.name}</span>
|
|
209
212
|
</DropdownMenuSubTrigger>
|
|
210
|
-
<DropdownMenuSubContent className="min-w-56">
|
|
213
|
+
<DropdownMenuSubContent className="dark min-w-56">
|
|
211
214
|
{SHADING_OPTIONS.map((option) => {
|
|
212
215
|
const OptionIcon = option.icon
|
|
213
216
|
return (
|
|
@@ -232,12 +235,12 @@ function DisplayMenu() {
|
|
|
232
235
|
<DropdownMenuSub>
|
|
233
236
|
<DropdownMenuSubTrigger>
|
|
234
237
|
<SwatchBook className="h-4 w-4" />
|
|
235
|
-
<span>
|
|
238
|
+
<span>Scene theme</span>
|
|
236
239
|
<span className="ml-auto truncate text-muted-foreground text-xs">
|
|
237
240
|
{activeTheme.name}
|
|
238
241
|
</span>
|
|
239
242
|
</DropdownMenuSubTrigger>
|
|
240
|
-
<DropdownMenuSubContent className="min-w-48">
|
|
243
|
+
<DropdownMenuSubContent className="dark min-w-48">
|
|
241
244
|
{SCENE_THEMES.map((t) => {
|
|
242
245
|
const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
|
|
243
246
|
(role) => t.clayTints?.[role] ?? CLAY_PALETTE[role],
|
|
@@ -270,7 +273,7 @@ function DisplayMenu() {
|
|
|
270
273
|
<span>Edges</span>
|
|
271
274
|
<span className="ml-auto text-muted-foreground text-xs">{activeEdges.name}</span>
|
|
272
275
|
</DropdownMenuSubTrigger>
|
|
273
|
-
<DropdownMenuSubContent className="min-w-56">
|
|
276
|
+
<DropdownMenuSubContent className="dark min-w-56">
|
|
274
277
|
{EDGE_OPTIONS.map((option) => (
|
|
275
278
|
<DropdownMenuItem
|
|
276
279
|
key={option.id}
|
|
@@ -360,6 +363,7 @@ export const ViewerControlsBar = ({
|
|
|
360
363
|
useViewer.getState().setLevelMode(modes[nextIndex] ?? 'stacked')
|
|
361
364
|
}}
|
|
362
365
|
size="icon"
|
|
366
|
+
tooltipClassName="dark"
|
|
363
367
|
tooltipSide="top"
|
|
364
368
|
variant="ghost"
|
|
365
369
|
>
|
|
@@ -383,6 +387,7 @@ export const ViewerControlsBar = ({
|
|
|
383
387
|
useViewer.getState().setWallMode(modes[nextIndex] ?? 'cutaway')
|
|
384
388
|
}}
|
|
385
389
|
size="icon"
|
|
390
|
+
tooltipClassName="dark"
|
|
386
391
|
tooltipSide="top"
|
|
387
392
|
variant="ghost"
|
|
388
393
|
>
|
|
@@ -406,6 +411,7 @@ export const ViewerControlsBar = ({
|
|
|
406
411
|
label={`Walkthrough: ${walkthroughActive ? 'On' : 'Off'}`}
|
|
407
412
|
onClick={onWalkthroughToggle}
|
|
408
413
|
size="icon"
|
|
414
|
+
tooltipClassName="dark"
|
|
409
415
|
tooltipSide="top"
|
|
410
416
|
variant="ghost"
|
|
411
417
|
>
|
|
@@ -414,12 +420,16 @@ export const ViewerControlsBar = ({
|
|
|
414
420
|
|
|
415
421
|
<div className="mx-1 h-5 w-px bg-border/40" />
|
|
416
422
|
|
|
423
|
+
{/* Follow Pascal */}
|
|
424
|
+
<FollowPascalToggle className="hidden sm:inline-flex" tooltipClassName="dark" />
|
|
425
|
+
|
|
417
426
|
{/* Camera actions */}
|
|
418
427
|
<ActionButton
|
|
419
428
|
className="group hidden hover:bg-white/5 sm:inline-flex"
|
|
420
429
|
label="Orbit left"
|
|
421
430
|
onClick={() => emitter.emit('camera-controls:orbit-ccw')}
|
|
422
431
|
size="icon"
|
|
432
|
+
tooltipClassName="dark"
|
|
423
433
|
tooltipSide="top"
|
|
424
434
|
variant="ghost"
|
|
425
435
|
>
|
|
@@ -435,6 +445,7 @@ export const ViewerControlsBar = ({
|
|
|
435
445
|
label="Orbit right"
|
|
436
446
|
onClick={() => emitter.emit('camera-controls:orbit-cw')}
|
|
437
447
|
size="icon"
|
|
448
|
+
tooltipClassName="dark"
|
|
438
449
|
tooltipSide="top"
|
|
439
450
|
variant="ghost"
|
|
440
451
|
>
|
|
@@ -450,6 +461,7 @@ export const ViewerControlsBar = ({
|
|
|
450
461
|
label="Top view"
|
|
451
462
|
onClick={() => emitter.emit('camera-controls:top-view')}
|
|
452
463
|
size="icon"
|
|
464
|
+
tooltipClassName="dark"
|
|
453
465
|
tooltipSide="top"
|
|
454
466
|
variant="ghost"
|
|
455
467
|
>
|
|
@@ -23,6 +23,7 @@ import { RoomAssemblyMesh } from '../editor/room-highlight'
|
|
|
23
23
|
import {
|
|
24
24
|
RoomLabelAnchor3D,
|
|
25
25
|
RoomPillChip,
|
|
26
|
+
RoomPillDetail,
|
|
26
27
|
RoomPillsLayer3D,
|
|
27
28
|
roomLabelEntries,
|
|
28
29
|
roomLabelsVisible,
|
|
@@ -108,7 +109,22 @@ export function ViewerRooms({ source }: { source: ViewerSource }) {
|
|
|
108
109
|
{pillsPresent && levelId ? (
|
|
109
110
|
<RoomPillsLayer3D
|
|
110
111
|
entries={entries}
|
|
112
|
+
// A level of its own: nothing open or placed on one floor carries to the next.
|
|
113
|
+
key={levelId}
|
|
111
114
|
levelObject={() => source.objectFor(levelId)}
|
|
115
|
+
renderDetail={(entry) => {
|
|
116
|
+
const zone = nodes[entry.zoneId]
|
|
117
|
+
if (zone?.type !== 'zone') return null
|
|
118
|
+
return (
|
|
119
|
+
<RoomPillDetail
|
|
120
|
+
areaM2={entry.area}
|
|
121
|
+
color={zone.color}
|
|
122
|
+
name={zoneLabel(zone)}
|
|
123
|
+
view="3d"
|
|
124
|
+
zoneId={zone.id}
|
|
125
|
+
/>
|
|
126
|
+
)
|
|
127
|
+
}}
|
|
112
128
|
renderLabel={(entry, anchors, elements) => {
|
|
113
129
|
const zone = nodes[entry.zoneId]
|
|
114
130
|
if (zone?.type !== 'zone') return null
|