From 69c483b5f26b12dc98bab1634d963b9fcc3efb04 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Fri, 26 Jun 2026 15:51:05 -0400 Subject: [PATCH 1/2] chore(items): refresh ceiling-fan model (On clip + base paint slot) The bundled ceiling-fan model.glb was stale (no animation clip, two slots). Replace it with the variant matching production storage: an `On` animation clip and a third `slot_base` paint slot. Same dimensions/offset, so no catalog metadata change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../editor/public/items/ceiling-fan/model.glb | Bin 3844 -> 13472 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/editor/public/items/ceiling-fan/model.glb b/apps/editor/public/items/ceiling-fan/model.glb index 093d54dad649ea518f0c9db1dae4df6b02e07672..b8092f740b0913d1208e596eb7daa6c15cd8c133 100644 GIT binary patch literal 13472 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 literal 3844 zcmbtXdt6g>8-LCjo7=ceMNl-E6MPjmJZIN4LKuPw3WD4+5n;f9)h@IRm54YBm6;b5 zU%Z#5SVqRB7t;h=VSPj`O3hGFDZ?}~Nqf_Hk@Y)Ml=|eWzVG>CXXp7n&-Zzr?{nE% zz9A(V2LKmOfS-l{%t%g{RU*<_tvZ`1q(qdjGwCc^o7o}?5#=LQNs2{lvgVmBM&SZ^ zusoO$iA4)^7OUQDLK;%^Tq+i6b8>W6tJz`|g`}5=Y{i8-be0?$5f_n!)N{2SUR3VlS z!7@Tg$W)|4rBcb%=&lqaEmBD;O$S zQYDoVa>N`Wf(%xx)fB^}S}7+G!!n(OLVS`WMX42(f+SQ5rCcpl4knaPDJe>hSduHG zN|l^aISG}^DVdazN!28!M&)vr$_YcsPz8{Xq6qopQmT{=VhFX)Ln%cDDAeqtygZ#H zO|M&cr(N$ni$Vx=RBY2F;qGIc&XjK}5QV5zPHzuTMyGeI2h>S|RNi-X*AYU$ zcdnu+=G>^Y=`4D!;f{t$Yh)(JYB1X}vklr@ofUP7ZjsHRMdu}=LajANYseh1L7E1A zwnb|x4k@(gP|y;awg`=0saTY2F3L9OlJ&W|Tv3S4Qlt}$3bQToI-AyD(B~wXi}DLh zXkr=AY^_xnX*QTG(b^p6*rU0k2O_;|6j;oNtN%Y$Qi+;aEmu%-N{*UY$t;XVlRZ|#OsRF&0#s0DL*C6j z9!Y|gXeXi63oUx1-lku0S3_&FS@hXOHs-NJl$ekln-ZHa3++p>XjVc}d_ zv^kLp2}x0zgeXLYxGFFr#c$6#==a3q)-_d9uZrNnl%Ugl>SLCnUNr+0RX}Y=hXX4 zaZ;_!;HHcIOM#Gw`}rs7o-5K@9&S@A6!vidrpC^KsHBL<1RTdO^e*5ZimO@RS@z8C zq_yEqX_F4WF}K!>jbTKd`ES3F)kNfPB%??Sma@@VyK)S9)hpezyGb4Pnz2W8ID& z#)iw|!Jof+`DCcPHWa>C;h{RTe~t3MzvFyIRUE@Lc|T0SHE+HOpy6<6I?6xsx1mr8 zg%^W9j_M!&e0eV27EyH@*1AJgsf$~&TCgu6Zja#7h&7`oxEh-`^@ZXZelvPq)%!xD zDB{3$&;;{fUc>C237uU(pA6E^6HMh!>d!|$vUh;}*KcN&cgsFr{PWg-1)19Y#g{%j zhk|of6k*usR5U|IMRR8Q%5b;AGoVVuXJXh$9yY?>yU%S^6o@c-Ncc8K0V(sCDG?@jsR71;x{Au+~$3t8&|lF8GxmjeMk;xXtm~<#L-- z<2ufB%dp{1H>9tf)0T0nuez6~&pYEoa__hOk*nw^7FQ7O zRevSCr_2X^YP@KT7eH5dj~{I>1Kfde`_TWKrRWRn?`K6ihWym)d8yY=n7}PG_0pSi zAnG%<@YJ`$7dtd(A*;qCZi-@Sqh#$A?phA~idKk*Nc4q5<3{^`+VHjmo|+9D+E_-1 z$KbHppDr^bZmNl7?Y$EIml0$03w?&V|C9HsKygbh2~duxa_THXAG*(;YtT6DHR{M;1Px4MDyQpZUS{kNBrMxa-pP1YB6P z&3lZa7VOK)PglLUx_Wsy?we0Ph1r+QhM3|J@b0BoN6f_SGv>2{epiYo1qAWnU4LoA z_o-m#(<{ZJ7(rakmMBJ*##|Pm+tN?@Wzg&hj59XH&4`{mtbgig?h!{I)LU8|>%#T~ zbg#meoM2+9E%&~7YONyk$>$-5HFku6v(lZ*TD!qES8l!tB#u$)DT^ zhZ3HZcBD79;B;LX1{~o?`*Rh)eNZqyuyw)g4&RqhL!--q=DypnHEexzUf7q5+rr%b E00ImHLjV8( From 70acf894d167dd8ea543fb2831de96abfeca39cd Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Fri, 26 Jun 2026 15:51:12 -0400 Subject: [PATCH 2/2] =?UTF-8?q?feat(viewer):=20widen=20first-person=20walk?= =?UTF-8?q?through=20FOV=20to=2060=C2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The walkthrough rode the default 50° orbit camera, which feels cramped on foot. Both walkthrough controllers (baked GlbWalkthroughController and the parametric WalkthroughControls fallback) now set a shared WALKTHROUGH_FOV = 60 on enter and restore the prior FOV on exit, leaving orbit framing untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../viewer/glb-walkthrough-controller.tsx | 18 +++++++++++++++ .../viewer/walkthrough-controls.tsx | 22 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/viewer/src/components/viewer/glb-walkthrough-controller.tsx b/packages/viewer/src/components/viewer/glb-walkthrough-controller.tsx index f235f773..50c83542 100644 --- a/packages/viewer/src/components/viewer/glb-walkthrough-controller.tsx +++ b/packages/viewer/src/components/viewer/glb-walkthrough-controller.tsx @@ -15,6 +15,7 @@ import { Mesh, MeshBasicMaterial, type Object3D, + type PerspectiveCamera, Quaternion, Vector3, } from 'three' @@ -24,6 +25,7 @@ import { useGLTFKTX2 } from '../../hooks/use-gltf-ktx2' import { SCENE_LAYER } from '../../lib/layers' import useViewer from '../../store/use-viewer' import BVHEcctrl, { type BVHEcctrlApi, type MovementInput } from './bvh-ecctrl' +import { WALKTHROUGH_FOV } from './walkthrough-controls' // Eye/capsule geometry mirrors the editor's first-person controller so the // baked walkthrough feels identical. The capsule centre sits below the eye; the @@ -264,6 +266,22 @@ export function GlbWalkthroughController({ url }: { url: string }) { } }, []) + // Widen FOV while walking; the baked walkthrough rides the default 50° orbit + // camera, which feels cramped on foot. Keyed on `camera` so it re-applies if + // the instance swaps (e.g. the ortho→perspective switch above), restoring the + // prior FOV on exit. + useEffect(() => { + const cam = camera as PerspectiveCamera + if (!cam.isPerspectiveCamera) return + const prevFov = cam.fov + cam.fov = WALKTHROUGH_FOV + cam.updateProjectionMatrix() + return () => { + cam.fov = prevFov + cam.updateProjectionMatrix() + } + }, [camera]) + useEffect(() => { worldRef.current = world if (world) { diff --git a/packages/viewer/src/components/viewer/walkthrough-controls.tsx b/packages/viewer/src/components/viewer/walkthrough-controls.tsx index b3d55f28..65c6b69f 100644 --- a/packages/viewer/src/components/viewer/walkthrough-controls.tsx +++ b/packages/viewer/src/components/viewer/walkthrough-controls.tsx @@ -3,12 +3,18 @@ import { PointerLockControls } from '@react-three/drei' import { useFrame, useThree } from '@react-three/fiber' import { useCallback, useEffect, useRef } from 'react' -import { Vector3 } from 'three' +import { type PerspectiveCamera, Vector3 } from 'three' import useViewer from '../../store/use-viewer' const MOVE_SPEED = 5 const EYE_HEIGHT = 1.6 +// First-person FOV. The orbit camera is 50° (set on the Canvas), which feels +// cramped on foot; ~60° vertical (~90° horizontal at 16:9) restores peripheral +// awareness without wide-angle distortion. Applied only while walking — both +// walkthrough controllers read this and restore the orbit FOV on exit. +export const WALKTHROUGH_FOV = 60 + const _direction = new Vector3() const _forward = new Vector3() const _right = new Vector3() @@ -26,6 +32,20 @@ export const WalkthroughControls = () => { } }, [walkthroughMode, camera]) + // Widen FOV while walking; restore the orbit FOV on exit. + useEffect(() => { + if (!walkthroughMode) return + const cam = camera as PerspectiveCamera + if (!cam.isPerspectiveCamera) return + const prevFov = cam.fov + cam.fov = WALKTHROUGH_FOV + cam.updateProjectionMatrix() + return () => { + cam.fov = prevFov + cam.updateProjectionMatrix() + } + }, [walkthroughMode, camera]) + // Keyboard handlers useEffect(() => { if (!walkthroughMode) return