@pascal-app/editor 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +465 -66
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +62 -5
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15849 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +21 -6
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +17 -6
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +44 -0
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +53 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Assembly,
|
|
5
|
+
getWallAssemblyPreset,
|
|
6
|
+
usePascalI18n,
|
|
7
|
+
type WallTypeDefinition,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { Copy, Pencil, Plus, Trash2 } from 'lucide-react'
|
|
10
|
+
import { useState } from 'react'
|
|
11
|
+
import { cn } from '../../../lib/utils'
|
|
12
|
+
import {
|
|
13
|
+
deleteProjectWallType,
|
|
14
|
+
newWallTypeBlockedReason,
|
|
15
|
+
useProjectWallTypes,
|
|
16
|
+
type WallTypePlacement,
|
|
17
|
+
wallTypePlacementOf,
|
|
18
|
+
} from '../../../lib/wall-type'
|
|
19
|
+
import {
|
|
20
|
+
DropdownMenu,
|
|
21
|
+
DropdownMenuContent,
|
|
22
|
+
DropdownMenuItem,
|
|
23
|
+
DropdownMenuLabel,
|
|
24
|
+
DropdownMenuSeparator,
|
|
25
|
+
DropdownMenuTrigger,
|
|
26
|
+
} from '../primitives/dropdown-menu'
|
|
27
|
+
import { type WallTypeEditorRequest, WallTypeEditorDialog } from './wall-type-editor-dialog'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A wall's project wall types, beside its Assembly preset: make a new type, edit the project
|
|
31
|
+
* type the wall is built with, duplicate its stack (a preset's, a project type's or its own
|
|
32
|
+
* custom layers) into a new type, or delete the project type. A type saved here is applied to
|
|
33
|
+
* the wall (`onApply`) in the same undo step. "New" and "Duplicate" are off, and say why, once
|
|
34
|
+
* the project keeps as many types as it can (`newWallTypeBlockedReason`).
|
|
35
|
+
*/
|
|
36
|
+
export function WallTypeMenu({
|
|
37
|
+
assembly,
|
|
38
|
+
className,
|
|
39
|
+
onApply,
|
|
40
|
+
placement,
|
|
41
|
+
}: {
|
|
42
|
+
/** The wall's stack; absent for a plain single-layer wall. */
|
|
43
|
+
assembly: Assembly | undefined
|
|
44
|
+
className?: string
|
|
45
|
+
onApply: (type: WallTypeDefinition) => void
|
|
46
|
+
/** The kind of wall a new type starts as when the wall has no stack to read it from. */
|
|
47
|
+
placement: WallTypePlacement
|
|
48
|
+
}) {
|
|
49
|
+
const i18n = usePascalI18n()
|
|
50
|
+
const projectTypes = useProjectWallTypes()
|
|
51
|
+
const [editing, setEditing] = useState<WallTypeEditorRequest | null>(null)
|
|
52
|
+
const presetId = assembly?.presetId
|
|
53
|
+
const project = presetId ? projectTypes.find((type) => type.id === presetId) : undefined
|
|
54
|
+
const preset = presetId ? getWallAssemblyPreset(presetId) : undefined
|
|
55
|
+
const label =
|
|
56
|
+
project?.label ??
|
|
57
|
+
preset?.label ??
|
|
58
|
+
(assembly
|
|
59
|
+
? i18n._({ id: 'editor.wallTypeMenu.custom', message: 'Custom' })
|
|
60
|
+
: i18n._({ id: 'editor.buildPanel.wallTypes.plainWall', message: 'Plain wall' }))
|
|
61
|
+
const blocked = newWallTypeBlockedReason(projectTypes)
|
|
62
|
+
const startPlacement = assembly ? wallTypePlacementOf(assembly) : placement
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<DropdownMenu>
|
|
67
|
+
<DropdownMenuTrigger asChild>
|
|
68
|
+
<button
|
|
69
|
+
aria-label={i18n._({
|
|
70
|
+
id: 'editor.wallTypeMenu.wallTypesOf',
|
|
71
|
+
message: 'Wall types: new, edit or duplicate {label}',
|
|
72
|
+
values: { label },
|
|
73
|
+
})}
|
|
74
|
+
className={cn(
|
|
75
|
+
'flex size-7 shrink-0 items-center justify-center rounded border border-border bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-foreground data-[state=open]:bg-accent',
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
title={i18n._({
|
|
79
|
+
id: 'editor.buildPanel.wallTypeControls.editOrDuplicateThisWallType',
|
|
80
|
+
message: 'Edit or duplicate this wall type',
|
|
81
|
+
})}
|
|
82
|
+
type="button"
|
|
83
|
+
>
|
|
84
|
+
<Pencil className="size-3.5" />
|
|
85
|
+
</button>
|
|
86
|
+
</DropdownMenuTrigger>
|
|
87
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
88
|
+
<DropdownMenuLabel className="truncate text-muted-foreground text-xs">
|
|
89
|
+
{label}
|
|
90
|
+
</DropdownMenuLabel>
|
|
91
|
+
<DropdownMenuItem
|
|
92
|
+
disabled={!project}
|
|
93
|
+
onSelect={() =>
|
|
94
|
+
project &&
|
|
95
|
+
setEditing({
|
|
96
|
+
mode: 'edit',
|
|
97
|
+
placement: wallTypePlacementOf(project.assembly),
|
|
98
|
+
id: project.id,
|
|
99
|
+
label: project.label,
|
|
100
|
+
assembly: project.assembly,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
<Pencil />
|
|
105
|
+
{i18n._({
|
|
106
|
+
id: 'editor.buildPanel.wallTypeControls.editLayers',
|
|
107
|
+
message: 'Edit layers…',
|
|
108
|
+
})}
|
|
109
|
+
</DropdownMenuItem>
|
|
110
|
+
<DropdownMenuItem
|
|
111
|
+
disabled={!assembly || !!blocked}
|
|
112
|
+
onSelect={() =>
|
|
113
|
+
assembly &&
|
|
114
|
+
setEditing({ mode: 'duplicate', placement: startPlacement, label, assembly })
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
<Copy />
|
|
118
|
+
{i18n._({
|
|
119
|
+
id: 'editor.buildPanel.wallTypeControls.duplicateAsANewType',
|
|
120
|
+
message: 'Duplicate as a new type…',
|
|
121
|
+
})}
|
|
122
|
+
</DropdownMenuItem>
|
|
123
|
+
{project ? (
|
|
124
|
+
<DropdownMenuItem
|
|
125
|
+
onSelect={() => deleteProjectWallType(project.id)}
|
|
126
|
+
variant="destructive"
|
|
127
|
+
>
|
|
128
|
+
<Trash2 />
|
|
129
|
+
{i18n._({
|
|
130
|
+
id: 'editor.buildPanel.wallTypeControls.deleteFromThisProject',
|
|
131
|
+
message: 'Delete from this project',
|
|
132
|
+
})}
|
|
133
|
+
</DropdownMenuItem>
|
|
134
|
+
) : null}
|
|
135
|
+
<DropdownMenuSeparator />
|
|
136
|
+
<DropdownMenuItem
|
|
137
|
+
disabled={!!blocked}
|
|
138
|
+
onSelect={() => setEditing({ mode: 'new', placement: startPlacement })}
|
|
139
|
+
>
|
|
140
|
+
<Plus />
|
|
141
|
+
{i18n._({
|
|
142
|
+
id: 'editor.buildPanel.wallTypeControls.newWallType',
|
|
143
|
+
message: 'New wall type…',
|
|
144
|
+
})}
|
|
145
|
+
</DropdownMenuItem>
|
|
146
|
+
{blocked ? (
|
|
147
|
+
<p className="px-2 pt-1 pb-1.5 text-[11px] text-muted-foreground leading-snug">
|
|
148
|
+
{blocked}
|
|
149
|
+
</p>
|
|
150
|
+
) : preset ? (
|
|
151
|
+
<p className="px-2 pt-1 pb-1.5 text-[11px] text-muted-foreground leading-snug">
|
|
152
|
+
{i18n._({
|
|
153
|
+
id: 'editor.buildPanel.wallTypeControls.presetsStayAsCitedDuplicateOne',
|
|
154
|
+
message: 'Presets stay as cited. Duplicate one to change its layers.',
|
|
155
|
+
})}
|
|
156
|
+
</p>
|
|
157
|
+
) : null}
|
|
158
|
+
</DropdownMenuContent>
|
|
159
|
+
</DropdownMenu>
|
|
160
|
+
<WallTypeEditorDialog
|
|
161
|
+
onClose={() => setEditing(null)}
|
|
162
|
+
onSaved={onApply}
|
|
163
|
+
request={editing}
|
|
164
|
+
/>
|
|
165
|
+
</>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { usePascalI18n } from '@pascal-app/core'
|
|
3
4
|
import type React from 'react'
|
|
4
5
|
import { cn } from '../../lib/utils'
|
|
5
6
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
|
|
7
|
+
import { SKY_COMPASS_BOTTOM, useSkyPanel } from './sky-control'
|
|
6
8
|
|
|
7
9
|
export type FloorplanCompassButtonProps = {
|
|
8
10
|
northRotationDeg: number
|
|
@@ -18,16 +20,23 @@ export function FloorplanCompassButton({
|
|
|
18
20
|
needleRef,
|
|
19
21
|
style,
|
|
20
22
|
}: FloorplanCompassButtonProps) {
|
|
23
|
+
const i18n = usePascalI18n()
|
|
24
|
+
// Beside the sky chip (not when moved to the plan's right corner), centre on the docks' row.
|
|
25
|
+
const besideSky = useSkyPanel((s) => s.hosts > 0) && style?.right === undefined
|
|
26
|
+
const label = i18n._({
|
|
27
|
+
id: 'viewer.floorplan.alignNorth',
|
|
28
|
+
message: 'Align view to north',
|
|
29
|
+
})
|
|
21
30
|
return (
|
|
22
31
|
<Tooltip>
|
|
23
32
|
<TooltipTrigger asChild>
|
|
24
33
|
<button
|
|
25
|
-
aria-label=
|
|
34
|
+
aria-label={label}
|
|
26
35
|
className={cn(
|
|
27
36
|
'group pointer-events-auto absolute bottom-3 z-30 flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white/85 shadow-sm backdrop-blur-md transition hover:bg-white hover:shadow-md dark:border-white/10 dark:bg-neutral-900/85 dark:hover:bg-neutral-900',
|
|
28
37
|
!style && 'left-3',
|
|
29
38
|
)}
|
|
30
|
-
style={style}
|
|
39
|
+
style={besideSky ? { bottom: SKY_COMPASS_BOTTOM, ...style } : style}
|
|
31
40
|
onClick={(event) => {
|
|
32
41
|
event.preventDefault()
|
|
33
42
|
event.stopPropagation()
|
|
@@ -52,7 +61,7 @@ export function FloorplanCompassButton({
|
|
|
52
61
|
</span>
|
|
53
62
|
</button>
|
|
54
63
|
</TooltipTrigger>
|
|
55
|
-
<TooltipContent side="right">
|
|
64
|
+
<TooltipContent side="right">{label}</TooltipContent>
|
|
56
65
|
</Tooltip>
|
|
57
66
|
)
|
|
58
67
|
}
|
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
type FloorplanGeometry,
|
|
7
7
|
type FloorplanPalette,
|
|
8
8
|
type GeometryContext,
|
|
9
|
-
getLevelDisplayName,
|
|
10
9
|
isNodeKindEnabled,
|
|
11
10
|
nodeRegistry,
|
|
11
|
+
useLocale,
|
|
12
|
+
usePascalI18n,
|
|
12
13
|
useScene,
|
|
13
14
|
} from '@pascal-app/core'
|
|
14
15
|
import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema'
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
useState,
|
|
28
29
|
} from 'react'
|
|
29
30
|
import { createPortal } from 'react-dom'
|
|
31
|
+
import { localizedLevelName } from '../../i18n/level-name'
|
|
30
32
|
import {
|
|
31
33
|
FLOORPLAN_VIEW_ROTATION_DEG,
|
|
32
34
|
floorplanLocalToWorldPoint,
|
|
@@ -143,13 +145,6 @@ function boundsToViewBox(bounds: FloorplanBounds): FloorplanViewBox {
|
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
function levelLabel(level: AnyNode): string {
|
|
147
|
-
return getLevelDisplayName({
|
|
148
|
-
name: (level as { name?: string }).name,
|
|
149
|
-
level: (level as { level?: number }).level ?? 0,
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
|
|
153
148
|
function collectLevelTree(root: AnyNode, nodes: Record<string, AnyNode>): AnyNode[] {
|
|
154
149
|
const result: AnyNode[] = [root]
|
|
155
150
|
const seen = new Set<string>()
|
|
@@ -309,6 +304,8 @@ export function FloorplanPreview({
|
|
|
309
304
|
showLevelSelector = true,
|
|
310
305
|
synchronizeNavigation = false,
|
|
311
306
|
}: FloorplanPreviewProps) {
|
|
307
|
+
const i18n = usePascalI18n()
|
|
308
|
+
const locale = useLocale()
|
|
312
309
|
const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes))
|
|
313
310
|
const storeInstalledPlugins = useScene((state) =>
|
|
314
311
|
scene ? EMPTY_INSTALLED_PLUGINS : state.installedPlugins,
|
|
@@ -869,7 +866,10 @@ export function FloorplanPreview({
|
|
|
869
866
|
className={className}
|
|
870
867
|
style={{ display: 'grid', placeItems: 'center', background: '#f8fafc', color: '#64748b' }}
|
|
871
868
|
>
|
|
872
|
-
|
|
869
|
+
{i18n._({
|
|
870
|
+
id: 'viewer.floorplan.empty',
|
|
871
|
+
message: 'No floor plans are available for this scene.',
|
|
872
|
+
})}
|
|
873
873
|
</div>
|
|
874
874
|
)
|
|
875
875
|
}
|
|
@@ -882,7 +882,15 @@ export function FloorplanPreview({
|
|
|
882
882
|
{synchronizeNavigation ? <FloorplanCameraSyncMount /> : null}
|
|
883
883
|
<div
|
|
884
884
|
aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight + - 0 F"
|
|
885
|
-
aria-label={
|
|
885
|
+
aria-label={i18n._({
|
|
886
|
+
id: 'viewer.floorplan.viewLabel',
|
|
887
|
+
message: '{floor} 2D view',
|
|
888
|
+
values: {
|
|
889
|
+
floor: activeLevel
|
|
890
|
+
? localizedLevelName(activeLevel, i18n, locale)
|
|
891
|
+
: i18n._({ id: 'viewer.floorplan.floorPlan', message: 'Floor plan' }),
|
|
892
|
+
},
|
|
893
|
+
})}
|
|
886
894
|
onKeyDown={onKeyDown}
|
|
887
895
|
role="application"
|
|
888
896
|
ref={interactionRef}
|
|
@@ -969,9 +977,9 @@ export function FloorplanPreview({
|
|
|
969
977
|
fontWeight: 600,
|
|
970
978
|
}}
|
|
971
979
|
>
|
|
972
|
-
Floor
|
|
980
|
+
{i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' })}
|
|
973
981
|
<select
|
|
974
|
-
aria-label=
|
|
982
|
+
aria-label={i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' })}
|
|
975
983
|
onChange={(event) => chooseLevel(event.target.value)}
|
|
976
984
|
style={{
|
|
977
985
|
border: '1px solid rgba(148,163,184,.55)',
|
|
@@ -985,7 +993,7 @@ export function FloorplanPreview({
|
|
|
985
993
|
>
|
|
986
994
|
{levels.map((level) => (
|
|
987
995
|
<option key={level.id} value={level.id}>
|
|
988
|
-
{
|
|
996
|
+
{localizedLevelName(level, i18n, locale)}
|
|
989
997
|
</option>
|
|
990
998
|
))}
|
|
991
999
|
</select>
|
|
@@ -1013,28 +1021,28 @@ export function FloorplanPreview({
|
|
|
1013
1021
|
}}
|
|
1014
1022
|
>
|
|
1015
1023
|
<button
|
|
1016
|
-
aria-label=
|
|
1024
|
+
aria-label={i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' })}
|
|
1017
1025
|
onClick={() => zoom(1.2)}
|
|
1018
1026
|
style={controlStyle}
|
|
1019
|
-
title=
|
|
1027
|
+
title={i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' })}
|
|
1020
1028
|
type="button"
|
|
1021
1029
|
>
|
|
1022
1030
|
<Minus size={16} />
|
|
1023
1031
|
</button>
|
|
1024
1032
|
<button
|
|
1025
|
-
aria-label=
|
|
1033
|
+
aria-label={i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' })}
|
|
1026
1034
|
onClick={() => updateLocalViewBox(fittedViewBox)}
|
|
1027
1035
|
style={controlStyle}
|
|
1028
|
-
title=
|
|
1036
|
+
title={i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' })}
|
|
1029
1037
|
type="button"
|
|
1030
1038
|
>
|
|
1031
1039
|
<Maximize2 size={15} />
|
|
1032
1040
|
</button>
|
|
1033
1041
|
<button
|
|
1034
|
-
aria-label=
|
|
1042
|
+
aria-label={i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' })}
|
|
1035
1043
|
onClick={() => zoom(0.8)}
|
|
1036
1044
|
style={controlStyle}
|
|
1037
|
-
title=
|
|
1045
|
+
title={i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' })}
|
|
1038
1046
|
type="button"
|
|
1039
1047
|
>
|
|
1040
1048
|
<Plus size={16} />
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type PascalI18n, usePascalI18n, useScene } from '@pascal-app/core'
|
|
4
|
+
import {
|
|
5
|
+
type RenderQuality,
|
|
6
|
+
requestDeviceLocation,
|
|
7
|
+
resolveLocation,
|
|
8
|
+
type TimeOfDay,
|
|
9
|
+
useViewer,
|
|
10
|
+
type Weather,
|
|
11
|
+
} from '@pascal-app/viewer'
|
|
12
|
+
import {
|
|
13
|
+
ArrowDownLeft,
|
|
14
|
+
Check,
|
|
15
|
+
Cloud,
|
|
16
|
+
CloudSun,
|
|
17
|
+
Gauge,
|
|
18
|
+
LocateFixed,
|
|
19
|
+
Moon,
|
|
20
|
+
PenLine,
|
|
21
|
+
Sun,
|
|
22
|
+
Sunrise,
|
|
23
|
+
Sunset,
|
|
24
|
+
} from 'lucide-react'
|
|
25
|
+
import { useState } from 'react'
|
|
26
|
+
import {
|
|
27
|
+
DropdownMenuItem,
|
|
28
|
+
DropdownMenuSub,
|
|
29
|
+
DropdownMenuSubContent,
|
|
30
|
+
DropdownMenuSubTrigger,
|
|
31
|
+
} from '../ui/primitives/dropdown-menu'
|
|
32
|
+
import { openSkyPanel, useSkyPanel, weatherIcon } from './sky-control'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render settings shared by every display dropdown:
|
|
36
|
+
* - `QualityMenu` (both appearances): Low / Medium / High.
|
|
37
|
+
* - `DaylightMenu` (colored): time-of-day and weather presets, plus "Use my location" when the
|
|
38
|
+
* site has no geocoded point — or, while the sky chip is in the viewport, a row opening it.
|
|
39
|
+
* - `EdgesToggleItem` (monochrome): ink edges on or off.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
function timeOptions(i18n: PascalI18n) {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
id: 'golden',
|
|
46
|
+
icon: Sunrise,
|
|
47
|
+
name: i18n._({ id: 'viewer.daylight.time.golden', message: 'Golden hour' }),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'midday',
|
|
51
|
+
icon: Sun,
|
|
52
|
+
name: i18n._({ id: 'viewer.daylight.time.midday', message: 'Midday' }),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'blue',
|
|
56
|
+
icon: Sunset,
|
|
57
|
+
name: i18n._({ id: 'viewer.daylight.time.blue', message: 'Blue hour' }),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'night',
|
|
61
|
+
icon: Moon,
|
|
62
|
+
name: i18n._({ id: 'viewer.daylight.time.night', message: 'Night' }),
|
|
63
|
+
},
|
|
64
|
+
] as const satisfies readonly { id: TimeOfDay; icon: typeof Sun; name: string }[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function weatherOptions(i18n: PascalI18n) {
|
|
68
|
+
return [
|
|
69
|
+
{ id: 'clear', name: i18n._({ id: 'viewer.daylight.weather.clear', message: 'Clear' }) },
|
|
70
|
+
{ id: 'fair', name: i18n._({ id: 'viewer.daylight.weather.fair', message: 'Fair' }) },
|
|
71
|
+
{ id: 'cloudy', name: i18n._({ id: 'viewer.daylight.weather.cloudy', message: 'Cloudy' }) },
|
|
72
|
+
{
|
|
73
|
+
id: 'overcast',
|
|
74
|
+
name: i18n._({ id: 'viewer.daylight.weather.overcast', message: 'Overcast' }),
|
|
75
|
+
},
|
|
76
|
+
] as const satisfies readonly { id: Weather; name: string }[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function qualityOptions(i18n: PascalI18n) {
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
id: 'low',
|
|
83
|
+
name: i18n._({ id: 'viewer.daylight.quality.low', message: 'Low' }),
|
|
84
|
+
detail: i18n._({
|
|
85
|
+
id: 'viewer.daylight.quality.lowDetail',
|
|
86
|
+
message: 'Fastest — no ambient occlusion or bloom',
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'medium',
|
|
91
|
+
name: i18n._({ id: 'viewer.daylight.quality.medium', message: 'Medium' }),
|
|
92
|
+
detail: i18n._({
|
|
93
|
+
id: 'viewer.daylight.quality.mediumDetail',
|
|
94
|
+
message: 'Half-resolution ambient occlusion',
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'high',
|
|
99
|
+
name: i18n._({ id: 'viewer.daylight.quality.high', message: 'High' }),
|
|
100
|
+
detail: i18n._({
|
|
101
|
+
id: 'viewer.daylight.quality.highDetail',
|
|
102
|
+
message: 'Lighting-aware ambient occlusion and bloom',
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
] as const satisfies readonly { id: RenderQuality; name: string; detail: string }[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function QualityMenu({
|
|
109
|
+
subContentClassName = 'dark min-w-56',
|
|
110
|
+
}: {
|
|
111
|
+
subContentClassName?: string
|
|
112
|
+
}) {
|
|
113
|
+
const i18n = usePascalI18n()
|
|
114
|
+
const quality = useViewer((s) => s.renderQuality)
|
|
115
|
+
const choice = useViewer((s) => s.renderQualityChoice)
|
|
116
|
+
const qualities = qualityOptions(i18n)
|
|
117
|
+
const active = qualities.find((o) => o.id === quality) ?? qualities[2]
|
|
118
|
+
const autoName = i18n._({ id: 'viewer.daylight.quality.auto', message: 'Auto' })
|
|
119
|
+
return (
|
|
120
|
+
<DropdownMenuSub>
|
|
121
|
+
<DropdownMenuSubTrigger>
|
|
122
|
+
<Gauge className="h-4 w-4" />
|
|
123
|
+
<span>{i18n._({ id: 'viewer.daylight.quality.title', message: 'Quality' })}</span>
|
|
124
|
+
<span className="ml-auto text-muted-foreground text-xs">
|
|
125
|
+
{choice === 'auto' ? `${autoName} (${active.name})` : active.name}
|
|
126
|
+
</span>
|
|
127
|
+
</DropdownMenuSubTrigger>
|
|
128
|
+
<DropdownMenuSubContent className={subContentClassName}>
|
|
129
|
+
<DropdownMenuItem onSelect={() => useViewer.getState().setRenderQuality('auto')}>
|
|
130
|
+
<div className="flex flex-col">
|
|
131
|
+
<span className="text-foreground">{autoName}</span>
|
|
132
|
+
<span className="text-muted-foreground text-xs">
|
|
133
|
+
{i18n._({
|
|
134
|
+
id: 'viewer.daylight.quality.autoDetail',
|
|
135
|
+
message: 'Picked for this device: {tier}',
|
|
136
|
+
values: { tier: active.name },
|
|
137
|
+
})}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
{choice === 'auto' ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
|
|
141
|
+
</DropdownMenuItem>
|
|
142
|
+
{qualities.map((option) => (
|
|
143
|
+
<DropdownMenuItem
|
|
144
|
+
key={option.id}
|
|
145
|
+
onSelect={() => useViewer.getState().setRenderQuality(option.id)}
|
|
146
|
+
>
|
|
147
|
+
<div className="flex flex-col">
|
|
148
|
+
<span className="text-foreground">{option.name}</span>
|
|
149
|
+
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
150
|
+
</div>
|
|
151
|
+
{choice === option.id ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
|
|
152
|
+
</DropdownMenuItem>
|
|
153
|
+
))}
|
|
154
|
+
</DropdownMenuSubContent>
|
|
155
|
+
</DropdownMenuSub>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function DaylightMenu({
|
|
160
|
+
subContentClassName = 'dark min-w-48',
|
|
161
|
+
}: {
|
|
162
|
+
subContentClassName?: string
|
|
163
|
+
}) {
|
|
164
|
+
const i18n = usePascalI18n()
|
|
165
|
+
const timeOfDay = useViewer((s) => s.timeOfDay)
|
|
166
|
+
const weather = useViewer((s) => s.weather)
|
|
167
|
+
const deviceLocation = useViewer((s) => s.deviceLocation)
|
|
168
|
+
const siteHasLocation = useScene((state) => {
|
|
169
|
+
for (const id of state.rootNodeIds) {
|
|
170
|
+
const node = state.nodes[id] as
|
|
171
|
+
| { type?: string; parcel?: { originLngLat?: unknown }; dossier?: { point?: unknown } }
|
|
172
|
+
| undefined
|
|
173
|
+
if (node?.type === 'site') return Boolean(node.parcel?.originLngLat || node.dossier?.point)
|
|
174
|
+
}
|
|
175
|
+
return false
|
|
176
|
+
})
|
|
177
|
+
const [locating, setLocating] = useState(false)
|
|
178
|
+
// The sky chip beside the compass carries time and weather wherever it is mounted.
|
|
179
|
+
const skyChip = useSkyPanel((s) => s.hosts > 0)
|
|
180
|
+
const times = timeOptions(i18n)
|
|
181
|
+
const weathers = weatherOptions(i18n)
|
|
182
|
+
const activeTime = times.find((o) => o.id === timeOfDay) ?? times[1]
|
|
183
|
+
const activeWeather = weathers.find((o) => o.id === weather) ?? weathers[1]
|
|
184
|
+
const source = siteHasLocation ? 'site' : resolveLocation(null, deviceLocation).source
|
|
185
|
+
|
|
186
|
+
if (skyChip) {
|
|
187
|
+
const WeatherIcon = weatherIcon(weather)
|
|
188
|
+
return (
|
|
189
|
+
<DropdownMenuItem onSelect={openSkyPanel}>
|
|
190
|
+
<WeatherIcon className="h-4 w-4" />
|
|
191
|
+
<span>{i18n._({ id: 'viewer.daylight.sky.menuItem', message: 'Time & weather' })}</span>
|
|
192
|
+
<span className="ml-auto flex items-center gap-1 text-violet-600 text-xs dark:text-violet-300">
|
|
193
|
+
{i18n._({ id: 'viewer.daylight.sky.inViewport', message: 'In viewport' })}
|
|
194
|
+
<ArrowDownLeft className="h-3.5 w-3.5" />
|
|
195
|
+
</span>
|
|
196
|
+
</DropdownMenuItem>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<>
|
|
202
|
+
<DropdownMenuSub>
|
|
203
|
+
<DropdownMenuSubTrigger>
|
|
204
|
+
<activeTime.icon className="h-4 w-4" />
|
|
205
|
+
<span>{i18n._({ id: 'viewer.daylight.time.title', message: 'Time of day' })}</span>
|
|
206
|
+
<span className="ml-auto text-muted-foreground text-xs">{activeTime.name}</span>
|
|
207
|
+
</DropdownMenuSubTrigger>
|
|
208
|
+
<DropdownMenuSubContent className={subContentClassName}>
|
|
209
|
+
{times.map((option) => (
|
|
210
|
+
<DropdownMenuItem
|
|
211
|
+
key={option.id}
|
|
212
|
+
onSelect={() => useViewer.getState().setTimeOfDay(option.id)}
|
|
213
|
+
>
|
|
214
|
+
<option.icon className="h-4 w-4" />
|
|
215
|
+
<span>{option.name}</span>
|
|
216
|
+
{timeOfDay === option.id ? <Check className="ml-auto h-4 w-4" /> : null}
|
|
217
|
+
</DropdownMenuItem>
|
|
218
|
+
))}
|
|
219
|
+
{source !== 'site' ? (
|
|
220
|
+
<DropdownMenuItem
|
|
221
|
+
disabled={locating}
|
|
222
|
+
onSelect={(event) => {
|
|
223
|
+
event.preventDefault()
|
|
224
|
+
setLocating(true)
|
|
225
|
+
requestDeviceLocation()
|
|
226
|
+
.then((location) => useViewer.getState().setDeviceLocation(location))
|
|
227
|
+
.catch(() => {})
|
|
228
|
+
.finally(() => setLocating(false))
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
<LocateFixed className="h-4 w-4" />
|
|
232
|
+
<div className="flex flex-col">
|
|
233
|
+
<span>
|
|
234
|
+
{i18n._({ id: 'viewer.daylight.location.use', message: 'Use my location' })}
|
|
235
|
+
</span>
|
|
236
|
+
<span className="text-muted-foreground text-xs">
|
|
237
|
+
{source === 'device'
|
|
238
|
+
? i18n._({
|
|
239
|
+
id: 'viewer.daylight.location.device',
|
|
240
|
+
message: 'Sun follows your location',
|
|
241
|
+
})
|
|
242
|
+
: i18n._({
|
|
243
|
+
id: 'viewer.daylight.location.approximate',
|
|
244
|
+
message: 'Sun position is approximate',
|
|
245
|
+
})}
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
</DropdownMenuItem>
|
|
249
|
+
) : null}
|
|
250
|
+
</DropdownMenuSubContent>
|
|
251
|
+
</DropdownMenuSub>
|
|
252
|
+
<DropdownMenuSub>
|
|
253
|
+
<DropdownMenuSubTrigger>
|
|
254
|
+
{weather === 'clear' ? <CloudSun className="h-4 w-4" /> : <Cloud className="h-4 w-4" />}
|
|
255
|
+
<span>{i18n._({ id: 'viewer.daylight.weather.title', message: 'Weather' })}</span>
|
|
256
|
+
<span className="ml-auto text-muted-foreground text-xs">{activeWeather.name}</span>
|
|
257
|
+
</DropdownMenuSubTrigger>
|
|
258
|
+
<DropdownMenuSubContent className={subContentClassName}>
|
|
259
|
+
{weathers.map((option) => (
|
|
260
|
+
<DropdownMenuItem
|
|
261
|
+
key={option.id}
|
|
262
|
+
onSelect={() => useViewer.getState().setWeather(option.id)}
|
|
263
|
+
>
|
|
264
|
+
<span>{option.name}</span>
|
|
265
|
+
{weather === option.id ? <Check className="ml-auto h-4 w-4" /> : null}
|
|
266
|
+
</DropdownMenuItem>
|
|
267
|
+
))}
|
|
268
|
+
</DropdownMenuSubContent>
|
|
269
|
+
</DropdownMenuSub>
|
|
270
|
+
</>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Ink edges on (soft) or off. The menu stays open on toggle. */
|
|
275
|
+
export function EdgesToggleItem() {
|
|
276
|
+
const i18n = usePascalI18n()
|
|
277
|
+
const on = useViewer((s) => s.edges !== 'off')
|
|
278
|
+
return (
|
|
279
|
+
<DropdownMenuItem
|
|
280
|
+
onSelect={(event) => {
|
|
281
|
+
event.preventDefault()
|
|
282
|
+
useViewer.getState().setEdges(on ? 'off' : 'soft')
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
<PenLine className="h-4 w-4" />
|
|
286
|
+
<span>{i18n._({ id: 'viewer.display.edges.title', message: 'Edges' })}</span>
|
|
287
|
+
<span className="ml-auto text-muted-foreground text-xs">
|
|
288
|
+
{on
|
|
289
|
+
? i18n._({ id: 'viewer.common.on', message: 'On' })
|
|
290
|
+
: i18n._({ id: 'viewer.common.off', message: 'Off' })}
|
|
291
|
+
</span>
|
|
292
|
+
</DropdownMenuItem>
|
|
293
|
+
)
|
|
294
|
+
}
|