import { useRef, useEffect, forwardRef, useMemo, useState } from "react"; import * as THREE from "three"; import { useFrame, useThree, createPortal } from "@react-three/fiber"; import { MaterialFactory } from "../materials/MaterialFactory"; import { useTexturePipeline } from "../hooks/useTexturePipeline"; interface ModelViewerProps { object: THREE.Object3D | null; sceneObjects: any[]; } interface MeshMaterialManagerProps { mesh: THREE.Mesh; sceneObj: any; textureCache: React.MutableRefObject>; } const MeshMaterialManager = ({ mesh, sceneObj, textureCache }: MeshMaterialManagerProps) => { useEffect(() => { if (!mesh || !sceneObj.materialProps) return; const props = sceneObj.materialProps; const mappingType = props.mappingType || 'uv'; const uvScale = props.uvScale || 1; const uvTiling = props.uvTiling || [1, 1]; const uvOffset = props.uvOffset || [0, 0]; const uvRotation = props.uvRotation || 0; const origMat = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as THREE.MeshPhysicalMaterial; const baseParams: any = { color: origMat.color, roughness: origMat.roughness !== undefined ? origMat.roughness : 0.5, metalness: origMat.metalness !== undefined ? origMat.metalness : 0, map: origMat.map, normalMap: origMat.normalMap, roughnessMap: origMat.roughnessMap, metalnessMap: origMat.metalnessMap, aoMap: origMat.aoMap, emissiveMap: origMat.emissiveMap, emissive: origMat.emissive, emissiveIntensity: origMat.emissiveIntensity, envMapIntensity: origMat.envMapIntensity, alphaMap: origMat.alphaMap, transparent: origMat.transparent, opacity: origMat.opacity, side: origMat.side, alphaTest: origMat.alphaTest, }; // Use MaterialFactory to get reusable materials let activeMat: any; if (mappingType === 'triplanar') { activeMat = MaterialFactory.getInstance().getTriplanarMaterial( baseParams, mesh.uuid ); activeMat.triplanarUniforms.uTriplanarScale.value = uvScale; activeMat.triplanarUniforms.uBlendSharpness.value = 5.0; // Default or from props } else { activeMat = MaterialFactory.getInstance().getStandardUVMaterial( baseParams, mesh.uuid ); let modeIndex = 0; if (mappingType === 'planar') modeIndex = 1; else if (mappingType === 'box') modeIndex = 2; else if (mappingType === 'cylinder') modeIndex = 3; activeMat.uvUniforms.uMappingMode.value = modeIndex; activeMat.updateUVTransform(uvOffset, [uvTiling[0] * uvScale, uvTiling[1] * uvScale], uvRotation * (Math.PI / 180)); } // Apply basic props if (props.color) activeMat.color.set(props.color); if (props.roughness !== undefined) activeMat.roughness = props.roughness; if (props.metalness !== undefined) activeMat.metalness = props.metalness; if (props.opacity !== undefined) activeMat.opacity = props.opacity; if (props.transmission !== undefined) activeMat.transmission = props.transmission; activeMat.transparent = (props.transmission > 0) || (props.opacity < 1) || !!props.alphaMap; if (Array.isArray(mesh.material)) { mesh.material = mesh.material.map(() => activeMat); } else { mesh.material = activeMat; } // Textures const textureTypes: ('map' | 'normalMap' | 'specularMap' | 'alphaMap')[] = ['map', 'normalMap', 'specularMap', 'alphaMap']; textureTypes.forEach(type => { const url = props[type]; if (url) { if (!activeMat[type] || activeMat[type]!.name !== url) { if (textureCache.current.has(url)) { activeMat[type] = textureCache.current.get(url)!; if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = activeMat[type]; activeMat.needsUpdate = true; } else { const loader = new THREE.TextureLoader(); loader.load(url, (tex) => { tex.name = url; if (type === 'map') tex.colorSpace = THREE.SRGBColorSpace; tex.wrapS = tex.wrapT = THREE.RepeatWrapping; textureCache.current.set(url, tex); activeMat[type] = tex; if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = tex; activeMat.needsUpdate = true; }); } } } else if (url === null && activeMat[type]) { activeMat[type] = null; if (mappingType === 'triplanar') activeMat.triplanarUniforms['t' + type.charAt(0).toUpperCase() + type.slice(1).replace('Map', '')].value = null; activeMat.needsUpdate = true; } }); mesh.visible = sceneObj.visible; mesh.castShadow = true; mesh.receiveShadow = true; mesh.frustumCulled = false; // Sync individual mesh transform if (sceneObj.transformProps) { const { position, rotation, scale } = sceneObj.transformProps; mesh.position.set(position[0], position[1], position[2]); mesh.rotation.set(rotation[0], rotation[1], rotation[2]); mesh.scale.set(scale[0], scale[1], scale[2]); } }, [sceneObj, mesh]); return null; }; const FillMesh = ({ fillProps, originalMesh }: { fillProps: any, originalMesh: THREE.Mesh }) => { const materialRef = useRef(null); const [clippingPlane] = useState(() => new THREE.Plane(new THREE.Vector3(0, -1, 0), 0)); const { size: winSize } = useThree(); // Clone geometry and fix interpolation const geometry = useMemo(() => { const geo = originalMesh.geometry.clone(); geo.computeVertexNormals(); return geo; }, [originalMesh.geometry]); // Update clipping plane based on height useEffect(() => { geometry.computeBoundingBox(); const box = geometry.boundingBox || new THREE.Box3().setFromObject(originalMesh); const minY = box.min.y; const maxY = box.max.y; // height is 0 to 1 const clipY = minY + (maxY - minY) * (fillProps.height ?? 0.5); // Since this plane is applied in world space by Three.js by default, // wait, localClippingEnabled = true means the plane is in LOCAL space! // If it's local space, clipY is exact. clippingPlane.constant = clipY; }, [geometry, fillProps.height, originalMesh]); useFrame((state) => { if (materialRef.current && fillProps.type === 'liquid') { const mat = materialRef.current; if (mat.uTime !== undefined) mat.uTime = state.clock.elapsedTime; if (mat.uResolution && mat.uResolution.set) { mat.uResolution.set(winSize.width || 800, winSize.height || 600); } } }); const fillNode = ( {fillProps.type === 'liquid' ? ( // @ts-ignore ) : ( )} ); return createPortal(fillNode, originalMesh); }; export const ModelViewer = forwardRef(({ object, sceneObjects }, ref) => { const internalRef = useRef(null); const groupRef = (ref as any) || internalRef; const textureCache = useRef>(new Map()); const meshCache = useRef>(new Map()); useTexturePipeline(object); // Cleanup textures on unmount useEffect(() => { return () => { textureCache.current.forEach(tex => tex.dispose()); textureCache.current.clear(); meshCache.current.clear(); }; }, []); // Build mesh cache when object changes useEffect(() => { meshCache.current.clear(); if (object) { let mIdx = 0; object.traverse((child) => { if (child instanceof THREE.Mesh) { const stableId = `mesh-${mIdx}`; meshCache.current.set(stableId, child); mIdx++; } }); } }, [object]); // Main transform update for the core model useEffect(() => { if (object && sceneObjects) { const mainModel = sceneObjects.find(obj => obj.type === 'model'); if (mainModel && mainModel.transformProps && groupRef.current) { const { position: p, rotation: r, scale: s } = mainModel.transformProps; groupRef.current.position.set(p[0], p[1], p[2]); groupRef.current.rotation.set(r[0], r[1], r[2]); groupRef.current.scale.set(s[0], s[1], s[2]); } } }, [object, sceneObjects]); if (!object) return null; const meshObjects = sceneObjects.filter(obj => obj.type === 'mesh'); return ( {meshObjects.map(obj => { const mesh = meshCache.current.get(obj.id); if (!mesh) return null; return ( {obj.fillProps?.enabled && ( )} ); })} ); });