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 {homeProgress} from './vendor/kinematics.js'; import {downloadCommand} from './download'; import {viewerFrame} from './spatial'; type Props={asset:Asset;x:number;y:number;scale:number;width:number;height:number;inBasket:boolean;onToggleBasket:()=>void;onClose:()=>void}; export default function ObjectViewer({asset,x,y,scale,width,height,inBasket,onToggleBasket,onClose}:Props){ const host=useRef(null),engine=useRef|null>(null); const values=useRef>({}),activeJoint=useRef(null); const [status,setStatus]=useState('Loading 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),[mode,setMode]=useState<'3d'|'video'>('3d'),[retry,setRetry]=useState(0),[allowLarge,setAllowLarge]=useState(false); const large=!!asset.bytes&&asset.bytes>50*1024*1024&&!allowLarge; useEffect(()=>{ if(mode!=='3d'||!asset.model||large)return; let cancelled=false;setReady(false);setError('');setJoints([]);setPlaying(false);setHighlighted(true);setStatus('Loading 3D…'); import('./viewer-engine').then(({createEngine})=>{ if(cancelled||!host.current)return; try{ engine.current=createEngine(host.current,{url:asset.model!,compact:asset.compact},setStatus,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){setError(failure.message);setStatus('');}},(id,value)=>{ if(cancelled)return;values.current[id]=value;if(activeJoint.current===id)setProgress(value); }); }catch(failure){setError(String(failure));setStatus('');} }).catch(failure=>{if(!cancelled){setError(String(failure));setStatus('');}}); return()=>{cancelled=true;engine.current?.dispose();engine.current=null;}; },[asset.id,mode,retry,large]); const joint=joints[jointIndex],hasMovement=mode==='3d'&&joints.length>0; const {dimension,left,top,inView}=viewerFrame(x,y,scale,width,height); const dockWidth=Math.min(440,width-30),dockHeight=hasMovement?158:52; const dockX=Math.max(15,Math.min(width-dockWidth-15,x-dockWidth/2)); const dockY=Math.max(width<700?126:95,Math.min(height-dockHeight-62,y+dimension/2+12)); const command=downloadCommand(asset); const copy=async()=>{try{await navigator.clipboard.writeText(command);setCopied(true);}catch{setCopied(false);}}; 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)=>{ setPlaying(false);values.current[joint.id]=value;setProgress(value);engine.current?.setJoint(joint.id,value); }; return <>
{mode==='3d'&&<>
{!ready&&}
{large?:error?3D preview unavailable :status&&{status}}
} {mode==='video'&&asset.video&&
{inView&&<>
{asset.name}{asset.author}
{hasMovement&&{engine.current?.play(!playing,joint.id);setPlaying(!playing);}} onReset={()=>setPosition(homeProgress(joint))} onHighlight={()=>{engine.current?.setHighlight(!highlighted);setHighlighted(!highlighted);}}/>}
{mode==='video'?'Recorded motion · all parts':ready?'Drag object to rotate':error?'Source preview':large?'Preview':'Opening object…'} {asset.video&&}
{panel==='info'&&

{asset.name}

Creator
{asset.author}
Movement
{asset.moving?`${asset.moving} controllable movements`:'Static object'}
License
{Array.isArray(asset.license)?asset.license.join(', '):asset.license||'See source asset'}
Original OpenUSD asset

Choose a movement to highlight its moving part. Play and the slider control only that movement, including any linked parts. Other movements keep their position.

Names and limits come from the source model. Generic names indicate that the source has no descriptive label. This is a movement preview; physics simulation requires a compatible simulator.

{error&&
Preview details

{error}

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

Take this object with you.

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

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