* feat(viewer,editor): extract snapshot pipeline, add hero framing + BakeThumbnail Move the offscreen SSGI capture pipeline out of ThumbnailGenerator into viewer's snapshot-pipeline (adding the missing scene-referred GRADE, uniform- driven), add hero-pose per-node-box corner framing helpers, and a BakeThumbnail component so the community bake page can render a publish-quality hero shot headlessly. Auto-save thumbnails now re-pose onto the same hero angle instead of copying the user's mid-edit camera; user-driven captures keep the exact viewport pose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): tune hero framing — 19° elevation, tighter fit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer,editor): ink edges in snapshot captures Captures now run the viewport's AO -> ink -> grade order with the same soft/strong edge derivation (edgeColorFor/edgeOpacityScaleFor), uniform-gated so one cached pipeline serves all modes. Radius scales with render height so supersampled captures keep the viewport's line weight. Preset/item (transparent) captures stay ink-free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): lower hero elevation to 13°, exact fit — match reference framing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(viewer): hero framing — site plate in frame, building-centered aim, facade-relative 45° Three framing fixes from review: the intentionally-shaped site plate joins the fit constraints (from scene data — the site Object3D carries the ±400m horizon disc and can't be measured), the aim centers the building (plate+structure on XZ, structure alone on Y) so outlying boxes take asymmetric margin instead of shifting the subject, and the azimuth sits 45° to the dominant wall axis (length-weighted fold-4 vector sum) so rotated plans still read as a proper corner shot. 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/core @pascal-app/viewer @pascal-app/editor @pascal-app/nodes
Peer Dependencies
npm install next react react-dom three @react-three/fiber @react-three/drei lucide-react zustand
What's Included
- Viewer Component - WebGPU-powered 3D viewer with camera controls
- Node Rendering Runtime - Registry-driven dispatch for node renderers supplied by
@pascal-app/nodes - 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 { loadPlugin } from '@pascal-app/core'
import { builtinPlugin } from '@pascal-app/nodes'
import { Viewer } from '@pascal-app/viewer'
import { useEffect, useState } from 'react'
const registryReady = loadPlugin(builtinPlugin)
function App() {
const [ready, setReady] = useState(false)
useEffect(() => {
void registryReady.then(() => setReady(true))
}, [])
if (!ready) return null
return (
<div style={{ width: '100vw', height: '100vh' }}>
<Viewer />
</div>
)
}
Load the built-in plugin once, before mounting any viewer. Without it, the registry has no node
definitions and scene nodes cannot render. Host-provided plugins use the same loadPlugin API.
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