feat: Implement wall asset management features including creation, instances, and rendering; enhance wall properties input validation
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
85
app/src/modules/builder/wallAsset/wallAssetCreator.tsx
Normal file
85
app/src/modules/builder/wallAsset/wallAssetCreator.tsx
Normal 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
|
||||
17
app/src/modules/builder/wallAsset/wallAssetGroup.tsx
Normal file
17
app/src/modules/builder/wallAsset/wallAssetGroup.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import WallAssetCreator from './wallAssetCreator'
|
||||
import WallAssetInstances from './Instances/wallAssetInstances'
|
||||
|
||||
function WallAssetGroup() {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<WallAssetCreator />
|
||||
|
||||
<WallAssetInstances />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WallAssetGroup
|
||||
Reference in New Issue
Block a user