Initial commit of VIZ 3D project

This commit is contained in:
balaji
2026-06-10 16:10:18 +05:30
commit 08aea85c2f
37 changed files with 13118 additions and 0 deletions
+271
View File
@@ -0,0 +1,271 @@
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<Map<string, THREE.Texture>>;
}
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<any>(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 = (
<mesh geometry={geometry} scale={[0.995, 0.995, 0.995]}>
{fillProps.type === 'liquid' ? (
// @ts-ignore
<liquidMaterialImpl
ref={materialRef}
uColor={new THREE.Color(fillProps.color)}
transparent
side={THREE.DoubleSide}
clippingPlanes={[clippingPlane]}
clipping={true}
/>
) : (
<meshPhysicalMaterial
color={fillProps.color}
side={THREE.DoubleSide}
clippingPlanes={[clippingPlane]}
roughness={0.2}
clearcoat={0.5}
/>
)}
</mesh>
);
return createPortal(fillNode, originalMesh);
};
export const ModelViewer = forwardRef<THREE.Group, ModelViewerProps>(({ object, sceneObjects }, ref) => {
const internalRef = useRef<THREE.Group>(null);
const groupRef = (ref as any) || internalRef;
const textureCache = useRef<Map<string, THREE.Texture>>(new Map());
const meshCache = useRef<Map<string, THREE.Mesh>>(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 (
<group ref={groupRef}>
<primitive object={object} />
{meshObjects.map(obj => {
const mesh = meshCache.current.get(obj.id);
if (!mesh) return null;
return (
<group key={obj.id}>
<MeshMaterialManager
mesh={mesh}
sceneObj={obj}
textureCache={textureCache}
/>
{obj.fillProps?.enabled && (
<FillMesh fillProps={obj.fillProps} originalMesh={mesh} />
)}
</group>
);
})}
</group>
);
});