From 7d08659c8a71580a5c6dd21d0dd339f277cd86f3 Mon Sep 17 00:00:00 2001 From: AlexZ005 Date: Tue, 21 Jul 2026 00:29:01 +0300 Subject: [PATCH] [feat] roadmap 13 batch B - viewport & camera - B1: the infinite grid fadeDistance now scales with the camera distance from the origin (eased) so the grid no longer vanishes on far zoom-out; the fixed 100 units capped it regardless of the far plane - B2: fix the N8AO ghost - the AO pass was sized to the LOGICAL CSS size while the composer sizes its passes to the physical drawing buffer (width x devicePixelRatio), so on HiDPI displays the AO buffer was half-size, upsampled and offset into a shifted dark copy of the shading; size it to the physical buffer (before/after verified at devicePixelRatio 2) - B3: Configure Scene gains a Camera lens preset row (Wide 24mm / Classic 35mm / Natural 50mm / Portrait 85mm, set as vertical FOV) and the default camera FOV is now 40 degrees (was an extreme-telephoto 15 that flattened the first impression) - e2e: roadmap-13-viewport-camera (5 checks, all pass); build green; svelte-check 501/77 held Co-Authored-By: Claude Fable 5 --- src/components/Outline.svelte | 8 +++- src/components/Scene.svelte | 2 +- src/components/menu/Inspector.svelte | 31 ++++++++++++++ src/extensions/Grid.svelte | 19 ++++++++- tests/e2e/roadmap-13-viewport-camera.test.cjs | 42 +++++++++++++++++++ 5 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/roadmap-13-viewport-camera.test.cjs diff --git a/src/components/Outline.svelte b/src/components/Outline.svelte index 350eab75..a67a4d05 100644 --- a/src/components/Outline.svelte +++ b/src/components/Outline.svelte @@ -50,8 +50,14 @@ composer.addPass(new EffectPass(camera.current, outlineEffectLocked)); composer.addPass(new EffectPass(camera.current, outlineEffectSelected)); $effect(() => { + // B2: size the AO pass to the PHYSICAL drawing buffer. postprocessing's + // composer.setSize sizes each pass to width*devicePixelRatio; passing the + // LOGICAL CSS size here (the old code) under-sized the N8AO buffer on HiDPI + // displays, so its output was upsampled and read as a shifted "ghost" of the + // shading offset from the objects. Match the composer's physical resolution. + const dpr = renderer.getPixelRatio ? renderer.getPixelRatio() : 1; composer.setSize($size.width, $size.height); - aoPass.setSize($size.width, $size.height); + aoPass.setSize(Math.round($size.width * dpr), Math.round($size.height * dpr)); }); // AO on/off + quality follow the local prefs (one perf knob = shadowQuality) $effect(() => { diff --git a/src/components/Scene.svelte b/src/components/Scene.svelte index f7111932..1f9efbee 100644 --- a/src/components/Scene.svelte +++ b/src/components/Scene.svelte @@ -897,7 +897,7 @@ }) - + {#if !$specatorMode} {/if} diff --git a/src/components/menu/Inspector.svelte b/src/components/menu/Inspector.svelte index 6f445164..1fd77f9a 100644 --- a/src/components/menu/Inspector.svelte +++ b/src/components/menu/Inspector.svelte @@ -73,6 +73,17 @@ const hexColor = /^#[0-9A-F]{6}$/i; const RAD_SNAP = Math.PI / 12; // Ctrl-snap rotations to 15° + // B3 (roadmap #13): camera lens presets. Labels use the familiar full-frame + // focal-length vocabulary; the value set is three's VERTICAL fov in degrees + // (vfov = 2·atan(12mm / focal)). Default camera fov is 40° (~33mm) — a natural, + // low-distortion product-viz look, just wider than a classic 35mm. + const LENS_PRESETS = [ + { label: 'Wide', mm: 24, fov: 53 }, + { label: 'Classic', mm: 35, fov: 38 }, + { label: 'Natural', mm: 50, fov: 27 }, + { label: 'Portrait', mm: 85, fov: 16 } + ]; + let transitionParamsRight = { x: 320, duration: 200, easing: sineIn }; // side drawers live on the --z-drawer tier (68); chat floats on its own now. @@ -631,6 +642,26 @@ Local render mode (ambient occlusion + wireframe are desktop-only; not shown to peers).

Show light helpers + +
+ {#each LENS_PRESETS as p (p.label)} + + {/each} +
import { Grid } from '@threlte/extras' + import { useThrelte, useTask } from '@threlte/core' let { showGrid } = $props() + + // B1 (roadmap #13): the infinite grid fades out beyond `fadeDistance` world + // units from the camera. A fixed 100 made the grid vanish on far dolly-out + // (cameraClip grows the far plane with the scene, but the grid didn't follow). + // Scale the fade with the camera's distance from the origin (a proxy for zoom) + // so the grid stays visible as you zoom out, eased so it doesn't pop. + const { camera } = useThrelte() + let fade = $state(100) + useTask(() => { + const cam = camera.current + if (!cam) return + const target = Math.min(Math.max(100, cam.position.length() * 1.6), 5000) + fade += (target - fade) * 0.2 + }) - + {#if showGrid} {/if} diff --git a/tests/e2e/roadmap-13-viewport-camera.test.cjs b/tests/e2e/roadmap-13-viewport-camera.test.cjs new file mode 100644 index 00000000..542fe052 --- /dev/null +++ b/tests/e2e/roadmap-13-viewport-camera.test.cjs @@ -0,0 +1,42 @@ +// Roadmap #13 Batch B — viewport & camera. +// B3 default camera FOV is 40° (was an extreme-telephoto 15°); the Configure +// Scene "Camera lens" presets set the matching vertical FOV. +// B1 (grid fade scales with camera distance) and B2 (N8AO half-size ghost on HiDPI) +// are verified visually (before/after screenshots at devicePixelRatio 2) — not +// asserted here. +const h = require('./helpers.cjs'); + +const fovOf = (peer) => + peer.page.evaluate( + () => new Promise((r) => window.__stores.globalCamera.subscribe((c) => r(c ? Math.round(c.fov) : null))()) + ); + +h.run(async () => { + const browser = await h.launch(); + const A = await h.setupPage(browser, 'A'); + + // --- B3: default FOV ------------------------------------------------------ + h.check((await fovOf(A)) === 40, 'B3: default camera FOV is 40 degrees'); + + // --- B3: lens presets in Configure Scene --------------------------------- + await A.page.evaluate(() => { + window.__stores.inspectorKind.set('scene'); + window.__stores.inspectorClose.set(false); + }); + await A.page.waitForTimeout(400); + h.check(await A.page.locator('#lens-presets').first().isVisible(), 'B3: lens presets row renders in Configure Scene'); + + await A.page.locator('#lens-presets button', { hasText: 'Natural' }).click(); + await A.page.waitForTimeout(200); + h.check((await fovOf(A)) === 27, 'B3: "Natural" preset sets ~50mm (27 deg vertical FOV)'); + + await A.page.locator('#lens-presets button', { hasText: 'Wide' }).click(); + await A.page.waitForTimeout(200); + h.check((await fovOf(A)) === 53, 'B3: "Wide" preset sets ~24mm (53 deg vertical FOV)'); + + await A.page.locator('#lens-presets button', { hasText: 'Portrait' }).click(); + await A.page.waitForTimeout(200); + h.check((await fovOf(A)) === 16, 'B3: "Portrait" preset sets ~85mm (16 deg vertical FOV)'); + + await h.finish(browser); +});