Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions packages/editor/src/components/editor/bake-thumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client'

import {
computeHeroFraming,
createSnapshotPipeline,
GRID_LAYER,
heroCameraPose,
temporarilyHideNodeTypes,
useViewer,
} from '@pascal-app/viewer'
import { useThree } from '@react-three/fiber'
import { useEffect, useRef } from 'react'
import { PerspectiveCamera } from 'three'
import type { WebGPURenderer } from 'three/webgpu'
import { EDITOR_LAYER } from '../../lib/constants'

export function BakeThumbnail({
active,
onComplete,
onError,
}: {
active: boolean
onComplete: (blob: Blob, size: { w: number; h: number }) => void
onError: (message: string) => void
}) {
const renderer = useThree((state) => state.gl)
const scene = useThree((state) => state.scene)
const doneRef = useRef(false)

useEffect(() => {
if (!(active && !doneRef.current)) return
doneRef.current = true

const run = async () => {
const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
let pipeline: Awaited<ReturnType<typeof createSnapshotPipeline>> = null

try {
const framing = computeHeroFraming()
if (!framing) {
onError('scene has no framable content')
return
}

const { width, height } = renderer.domElement
const aspect = width / height
const camera = new PerspectiveCamera(60, aspect, 0.1, 1000)
camera.layers.disable(EDITOR_LAYER)
camera.layers.disable(GRID_LAYER)
const pose = heroCameraPose({
boxes: framing.boxes,
aim: framing.aim,
azimuthRad: framing.azimuthRad,
aspect,
})
camera.position.set(pose.position[0], pose.position[1], pose.position[2])
camera.lookAt(pose.target[0], pose.target[1], pose.target[2])
camera.updateMatrixWorld()

pipeline = await createSnapshotPipeline({
renderer: renderer as unknown as WebGPURenderer,
scene,
camera,
})
if (!pipeline) {
onError('thumbnail pipeline failed to build')
return
}

pipeline.applyEnvironment({
theme: useViewer.getState().sceneTheme,
transparent: false,
grade: true,
edges: useViewer.getState().edges,
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.

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.

} catch (error) {
console.error(
'[bake-thumbnail]',
error instanceof Error ? (error.stack ?? error.message) : error,
)
onError(error instanceof Error ? error.message : String(error))
} finally {
pipeline?.dispose()
restoreNodeVisibility()
}
}

void run()
}, [active, onComplete, onError, renderer, scene])

return null
}
Loading
Loading