feat: Implement wall asset management features including creation, instances, and rendering; enhance wall properties input validation

This commit is contained in:
2025-06-30 16:21:54 +05:30
parent 943ad3ba49
commit 997775c27e
15 changed files with 313 additions and 47 deletions

View File

@@ -0,0 +1,135 @@
import * as THREE from 'three';
import { useEffect, useMemo, useRef, useState } from 'react';
import { retrieveGLTF, storeGLTF } from '../../../../../utils/indexDB/idbUtils';
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { Base, Geometry, Subtraction } from '@react-three/csg';
import { useFrame } from '@react-three/fiber';
import { useSceneContext } from '../../../../scene/sceneContext';
function WallAssetInstance({ wallAsset }: { wallAsset: WallAsset }) {
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const { wallStore } = useSceneContext();
const { walls, getWallById } = wallStore();
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
const [boundingBox, setBoundingBox] = useState<THREE.Box3 | null>(null);
const groupRef = useRef<THREE.Group>(null);
const csgRef = useRef<any>(null);
const wall = useMemo(() => getWallById(wallAsset.wallUuid), [getWallById, wallAsset.wallUuid, walls]);
useEffect(() => {
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/');
loader.setDRACOLoader(dracoLoader);
const loadModel = async () => {
try {
// Check Cache
const assetId = wallAsset.assetId;
const cachedModel = THREE.Cache.get(assetId);
if (cachedModel) {
setGltfScene(cachedModel.scene.clone());
calculateBoundingBox(cachedModel.scene);
return;
}
// Check IndexedDB
const indexedDBModel = await retrieveGLTF(assetId);
if (indexedDBModel) {
const blobUrl = URL.createObjectURL(indexedDBModel);
loader.load(blobUrl, (gltf) => {
URL.revokeObjectURL(blobUrl);
THREE.Cache.remove(blobUrl);
THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene);
},
undefined,
(error) => {
echo.error(`[IndexedDB] Error loading ${wallAsset.modelName}:`);
URL.revokeObjectURL(blobUrl);
}
);
return;
}
// Fetch from Backend
const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${assetId}`;
const handleBackendLoad = async (gltf: GLTF) => {
try {
const response = await fetch(modelUrl);
const modelBlob = await response.blob();
await storeGLTF(assetId, modelBlob);
THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene);
} catch (error) {
console.error(`[Backend] Error storing/loading ${wallAsset.modelName}:`, error);
}
};
loader.load(
modelUrl,
handleBackendLoad,
undefined,
(error) => {
echo.error(`[Backend] Error loading ${wallAsset.modelName}:`);
}
);
} catch (err) {
console.error("Failed to load model:", wallAsset.assetId, err);
}
};
const calculateBoundingBox = (scene: THREE.Object3D) => {
const box = new THREE.Box3().setFromObject(scene);
setBoundingBox(box);
};
loadModel();
}, []);
useFrame(() => {
if (csgRef.current) {
csgRef.current.update();
}
})
if (!gltfScene || !boundingBox || !wall) { return null }
const size = new THREE.Vector3();
boundingBox.getSize(size);
const center = new THREE.Vector3();
boundingBox.getCenter(center);
return (
<>
<group
key={wallAsset.modelUuid}
name={wallAsset.modelName}
ref={groupRef}
position={wallAsset.position}
rotation={wallAsset.rotation}
visible={wallAsset.isVisible}
userData={wallAsset}
>
<Subtraction position={[center.x, center.y, center.z]} scale={[size.x, size.y, wall.wallThickness + 0.05]}>
<Geometry ref={csgRef}>
<Base geometry={new THREE.BoxGeometry()} />
</Geometry>
</Subtraction>
{gltfScene && (
<mesh
onClick={() => {
console.log(wallAsset);
}}
>
<primitive object={gltfScene} />
</mesh>
)}
</group>
</>
)
}
export default WallAssetInstance

View File

@@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { useSceneContext } from '../../../scene/sceneContext'
import { useToggleView } from '../../../../store/builder/store';
import WallAssetInstance from './Instances/wallAssetInstance';
function WallAssetInstances() {
const { wallAssetStore } = useSceneContext();
const { wallAssets } = wallAssetStore();
const { toggleView } = useToggleView();
useEffect(() => {
// console.log('wallAssets: ', wallAssets);
}, [wallAssets])
return (
<>
{!toggleView && wallAssets.length > 0 && (
<>
{wallAssets.map((wallAsset) => (
<WallAssetInstance key={wallAsset.modelUuid} wallAsset={wallAsset} />
))}
</>
)}
</>
)
}
export default WallAssetInstances

View File

@@ -0,0 +1,85 @@
import { useThree } from '@react-three/fiber';
import { useEffect } from 'react'
import { useSelectedItem, useSocketStore, useToggleView } from '../../../store/builder/store';
import useModuleStore from '../../../store/useModuleStore';
import { MathUtils, Vector3 } from 'three';
import { useSceneContext } from '../../scene/sceneContext';
function WallAssetCreator() {
const { socket } = useSocketStore();
const { pointer, camera, raycaster, scene, gl } = useThree();
const { togglView } = useToggleView();
const { activeModule } = useModuleStore();
const { wallAssetStore } = useSceneContext();
const { addWallAsset } = wallAssetStore();
const { selectedItem, setSelectedItem } = useSelectedItem();
function closestPointOnLineSegment(p: Vector3, a: Vector3, b: Vector3) {
const ab = new Vector3().subVectors(b, a);
const ap = new Vector3().subVectors(p, a);
const abLengthSq = ab.lengthSq();
const dot = ap.dot(ab);
const t = Math.max(0, Math.min(1, dot / abLengthSq));
return new Vector3().copy(a).add(ab.multiplyScalar(t));
}
useEffect(() => {
const canvasElement = gl.domElement;
const onDrop = (event: DragEvent) => {
if (!event.dataTransfer?.files[0]) return;
if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category === 'Fenestration') {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
const intersect = intersects.find((intersect) => intersect.object.name.includes('WallReference'));
if (intersect) {
const wall = intersect.object.userData as Wall;
const closestPoint = closestPointOnLineSegment(
new Vector3(intersect.point.x, 0, intersect.point.z),
new Vector3(...wall.points[0].position),
new Vector3(...wall.points[1].position)
)
const wallRotation = intersect.object.rotation.clone();
const newWallAsset: WallAsset = {
modelName: selectedItem.name,
modelUuid: MathUtils.generateUUID(),
wallUuid: wall.wallUuid,
wallAssetType: selectedItem.subCategory,
assetId: selectedItem.id,
position: [closestPoint.x, selectedItem.subCategory === "fixed-move" ? 0 : intersect.point.y, closestPoint.z],
rotation: [wallRotation.x, wallRotation.y, wallRotation.z],
isLocked: false,
isVisible: true,
opacity: 1,
};
addWallAsset(newWallAsset);
}
}
};
if (!togglView && activeModule === 'builder') {
canvasElement.addEventListener('drop', onDrop);
}
return () => {
canvasElement.removeEventListener('drop', onDrop);
};
}, [gl, camera, togglView, activeModule, socket, selectedItem, setSelectedItem]);
return (
<>
</>
)
}
export default WallAssetCreator

View File

@@ -0,0 +1,17 @@
import WallAssetCreator from './wallAssetCreator'
import WallAssetInstances from './Instances/wallAssetInstances'
function WallAssetGroup() {
return (
<>
<WallAssetCreator />
<WallAssetInstances />
</>
)
}
export default WallAssetGroup