fix(3d): media planes in 4:3 with cover-fit crop

PcbVideoMesh and PcbCarouselMesh now render in 4:3
landscape (1.9 x 1.425) instead of portrait (1.9 x
4.275). Add applyCoverFit() helper that computes
texture.repeat and texture.offset to crop centered
when source aspect differs from target — equivalent
to CSS object-fit: cover. Hooked on loadedmetadata
for videos and on TextureLoader callback for photos.
This commit is contained in:
L'électron rare
2026-04-28 15:14:57 +02:00
parent 8bc027d9b4
commit 072d792017
+32 -6
View File
@@ -248,6 +248,26 @@ function CardModel({ file }: { file: string }) {
return <primitive object={cloned} />;
}
/* ── Cover-fit helper: crop centered to target aspect (like CSS object-fit:cover) ── */
function applyCoverFit(texture: THREE.Texture, targetAspect: number) {
const img = texture.image as (HTMLImageElement | HTMLVideoElement | { width: number; height: number; videoWidth?: number; videoHeight?: number } | undefined);
if (!img) return;
const w = (img as HTMLVideoElement).videoWidth || (img as HTMLImageElement).width;
const h = (img as HTMLVideoElement).videoHeight || (img as HTMLImageElement).height;
if (!w || !h) return;
const sourceAspect = w / h;
if (sourceAspect > targetAspect) {
texture.repeat.set(targetAspect / sourceAspect, 1);
texture.offset.set((1 - targetAspect / sourceAspect) / 2, 0);
} else {
texture.repeat.set(1, sourceAspect / targetAspect);
texture.offset.set(0, (1 - sourceAspect / targetAspect) / 2);
}
texture.needsUpdate = true;
}
const MEDIA_ASPECT = 4 / 3;
/* ── Video plane on PCB bottom (real 3D mesh with VideoTexture) ── */
function PcbVideoMesh({ offset, src, visible }: {
offset: [number, number, number]; src: string; visible: boolean;
@@ -256,9 +276,8 @@ function PcbVideoMesh({ offset, src, visible }: {
const videoRef = useRef<HTMLVideoElement | null>(null);
const texRef = useRef<THREE.VideoTexture | null>(null);
// Mosfet card local space: ~0.38 x 0.86 (XZ), Y = thickness
// Plane size: cover most of the bottom face
const planeW = 1.9, planeH = 4.275;
// Plane size in 4:3 landscape; covers center of the mosfet bottom face
const planeW = 1.9, planeH = planeW / MEDIA_ASPECT;
useEffect(() => {
const video = document.createElement('video');
@@ -276,7 +295,10 @@ function PcbVideoMesh({ offset, src, visible }: {
tex.colorSpace = THREE.SRGBColorSpace;
texRef.current = tex;
return () => { video.pause(); video.src = ''; tex.dispose(); };
const onMeta = () => applyCoverFit(tex, MEDIA_ASPECT);
video.addEventListener('loadedmetadata', onMeta);
return () => { video.removeEventListener('loadedmetadata', onMeta); video.pause(); video.src = ''; tex.dispose(); };
}, [src]);
useEffect(() => {
@@ -331,12 +353,16 @@ function PcbCarouselMesh({ offset, visible }: {
const textures = useRef<THREE.Texture[]>([]);
const [idx, setIdx] = useState(0);
const autoTimer = useRef(0);
const planeW = 1.9, planeH = 4.275;
const planeW = 1.9, planeH = planeW / MEDIA_ASPECT;
useEffect(() => {
const loader = new THREE.TextureLoader();
textures.current = CAROUSEL_PHOTOS.map(url => {
const t = loader.load(url);
const t = loader.load(url, (loaded) => {
loaded.colorSpace = THREE.SRGBColorSpace;
loaded.minFilter = THREE.LinearFilter;
applyCoverFit(loaded, MEDIA_ASPECT);
});
t.colorSpace = THREE.SRGBColorSpace;
t.minFilter = THREE.LinearFilter;
return t;