import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { Canvas, useThree } from '@react-three/fiber'; import { Html, OrbitControls } from '@react-three/drei'; import { gsap } from 'gsap'; import type { Object3D } from 'three'; import { AnimationTransport, GuidedTourOverlay } from '../../../components/animation'; import { useGuidedTourPlayer, useMachineAnimation } from '../../../animations'; import type { GuidedTourCamera, GuidedTourSinks } from '../../../animations/guidedTour'; import type { MachineCameraRequest } from '../../../animations/types'; import { getRelatedProceduralMachines } from './proceduralDemoManifest'; import { useProceduralDemoMachine } from './useProceduralDemoMachine'; import type { PartVisualState, ProceduralMachineId, ProceduralMachineSceneProps, } from './types'; function parseInitialMachineId(): string | null { if (typeof window === 'undefined') return null; return new URLSearchParams(window.location.search).get('machine'); } function updateUrl(machineId: string, rpm: number, explode: number): void { if (typeof window === 'undefined') return; const url = new URL(window.location.href); url.searchParams.set('machine', machineId); url.searchParams.set('rpm', Math.round(rpm).toString()); url.searchParams.set('explode', explode.toFixed(2)); window.history.replaceState({}, '', url.toString()); } function ViewerCameraController({ request, controlsRef, }: { request: MachineCameraRequest | GuidedTourCamera | null; controlsRef: React.MutableRefObject<{ target?: { x: number; y: number; z: number }; update?: () => void } | null>; }): null { const { camera } = useThree(); useEffect(() => { if (!request) return undefined; const duration = request.duration ?? 1.1; const easing = request.easing ?? 'power3.out'; const cameraTween = gsap.to(camera.position, { x: request.position[0], y: request.position[1], z: request.position[2], duration, ease: easing, onUpdate: () => { camera.lookAt(request.target[0], request.target[1], request.target[2]); controlsRef.current?.update?.(); }, }); let targetTween: gsap.core.Tween | undefined; if (controlsRef.current?.target) { targetTween = gsap.to(controlsRef.current.target, { x: request.target[0], y: request.target[1], z: request.target[2], duration, ease: easing, onUpdate: () => controlsRef.current?.update?.(), }); } if (request.fov && 'fov' in camera) { gsap.to(camera, { fov: request.fov, duration, ease: easing, onUpdate: () => camera.updateProjectionMatrix(), }); } return () => { cameraTween.kill(); targetTween?.kill(); }; }, [camera, controlsRef, request]); return null; } function LoadingFallback(): JSX.Element { return (

Loading procedural rig…

); } function MachineCanvas({ sceneProps, Scene, cameraRequest, }: { sceneProps: ProceduralMachineSceneProps; Scene: React.ComponentType; cameraRequest: MachineCameraRequest | GuidedTourCamera | null; }): JSX.Element { const controlsRef = useRef<{ target?: { x: number; y: number; z: number }; update?: () => void; } | null>(null); return ( }> ); } export function ProceduralDemoExperience(): JSX.Element { const { machine, machines, selectedMachineId, selectMachine, selectNextMachine, selectPreviousMachine, } = useProceduralDemoMachine(parseInitialMachineId()); const animation = useMachineAnimation(machine.animationModule, { autoPlay: true }); const [selectedPartId, setSelectedPartId] = useState(null); const [hoveredPartId, setHoveredPartId] = useState(null); const [tourHighlightedPartIds, setTourHighlightedPartIds] = useState([]); const [partState, setPartState] = useState>({}); const [explodedDistance, setExplodedDistance] = useState(0); const [wireframe, setWireframe] = useState(false); const [labelsVisible, setLabelsVisible] = useState(true); const [cameraRequest, setCameraRequest] = useState( machine.definition.cameraPresets[0], ); useEffect(() => { setSelectedPartId(null); setHoveredPartId(null); setTourHighlightedPartIds([]); setPartState( Object.fromEntries( machine.definition.parts.map((part) => [ part.id, { visible: true, opacity: part.defaultOpacity ?? 1, }, ]), ), ); setCameraRequest(machine.definition.cameraPresets[0]); animation.restart(true); }, [machine.definition.id]); useEffect(() => { updateUrl(machine.definition.id, animation.snapshot.rpm, explodedDistance); }, [animation.snapshot.rpm, explodedDistance, machine.definition.id]); const tourSinks = useMemo( () => ({ highlightParts: (partIds) => setTourHighlightedPartIds(partIds), requestCamera: (camera, durationSeconds) => setCameraRequest({ ...camera, duration: durationSeconds }), setAnimationPhase: (phaseRange, rpm) => { if (rpm) animation.setRpm(rpm); if (phaseRange) animation.seekCycleProgress(phaseRange[0]); if (animation.snapshot.status !== 'playing') animation.play(); }, }), [animation], ); const tour = useGuidedTourPlayer(machine.definition.tour, tourSinks); const highlightedPartIds = useMemo( () => [ ...new Set([ ...animation.snapshot.highlightedPartIds, ...tourHighlightedPartIds, ...(selectedPartId ? [selectedPartId] : []), ...(hoveredPartId ? [hoveredPartId] : []), ]), ], [ animation.snapshot.highlightedPartIds, hoveredPartId, selectedPartId, tourHighlightedPartIds, ], ); const selectedPart = selectedPartId ? machine.definition.parts.find((part) => part.id === selectedPartId) : null; const sceneProps = useMemo( () => ({ registerPart: (partId: string, object: Object3D | null) => animation.registerPart(partId, object), selectedPartId, hoveredPartId, highlightedPartIds, partState, explodedDistance, wireframe, labelsVisible, onSelectPart: setSelectedPartId, onHoverPart: setHoveredPartId, }), [ animation.registerPart, explodedDistance, highlightedPartIds, hoveredPartId, labelsVisible, partState, selectedPartId, wireframe, ], ); const relatedMachines = getRelatedProceduralMachines(machine.definition.id); const copyShareLink = useCallback(async () => { if (typeof window === 'undefined') return; const url = new URL(window.location.href); url.searchParams.set('machine', machine.definition.id); url.searchParams.set('rpm', Math.round(animation.snapshot.rpm).toString()); url.searchParams.set('explode', explodedDistance.toFixed(2)); try { await navigator.clipboard.writeText(url.toString()); } catch { window.prompt('Copy this Mechanica view link:', url.toString()); } }, [animation.snapshot.rpm, explodedDistance, machine.definition.id]); return (

Mechanica

Procedural machine animation lab

Current phase

{animation.snapshot.phaseLabel ?? machine.definition.summary}

{machine.definition.parts.length} components {Math.round(animation.snapshot.rpm)} RPM {machine.definition.typicalRpm}
); }