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