@pascal-app/editor 1.0.7 → 1.1.1
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 +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +101 -45
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1983
- 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 +5 -3
- 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 +8 -4
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +199 -30
- 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/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +43 -13
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +2 -2
- 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/room-controls.tsx +10 -6
- package/src/components/editor/room-handles-2d.tsx +3 -3
- package/src/components/editor/room-handles.tsx +4 -5
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +41 -1670
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +11 -7
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
- 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 +30 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +33 -21
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
- 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/use-placement-coordinator.tsx +3 -4
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- 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-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
- package/src/components/ui/helpers/helper-manager.tsx +55 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
- package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +42 -19
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +7 -7
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +7 -9
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +22 -11
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +1036 -306
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2267 -439
- package/src/i18n/locales/fr/messages.po +2267 -439
- package/src/i18n/locales/ja/messages.po +2267 -439
- package/src/i18n/locales/zh-CN/messages.po +2267 -439
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +183 -178
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -3
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +2 -2
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- 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/material-paint.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +7 -5
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/paint-region-mode.ts +1 -1
- 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/point-ask/session.ts +1 -2
- 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/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +8 -37
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +22 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- 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/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/measurements.ts +0 -225
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -183
- package/src/lib/scene-bounds.ts +0 -169
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/window-interaction.ts +0 -142
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type AnyNode,
|
|
5
|
-
buildUnitReport,
|
|
6
|
-
polygonInteriorPoint,
|
|
7
|
-
sceneRegistry,
|
|
8
|
-
useLocale,
|
|
9
|
-
usePascalI18n,
|
|
10
|
-
useScene,
|
|
11
|
-
type ZoneNode,
|
|
12
|
-
} from '@pascal-app/core'
|
|
13
|
-
import { SelectionCallout, useViewer } from '@pascal-app/viewer'
|
|
14
|
-
import { useCallback, useEffect, useMemo } from 'react'
|
|
15
|
-
import { Vector3 } from 'three'
|
|
16
|
-
import { useShallow } from 'zustand/react/shallow'
|
|
17
|
-
import { resolveRoomAssemblyHeights } from '../../lib/room-assembly-overlay'
|
|
18
|
-
import { roomFloorElevation } from '../../lib/room-handle-drag'
|
|
19
|
-
import { describeZoneOutline, type RoomSelectionRecord } from '../../lib/room-selection'
|
|
20
|
-
import {
|
|
21
|
-
selectViewerRoom,
|
|
22
|
-
type ViewerSource,
|
|
23
|
-
viewerRooms,
|
|
24
|
-
zoneLabel,
|
|
25
|
-
} from '../../lib/viewer-selection'
|
|
26
|
-
import { RoomAssemblyMesh } from '../editor/room-highlight'
|
|
27
|
-
import {
|
|
28
|
-
formatRoomArea,
|
|
29
|
-
RoomLabelAnchor3D,
|
|
30
|
-
type RoomLabelEntry,
|
|
31
|
-
RoomPillChip,
|
|
32
|
-
RoomPillDetail,
|
|
33
|
-
RoomPillsLayer3D,
|
|
34
|
-
roomLabelEntries,
|
|
35
|
-
roomLabelsVisible,
|
|
36
|
-
useFadePresence,
|
|
37
|
-
} from '../editor/room-labels'
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Rooms on every viewer surface, as the editor shows them: the editor's pills
|
|
41
|
-
* on the current floor while nothing but the floor is selected, and the
|
|
42
|
-
* editor's room highlight on the selected or hovered room (or a focused
|
|
43
|
-
* unit's rooms). Zones draw no tinted volume here, and their meshes are not
|
|
44
|
-
* mounted at all, so they never catch a click.
|
|
45
|
-
*/
|
|
46
|
-
export function ViewerRooms({ source }: { source: ViewerSource }) {
|
|
47
|
-
const i18n = usePascalI18n()
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
const before = useViewer.getState().showZones
|
|
50
|
-
useViewer.getState().setShowZones(false)
|
|
51
|
-
return () => useViewer.getState().setShowZones(before)
|
|
52
|
-
}, [])
|
|
53
|
-
const { levelId, zoneId, selectedIds } = useViewer(
|
|
54
|
-
useShallow((s) => ({
|
|
55
|
-
levelId: s.selection.levelId,
|
|
56
|
-
zoneId: s.selection.zoneId,
|
|
57
|
-
selectedIds: s.selection.selectedIds,
|
|
58
|
-
})),
|
|
59
|
-
)
|
|
60
|
-
const focusedUnitId = useViewer((s) => s.focusedUnitId)
|
|
61
|
-
const hoveredId = useViewer((s) => s.hoveredId)
|
|
62
|
-
const walkthrough = useViewer((s) => s.walkthroughMode)
|
|
63
|
-
const { nodes } = source
|
|
64
|
-
|
|
65
|
-
const pillsShown =
|
|
66
|
-
!!levelId &&
|
|
67
|
-
!walkthrough &&
|
|
68
|
-
roomLabelsVisible({
|
|
69
|
-
phase: 'building',
|
|
70
|
-
mode: 'select',
|
|
71
|
-
scopeIdle: true,
|
|
72
|
-
room: null,
|
|
73
|
-
zoneId,
|
|
74
|
-
focusedUnitId,
|
|
75
|
-
isCaptureMode: false,
|
|
76
|
-
isThumbnailCapture: false,
|
|
77
|
-
selectedTypes: selectedIds.map((id) => nodes[id]?.type),
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
// The layer stays mounted through its fade-out, like the editor's.
|
|
81
|
-
const pillsPresent = useFadePresence(pillsShown)
|
|
82
|
-
|
|
83
|
-
const rooms = useMemo(() => (levelId ? viewerRooms(levelId, nodes) : []), [levelId, nodes])
|
|
84
|
-
const entries = useMemo(() => {
|
|
85
|
-
if (!levelId) return []
|
|
86
|
-
const zones = Object.values(nodes).filter(
|
|
87
|
-
(node): node is ZoneNode =>
|
|
88
|
-
node.type === 'zone' && node.parentId === levelId && node.visible !== false,
|
|
89
|
-
)
|
|
90
|
-
return roomLabelEntries(
|
|
91
|
-
rooms.filter((room) => nodes[room.zoneId]?.visible !== false),
|
|
92
|
-
zones,
|
|
93
|
-
)
|
|
94
|
-
}, [levelId, nodes, rooms])
|
|
95
|
-
|
|
96
|
-
const highlighted = useMemo(() => {
|
|
97
|
-
if (walkthrough) return []
|
|
98
|
-
const ids = new Set<string>()
|
|
99
|
-
if (zoneId) ids.add(zoneId)
|
|
100
|
-
if (hoveredId && nodes[hoveredId]?.type === 'zone') ids.add(hoveredId)
|
|
101
|
-
const unit = focusedUnitId ? nodes[focusedUnitId] : undefined
|
|
102
|
-
if (unit?.type === 'unit') {
|
|
103
|
-
for (const member of buildUnitReport(unit, nodes).members)
|
|
104
|
-
if (member.levelId === levelId) ids.add(member.zoneId)
|
|
105
|
-
}
|
|
106
|
-
return [...ids].flatMap((id): RoomSelectionRecord[] => {
|
|
107
|
-
const zone = nodes[id]
|
|
108
|
-
if (zone?.type !== 'zone' || !zone.parentId) return []
|
|
109
|
-
const detected = viewerRooms(zone.parentId, nodes).find((room) => room.zoneId === id)
|
|
110
|
-
return [detected ?? describeZoneOutline(zone.parentId, zone)]
|
|
111
|
-
})
|
|
112
|
-
}, [focusedUnitId, hoveredId, levelId, nodes, walkthrough, zoneId])
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<>
|
|
116
|
-
{pillsPresent && levelId ? (
|
|
117
|
-
<RoomPillsLayer3D
|
|
118
|
-
entries={entries}
|
|
119
|
-
// A level of its own: nothing open or placed on one floor carries to the next.
|
|
120
|
-
key={levelId}
|
|
121
|
-
levelObject={() => source.objectFor(levelId)}
|
|
122
|
-
renderDetail={(entry) => {
|
|
123
|
-
const zone = nodes[entry.zoneId]
|
|
124
|
-
if (zone?.type !== 'zone') return null
|
|
125
|
-
return (
|
|
126
|
-
<RoomPillDetail
|
|
127
|
-
areaM2={entry.area}
|
|
128
|
-
color={zone.color}
|
|
129
|
-
name={zoneLabel(zone, i18n)}
|
|
130
|
-
view="3d"
|
|
131
|
-
zoneId={zone.id}
|
|
132
|
-
/>
|
|
133
|
-
)
|
|
134
|
-
}}
|
|
135
|
-
renderLabel={(entry, anchors, elements) => {
|
|
136
|
-
const zone = nodes[entry.zoneId]
|
|
137
|
-
if (zone?.type !== 'zone') return null
|
|
138
|
-
return (
|
|
139
|
-
<RoomLabelAnchor3D
|
|
140
|
-
anchors={anchors}
|
|
141
|
-
entry={entry}
|
|
142
|
-
floorY={roomFloorElevation(nodes as Record<string, AnyNode>, entry.zoneId)}
|
|
143
|
-
>
|
|
144
|
-
<RoomPillChip
|
|
145
|
-
areaM2={entry.area}
|
|
146
|
-
color={zone.color}
|
|
147
|
-
elements={elements}
|
|
148
|
-
name={zoneLabel(zone, i18n)}
|
|
149
|
-
onClick={() => selectViewerRoom(zone.id, source)}
|
|
150
|
-
onHover={() => useViewer.getState().setHoveredId(zone.id)}
|
|
151
|
-
onLeave={() => {
|
|
152
|
-
if (useViewer.getState().hoveredId === zone.id)
|
|
153
|
-
useViewer.getState().setHoveredId(null)
|
|
154
|
-
}}
|
|
155
|
-
view="3d"
|
|
156
|
-
zoneId={zone.id}
|
|
157
|
-
/>
|
|
158
|
-
</RoomLabelAnchor3D>
|
|
159
|
-
)
|
|
160
|
-
}}
|
|
161
|
-
shown={pillsShown}
|
|
162
|
-
/>
|
|
163
|
-
) : null}
|
|
164
|
-
{highlighted.map((room) => (
|
|
165
|
-
<ViewerRoomHighlight key={room.zoneId} room={room} source={source} />
|
|
166
|
-
))}
|
|
167
|
-
{zoneId && levelId && !walkthrough && selectedIds.length === 0
|
|
168
|
-
? (() => {
|
|
169
|
-
const entry = entries.find((candidate) => candidate.zoneId === zoneId)
|
|
170
|
-
const zone = nodes[zoneId]
|
|
171
|
-
const level = source.objectFor(levelId)
|
|
172
|
-
return entry && zone?.type === 'zone' && level ? (
|
|
173
|
-
<ViewerRoomCallout
|
|
174
|
-
entry={entry}
|
|
175
|
-
floorY={roomFloorElevation(nodes as Record<string, AnyNode>, zoneId)}
|
|
176
|
-
key={zoneId}
|
|
177
|
-
level={level}
|
|
178
|
-
name={zoneLabel(zone, i18n)}
|
|
179
|
-
/>
|
|
180
|
-
) : null
|
|
181
|
-
})()
|
|
182
|
-
: null}
|
|
183
|
-
</>
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** The selected room's callout: its name and area at a point inside it, and the close button. */
|
|
188
|
-
function ViewerRoomCallout({
|
|
189
|
-
entry,
|
|
190
|
-
floorY,
|
|
191
|
-
level,
|
|
192
|
-
name,
|
|
193
|
-
}: {
|
|
194
|
-
entry: RoomLabelEntry
|
|
195
|
-
floorY: number
|
|
196
|
-
level: NonNullable<ReturnType<ViewerSource['objectFor']>>
|
|
197
|
-
name: string
|
|
198
|
-
}) {
|
|
199
|
-
const unit = useViewer((s) => s.unit)
|
|
200
|
-
const locale = useLocale()
|
|
201
|
-
const anchor = useMemo(() => {
|
|
202
|
-
const [x, z] = polygonInteriorPoint({ polygon: entry.polygon, holes: entry.holes })
|
|
203
|
-
return new Vector3(x, floorY + 0.05, z)
|
|
204
|
-
}, [entry.polygon, entry.holes, floorY])
|
|
205
|
-
const dismiss = useCallback(() => {
|
|
206
|
-
const { selection, setSelection } = useViewer.getState()
|
|
207
|
-
if (selection.zoneId === entry.zoneId) setSelection({ zoneId: null })
|
|
208
|
-
}, [entry.zoneId])
|
|
209
|
-
return (
|
|
210
|
-
<SelectionCallout
|
|
211
|
-
actions={[]}
|
|
212
|
-
anchor={anchor}
|
|
213
|
-
object={level}
|
|
214
|
-
onDismiss={dismiss}
|
|
215
|
-
subtitle={formatRoomArea(entry.area, unit, locale)}
|
|
216
|
-
title={name}
|
|
217
|
-
/>
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
|
|
222
|
-
|
|
223
|
-
/** `ViewerRooms` over the parametric scene store. */
|
|
224
|
-
export function ParametricViewerRooms() {
|
|
225
|
-
const nodes = useScene((s) => s.nodes)
|
|
226
|
-
const source = useMemo<ViewerSource>(() => ({ nodes, objectFor: parametricObject }), [nodes])
|
|
227
|
-
return <ViewerRooms source={source} />
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function ViewerRoomHighlight({
|
|
231
|
-
room,
|
|
232
|
-
source,
|
|
233
|
-
}: {
|
|
234
|
-
room: RoomSelectionRecord
|
|
235
|
-
source: ViewerSource
|
|
236
|
-
}) {
|
|
237
|
-
const heights = useMemo(
|
|
238
|
-
() => resolveRoomAssemblyHeights(room, source.nodes as Record<string, AnyNode>),
|
|
239
|
-
[room, source.nodes],
|
|
240
|
-
)
|
|
241
|
-
return (
|
|
242
|
-
<RoomAssemblyMesh
|
|
243
|
-
heights={heights}
|
|
244
|
-
levelObject={() => source.objectFor(room.key.levelId)}
|
|
245
|
-
room={room}
|
|
246
|
-
/>
|
|
247
|
-
)
|
|
248
|
-
}
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type LevelNode,
|
|
5
|
-
type PascalI18n,
|
|
6
|
-
sceneRegistry,
|
|
7
|
-
useLocale,
|
|
8
|
-
usePascalI18n,
|
|
9
|
-
useScene,
|
|
10
|
-
} from '@pascal-app/core'
|
|
11
|
-
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
12
|
-
import { ArrowLeft, ChevronRight, Layers } from 'lucide-react'
|
|
13
|
-
import Link from 'next/link'
|
|
14
|
-
import { type ReactNode, useMemo } from 'react'
|
|
15
|
-
import { localizedLevelName } from '../../i18n/level-name'
|
|
16
|
-
import { cn } from '../../lib/utils'
|
|
17
|
-
import {
|
|
18
|
-
applyViewerClick,
|
|
19
|
-
frameViewerNode,
|
|
20
|
-
roomOfObject,
|
|
21
|
-
selectViewerBuilding,
|
|
22
|
-
selectViewerLevel,
|
|
23
|
-
selectViewerRoom,
|
|
24
|
-
selectViewerUnit,
|
|
25
|
-
type ViewerCrumb,
|
|
26
|
-
type ViewerSidebarSection,
|
|
27
|
-
type ViewerSource,
|
|
28
|
-
viewerBreadcrumb,
|
|
29
|
-
viewerSidebarSections,
|
|
30
|
-
} from '../../lib/viewer-selection'
|
|
31
|
-
import { formatRoomArea } from '../editor/room-labels'
|
|
32
|
-
|
|
33
|
-
export type ViewerSceneHeaderProps = {
|
|
34
|
-
projectName?: string | null
|
|
35
|
-
owner?: { username?: string | null } | null
|
|
36
|
-
onBack?: () => void
|
|
37
|
-
/** Fallback destination when no `onBack` handler is supplied. Must already be
|
|
38
|
-
* sanitized by the caller. `null` shows no back arrow (an embedded viewer with
|
|
39
|
-
* nowhere to go back to). */
|
|
40
|
-
backHref?: string | null
|
|
41
|
-
/** Extra row under the project info (e.g. like and duplicate actions). */
|
|
42
|
-
stats?: ReactNode
|
|
43
|
-
/** The scene the header navigates; the parametric scene store by default. */
|
|
44
|
-
source?: ViewerSource
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function sectionTitle(kind: ViewerSidebarSection['kind'], i18n: PascalI18n): string {
|
|
48
|
-
if (kind === 'units') return i18n._({ id: 'viewer.header.units', message: 'Units' })
|
|
49
|
-
if (kind === 'rooms') return i18n._({ id: 'viewer.header.rooms', message: 'Rooms' })
|
|
50
|
-
return i18n._({ id: 'viewer.header.zones', message: 'Zones' })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The navigation of every viewer surface: the project card, the bar (building
|
|
57
|
-
* › floor › room › element) and the floor list with, under the current floor,
|
|
58
|
-
* its units, rooms and zones. A row or a crumb selects and frames; the canvas
|
|
59
|
-
* selects in place (`lib/viewer-selection.ts`).
|
|
60
|
-
*/
|
|
61
|
-
export const ViewerSceneHeader = ({
|
|
62
|
-
projectName,
|
|
63
|
-
owner,
|
|
64
|
-
onBack,
|
|
65
|
-
backHref = '/',
|
|
66
|
-
stats,
|
|
67
|
-
source: sourceProp,
|
|
68
|
-
}: ViewerSceneHeaderProps) => {
|
|
69
|
-
const i18n = usePascalI18n()
|
|
70
|
-
const catalogVersion = i18n.getVersion()
|
|
71
|
-
const locale = useLocale()
|
|
72
|
-
const sceneNodes = useScene((s) => s.nodes)
|
|
73
|
-
const source = useMemo<ViewerSource>(
|
|
74
|
-
() => sourceProp ?? { nodes: sceneNodes, objectFor: parametricObject },
|
|
75
|
-
[sourceProp, sceneNodes],
|
|
76
|
-
)
|
|
77
|
-
const { nodes } = source
|
|
78
|
-
const selection = useViewer((s) => s.selection)
|
|
79
|
-
const focusedUnitId = useViewer((s) => s.focusedUnitId)
|
|
80
|
-
const unit = useViewer((s) => s.unit)
|
|
81
|
-
|
|
82
|
-
const crumbs = useMemo(
|
|
83
|
-
() =>
|
|
84
|
-
viewerBreadcrumb(selection, focusedUnitId, nodes, i18n, (nodeId, levelId) =>
|
|
85
|
-
roomOfObject(nodeId, levelId, source),
|
|
86
|
-
),
|
|
87
|
-
[selection, focusedUnitId, nodes, source, i18n, locale, catalogVersion],
|
|
88
|
-
)
|
|
89
|
-
const buildingId = crumbs.find((crumb) => crumb.kind === 'building')?.id ?? null
|
|
90
|
-
// Highest first so the list reads top-down like a building section.
|
|
91
|
-
const levels = useMemo(() => {
|
|
92
|
-
const building = buildingId ? nodes[buildingId] : undefined
|
|
93
|
-
if (building?.type !== 'building') return []
|
|
94
|
-
return building.children
|
|
95
|
-
.map((id) => nodes[id])
|
|
96
|
-
.filter((node): node is LevelNode => node?.type === 'level')
|
|
97
|
-
.sort((a, b) => b.level - a.level)
|
|
98
|
-
}, [buildingId, nodes])
|
|
99
|
-
const sections = useMemo(
|
|
100
|
-
() => (selection.levelId ? viewerSidebarSections(selection.levelId, nodes, i18n) : []),
|
|
101
|
-
[selection.levelId, nodes, i18n, locale, catalogVersion],
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
const handleCrumb = (crumb: ViewerCrumb) => {
|
|
105
|
-
switch (crumb.kind) {
|
|
106
|
-
case 'building':
|
|
107
|
-
return selectViewerBuilding(source)
|
|
108
|
-
case 'level':
|
|
109
|
-
return selectViewerLevel(crumb.id, source)
|
|
110
|
-
case 'unit':
|
|
111
|
-
return selectViewerUnit(crumb.id, source)
|
|
112
|
-
case 'room':
|
|
113
|
-
return selectViewerRoom(crumb.id, source)
|
|
114
|
-
case 'node':
|
|
115
|
-
applyViewerClick({ nodeId: crumb.id, point: null }, nodes)
|
|
116
|
-
return frameViewerNode(crumb.id, source)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<div className="dark absolute top-4 left-4 z-20 flex max-h-[calc(100%-7rem)] flex-col gap-3 text-foreground">
|
|
122
|
-
<div className="corner-smooth pointer-events-auto flex min-w-[200px] max-w-[min(28rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
|
|
123
|
-
{/* Project info + back */}
|
|
124
|
-
<div className="flex items-center gap-3 px-3 py-2.5">
|
|
125
|
-
{onBack ? (
|
|
126
|
-
<button
|
|
127
|
-
aria-label={i18n._({ id: 'viewer.header.back', message: 'Back' })}
|
|
128
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
|
|
129
|
-
onClick={onBack}
|
|
130
|
-
type="button"
|
|
131
|
-
>
|
|
132
|
-
<ArrowLeft className="h-4 w-4 text-muted-foreground" />
|
|
133
|
-
</button>
|
|
134
|
-
) : backHref === null ? null : (
|
|
135
|
-
<Link
|
|
136
|
-
aria-label={i18n._({ id: 'viewer.header.back', message: 'Back' })}
|
|
137
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
|
|
138
|
-
href={backHref}
|
|
139
|
-
prefetch={false}
|
|
140
|
-
>
|
|
141
|
-
<ArrowLeft className="h-4 w-4 text-muted-foreground" />
|
|
142
|
-
</Link>
|
|
143
|
-
)}
|
|
144
|
-
<div className="min-w-0 flex-1">
|
|
145
|
-
<div className="truncate font-medium text-foreground text-sm">
|
|
146
|
-
{projectName || i18n._({ id: 'viewer.header.untitled', message: 'Untitled' })}
|
|
147
|
-
</div>
|
|
148
|
-
{owner?.username && (
|
|
149
|
-
<Link
|
|
150
|
-
className="text-muted-foreground text-xs transition-colors hover:text-foreground"
|
|
151
|
-
href={`/u/${owner.username}`}
|
|
152
|
-
>
|
|
153
|
-
@{owner.username}
|
|
154
|
-
</Link>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
{stats && (
|
|
160
|
-
<div className="flex items-center gap-1 border-border/40 border-t px-3 py-2">{stats}</div>
|
|
161
|
-
)}
|
|
162
|
-
|
|
163
|
-
{crumbs.length > 0 && (
|
|
164
|
-
<nav
|
|
165
|
-
aria-label={i18n._({ id: 'viewer.header.selection', message: 'Selection' })}
|
|
166
|
-
className="flex flex-wrap items-center gap-x-1.5 gap-y-1 border-border/40 border-t px-3 py-2 text-xs"
|
|
167
|
-
data-testid="viewer-breadcrumb"
|
|
168
|
-
>
|
|
169
|
-
{crumbs.map((crumb, index) => {
|
|
170
|
-
const isLast = index === crumbs.length - 1
|
|
171
|
-
return (
|
|
172
|
-
<span
|
|
173
|
-
className="flex min-w-0 items-center gap-1.5"
|
|
174
|
-
key={`${crumb.kind}:${crumb.id}`}
|
|
175
|
-
>
|
|
176
|
-
{index > 0 && <ChevronRight className="h-3 w-3 text-muted-foreground/50" />}
|
|
177
|
-
<button
|
|
178
|
-
className={cn(
|
|
179
|
-
'truncate transition-colors',
|
|
180
|
-
isLast
|
|
181
|
-
? 'font-medium text-foreground'
|
|
182
|
-
: 'text-muted-foreground hover:text-foreground',
|
|
183
|
-
)}
|
|
184
|
-
data-crumb={crumb.kind}
|
|
185
|
-
onClick={() => handleCrumb(crumb)}
|
|
186
|
-
type="button"
|
|
187
|
-
>
|
|
188
|
-
{crumb.label}
|
|
189
|
-
</button>
|
|
190
|
-
</span>
|
|
191
|
-
)
|
|
192
|
-
})}
|
|
193
|
-
</nav>
|
|
194
|
-
)}
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
{levels.length > 0 && (
|
|
198
|
-
<div className="corner-smooth pointer-events-auto flex min-h-0 w-56 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
|
|
199
|
-
<span className="px-3 py-2 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
200
|
-
{i18n._({ id: 'viewer.header.levels', message: 'Levels' })}
|
|
201
|
-
</span>
|
|
202
|
-
<div className="flex min-h-0 flex-col overflow-y-auto">
|
|
203
|
-
{levels.map((lvl) => {
|
|
204
|
-
const isSelected = lvl.id === selection.levelId
|
|
205
|
-
return (
|
|
206
|
-
<div key={lvl.id}>
|
|
207
|
-
<button
|
|
208
|
-
className={cn(
|
|
209
|
-
'group/row relative flex h-8 w-full cursor-pointer select-none items-center border-border/50 border-r border-r-transparent border-b px-3 text-sm transition-all duration-200',
|
|
210
|
-
isSelected
|
|
211
|
-
? 'border-r-3 border-r-white bg-accent/50 text-foreground'
|
|
212
|
-
: 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
|
|
213
|
-
)}
|
|
214
|
-
onClick={() => {
|
|
215
|
-
if (!isSelected) markPerfAction('level-switch', lvl.id)
|
|
216
|
-
selectViewerLevel(lvl.id, source)
|
|
217
|
-
}}
|
|
218
|
-
type="button"
|
|
219
|
-
>
|
|
220
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
221
|
-
<span
|
|
222
|
-
className={cn(
|
|
223
|
-
'flex h-4 w-4 shrink-0 items-center justify-center transition-all duration-200',
|
|
224
|
-
!isSelected && 'opacity-60 grayscale',
|
|
225
|
-
)}
|
|
226
|
-
>
|
|
227
|
-
<Layers className="h-3.5 w-3.5" />
|
|
228
|
-
</span>
|
|
229
|
-
<div className="min-w-0 flex-1 truncate text-left">
|
|
230
|
-
{localizedLevelName(lvl, i18n, locale)}
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
</button>
|
|
234
|
-
{isSelected &&
|
|
235
|
-
sections.map((section) => (
|
|
236
|
-
<div
|
|
237
|
-
className="border-border/50 border-b pb-1"
|
|
238
|
-
data-viewer-section={section.kind}
|
|
239
|
-
key={section.kind}
|
|
240
|
-
>
|
|
241
|
-
<span className="block px-3 pt-2 pb-1 pl-9 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
242
|
-
{sectionTitle(section.kind, i18n)}
|
|
243
|
-
</span>
|
|
244
|
-
{section.rows.map((row) => {
|
|
245
|
-
const active =
|
|
246
|
-
section.kind === 'units'
|
|
247
|
-
? row.id === focusedUnitId
|
|
248
|
-
: row.id === selection.zoneId
|
|
249
|
-
return (
|
|
250
|
-
<button
|
|
251
|
-
className={cn(
|
|
252
|
-
'flex h-7 w-full cursor-pointer select-none items-center gap-2 pr-3 pl-9 text-left text-sm transition-colors',
|
|
253
|
-
active
|
|
254
|
-
? 'bg-accent/50 text-foreground'
|
|
255
|
-
: 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
|
|
256
|
-
)}
|
|
257
|
-
key={row.id}
|
|
258
|
-
onClick={() =>
|
|
259
|
-
section.kind === 'units'
|
|
260
|
-
? selectViewerUnit(row.id, source)
|
|
261
|
-
: selectViewerRoom(row.id, source)
|
|
262
|
-
}
|
|
263
|
-
type="button"
|
|
264
|
-
>
|
|
265
|
-
<span
|
|
266
|
-
className="h-2 w-2 shrink-0 rounded-full"
|
|
267
|
-
style={{ backgroundColor: row.color }}
|
|
268
|
-
/>
|
|
269
|
-
<span className="min-w-0 flex-1 truncate">{row.label}</span>
|
|
270
|
-
<span className="shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums">
|
|
271
|
-
{formatRoomArea(row.areaM2, unit, locale)}
|
|
272
|
-
</span>
|
|
273
|
-
</button>
|
|
274
|
-
)
|
|
275
|
-
})}
|
|
276
|
-
</div>
|
|
277
|
-
))}
|
|
278
|
-
</div>
|
|
279
|
-
)
|
|
280
|
-
})}
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
)}
|
|
284
|
-
</div>
|
|
285
|
-
)
|
|
286
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { type AnyNodeId, emitter, type NodeEvent, sceneRegistry, useScene } from '@pascal-app/core'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import { useThree } from '@react-three/fiber'
|
|
6
|
-
import { useEffect } from 'react'
|
|
7
|
-
import { selectionModifiersFromEvent } from '../../lib/selection-routing'
|
|
8
|
-
import {
|
|
9
|
-
applyViewerClick,
|
|
10
|
-
applyViewerHover,
|
|
11
|
-
viewerPickFromNodeEvent,
|
|
12
|
-
} from '../../lib/viewer-selection'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Selection for a parametric viewer surface (the published viewer's fallback,
|
|
16
|
-
* its embed, the editor's Preview): free-form and room-first, through the same
|
|
17
|
-
* rules as the baked viewer. Mount it with `<Viewer selectionManager="custom">`.
|
|
18
|
-
*/
|
|
19
|
-
export function ViewerSelectionManager() {
|
|
20
|
-
const gl = useThree((state) => state.gl)
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
let clickHandled = false
|
|
24
|
-
let hoveredFrom: string | null = null
|
|
25
|
-
const pendingClicks = new Set<number>()
|
|
26
|
-
|
|
27
|
-
const onMove = (event: NodeEvent) => {
|
|
28
|
-
if (useViewer.getState().walkthroughMode) return
|
|
29
|
-
const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
|
|
30
|
-
if (!pick) return
|
|
31
|
-
event.stopPropagation()
|
|
32
|
-
hoveredFrom = event.node.id
|
|
33
|
-
applyViewerHover(pick, useScene.getState().nodes)
|
|
34
|
-
}
|
|
35
|
-
const onLeave = (event: NodeEvent) => {
|
|
36
|
-
if (event.node.id !== hoveredFrom) return
|
|
37
|
-
hoveredFrom = null
|
|
38
|
-
useViewer.getState().setHoveredId(null)
|
|
39
|
-
}
|
|
40
|
-
const onClick = (event: NodeEvent) => {
|
|
41
|
-
if (useViewer.getState().walkthroughMode) return
|
|
42
|
-
const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
|
|
43
|
-
if (!pick) return
|
|
44
|
-
event.stopPropagation()
|
|
45
|
-
clickHandled = true
|
|
46
|
-
applyViewerClick(
|
|
47
|
-
pick,
|
|
48
|
-
useScene.getState().nodes,
|
|
49
|
-
selectionModifiersFromEvent(event.nativeEvent),
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
// A click on nothing selectable clears the room and the element.
|
|
53
|
-
const onCanvasClick = (event: MouseEvent) => {
|
|
54
|
-
const viewer = useViewer.getState()
|
|
55
|
-
if (event.button !== 0 || viewer.walkthroughMode) return
|
|
56
|
-
if (viewer.cameraDragging || viewer.inputDragging) return
|
|
57
|
-
const frame = requestAnimationFrame(() => {
|
|
58
|
-
pendingClicks.delete(frame)
|
|
59
|
-
if (useViewer.getState().walkthroughMode) return
|
|
60
|
-
if (clickHandled) {
|
|
61
|
-
clickHandled = false
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
applyViewerClick(null, useScene.getState().nodes)
|
|
65
|
-
})
|
|
66
|
-
pendingClicks.add(frame)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
emitter.on('node:move', onMove)
|
|
70
|
-
emitter.on('node:enter', onMove)
|
|
71
|
-
emitter.on('node:leave', onLeave)
|
|
72
|
-
emitter.on('node:click', onClick)
|
|
73
|
-
const canvas = gl.domElement
|
|
74
|
-
canvas.addEventListener('click', onCanvasClick)
|
|
75
|
-
return () => {
|
|
76
|
-
emitter.off('node:move', onMove)
|
|
77
|
-
emitter.off('node:enter', onMove)
|
|
78
|
-
emitter.off('node:leave', onLeave)
|
|
79
|
-
emitter.off('node:click', onClick)
|
|
80
|
-
canvas.removeEventListener('click', onCanvasClick)
|
|
81
|
-
for (const frame of pendingClicks) cancelAnimationFrame(frame)
|
|
82
|
-
useViewer.getState().setHoveredId(null)
|
|
83
|
-
}
|
|
84
|
-
}, [gl])
|
|
85
|
-
|
|
86
|
-
return <ViewerOutlineSync />
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The selected elements and the hovered one, outlined (rooms highlight instead). */
|
|
90
|
-
function ViewerOutlineSync() {
|
|
91
|
-
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
92
|
-
const externalSelectedIds = useViewer((s) => s.externalSelectedIds)
|
|
93
|
-
const hoveredId = useViewer((s) => s.hoveredId)
|
|
94
|
-
const outliner = useViewer((s) => s.outliner)
|
|
95
|
-
const geometryRevision = useViewer((s) => s.geometryRevision)
|
|
96
|
-
const walkthrough = useViewer((s) => s.walkthroughMode)
|
|
97
|
-
const nodes = useScene((s) => s.nodes)
|
|
98
|
-
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
void geometryRevision
|
|
101
|
-
outliner.selectedObjects.length = 0
|
|
102
|
-
outliner.hoveredObjects.length = 0
|
|
103
|
-
if (walkthrough) return
|
|
104
|
-
for (const id of new Set([...selectedIds, ...externalSelectedIds])) {
|
|
105
|
-
const object = sceneRegistry.nodes.get(id)
|
|
106
|
-
if (object) outliner.selectedObjects.push(object)
|
|
107
|
-
}
|
|
108
|
-
const hovered = hoveredId ? nodes[hoveredId as AnyNodeId] : undefined
|
|
109
|
-
if (hovered && hovered.type !== 'zone') {
|
|
110
|
-
const object = sceneRegistry.nodes.get(hovered.id)
|
|
111
|
-
if (object) outliner.hoveredObjects.push(object)
|
|
112
|
-
}
|
|
113
|
-
}, [selectedIds, externalSelectedIds, hoveredId, outliner, nodes, geometryRevision, walkthrough])
|
|
114
|
-
|
|
115
|
-
useEffect(
|
|
116
|
-
() => () => {
|
|
117
|
-
outliner.selectedObjects.length = 0
|
|
118
|
-
outliner.hoveredObjects.length = 0
|
|
119
|
-
},
|
|
120
|
-
[outliner],
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
return null
|
|
124
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
normalizeViewerStageModes,
|
|
4
|
-
resolveMobileViewerStageMode,
|
|
5
|
-
resolveViewerStageMode,
|
|
6
|
-
viewerStageIncludes3D,
|
|
7
|
-
} from './viewer-stage-modes'
|
|
8
|
-
|
|
9
|
-
describe('viewer stage modes', () => {
|
|
10
|
-
test('supports every ordered combination without duplicates', () => {
|
|
11
|
-
expect(normalizeViewerStageModes(['split', '3d', 'split'])).toEqual(['3d', 'split'])
|
|
12
|
-
expect(normalizeViewerStageModes(['2d'])).toEqual(['2d'])
|
|
13
|
-
expect(normalizeViewerStageModes([])).toEqual(['3d'])
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test('reuses normalized combinations across inline prop arrays', () => {
|
|
17
|
-
expect(normalizeViewerStageModes(['split', '3d'])).toBe(
|
|
18
|
-
normalizeViewerStageModes(['3d', 'split']),
|
|
19
|
-
)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('falls back to the first enabled mode', () => {
|
|
23
|
-
expect(resolveViewerStageMode('split', ['3d', '2d'])).toBe('3d')
|
|
24
|
-
expect(resolveViewerStageMode(undefined, ['2d', 'split'])).toBe('2d')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('uses an enabled single-pane mode on mobile but preserves split-only embeds', () => {
|
|
28
|
-
expect(resolveMobileViewerStageMode('split', ['3d', 'split'])).toBe('3d')
|
|
29
|
-
expect(resolveMobileViewerStageMode('split', ['2d', 'split'])).toBe('2d')
|
|
30
|
-
expect(resolveMobileViewerStageMode('split', ['split'])).toBe('split')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('does not require a GPU canvas for a 2D-only embed', () => {
|
|
34
|
-
expect(viewerStageIncludes3D(['2d'])).toBe(false)
|
|
35
|
-
expect(viewerStageIncludes3D(['split'])).toBe(true)
|
|
36
|
-
expect(viewerStageIncludes3D(['3d', '2d'])).toBe(true)
|
|
37
|
-
})
|
|
38
|
-
})
|