Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions fixtures/viewer-live/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ SwiftShader, with normal and denied localStorage. It requires a local Chrome or

`scene.json` was generated with core's node parsers and its recessed-downlight recipe fixture.
It contains four walls, a door/window, a roof segment, a zone, a separator,
a measurement, a ceiling with a procedural downlight cut, and a GLTF item with a painted material slot. `public/cube.gltf`
a placed spawn facing the door, a measurement, a ceiling with a procedural downlight cut, and a GLTF item with a painted material slot. `public/cube.gltf`
is a tiny self-contained cube with a `slot_body` material. There is no baked scene asset.

The readiness probe requires the viewer's scene-ready callback, no pending build work, real node
geometry and actual draw calls. It also rejects model failures, browser errors and a process
polyfill. The page styles `@pascal-app/viewer/ui` with Tailwind and the shipped `theme.css`, as
that entry's README describes, and mounts the viewer inside `ViewerStage` with the controls bar and
loader; the check requires their icons to load from `assetsBaseUrl` and a sound from `sfxBaseUrl`.
loader; the check requires their icons to load from `iconsBaseUrl` and a sound from `sfxBaseUrl`.
It then mounts `ParametricViewerRooms` with only the level selected, requires the zone's room pill,
clicks it, and requires the room's highlight on the overlay layer and its callout. Last, it clicks
the stage switcher's 2D and Split buttons and requires the read-only plan to draw outlines for the
clicks it, and requires the room's highlight on the overlay layer and its callout. It clicks the
stage switcher's 2D and Split buttons and requires the read-only plan to draw outlines for the
walls, door, window, roof, ceiling, zone and item from `renderPlugin` alone, beside the 3D view in Split.
No Next configuration or editor package is used.

The fixture then enters `LiveWalkthroughController` through viewer state with orbit controls
disabled, holds W until the camera moves, and requires the shared HUD/crosshair. E operates the
facing door: the check requires the controller's core `interaction:sound` cue and a request to
`/fixture-sfx/door_open.mp3`. It verifies that scene nodes remain unchanged and Escape exits.
4 changes: 4 additions & 0 deletions fixtures/viewer-live/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<style>
html, body, #root { width: 100%; height: 100%; margin: 0; }
body { font-family: system-ui, sans-serif; }
[data-enter-walkthrough] { position: fixed; top: 12px; right: 12px; z-index: 50; }
[data-walkthrough-hud] { position: fixed; inset: 0; pointer-events: none; }
[data-walkthrough-crosshair] { position: absolute; top: 50%; left: 50%; }
[data-walkthrough-crosshair] > div { width: 6px; height: 6px; border-radius: 50%; background: white; }
output { position: fixed; top: 12px; left: 12px; }
</style>
</head>
Expand Down
47 changes: 44 additions & 3 deletions fixtures/viewer-live/main.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import {
acquireSceneReadOnlyLease,
emitter,
type InteractionSound,
loadPlugin,
sceneRegistry,
useScene,
} from '@pascal-app/core'
import { renderPlugin } from '@pascal-app/nodes/render'
import {
configureViewer,
LiveWalkthroughController,
OVERLAY_LAYER,
pendingSceneBuildCount,
useFirstPersonHud,
useViewer,
Viewer,
} from '@pascal-app/viewer'
Expand All @@ -19,10 +22,12 @@ import {
SceneLoader,
ViewerControlsBar,
ViewerStage,
WalkthroughHud,
} from '@pascal-app/viewer/ui'
import { OrbitControls } from '@react-three/drei'
import { useFrame, useThree } from '@react-three/fiber'
import { useState } from 'react'
import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client'
import type { Mesh } from 'three'
import type { WebGPURenderer } from 'three/webgpu'
Expand All @@ -43,7 +48,8 @@ configureViewer({
})
// The packed sfx entry loads its files from the configured base; the check intercepts them.
initInteractionSounds()
Object.assign(window, { playFixtureSound: () => emitter.emit('interaction:sound', 'door-open') })
const interactionSounds: InteractionSound[] = []
emitter.on('interaction:sound', (sound) => interactionSounds.push(sound))
await loadPlugin(renderPlugin)
const releaseReadOnly = acquireSceneReadOnlyLease()
useScene.getState().setScene(scene.nodes, scene.rootNodeIds, {
Expand All @@ -63,6 +69,7 @@ useViewer.getState().setSelection({
function GeometryProbe() {
const gl = useThree((state) => state.gl) as unknown as WebGPURenderer
const root = useThree((state) => state.scene)
const camera = useThree((state) => state.camera)
useFrame(() => {
const geometry: Record<string, number> = {}
for (const id of Object.keys(scene.nodes)) {
Expand All @@ -83,6 +90,10 @@ function GeometryProbe() {
Object.assign(window, {
viewerFixture: {
geometry,
camera: camera.position.toArray(),
walking: useViewer.getState().walkthroughMode,
interactionSounds,
document: JSON.stringify(useScene.getState().nodes),
pending: pendingSceneBuildCount(),
overlay,
itemFailures: useViewer.getState().itemLoadFailures,
Expand All @@ -94,7 +105,32 @@ function GeometryProbe() {
return null
}

function FixtureHud() {
const floorLabel = useFirstPersonHud((state) => state.floorLabel)
const zoneLabel = useFirstPersonHud((state) => state.zoneLabel)
const interact = useFirstPersonHud((state) => state.interact)
const suspended = useViewer((state) => state.walkthroughSuspended)
return (
<WalkthroughHud
floorLabel={floorLabel}
zoneLabel={zoneLabel}
interact={interact}
suspended={suspended}
onExit={() => useViewer.getState().setWalkthroughMode(false)}
/>
)
}

function App() {
const walking = useViewer((state) => state.walkthroughMode)
const toggleWalkthrough = () => {
flushSync(() => useViewer.getState().setWalkthroughMode(!walking))
if (!walking) {
const canvas = document.querySelector('canvas')!
const result = canvas.requestPointerLock?.() as Promise<void> | undefined
result?.catch(() => {})
}
}
const [ready, setReady] = useState(false)
// Mounted once the zone's own geometry is proven, as it hides zone volumes. With only the
// level selected, the level shows its room pills.
Expand All @@ -116,13 +152,18 @@ function App() {
onSceneReadyChange={setReady}
selectionManager="custom"
>
<OrbitControls makeDefault target={[0, 1, 0]} />
<OrbitControls enabled={!walking} makeDefault target={[0, 1, 0]} />
<LiveWalkthroughController />
<GeometryProbe />
{rooms && <ParametricViewerRooms />}
</Viewer>
</ViewerStage>
{/* The packed UI entry mounts without the editor; its icons load from iconsBaseUrl. */}
<ViewerControlsBar onWalkthroughToggle={() => {}} />
<ViewerControlsBar onWalkthroughToggle={toggleWalkthrough} walkthroughActive={walking} />
<button data-enter-walkthrough onClick={toggleWalkthrough} type="button">
{walking ? 'Exit walkthrough' : 'Enter walkthrough'}
</button>
{walking && <FixtureHud />}
{!ready && <SceneLoader />}
<output data-scene-ready={ready}>{ready ? 'Scene ready' : 'Building scene…'}</output>
</>
Expand Down
15 changes: 13 additions & 2 deletions fixtures/viewer-live/scene.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,23 @@
"measurement_fixture",
"separator_fixture",
"ceiling_fixture",
"item_fixture"
"item_fixture",
"spawn_fixture"
],
"level": 0,
"baseElevation": 0,
"height": 3
},
"spawn_fixture": {
"object": "node",
"id": "spawn_fixture",
"type": "spawn",
"parentId": "level_fixture",
"visible": true,
"metadata": {},
"position": [-1, 0, -0.2],
"rotation": 0
},
"wall_south": {
"object": "node",
"id": "wall_south",
Expand Down Expand Up @@ -106,7 +117,7 @@
"parentId": "wall_south",
"visible": true,
"metadata": {},
"position": [2, 0, 0],
"position": [2, 1.05, 0],
"rotation": [0, 0, 0],
"floorThresholdVersion": 1,
"wallId": "wall_south",
Expand Down
16 changes: 9 additions & 7 deletions i18n/literal-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,7 @@
"{hosted} {?} on it. Keep {it} at {its} height, or delete {it} too."
],
"packages/editor/src/components/editor/first-person-controls.tsx": [
"Lights",
"Place a spawn point from the Build tab to control where walkthrough starts.",
"door",
"door button",
"elevator button",
"item",
"window"
"Place a spawn point from the Build tab to control where walkthrough starts."
],
"packages/editor/src/components/editor/floorplan-mode-coordinator.tsx": [
"Dismiss",
Expand Down Expand Up @@ -798,6 +792,14 @@
],
"packages/viewer/src/ui/floorplan/floorplan-dimension-renderer.tsx": [
"override"
],
"packages/viewer/src/components/viewer/live-walkthrough-controller.tsx": [
"Lights",
"door",
"door button",
"elevator button",
"item",
"window"
]
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,7 @@ export { wallTrimPatch } from './systems/wall/wall-trim'
export type { SceneGraph } from './utils/clone-scene-graph'
export { cloneLevelSubtree, cloneSceneGraph, forkSceneGraph } from './utils/clone-scene-graph'
export * from './utils/measurement-format'
export { computeSceneBoundsXZ, type SceneBoundsXZ } from './utils/scene-bounds'
export { areSemanticValuesEqual } from './utils/semantic-equal'
export { isObject } from './utils/types'
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from 'bun:test'
import { computeSceneBoundsXZ } from '@pascal-app/core'
import type { AnyNode } from '@pascal-app/core/schema'
import { computeSceneBoundsXZ } from './scene-bounds'

function makeWall(start: [number, number], end: [number, number]): AnyNode {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
/**
* Scene bounds in the X/Z plane.
*
* Used by the auto-frame hook to fit the camera onto a freshly loaded scene
* (see `../hooks/use-auto-frame`). The hook subscribes to the core scene
* store and, when `nodes` transitions from empty → non-empty, fires a
* `camera-controls:fit-scene` event on the core event bus carrying the
* computed bounds.
*
* This module contains no rendering code: it only walks the flat-dict node
* tree and derives an axis-aligned bounding box on the XZ (plan) plane.
*/

import { turnPlanPoint, yawOf } from '@pascal-app/core'
import type { AnyNode } from '@pascal-app/core/schema'
import { turnPlanPoint, yawOf } from '../lib/building-footprint'
import type { AnyNode } from '../schema'

export type SceneBoundsXZ = {
/** Min [x, z] in world units (meters). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
useLiveTransforms,
useScene,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { isTypingTarget, useViewer } from '@pascal-app/viewer'
import { FloorplanDimensionRenderer } from '@pascal-app/viewer/ui'
import { memo, type PointerEvent as ReactPointerEvent, useEffect, useMemo, useState } from 'react'
import { create } from 'zustand'
Expand All @@ -28,7 +28,6 @@ import { isHistoryShortcut } from '../../lib/history'
import type { WallGroupPush } from '../../lib/room-handle-drag'
import { useRoomHandleDrag } from '../../lib/room-handle-drag'
import { sfxEmitter } from '../../lib/sfx-bus'
import { isTypingTarget } from '../../lib/typing-target'
import useAlignmentGuides from '../../store/use-alignment-guides'
import useEditor, {
isAlignmentGuideActive,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
useScene,
} from '@pascal-app/core'
import { nodeLevelFrame } from '@pascal-app/core/procedural-items'
import { useViewer } from '@pascal-app/viewer'
import { isTypingTarget, useViewer } from '@pascal-app/viewer'
import { useEffect, useMemo } from 'react'
import { commitFreshPlacementSubtree } from '../../lib/fresh-planar-placement'
import { isHistoryShortcut } from '../../lib/history'
Expand All @@ -39,7 +39,6 @@ import {
import { movementSfxStepKey } from '../../lib/sfx/movement-tick'
import { sfxEmitter } from '../../lib/sfx-bus'
import { surfaceAttachmentId } from '../../lib/surface-attachment'
import { isTypingTarget } from '../../lib/typing-target'
import { resolveAlignmentForFloorplanView } from '../../lib/world-grid-snap'
import useAlignmentGuides from '../../store/use-alignment-guides'
import useEditor, {
Expand Down
Loading
Loading