import React, { useEffect, useMemo, useRef } from 'react'; import { Billboard, Text } from '@react-three/drei'; import type { Group, Material, Mesh, Object3D } from 'three'; import { Color } from 'three'; import type { Vec3 } from '../../../animations/types'; const SELECTED_COLOR = new Color('#ffb04c'); const HIGHLIGHT_COLOR = new Color('#4c8dff'); const DEFAULT_EMISSIVE = new Color('#000000'); function materialArray(material: Material | Material[] | undefined): Material[] { if (!material) return []; return Array.isArray(material) ? material : [material]; } function ensureUniqueMaterials(root: Object3D): void { root.traverse((child) => { const mesh = child as Mesh; if (!mesh.isMesh || mesh.userData.mechanicaMaterialCloned) return; if (Array.isArray(mesh.material)) { mesh.material = mesh.material.map((material) => material.clone()); } else if (mesh.material) { mesh.material = mesh.material.clone(); } mesh.userData.mechanicaMaterialCloned = true; }); } function applyMaterialState( root: Object3D, opacity: number, wireframe: boolean, selected: boolean, highlighted: boolean, ): void { ensureUniqueMaterials(root); root.traverse((child) => { const mesh = child as Mesh; if (!mesh.isMesh) return; materialArray(mesh.material).forEach((material) => { material.transparent = opacity < 0.999; material.opacity = opacity; material.depthWrite = opacity > 0.25; material.needsUpdate = true; const maybeStandard = material as Material & { wireframe?: boolean; emissive?: Color; emissiveIntensity?: number; }; if (typeof maybeStandard.wireframe === 'boolean') { maybeStandard.wireframe = wireframe; } if (maybeStandard.emissive) { maybeStandard.emissive.copy(selected ? SELECTED_COLOR : highlighted ? HIGHLIGHT_COLOR : DEFAULT_EMISSIVE); maybeStandard.emissiveIntensity = selected ? 0.55 : highlighted ? 0.38 : 0; } }); }); } export interface MachinePartProps { partId: string; registerPart?: (partId: string, object: Object3D | null) => void; selected?: boolean; highlighted?: boolean; visible?: boolean; opacity?: number; wireframe?: boolean; explodeDirection?: Vec3; explodedDistance?: number; position?: Vec3; rotation?: Vec3; scale?: Vec3 | number; children: React.ReactNode; onSelectPart?: (partId: string) => void; onHoverPart?: (partId: string | null) => void; } export function MachinePart({ partId, registerPart, selected = false, highlighted = false, visible = true, opacity = 1, wireframe = false, explodeDirection = [0, 0, 0], explodedDistance = 0, position = [0, 0, 0], rotation = [0, 0, 0], scale = 1, children, onSelectPart, onHoverPart, }: MachinePartProps): JSX.Element { const outerRef = useRef(null); const innerRef = useRef(null); const explodedPosition = useMemo( () => [ explodeDirection[0] * explodedDistance, explodeDirection[1] * explodedDistance, explodeDirection[2] * explodedDistance, ], [explodeDirection, explodedDistance], ); useEffect(() => { const inner = innerRef.current; if (!inner) return undefined; registerPart?.(partId, inner); return () => registerPart?.(partId, null); }, [partId, registerPart]); useEffect(() => { const outer = outerRef.current; if (!outer) return; applyMaterialState(outer, opacity, wireframe, selected, highlighted); }, [highlighted, opacity, selected, wireframe, children]); return ( { event.stopPropagation(); onSelectPart?.(partId); }} onPointerOver={(event) => { event.stopPropagation(); onHoverPart?.(partId); }} onPointerOut={(event) => { event.stopPropagation(); onHoverPart?.(null); }} > {children} ); } export interface PartLabelProps { text: string; position: Vec3; visible?: boolean; } export function PartLabel({ text, position, visible = true }: PartLabelProps): JSX.Element | null { if (!visible) return null; return ( {text} ); } export interface ShaftProps { length: number; radius?: number; axis?: 'x' | 'y' | 'z'; color?: string; metalness?: number; roughness?: number; } export function Shaft({ length, radius = 0.08, axis = 'x', color = '#7c8799', metalness = 0.82, roughness = 0.28, }: ShaftProps): JSX.Element { const rotation: Vec3 = axis === 'x' ? [0, 0, Math.PI / 2] : axis === 'z' ? [Math.PI / 2, 0, 0] : [0, 0, 0]; return ( ); } export interface ToothedGearProps { radius: number; thickness?: number; teeth?: number; toothDepth?: number; color?: string; accentColor?: string; boreRadius?: number; } export function ToothedGear({ radius, thickness = 0.22, teeth = 24, toothDepth = 0.12, color = '#748196', accentColor = '#aab8cc', boreRadius = 0.12, }: ToothedGearProps): JSX.Element { const toothNodes = useMemo( () => Array.from({ length: teeth }, (_, index) => { const angle = (index / teeth) * Math.PI * 2; const x = Math.cos(angle) * (radius + toothDepth * 0.46); const y = Math.sin(angle) * (radius + toothDepth * 0.46); return ( ); }), [accentColor, radius, teeth, thickness, toothDepth], ); return ( {toothNodes} ); } export interface SpokedWheelProps { radius: number; tube?: number; spokes?: number; color?: string; spokeColor?: string; } export function SpokedWheel({ radius, tube = 0.055, spokes = 6, color = '#7d8798', spokeColor = '#aab4c2', }: SpokedWheelProps): JSX.Element { return ( {Array.from({ length: spokes }, (_, index) => { const angle = (index / spokes) * Math.PI * 2; return ( ); })} ); } export interface FlowArrowProps { length?: number; color?: string; opacity?: number; } export function FlowArrow({ length = 0.6, color = '#4c8dff', opacity = 0.72, }: FlowArrowProps): JSX.Element { return ( ); } export function BoltCircle({ radius, count = 8, boltRadius = 0.035, color = '#9aa7bb', }: { radius: number; count?: number; boltRadius?: number; color?: string; }): JSX.Element { return ( {Array.from({ length: count }, (_, index) => { const angle = (index / count) * Math.PI * 2; return ( ); })} ); }