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
8 changes: 7 additions & 1 deletion src/components/Outline.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Scene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@
})
</script>

<T.PerspectiveCamera makeDefault position={[-10, 10, 10]} fov={15} far={5000} bind:ref={$editorCam}>
<T.PerspectiveCamera makeDefault position={[-10, 10, 10]} fov={40} far={5000} bind:ref={$editorCam}>
{#if !$specatorMode}
<OrbitControls bind:ref={$orbitControls} enableZoom={true} enableDamping autoRotateSpeed={0.5} target.y={1.5} />
{/if}
Expand Down
31 changes: 31 additions & 0 deletions src/components/menu/Inspector.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -631,6 +642,26 @@
Local render mode (ambient occlusion + wireframe are desktop-only; not shown to peers).
</p>
<Checkbox bind:checked={$showLightHelpers}>Show light helpers</Checkbox>
<p class="ui-section-label">Camera lens</p>
<div id="lens-presets" class="flex flex-wrap gap-1">
{#each LENS_PRESETS as p (p.label)}
<button
class={'ui-chip ' +
(Math.round($globalCamera?.fov ?? 0) === p.fov
? 'bg-primary-600 text-white'
: 'bg-gray-600 text-gray-200 hover:bg-gray-500')}
title={p.mm + 'mm equivalent · ' + p.fov + '° vertical FOV'}
onclick={() => {
if ($globalCamera) {
$globalCamera.fov = p.fov;
$globalCamera.updateProjectionMatrix();
}
}}
>
{p.label}
</button>
{/each}
</div>
<SliderRow
label="Camera FOV"
min={15}
Expand Down
19 changes: 17 additions & 2 deletions src/extensions/Grid.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
<script lang="ts">
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
})
</script>

{#if showGrid}
<Grid
infiniteGrid
Expand All @@ -11,7 +26,7 @@
cellColor={0x484d55}
sectionColor={0x77808d}
sectionThickness={1.2}
fadeDistance={100}
fadeDistance={fade}
fadeStrength={1.5}
/>
{/if}
42 changes: 42 additions & 0 deletions tests/e2e/roadmap-13-viewport-camera.test.cjs
Original file line number Diff line number Diff line change
@@ -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);
});