Skip to content

viewer: snapshot capture pipeline + hero framing for thumbnails - #538

Merged
wass08 merged 5 commits into
mainfrom
feat/thumbnail-capture-core
Jul 23, 2026
Merged

viewer: snapshot capture pipeline + hero framing for thumbnails#538
wass08 merged 5 commits into
mainfrom
feat/thumbnail-capture-core

Conversation

@wass08

@wass08 wass08 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

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 from ThumbnailGenerator. 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 — a BakeExporter-style component the community bake page mounts to capture one framed shot and hand back a PNG blob.
  • ThumbnailGenerator is 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

  1. 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.
  2. Take a manual snapshot (Cmd+K → snapshot): it should match the viewport exactly as before, now including edges and grade parity.
  3. Capture a preset/item thumbnail: still transparent, no ink.
  4. Typecheck/build: bun typecheck (viewer tsc --build and editor tsgo both 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

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

Note

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 edgesgrade → theme backdrop → FXAA) and adds hero-pose helpers 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.

ThumbnailGenerator now delegates to the shared pipeline; auto-save thumbnails re-pose with computeHeroFraming / heroCameraPose instead of the user’s edit camera, while manual captures still match the viewport. New headless BakeThumbnail (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.

wass08 and others added 5 commits July 23, 2026 16:38
…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>
@wass08
wass08 merged commit 7cfb88d into main Jul 23, 2026
2 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.

Fix All in Cursor

❌ 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 27335c5. Configure here.

depthTex: scenePassDepth,
normalTex: scenePassNormal,
inkColor: inkColorUniform,
radius: inkRadius,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 27335c5. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant