feat: improve thumbnails, placement, and editor responsiveness (#258)

* wip: backport monorepo PRs 278 280 281 283 284

* refactor(editor): align floorplan panel with monorepo split version
This commit is contained in:
Pascal
2026-04-17 15:14:40 -04:00
committed by GitHub
parent d948f09b2c
commit 5c16aa4225
53 changed files with 1744 additions and 1409 deletions
@@ -0,0 +1,42 @@
import { useThree } from '@react-three/fiber'
import { useLayoutEffect } from 'react'
type FrameLimiterProps = {
fps?: number
}
const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
const { advance, set, frameloop: initFrameloop } = useThree()
useLayoutEffect(() => {
let elapsed = 0
let then = 0
let i = 0
let raf: number | null = null
const interval = 1000 / fps
function tick(t: DOMHighResTimeStamp) {
raf = requestAnimationFrame(tick)
elapsed = t - then
if (elapsed > interval) {
advance(i)
i += elapsed / 1000 - (elapsed % interval) / 1000
then = t - (elapsed % interval)
}
}
set({ frameloop: 'never' })
raf = requestAnimationFrame(tick)
return () => {
if (raf) {
cancelAnimationFrame(raf)
}
set({ frameloop: initFrameloop })
}
}, [fps, advance, set, initFrameloop])
return null
}
export default FrameLimiter
@@ -23,6 +23,7 @@ import { ScanSystem } from '../../systems/scan/scan-system'
import { WallCutout } from '../../systems/wall/wall-cutout'
import { ZoneSystem } from '../../systems/zone/zone-system'
import { SceneRenderer } from '../renderers/scene-renderer'
import FrameLimiter from './frame-limiter'
import { Lights } from './lights'
import { PerfMonitor } from './perf-monitor'
import PostProcessing from './post-processing'
@@ -69,18 +70,27 @@ extend(THREE as any)
* - Driver crash or GPU reset
* - Browser security policy kills the context
*/
type WebGPUDeviceLossInfo = {
reason?: string
message?: string
}
type WebGPUDeviceLike = {
lost: Promise<WebGPUDeviceLossInfo>
}
function GPUDeviceWatcher() {
const gl = useThree((s) => s.gl)
useEffect(() => {
const backend = (gl as any).backend
const device: GPUDevice | undefined = backend?.device
const device = backend?.device as WebGPUDeviceLike | undefined
if (!device) return
device.lost.then((info) => {
device.lost.then((info: WebGPUDeviceLossInfo) => {
console.error(
`[viewer] WebGPU device lost: reason="${info.reason}", message="${info.message}". ` +
`[viewer] WebGPU device lost: reason="${info.reason ?? 'unknown'}", message="${info.message ?? ''}". ` +
'The page must be reloaded to recover the GPU context.',
)
})
@@ -106,6 +116,7 @@ const Viewer: React.FC<ViewerProps> = ({
camera={{ position: [50, 50, 50], fov: 50 }}
className={`transition-colors duration-700 ${theme === 'dark' ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`}
dpr={[1, 1.5]}
frameloop="never"
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props as any)
renderer.toneMapping = THREE.ACESFilmicToneMapping
@@ -127,6 +138,7 @@ const Viewer: React.FC<ViewerProps> = ({
enabled: true,
}}
>
<FrameLimiter fps={50} />
{/* <AnimatedBackground isDark={theme === 'dark'} /> */}
<ViewerCamera />
@@ -140,7 +140,7 @@ const PostProcessingPasses = () => {
// loop fights the direct-render fallback path. Short-circuit here so
// `useFrame` uses the direct `renderer.render(scene, camera)` path
// exclusively and never attempts the TSL pipeline.
const hasWebGPU = typeof navigator !== 'undefined' && typeof navigator.gpu !== 'undefined'
const hasWebGPU = typeof navigator !== 'undefined' && 'gpu' in navigator
if (!hasWebGPU) {
console.warn(
'[viewer] WebGPU unavailable — rendering without post-processing (SSGI, outlines, denoise).',