* chore: sync bun.lock with 0.9.1 workspace versions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(viewer): stop interactive overlays from starving frames and flickering the UI Every interactive item mounted a drei <Html occlude> overlay unconditionally — invisible (opacity 0) when no zone was selected, but still alive. With `occlude` as a bare boolean, drei raycasts the entire scene per overlay on every camera-move frame, and rewrites each element's z-index while toggling display when the occlusion flips. On scenes with hundreds of interactive items (recessed lights, ceiling fans) this starved the frame budget and made the whole DOM UI blink during camera moves while the WebGPU canvas stayed healthy. Overlays now mount only while a zone is selected and the item sits inside its polygon, fade in/out over 300ms (the child components stay rendered so the exit transition can play before the <Html> unmounts), and drop `occlude` entirely. eps=-1 works around a drei mount bug: its mount path writes the element transform without the distanceFactor scale, and with a static camera the eps guard never re-applies it, so freshly mounted overlays stayed mis-scaled until the camera moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): break down dirty nodes by kind in the perf overlay DIRTY now reads e.g. "29 (12 wall, 9 ceiling, 8 item)" — sorted by count, only non-zero kinds, with a "missing" bucket for dirty ids whose node no longer exists. Makes dirty-mark leaks attributable at a glance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(core): skip dirty marks for kinds with no dirty consumer dirtyNodes is consumed by GeometrySystem (def.geometry), FloorElevationSystem (capabilities.floorPlaced), and the legacy per-kind viewer systems. Site, building, level, zone, and guide match none of those, so their marks were never cleared: they accumulated for the whole session (every child create/delete dirties its parent), permanently defeated every consumer's empty-set early exit each frame, and polluted the perf overlay's DIRTY readout. NodeDefinition gains an explicit dirtyTracking?: boolean opt-out (default tracked — no derivable predicate exists since wall's dirty consumption lives in the viewer while zone/guide/level declare def.system for unrelated per-frame work). markDirty consults the registry; the five structural kinds opt out. Also fixes a second leak: deleteNodesAction never removed deleted ids from the dirty set, and every consumer skips missing nodes without clearing them, so marks on deleted nodes lived forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * style: replace concise-arrow forEach with for...of in deleteNodesAction biome's useIterableCallbackReturn rejects forEach callbacks that implicitly return a value (Set.add / clearDirty). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@pascal-app/viewer
3D viewer component for Pascal building editor.
Installation
npm install @pascal-app/viewer @pascal-app/core
Peer Dependencies
npm install react three @react-three/fiber @react-three/drei
What's Included
- Viewer Component - WebGPU-powered 3D viewer with camera controls
- Node Renderers - React Three Fiber components for all node types
- Post-Processing - SSGI (ambient occlusion + global illumination), TRAA (anti-aliasing), outline effects
- Level System - Level visibility and positioning (stacked/exploded/solo modes)
- Wall Cutout System - Dynamic wall hiding based on camera position
- Asset URL Helpers - CDN URL resolution for models and textures
Usage
import { Viewer, useViewer } from '@pascal-app/viewer'
import { useScene } from '@pascal-app/core'
function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Viewer />
</div>
)
}
Custom Camera Controls
import { Viewer } from '@pascal-app/viewer'
import { CameraControls } from '@react-three/drei'
function App() {
return (
<Viewer selectionManager="custom">
<CameraControls />
</Viewer>
)
}
Viewer State
import { useViewer } from '@pascal-app/viewer'
function ViewerControls() {
const levelMode = useViewer(s => s.levelMode)
const setLevelMode = useViewer(s => s.setLevelMode)
const wallMode = useViewer(s => s.wallMode)
const setWallMode = useViewer(s => s.setWallMode)
return (
<div>
<button onClick={() => setLevelMode('stacked')}>Stacked</button>
<button onClick={() => setLevelMode('exploded')}>Exploded</button>
<button onClick={() => setWallMode('cutaway')}>Cutaway</button>
<button onClick={() => setWallMode('up')}>Full Height</button>
</div>
)
}
Asset CDN Helpers
import { resolveCdnUrl, ASSETS_CDN_URL } from '@pascal-app/viewer'
// Resolves relative paths to CDN URLs
const url = resolveCdnUrl('/items/chair/model.glb')
// → 'https://pascal-cdn.wawasensei.dev/items/chair/model.glb'
// Handles external URLs and asset:// protocol
const externalUrl = resolveCdnUrl('https://example.com/model.glb')
// → 'https://example.com/model.glb' (unchanged)
Features
- WebGPU Rendering - Hardware-accelerated rendering via Three.js WebGPU
- Post-Processing - SSGI for realistic lighting, outline effects for selection
- Level Modes - Stacked, exploded, or solo level display
- Wall Cutaway - Automatic wall hiding for interior views
- Camera Modes - Perspective and orthographic projection
- Scan/Guide Support - 3D scans and 2D guide images
License
MIT