viewer: snapshot capture pipeline + hero framing for thumbnails - #538
Conversation
…keThumbnail 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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
…e framing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…im, 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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 27335c5. Configure here.
| pipeline.outputNode = aaOutput | ||
| pipelineRef.current = pipeline | ||
|
|
||
| // Dedicated render target — pipeline outputs here instead of the canvas, |
There was a problem hiding this comment.
Hero framing FOV mismatch
High Severity
Auto-save hero thumbnails clip the scene. The thumbnailCamera uses the viewport's 50° FOV, but heroCameraPose calculates camera distance assuming a 60° FOV. This FOV mismatch positions the camera too close, cropping the scene.
Reviewed by Cursor Bugbot for commit 27335c5. Configure here.
| const finalOutput = vec4( | ||
| mix(sceneRgb, mix(bgGradient, sceneRgb, alpha), bgMixUniform.current), | ||
| mix(alpha, float(1), bgMixUniform.current), | ||
| ) |
There was a problem hiding this comment.
Crop aspect ignores framing
High Severity
The hero camera's framing for auto-save and bake thumbnails is calculated using the live editor canvas aspect ratio. However, these thumbnails are then center-cropped to a fixed 16:9 aspect, which can clip content if the canvas aspect differs from the output.
Reviewed by Cursor Bugbot for commit 27335c5. Configure here.
| camera, | ||
| }) | ||
| const { blob, outW, outH } = await pipeline.capture({ captureMode: 'standard' }) | ||
| onComplete(blob, { w: outW, h: outH }) |
There was a problem hiding this comment.
Bake path skips level snap
High Severity
The BakeThumbnail component's computeHeroFraming doesn't account for multi-level scenes not being in a stacked configuration. This can result in mis-framed or incomplete thumbnails for exploded, solo, or mid-lerp level layouts, diverging from auto-save previews.
Reviewed by Cursor Bugbot for commit 27335c5. Configure here.
| camera, | ||
| }) | ||
| const { blob, outW, outH } = await pipeline.capture({ captureMode: 'standard' }) | ||
| onComplete(blob, { w: outW, h: outH }) |
There was a problem hiding this comment.
Bake omits capture events
Medium Severity
BakeThumbnail never emits thumbnail:before-capture / thumbnail:after-capture. SelectionManager relies on those to strip selection highlight materials for the frame. When bake runs inside an editor surface with an active selection, highlighted meshes stay in the PNG even though the camera layer mask cannot filter them.
Reviewed by Cursor Bugbot for commit 27335c5. Configure here.
| depthTex: scenePassDepth, | ||
| normalTex: scenePassNormal, | ||
| inkColor: inkColorUniform, | ||
| radius: inkRadius, |
There was a problem hiding this comment.
Stale ink radius after resize
Medium Severity
inkRadius is computed once from renderer.domElement.height when the pipeline is built and baked into the TSL graph. ThumbnailGenerator caches that pipeline across captures while only resizing the render target. After a window or DPR change, supersampled standard crops keep the old line weight, so ink no longer matches the viewport scale the comment describes.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 27335c5. Configure here.


What does this PR do?
Extracts the editor's offscreen thumbnail render pipeline into the viewer package and builds the pieces the community app needs to render publish-quality project thumbnails headlessly:
viewer/lib/snapshot-pipeline.ts— the SSGI → denoise → AO-fade → ink → grade → backdrop → FXAA capture pipeline, extracted fromThumbnailGenerator. Adds two things captures always lacked vs the viewport: the scene-referred grade and the ink edge pass (both uniform-gated; ink radius scales with render height so supersampled captures keep the viewport's line weight).viewer/lib/hero-pose.ts— hero-shot framing: per-node-box corner fitting (union-AABB corners are empty diamond tips at a diagonal azimuth and read far too wide), structural-first bounds (items join only near the structure; the shaped site plate joins from scene data since the site Object3D carries the ±400 m horizon disc), building-centered aim, and an azimuth at a true 45° to the dominant wall axis (length-weighted fold-4 sum) so rotated plans still get a corner shot.editor/BakeThumbnail— aBakeExporter-style component the community bake page mounts to capture one framed shot and hand back a PNG blob.ThumbnailGeneratoris refactored onto the shared pipeline; auto-save thumbnails now re-pose onto the computed hero angle instead of copying the user's mid-edit camera (user-driven captures keep the exact viewport pose; transparent preset/item captures stay ink-free).Consumed by pascalorg/private-editor (published-thumbnail worker jobs) — companion PR there.
How to test
bun dev, open a project with a building, and let the thumbnail auto-save fire (~30 s after an edit, tab visible): the saved thumbnail (project card in the community shell) should be a composed 3/4 hero shot — studio-style angle, ink edges, graded colors — not your current camera view.bun typecheck(viewertsc --buildand editortsgoboth green locally).Screenshots / screen recording
Headless captures rendered through the new pipeline (studio theme, soft ink, 13° hero angle) are in the private-editor companion PR description.
Checklist
bun devbun checkto verify)mainbranchNote
Medium Risk
Large refactor of GPU capture and thumbnail behavior (auto-save camera changes user-visible project cards); rendering parity depends on shared pipeline uniforms and WebGPU/WebGL readback paths.
Overview
Moves offscreen thumbnail rendering from the editor into viewer as a reusable
createSnapshotPipeline(SSGI → denoise → AO fade → ink edges → grade → theme backdrop → FXAA) and addshero-posehelpers for automatic hero shots: per-node corner fitting, structural-first bounds (nearby items only, site plate from scene data), and azimuth aligned ~45° to dominant walls.ThumbnailGeneratornow delegates to the shared pipeline; auto-save thumbnails re-pose withcomputeHeroFraming/heroCameraPoseinstead of the user’s edit camera, while manual captures still match the viewport. New headlessBakeThumbnail(exported from@pascal-app/editor) runs the same framed capture for community/publish flows.Reviewed by Cursor Bugbot for commit 27335c5. Bugbot is set up for automated code reviews on this repo. Configure here.