@pascal-app/editor 1.1.0 → 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 +11 -11
- 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 -1981
- package/src/components/editor/floating-action-menu.tsx +1 -1
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +6 -2
- 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 +26 -22
- package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
- 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 +1 -1
- 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/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +24 -14
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/room-controls.tsx +7 -4
- package/src/components/editor/room-handles-2d.tsx +2 -3
- package/src/components/editor/room-handles.tsx +1 -2
- 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 +40 -1671
- 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 +1 -1
- 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 +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +1 -1
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
- 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-placement-preview-layer.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
- package/src/components/systems/roof/roof-edit-system.tsx +4 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
- 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 +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- 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/terrain-sculpt-panel.tsx +1 -1
- 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 +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +1 -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 +2 -2
- package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +1 -1
- package/src/components/ui/panels/panel-wrapper.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +6 -6
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-panel.tsx +6 -8
- 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.tsx +1 -1
- 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 +1 -1
- 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/index.tsx +2 -2
- 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/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/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 +1 -1
- 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 +180 -182
- package/src/i18n/locales/es/messages.po +180 -182
- package/src/i18n/locales/fr/messages.po +180 -182
- package/src/i18n/locales/ja/messages.po +180 -182
- package/src/i18n/locales/zh-CN/messages.po +180 -182
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +109 -131
- 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/editor-level-display.ts +1 -1
- package/src/lib/floor-footprints.test.ts +2 -2
- package/src/lib/floor-opening-draft.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/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/glb-export.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurements.test.ts +2 -1
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-handle-drag.ts +2 -12
- 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-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/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/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/store/use-editor.tsx +17 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- 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/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/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/measurements.ts +0 -8
- 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 -327
- package/src/lib/scene-bounds.ts +0 -233
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- 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,49 +0,0 @@
|
|
|
1
|
-
export const VIEWER_STAGE_MODES = ['3d', '2d', 'split'] as const
|
|
2
|
-
|
|
3
|
-
export type ViewerStageMode = (typeof VIEWER_STAGE_MODES)[number]
|
|
4
|
-
|
|
5
|
-
const THREE_D_MODES = ['3d'] as const
|
|
6
|
-
const TWO_D_MODES = ['2d'] as const
|
|
7
|
-
const SPLIT_MODES = ['split'] as const
|
|
8
|
-
const THREE_D_TWO_D_MODES = ['3d', '2d'] as const
|
|
9
|
-
const THREE_D_SPLIT_MODES = ['3d', 'split'] as const
|
|
10
|
-
const TWO_D_SPLIT_MODES = ['2d', 'split'] as const
|
|
11
|
-
|
|
12
|
-
export function normalizeViewerStageModes(
|
|
13
|
-
modes: readonly ViewerStageMode[] | undefined,
|
|
14
|
-
): readonly ViewerStageMode[] {
|
|
15
|
-
if (!modes) return VIEWER_STAGE_MODES
|
|
16
|
-
|
|
17
|
-
const has3D = modes.includes('3d')
|
|
18
|
-
const has2D = modes.includes('2d')
|
|
19
|
-
const hasSplit = modes.includes('split')
|
|
20
|
-
|
|
21
|
-
if (has3D && has2D && hasSplit) return VIEWER_STAGE_MODES
|
|
22
|
-
if (has3D && has2D) return THREE_D_TWO_D_MODES
|
|
23
|
-
if (has3D && hasSplit) return THREE_D_SPLIT_MODES
|
|
24
|
-
if (has2D && hasSplit) return TWO_D_SPLIT_MODES
|
|
25
|
-
if (has2D) return TWO_D_MODES
|
|
26
|
-
if (hasSplit) return SPLIT_MODES
|
|
27
|
-
return THREE_D_MODES
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function resolveViewerStageMode(
|
|
31
|
-
mode: ViewerStageMode | undefined,
|
|
32
|
-
modes: readonly ViewerStageMode[],
|
|
33
|
-
): ViewerStageMode {
|
|
34
|
-
return mode && modes.includes(mode) ? mode : (modes[0] ?? '3d')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function resolveMobileViewerStageMode(
|
|
38
|
-
mode: ViewerStageMode,
|
|
39
|
-
modes: readonly ViewerStageMode[],
|
|
40
|
-
): ViewerStageMode {
|
|
41
|
-
if (mode !== 'split') return mode
|
|
42
|
-
if (modes.includes('2d')) return '2d'
|
|
43
|
-
if (modes.includes('3d')) return '3d'
|
|
44
|
-
return 'split'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function viewerStageIncludes3D(modes: readonly ViewerStageMode[]) {
|
|
48
|
-
return modes.includes('3d') || modes.includes('split')
|
|
49
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { usePascalI18n } from '@pascal-app/core'
|
|
4
|
-
import { Box, Columns2, Map as MapIcon } from 'lucide-react'
|
|
5
|
-
import type { ReactNode } from 'react'
|
|
6
|
-
import { cn } from '../../lib/utils'
|
|
7
|
-
import { normalizeViewerStageModes, type ViewerStageMode } from './viewer-stage-modes'
|
|
8
|
-
|
|
9
|
-
export type { ViewerStageMode } from './viewer-stage-modes'
|
|
10
|
-
|
|
11
|
-
export type ViewerStageSwitcherProps = {
|
|
12
|
-
className?: string
|
|
13
|
-
hideSplitOnMobile?: boolean
|
|
14
|
-
mode: ViewerStageMode
|
|
15
|
-
modes?: readonly ViewerStageMode[]
|
|
16
|
-
onChange: (mode: ViewerStageMode) => void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function ViewerStageSwitcher({
|
|
20
|
-
className,
|
|
21
|
-
hideSplitOnMobile = true,
|
|
22
|
-
mode,
|
|
23
|
-
modes,
|
|
24
|
-
onChange,
|
|
25
|
-
}: ViewerStageSwitcherProps) {
|
|
26
|
-
const i18n = usePascalI18n()
|
|
27
|
-
const enabledModes = normalizeViewerStageModes(modes)
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
aria-label={i18n._({ id: 'viewer.stage.layout', message: 'Viewer layout' })}
|
|
32
|
-
className={cn(
|
|
33
|
-
'dark absolute top-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-1 rounded-full border border-white/10 bg-neutral-950/82 p-1 text-white shadow-elevation-4 backdrop-blur-xl',
|
|
34
|
-
className,
|
|
35
|
-
)}
|
|
36
|
-
role="group"
|
|
37
|
-
>
|
|
38
|
-
{enabledModes.includes('3d') ? (
|
|
39
|
-
<StageButton
|
|
40
|
-
active={mode === '3d'}
|
|
41
|
-
icon={<Box />}
|
|
42
|
-
label="3D"
|
|
43
|
-
onClick={() => onChange('3d')}
|
|
44
|
-
/>
|
|
45
|
-
) : null}
|
|
46
|
-
{enabledModes.includes('2d') ? (
|
|
47
|
-
<StageButton
|
|
48
|
-
active={mode === '2d'}
|
|
49
|
-
icon={<MapIcon />}
|
|
50
|
-
label="2D"
|
|
51
|
-
onClick={() => onChange('2d')}
|
|
52
|
-
/>
|
|
53
|
-
) : null}
|
|
54
|
-
{enabledModes.includes('split') ? (
|
|
55
|
-
<StageButton
|
|
56
|
-
active={mode === 'split'}
|
|
57
|
-
className={hideSplitOnMobile && enabledModes.length > 1 ? 'hidden md:flex' : undefined}
|
|
58
|
-
icon={<Columns2 />}
|
|
59
|
-
label={i18n._({ id: 'viewer.stage.split', message: 'Split' })}
|
|
60
|
-
onClick={() => onChange('split')}
|
|
61
|
-
/>
|
|
62
|
-
) : null}
|
|
63
|
-
</div>
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function StageButton({
|
|
68
|
-
active,
|
|
69
|
-
className,
|
|
70
|
-
icon,
|
|
71
|
-
label,
|
|
72
|
-
onClick,
|
|
73
|
-
}: {
|
|
74
|
-
active: boolean
|
|
75
|
-
className?: string
|
|
76
|
-
icon: ReactNode
|
|
77
|
-
label: string
|
|
78
|
-
onClick: () => void
|
|
79
|
-
}) {
|
|
80
|
-
return (
|
|
81
|
-
<button
|
|
82
|
-
aria-pressed={active}
|
|
83
|
-
className={cn(
|
|
84
|
-
'flex h-8 items-center gap-1.5 rounded-full px-3 font-medium text-xs',
|
|
85
|
-
active
|
|
86
|
-
? 'bg-white text-neutral-950 shadow-sm'
|
|
87
|
-
: 'text-neutral-300 transition-colors hover:bg-white/10 hover:text-white',
|
|
88
|
-
className,
|
|
89
|
-
)}
|
|
90
|
-
onClick={onClick}
|
|
91
|
-
type="button"
|
|
92
|
-
>
|
|
93
|
-
<span className="[&>svg]:size-3.5">{icon}</span>
|
|
94
|
-
{label}
|
|
95
|
-
</button>
|
|
96
|
-
)
|
|
97
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { LevelNode } from '@pascal-app/core/schema'
|
|
3
|
-
import { renderToStaticMarkup } from 'react-dom/server'
|
|
4
|
-
import type { FloorplanPreviewScene } from './floorplan-preview'
|
|
5
|
-
import { ViewerStage } from './viewer-stage'
|
|
6
|
-
|
|
7
|
-
const level = LevelNode.parse({ id: 'level_ground', type: 'level' })
|
|
8
|
-
const scene: FloorplanPreviewScene = { nodes: { [level.id]: level } }
|
|
9
|
-
|
|
10
|
-
describe('ViewerStage', () => {
|
|
11
|
-
test('owns synchronized 2D and 3D composition by default', () => {
|
|
12
|
-
const markup = renderToStaticMarkup(
|
|
13
|
-
<ViewerStage mode="split" scene={scene} showLevelSelector={false}>
|
|
14
|
-
<div data-test-viewer-content="" />
|
|
15
|
-
</ViewerStage>,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
expect(markup).toContain('data-pascal-viewer-stage="split"')
|
|
19
|
-
expect(markup).toContain('data-pascal-navigation-sync="on"')
|
|
20
|
-
expect(markup).toContain('data-pascal-viewer-3d="true"')
|
|
21
|
-
expect(markup).toContain('data-floorplan-preview=""')
|
|
22
|
-
expect(markup).toContain('viewBox="0 0 48 48"')
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('keeps a 2D-only embed free of a 3D canvas mount', () => {
|
|
26
|
-
const markup = renderToStaticMarkup(
|
|
27
|
-
<ViewerStage mode="2d" modes={['2d']} scene={scene} showLevelSelector={false} />,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
expect(markup).toContain('data-pascal-viewer-stage="2d"')
|
|
31
|
-
expect(markup).not.toContain('data-pascal-viewer-3d')
|
|
32
|
-
expect(markup).toContain('data-floorplan-preview=""')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('supports an explicit navigation synchronization opt-out', () => {
|
|
36
|
-
const markup = renderToStaticMarkup(
|
|
37
|
-
<ViewerStage
|
|
38
|
-
mode="split"
|
|
39
|
-
scene={scene}
|
|
40
|
-
showLevelSelector={false}
|
|
41
|
-
synchronizeNavigation={false}
|
|
42
|
-
/>,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
expect(markup).toContain('data-pascal-navigation-sync="off"')
|
|
46
|
-
})
|
|
47
|
-
})
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
-
import { markPerfAction, useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import type { ReactNode } from 'react'
|
|
6
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
7
|
-
import { useShallow } from 'zustand/react/shallow'
|
|
8
|
-
import { cn } from '../../lib/utils'
|
|
9
|
-
import { FloorplanPreview, type FloorplanPreviewScene } from './floorplan-preview'
|
|
10
|
-
import {
|
|
11
|
-
normalizeViewerStageModes,
|
|
12
|
-
resolveMobileViewerStageMode,
|
|
13
|
-
resolveViewerStageMode,
|
|
14
|
-
type ViewerStageMode,
|
|
15
|
-
viewerStageIncludes3D,
|
|
16
|
-
} from './viewer-stage-modes'
|
|
17
|
-
import { ViewerStageSwitcher } from './viewer-stage-switcher'
|
|
18
|
-
|
|
19
|
-
export type ViewerStageProps = {
|
|
20
|
-
children?: ReactNode
|
|
21
|
-
className?: string
|
|
22
|
-
collapseSplitOnMobile?: boolean
|
|
23
|
-
compassHost?: Element | null
|
|
24
|
-
defaultMode?: ViewerStageMode
|
|
25
|
-
floorplanClassName?: string
|
|
26
|
-
levelId?: string | null
|
|
27
|
-
mode?: ViewerStageMode
|
|
28
|
-
modes?: readonly ViewerStageMode[]
|
|
29
|
-
onLevelChange?: (levelId: string) => void
|
|
30
|
-
onModeChange?: (mode: ViewerStageMode) => void
|
|
31
|
-
scene?: FloorplanPreviewScene | null
|
|
32
|
-
showCompass?: boolean
|
|
33
|
-
showLevelSelector?: boolean
|
|
34
|
-
showSwitcher?: boolean
|
|
35
|
-
switcherClassName?: string
|
|
36
|
-
synchronizeNavigation?: boolean
|
|
37
|
-
threeDClassName?: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const EMPTY_LEVEL_IDS: string[] = []
|
|
41
|
-
|
|
42
|
-
function levelNodeIds(nodes: Record<string, AnyNode>) {
|
|
43
|
-
return Object.values(nodes)
|
|
44
|
-
.filter((node) => node.type === 'level')
|
|
45
|
-
.sort(
|
|
46
|
-
(left, right) =>
|
|
47
|
-
((left as { level?: number }).level ?? 0) - ((right as { level?: number }).level ?? 0),
|
|
48
|
-
)
|
|
49
|
-
.map((node) => node.id)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function selectViewerLevel(nodes: Record<string, AnyNode>, levelId: string) {
|
|
53
|
-
const level = nodes[levelId as AnyNodeId]
|
|
54
|
-
if (level?.type !== 'level') return
|
|
55
|
-
const building = level.parentId ? nodes[level.parentId as AnyNodeId] : null
|
|
56
|
-
const viewer = useViewer.getState()
|
|
57
|
-
viewer.setSelection({
|
|
58
|
-
buildingId: building?.type === 'building' ? building.id : null,
|
|
59
|
-
levelId: level.id,
|
|
60
|
-
selectedIds: [],
|
|
61
|
-
zoneId: null,
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function ViewerStage({
|
|
66
|
-
children,
|
|
67
|
-
className,
|
|
68
|
-
collapseSplitOnMobile = true,
|
|
69
|
-
compassHost,
|
|
70
|
-
defaultMode,
|
|
71
|
-
floorplanClassName,
|
|
72
|
-
levelId,
|
|
73
|
-
mode: controlledMode,
|
|
74
|
-
modes,
|
|
75
|
-
onLevelChange,
|
|
76
|
-
onModeChange,
|
|
77
|
-
scene,
|
|
78
|
-
showCompass = true,
|
|
79
|
-
showLevelSelector = true,
|
|
80
|
-
showSwitcher = true,
|
|
81
|
-
switcherClassName,
|
|
82
|
-
synchronizeNavigation = true,
|
|
83
|
-
threeDClassName,
|
|
84
|
-
}: ViewerStageProps) {
|
|
85
|
-
const enabledModes = normalizeViewerStageModes(modes)
|
|
86
|
-
const [internalMode, setInternalMode] = useState(() =>
|
|
87
|
-
resolveViewerStageMode(defaultMode, enabledModes),
|
|
88
|
-
)
|
|
89
|
-
const activeMode = resolveViewerStageMode(controlledMode ?? internalMode, enabledModes)
|
|
90
|
-
const storeLevelIds = useScene(
|
|
91
|
-
useShallow((state) => (scene ? EMPTY_LEVEL_IDS : levelNodeIds(state.nodes))),
|
|
92
|
-
)
|
|
93
|
-
const externalLevelIds = useMemo(() => (scene ? levelNodeIds(scene.nodes) : null), [scene])
|
|
94
|
-
const levelIds = externalLevelIds ?? storeLevelIds
|
|
95
|
-
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
96
|
-
const [internalLevelId, setInternalLevelId] = useState<string | null>(null)
|
|
97
|
-
const [internalCompassHost, setInternalCompassHost] = useState<HTMLDivElement | null>(null)
|
|
98
|
-
const resolvedCompassHost = compassHost ?? internalCompassHost
|
|
99
|
-
const floorplanEnabled = enabledModes.includes('2d') || enabledModes.includes('split')
|
|
100
|
-
const threeDEnabled = viewerStageIncludes3D(enabledModes)
|
|
101
|
-
const mountFloorplan = floorplanEnabled || showCompass
|
|
102
|
-
|
|
103
|
-
const requestMode = useCallback(
|
|
104
|
-
(nextMode: ViewerStageMode) => {
|
|
105
|
-
if (!enabledModes.includes(nextMode)) return
|
|
106
|
-
if (controlledMode === undefined) setInternalMode(nextMode)
|
|
107
|
-
onModeChange?.(nextMode)
|
|
108
|
-
},
|
|
109
|
-
[controlledMode, enabledModes, onModeChange],
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (controlledMode !== undefined) {
|
|
114
|
-
if (controlledMode !== activeMode) onModeChange?.(activeMode)
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
if (internalMode !== activeMode) setInternalMode(activeMode)
|
|
118
|
-
}, [activeMode, controlledMode, internalMode, onModeChange])
|
|
119
|
-
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
if (!collapseSplitOnMobile) return
|
|
122
|
-
const mediaQuery = window.matchMedia('(max-width: 767px)')
|
|
123
|
-
const resolveMode = () => {
|
|
124
|
-
if (!mediaQuery.matches) return
|
|
125
|
-
const nextMode = resolveMobileViewerStageMode(activeMode, enabledModes)
|
|
126
|
-
if (nextMode !== activeMode) requestMode(nextMode)
|
|
127
|
-
}
|
|
128
|
-
resolveMode()
|
|
129
|
-
mediaQuery.addEventListener('change', resolveMode)
|
|
130
|
-
return () => mediaQuery.removeEventListener('change', resolveMode)
|
|
131
|
-
}, [activeMode, collapseSplitOnMobile, enabledModes, requestMode])
|
|
132
|
-
|
|
133
|
-
const chooseLevel = useCallback(
|
|
134
|
-
(nextLevelId: string, notify = true) => {
|
|
135
|
-
setInternalLevelId(nextLevelId)
|
|
136
|
-
if (notify && nextLevelId !== useViewer.getState().selection.levelId) {
|
|
137
|
-
markPerfAction('level-switch', nextLevelId)
|
|
138
|
-
}
|
|
139
|
-
if (nextLevelId !== useViewer.getState().selection.levelId)
|
|
140
|
-
selectViewerLevel(scene?.nodes ?? useScene.getState().nodes, nextLevelId)
|
|
141
|
-
if (notify) onLevelChange?.(nextLevelId)
|
|
142
|
-
},
|
|
143
|
-
[onLevelChange, scene],
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
if (activeMode === '3d' || levelIds.length === 0) return
|
|
148
|
-
const nextLevelId =
|
|
149
|
-
(levelId && levelIds.includes(levelId) ? levelId : null) ??
|
|
150
|
-
(selectedLevelId && levelIds.includes(selectedLevelId) ? selectedLevelId : null) ??
|
|
151
|
-
(internalLevelId && levelIds.includes(internalLevelId) ? internalLevelId : null) ??
|
|
152
|
-
levelIds[0] ??
|
|
153
|
-
null
|
|
154
|
-
if (nextLevelId) chooseLevel(nextLevelId, false)
|
|
155
|
-
}, [activeMode, chooseLevel, internalLevelId, levelId, levelIds, selectedLevelId])
|
|
156
|
-
|
|
157
|
-
return (
|
|
158
|
-
<div
|
|
159
|
-
className={cn('relative h-full w-full overflow-hidden bg-neutral-100', className)}
|
|
160
|
-
data-pascal-navigation-sync={synchronizeNavigation ? 'on' : 'off'}
|
|
161
|
-
data-pascal-viewer-stage={activeMode}
|
|
162
|
-
>
|
|
163
|
-
{showCompass && compassHost === undefined ? (
|
|
164
|
-
<div className="pointer-events-none absolute inset-0 z-30" ref={setInternalCompassHost} />
|
|
165
|
-
) : null}
|
|
166
|
-
|
|
167
|
-
{showSwitcher && enabledModes.length > 1 ? (
|
|
168
|
-
<ViewerStageSwitcher
|
|
169
|
-
className={switcherClassName}
|
|
170
|
-
hideSplitOnMobile={collapseSplitOnMobile}
|
|
171
|
-
mode={activeMode}
|
|
172
|
-
modes={enabledModes}
|
|
173
|
-
onChange={requestMode}
|
|
174
|
-
/>
|
|
175
|
-
) : null}
|
|
176
|
-
|
|
177
|
-
<div
|
|
178
|
-
className={
|
|
179
|
-
activeMode === 'split'
|
|
180
|
-
? 'absolute inset-0 grid grid-rows-2 md:grid-cols-2 md:grid-rows-1'
|
|
181
|
-
: 'absolute inset-0'
|
|
182
|
-
}
|
|
183
|
-
>
|
|
184
|
-
{threeDEnabled ? (
|
|
185
|
-
<div
|
|
186
|
-
className={cn(
|
|
187
|
-
activeMode === '2d'
|
|
188
|
-
? 'pointer-events-none invisible absolute inset-0 h-full w-full'
|
|
189
|
-
: 'relative h-full min-h-0 w-full min-w-0',
|
|
190
|
-
threeDClassName,
|
|
191
|
-
)}
|
|
192
|
-
data-pascal-viewer-3d
|
|
193
|
-
>
|
|
194
|
-
{children}
|
|
195
|
-
</div>
|
|
196
|
-
) : null}
|
|
197
|
-
|
|
198
|
-
{mountFloorplan ? (
|
|
199
|
-
<FloorplanPreview
|
|
200
|
-
className={cn(
|
|
201
|
-
activeMode === '3d'
|
|
202
|
-
? 'hidden'
|
|
203
|
-
: activeMode === 'split'
|
|
204
|
-
? 'min-h-0 min-w-0 border-border border-t md:border-t-0 md:border-l'
|
|
205
|
-
: 'h-full w-full',
|
|
206
|
-
floorplanClassName,
|
|
207
|
-
)}
|
|
208
|
-
compassHost={resolvedCompassHost}
|
|
209
|
-
levelId={levelId ?? internalLevelId}
|
|
210
|
-
navigationVisible={activeMode !== '3d'}
|
|
211
|
-
onLevelChange={chooseLevel}
|
|
212
|
-
scene={scene}
|
|
213
|
-
showCompass={showCompass}
|
|
214
|
-
showLevelSelector={showLevelSelector}
|
|
215
|
-
synchronizeNavigation={synchronizeNavigation}
|
|
216
|
-
/>
|
|
217
|
-
) : null}
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
220
|
-
)
|
|
221
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { usePascalI18n } from '@pascal-app/core'
|
|
4
|
-
import { type ReactNode, useMemo } from 'react'
|
|
5
|
-
import { displayRoomName } from '../lib/room-name-messages'
|
|
6
|
-
import { cn } from '../lib/utils'
|
|
7
|
-
import type { WalkthroughInteract } from '../store/use-first-person-hud'
|
|
8
|
-
|
|
9
|
-
export type { WalkthroughInteract } from '../store/use-first-person-hud'
|
|
10
|
-
|
|
11
|
-
/** A control shown in the HUD's key strip: its keys (as printed on the keycaps) and what they do. */
|
|
12
|
-
export type WalkthroughHint = { keys: readonly string[]; action: string }
|
|
13
|
-
|
|
14
|
-
export type WalkthroughHudProps = {
|
|
15
|
-
floorLabel?: string | null
|
|
16
|
-
zoneLabel?: string | null
|
|
17
|
-
interact?: WalkthroughInteract
|
|
18
|
-
/** Movement controls listed before the cursor and exit pills. */
|
|
19
|
-
hints?: readonly WalkthroughHint[]
|
|
20
|
-
/** Pointer lock temporarily released (OS screenshot) — the pill flips to
|
|
21
|
-
* "Click to resume" and lets clicks fall through to the canvas. */
|
|
22
|
-
suspended?: boolean
|
|
23
|
-
onExit?: () => void
|
|
24
|
-
children?: ReactNode
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** The centered walkthrough pointer: a dot that grows into a green ring over
|
|
28
|
-
* an interactable (door / window / elevator). Also mounted by the snapshot
|
|
29
|
-
* capture overlay so walk / drone framing keeps the same E-to-open pointer. */
|
|
30
|
-
export function WalkthroughCrosshair({ interact }: { interact: WalkthroughInteract }) {
|
|
31
|
-
return (
|
|
32
|
-
<div className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
|
33
|
-
<div
|
|
34
|
-
className={cn(
|
|
35
|
-
'rounded-full transition-all duration-150',
|
|
36
|
-
interact
|
|
37
|
-
? 'h-4 w-4 border-2 border-emerald-400 bg-emerald-400/10'
|
|
38
|
-
: 'h-1.5 w-1.5 bg-white/80 shadow-[0_0_2px_rgba(0,0,0,0.6)]',
|
|
39
|
-
)}
|
|
40
|
-
/>
|
|
41
|
-
</div>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function WalkthroughHud({
|
|
46
|
-
floorLabel,
|
|
47
|
-
zoneLabel,
|
|
48
|
-
interact = null,
|
|
49
|
-
hints,
|
|
50
|
-
suspended = false,
|
|
51
|
-
onExit,
|
|
52
|
-
children,
|
|
53
|
-
}: WalkthroughHudProps) {
|
|
54
|
-
const kbdClass = 'rounded border border-border/60 bg-white/10 px-1.5 py-0.5 font-mono text-[10px]'
|
|
55
|
-
const pillClass =
|
|
56
|
-
'flex items-center gap-1.5 rounded-full border border-border/40 bg-background/70 px-3 py-1 text-muted-foreground text-xs backdrop-blur-xl'
|
|
57
|
-
const exitContent = (
|
|
58
|
-
<>
|
|
59
|
-
<kbd className={kbdClass}>Esc</kbd>
|
|
60
|
-
to exit
|
|
61
|
-
</>
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div className="dark pointer-events-none absolute inset-0 z-30 text-foreground">
|
|
66
|
-
<div className="absolute top-6 left-1/2 flex -translate-x-1/2 flex-col items-center gap-1.5">
|
|
67
|
-
{floorLabel && (
|
|
68
|
-
<div className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
69
|
-
{floorLabel}
|
|
70
|
-
</div>
|
|
71
|
-
)}
|
|
72
|
-
{zoneLabel && (
|
|
73
|
-
<div className="corner-smooth rounded-full border border-border/40 bg-background/80 px-3 py-1 font-medium text-sm shadow-elevation-3 backdrop-blur-xl">
|
|
74
|
-
<WalkthroughZoneName name={zoneLabel} />
|
|
75
|
-
</div>
|
|
76
|
-
)}
|
|
77
|
-
{children}
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<WalkthroughCrosshair interact={interact} />
|
|
81
|
-
|
|
82
|
-
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center gap-2">
|
|
83
|
-
{suspended ? (
|
|
84
|
-
<div className={pillClass}>
|
|
85
|
-
<span className="font-medium text-foreground">Click</span>
|
|
86
|
-
<span>or</span>
|
|
87
|
-
<kbd className={kbdClass}>P</kbd>
|
|
88
|
-
<span>to resume</span>
|
|
89
|
-
<span className="text-muted-foreground/60">·</span>
|
|
90
|
-
{exitContent}
|
|
91
|
-
</div>
|
|
92
|
-
) : (
|
|
93
|
-
<>
|
|
94
|
-
{hints && hints.length > 0 ? (
|
|
95
|
-
<div className={pillClass}>
|
|
96
|
-
{hints.map(({ keys, action }, index) => (
|
|
97
|
-
<span className="flex items-center gap-1.5" key={action}>
|
|
98
|
-
{index > 0 && <span className="text-muted-foreground/60">·</span>}
|
|
99
|
-
{keys.map((key, keyIndex) => (
|
|
100
|
-
<span className="flex items-center gap-1" key={key}>
|
|
101
|
-
{keyIndex > 0 && <span className="text-muted-foreground/60">/</span>}
|
|
102
|
-
<kbd className={kbdClass}>{key}</kbd>
|
|
103
|
-
</span>
|
|
104
|
-
))}
|
|
105
|
-
{action}
|
|
106
|
-
</span>
|
|
107
|
-
))}
|
|
108
|
-
</div>
|
|
109
|
-
) : null}
|
|
110
|
-
<div className={pillClass}>
|
|
111
|
-
<kbd className={kbdClass}>P</kbd>
|
|
112
|
-
free cursor
|
|
113
|
-
</div>
|
|
114
|
-
{onExit ? (
|
|
115
|
-
<button
|
|
116
|
-
className={cn(pillClass, 'pointer-events-auto')}
|
|
117
|
-
onClick={onExit}
|
|
118
|
-
type="button"
|
|
119
|
-
>
|
|
120
|
-
{exitContent}
|
|
121
|
-
</button>
|
|
122
|
-
) : (
|
|
123
|
-
<div className={pillClass}>{exitContent}</div>
|
|
124
|
-
)}
|
|
125
|
-
</>
|
|
126
|
-
)}
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
{interact && (
|
|
130
|
-
<div className="absolute top-1/2 left-1/2 mt-7 -translate-x-1/2 whitespace-nowrap">
|
|
131
|
-
<div className="corner-smooth flex items-center gap-1.5 rounded-full border border-border/40 bg-background/80 px-3 py-1 text-xs shadow-elevation-3 backdrop-blur-xl">
|
|
132
|
-
<kbd className="rounded border border-border/60 bg-white/10 px-1.5 py-0.5 font-mono text-[10px]">
|
|
133
|
-
E
|
|
134
|
-
</kbd>
|
|
135
|
-
<span className="text-muted-foreground">or click to</span>
|
|
136
|
-
<span className="font-medium">
|
|
137
|
-
{interact.verb} {interact.label}
|
|
138
|
-
</span>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
)}
|
|
142
|
-
</div>
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** The drone's key strip: the movement controls of the viewer's `DroneController`. */
|
|
147
|
-
function useDroneHints(): readonly WalkthroughHint[] {
|
|
148
|
-
const i18n = usePascalI18n()
|
|
149
|
-
return useMemo(
|
|
150
|
-
() => [
|
|
151
|
-
{
|
|
152
|
-
keys: ['WASD'],
|
|
153
|
-
action: i18n._({ id: 'editor.walkthroughHud.drone.move', message: 'move' }),
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
keys: ['Space', 'E'],
|
|
157
|
-
action: i18n._({ id: 'editor.walkthroughHud.drone.up', message: 'up' }),
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
keys: ['Q'],
|
|
161
|
-
action: i18n._({ id: 'editor.walkthroughHud.drone.down', message: 'down' }),
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
keys: ['Shift'],
|
|
165
|
-
action: i18n._({ id: 'editor.walkthroughHud.drone.boost', message: 'boost' }),
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
keys: ['Alt'],
|
|
169
|
-
action: i18n._({ id: 'editor.walkthroughHud.drone.slow', message: 'slow' }),
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
[i18n],
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** The walkthrough HUD for the drone: the plain pointer dot and the flight keys, no room or door prompts. */
|
|
177
|
-
export function DroneHud({
|
|
178
|
-
suspended,
|
|
179
|
-
onExit,
|
|
180
|
-
children,
|
|
181
|
-
}: Pick<WalkthroughHudProps, 'suspended' | 'onExit' | 'children'>) {
|
|
182
|
-
return (
|
|
183
|
-
<WalkthroughHud hints={useDroneHints()} onExit={onExit} suspended={suspended}>
|
|
184
|
-
{children}
|
|
185
|
-
</WalkthroughHud>
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** The room the walker is in, in the UI language (the scene keeps its stored name). */
|
|
190
|
-
function WalkthroughZoneName({ name }: { name: string }) {
|
|
191
|
-
const i18n = usePascalI18n()
|
|
192
|
-
return displayRoomName(name, i18n)
|
|
193
|
-
}
|
package/src/hooks/use-mobile.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
|
|
3
|
-
const MOBILE_BREAKPOINT = 768
|
|
4
|
-
|
|
5
|
-
const subscribe = (callback: () => void): (() => void) => {
|
|
6
|
-
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
7
|
-
mql.addEventListener('change', callback)
|
|
8
|
-
return () => mql.removeEventListener('change', callback)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const getClientSnapshot = (): boolean => window.innerWidth < MOBILE_BREAKPOINT
|
|
12
|
-
|
|
13
|
-
// Server can't know the viewport — assume desktop. React's useSyncExternalStore
|
|
14
|
-
// reconciles the SSR / client snapshots without a hydration mismatch warning.
|
|
15
|
-
const getServerSnapshot = (): boolean => false
|
|
16
|
-
|
|
17
|
-
export function useIsMobile(): boolean {
|
|
18
|
-
return React.useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot)
|
|
19
|
-
}
|
package/src/i18n/level-name.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { getDefaultLevelName, levelDisplayNumber, type PascalI18n } from '@pascal-app/core'
|
|
2
|
-
|
|
3
|
-
type DisplayLevel = {
|
|
4
|
-
level?: number
|
|
5
|
-
name?: string
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** The level's own name, or undefined while it carries a generated default. */
|
|
9
|
-
export function authoredLevelName(level: DisplayLevel): string | undefined {
|
|
10
|
-
const levelNumber = level.level ?? 0
|
|
11
|
-
const name = level.name?.trim()
|
|
12
|
-
return name && name !== `Level ${levelNumber}` && name !== getDefaultLevelName(levelNumber)
|
|
13
|
-
? name
|
|
14
|
-
: undefined
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function localizedLevelName(level: DisplayLevel, i18n: PascalI18n, locale: string): string {
|
|
18
|
-
const levelNumber = level.level ?? 0
|
|
19
|
-
const authoredName = authoredLevelName(level)
|
|
20
|
-
if (authoredName) return authoredName
|
|
21
|
-
if (levelNumber === 0) {
|
|
22
|
-
return i18n._({ id: 'core.level.groundFloor', message: 'Ground floor' })
|
|
23
|
-
}
|
|
24
|
-
if (levelNumber === -1) {
|
|
25
|
-
return i18n._({ id: 'core.level.basement', message: 'Basement' })
|
|
26
|
-
}
|
|
27
|
-
if (levelNumber < -1) {
|
|
28
|
-
return i18n._({
|
|
29
|
-
id: 'core.level.basementNumber',
|
|
30
|
-
message: 'Basement {number}',
|
|
31
|
-
values: { number: -levelNumber },
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
return i18n._({
|
|
35
|
-
id: 'core.level.floorNumber',
|
|
36
|
-
message: 'Floor {number}',
|
|
37
|
-
values: { number: levelDisplayNumber(levelNumber, locale) },
|
|
38
|
-
})
|
|
39
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { PascalI18n } from '@pascal-app/core'
|
|
2
|
-
|
|
3
|
-
/** Display name of a built-in scene theme; unknown (plugin) themes keep their own name. */
|
|
4
|
-
export function sceneThemeName(id: string, fallback: string, i18n: PascalI18n): string {
|
|
5
|
-
if (id === 'studio') return i18n._({ id: 'viewer.theme.studio', message: 'Studio' })
|
|
6
|
-
if (id === 'paper') return i18n._({ id: 'viewer.theme.paper', message: 'Paper' })
|
|
7
|
-
if (id === 'sunset') return i18n._({ id: 'viewer.theme.sunset', message: 'Sunset' })
|
|
8
|
-
if (id === 'overcast') return i18n._({ id: 'viewer.theme.overcast', message: 'Overcast' })
|
|
9
|
-
if (id === 'blueprint') return i18n._({ id: 'viewer.theme.blueprint', message: 'Blueprint' })
|
|
10
|
-
if (id === 'mediterranean')
|
|
11
|
-
return i18n._({ id: 'viewer.theme.mediterranean', message: 'Mediterranean' })
|
|
12
|
-
if (id === 'twilight') return i18n._({ id: 'viewer.theme.twilight', message: 'Twilight' })
|
|
13
|
-
if (id === 'night') return i18n._({ id: 'viewer.theme.night', message: 'Night' })
|
|
14
|
-
if (id === 'verdant') return i18n._({ id: 'viewer.theme.verdant', message: 'Verdant' })
|
|
15
|
-
return fallback
|
|
16
|
-
}
|