Files
editor/packages/viewer
feeabf4bd3 fix(items): retry failed model loads, settle skipped items, keep exports clean (#480)
* fix(items): retry failed model loads, settle skipped items, keep exports clean

A transient storage failure (observed: Supabase 504s under the bake page's
~200-concurrent-request burst) permanently broke an item for the whole
session: drei's useGLTF caches the rejected load by URL, the per-item
ErrorBoundary swallowed it, and the red debug-box fallback was BAKED into
the exported GLB (observed in a prod artifact). Meanwhile ItemSystem
cleared the dirty mark at group registration — before the model resolved —
so scene-ready could fire while GLBs were still loading, risking exported
placeholder geometry.

- ModelWithRetry: bounded retries (1s/3s) that clear the useGLTF cache
  entry and re-mount via the boundary's new resetKey. The timer is owned
  by an effect keyed on the failure count, so StrictMode's synthetic
  unmount/remount re-arms it instead of silently discarding it (the
  naive onError-scheduled timer died exactly that way in dev).
- Exhausted retries settle the item as SKIPPED: the debug box renders
  nothing during exports, and the failure lands in
  useViewer.itemLoadFailures (nodeId -> url) so a bake host can persist
  which items are missing from the artifact.
- ItemSystem holds the dirty mark until the item settles (model mounted,
  terminally failed, or never expected) — scene-ready now genuinely waits
  for item content; loading placeholders also hide during exports.
- ErrorBoundary: onError + resetKey props.

Verified against a 200+-item prod scene locally: permanent 504 -> exactly
3 fetch attempts, bake completes without the item and without debug
boxes; 504-once -> retry heals, artifact byte-equivalent to the intact
run; no-failure runs unchanged (demo_1 byte-identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(items): reset retry budget + settled flag when the asset URL changes

Bugbot: after a terminal load failure, swapping the item's model kept the
stale failures/epoch state and the settled flag — the new URL never even
attempted to load. ModelWithRetry is now keyed by asset src (clean retry
budget per URL) and un-settles the item on mount so the replacement load
is awaited by ItemSystem/scene-ready too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:50:20 -04:00
..
2026-02-05 08:22:19 +09:00

@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