import {useEffect,useRef,useState} from 'react'; import {X,Info,Download,Copy,Check,ArrowUpRight,Film,Box,RotateCw,ShoppingBag} from 'lucide-react'; import type {Asset} from './types'; import type {createEngine,Joint} from './viewer-engine'; import AssetImage from './AssetImage'; import MotionControls from './MotionControls'; import SelectionIndicator from './SelectionIndicator'; import {homeProgress} from './vendor/kinematics.js'; import {downloadCommand} from './download'; import {viewerFrame,selectionInFocus} from './spatial'; import {formatFileSize} from './file-size'; type Props={asset:Asset;moving?:boolean;x:number;y:number;scale:number;objectSize:number;width:number;height:number;inBasket:boolean;onToggleBasket:()=>void;onClose:()=>void}; export default function ObjectViewer({asset,moving=false,x,y,scale,objectSize,width,height,inBasket,onToggleBasket,onClose}:Props){ const host=useRef(null),video=useRef(null),engine=useRef|null>(null); const movingRef=useRef(moving);movingRef.current=moving; const values=useRef>({}),activeJoint=useRef(null); const [status,setStatus]=useState('Preparing 3D…'),[error,setError]=useState(''),[ready,setReady]=useState(false); const [joints,setJoints]=useState([]),[jointIndex,setJointIndex]=useState(0),[progress,setProgress]=useState(0),[playing,setPlaying]=useState(false),[highlighted,setHighlighted]=useState(true); const [panel,setPanel]=useState<'info'|'download'|null>(null),[copied,setCopied]=useState(false),[copyError,setCopyError]=useState(false),[mode,setMode]=useState<'3d'|'video'>('3d'),[retry,setRetry]=useState(0),[allowLarge,setAllowLarge]=useState(false); const large=!!asset.bytes&&asset.bytes>50*1024*1024&&!allowLarge; const {dimension,left,top,inView}=viewerFrame(x,y,scale,width,height,objectSize); const showControls=inView&&selectionInFocus(x,y,width,height); useEffect(()=>{ if(mode!=='3d'||!asset.model||large)return; let cancelled=false,start=0;setReady(false);setError('');setJoints([]);setPlaying(false);setHighlighted(true);setStatus('Preparing 3D…'); // Center first: rapidly changing selection can cancel before allocating a renderer. const begin=()=>{ if(cancelled)return; if(movingRef.current){start=requestAnimationFrame(begin);return;} start=requestAnimationFrame(()=>{ if(movingRef.current){begin();return;} import('./viewer-engine').then(({createEngine})=>{ if(cancelled||!host.current)return; try{ engine.current=createEngine(host.current,{url:asset.model!,compact:asset.compact},value=>{if(!cancelled)setStatus(value);},items=>{ if(cancelled)return; setJoints(items);setJointIndex(0);activeJoint.current=items[0]?.id??null; values.current=Object.fromEntries(items.map(j=>[j.id,homeProgress(j)])); setProgress(items[0]?homeProgress(items[0]):0);setReady(true); },failure=>{if(!cancelled){setReady(false);setPlaying(false);setJoints([]);setError(failure.message);setStatus('');}},(id,value)=>{ if(cancelled)return;values.current[id]=value;if(activeJoint.current===id)setProgress(value); }); }catch(failure){if(!cancelled){setError(String(failure));setStatus('');}} }).catch(failure=>{if(!cancelled){setError(String(failure));setStatus('');}}); }); };start=requestAnimationFrame(begin); return()=>{cancelled=true;cancelAnimationFrame(start);engine.current?.dispose();engine.current=null;}; },[asset.id,mode,retry,large]); useEffect(()=>{ engine.current?.setVisible(inView); if(!showControls){engine.current?.play(false);setPlaying(false);video.current?.pause();} },[inView,showControls,ready,mode]); const joint=joints[jointIndex],hasMovement=mode==='3d'&&ready&&joints.length>0; const command=downloadCommand(asset); const copy=async()=>{try{await navigator.clipboard.writeText(command);setCopied(true);setCopyError(false);}catch{setCopied(false);setCopyError(true);}}; const chooseMovement=(index:number)=>{ engine.current?.play(false);setPlaying(false);setJointIndex(index); const next=joints[index];activeJoint.current=next.id;engine.current?.selectJoint(next.id); setProgress(values.current[next.id]??homeProgress(next)); }; const setPosition=(value:number)=>{if(!joint)return;setPlaying(false);values.current[joint.id]=value;setProgress(value);engine.current?.setJoint(joint.id,value);}; const indicator=error?'error':ready?'ready':large||!asset.model?'idle':'loading'; return <>
{mode==='3d'&&<>
{!ready&&}{inView&&}} {mode==='video'&&asset.video&&
{showControls&&
{asset.name}{asset.author}USD preview{asset.bytes!=null?` · ${formatFileSize(asset.bytes)}`:''}
{mode==='video'?{error||'Recorded motion · all parts'}:!asset.model?Image preview · 3D unavailable for this object:large?:error?<>3D unavailable · Showing source image:ready?<>3D ready · Drag object to rotate:<>{moving?'Centering object…':status}} {asset.video&&}
{hasMovement&&{engine.current?.play(!playing,joint.id);setPlaying(!playing);}} onReset={()=>setPosition(homeProgress(joint))} onHighlight={()=>{engine.current?.setHighlight(!highlighted);setHighlighted(!highlighted);}}/>} {panel==='info'&&

{asset.name}

Creator
{asset.author}
Dataset
{asset.dataset}
Source format
OpenUSD
3D preview size
{asset.bytes!=null?formatFileSize(asset.bytes):'Not provided'}
Movement
{ready?joints.length?`${joints.length} controllable movements`:'Static object':asset.moving?`${asset.moving} movements listed in catalogue`:'Static object'}
License
{Array.isArray(asset.license)?asset.license.join(', '):asset.license||'See source asset'}
Source version
{asset.revision.slice(0,8)}
Original OpenUSD asset

The preview size comes from the prepared-media manifest. The complete source download includes textures and referenced files and may be larger.

{asset.moving>0&&
About movements

Choose a movement to highlight its moving part. Play and the slider control that movement and linked parts; other movements keep their positions. The recording shows all parts independently.

Names and limits come from the source model. Generic names mean no descriptive label was provided. This is a kinematic preview, not physics simulation.

} {error&&
Preview details

{error}

}
} {panel==='download'&&

Take this object with you.

Download the complete source folder, including textures and referenced files.

{copyError&&

Clipboard unavailable. Open the command below and copy it manually.

}Browse source files
Show command{command}
Source version: {asset.revision.slice(0,8)}
}
} ; }