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

@@ -133,11 +133,17 @@ const SelectedWallProperties = () => {
<InputWithDropDown <InputWithDropDown
label="Height" label="Height"
value={height} value={height}
min={1}
max={25}
step={1}
onChange={handleHeightChange} onChange={handleHeightChange}
/> />
<InputWithDropDown <InputWithDropDown
label="Thickness" label="Thickness"
value={thickness} value={thickness}
min={0.1}
max={2}
step={0.1}
onChange={handleThicknessChange} onChange={handleThicknessChange}
/> />
</div> </div>

View File

@@ -63,11 +63,17 @@ const WallProperties = () => {
<InputWithDropDown <InputWithDropDown
label="Height" label="Height"
value={`${wallHeight}`} value={`${wallHeight}`}
min={1}
max={25}
step={1}
onChange={(val) => handleHeightChange(val)} onChange={(val) => handleHeightChange(val)}
/> />
<InputWithDropDown <InputWithDropDown
label="Thickness" label="Thickness"
value={`${wallThickness}`} value={`${wallThickness}`}
min={0.1}
max={2}
step={0.1}
onChange={(val) => handleThicknessChange(val)} onChange={(val) => handleThicknessChange(val)}
/> />
</div> </div>

View File

@@ -7,7 +7,7 @@ import { useBuilderStore } from '../../../store/builder/useBuilderStore';
import defaultMaterial from '../../../assets/textures/floor/wall-tex.png'; import defaultMaterial from '../../../assets/textures/floor/wall-tex.png';
import useModuleStore from '../../../store/useModuleStore'; import useModuleStore from '../../../store/useModuleStore';
function DecalInstance({ visible = true, decal }: { visible?: boolean, decal: Decal }) { function DecalInstance({ visible = true, decal, zPosition = decal.decalPosition[2] }: { visible?: boolean, decal: Decal, zPosition?: number }) {
const { setSelectedWall, setSelectedFloor, selectedDecal, setSelectedDecal } = useBuilderStore(); const { setSelectedWall, setSelectedFloor, selectedDecal, setSelectedDecal } = useBuilderStore();
const { togglView } = useToggleView(); const { togglView } = useToggleView();
const { activeModule } = useModuleStore(); const { activeModule } = useModuleStore();
@@ -17,7 +17,7 @@ function DecalInstance({ visible = true, decal }: { visible?: boolean, decal: De
<Decal <Decal
// debug // debug
visible={visible} visible={visible}
position={decal.decalPosition} position={[decal.decalPosition[0], decal.decalPosition[1], zPosition]}
rotation={[0, 0, decal.decalRotation]} rotation={[0, 0, decal.decalRotation]}
scale={[decal.decalScale, decal.decalScale, 0.01]} scale={[decal.decalScale, decal.decalScale, 0.01]}
userData={decal} userData={decal}

View File

@@ -269,7 +269,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
useEffect(() => { useEffect(() => {
const canvasElement = gl.domElement; const canvasElement = gl.domElement;
const onDrop = (event: any) => { const onDrop = (event: DragEvent) => {
if (!event.dataTransfer?.files[0]) return; if (!event.dataTransfer?.files[0]) return;
if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category !== 'Fenestration') { if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category !== 'Fenestration') {

View File

@@ -2,8 +2,8 @@
import * as THREE from "three"; import * as THREE from "three";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import { Bvh } from "@react-three/drei"; import { Geometry } from "@react-three/csg";
////////// Zustand State Imports ////////// ////////// Zustand State Imports //////////
@@ -39,16 +39,14 @@ import ZoneGroup from "./zone/zoneGroup";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useBuilderStore } from "../../store/builder/useBuilderStore"; import { useBuilderStore } from "../../store/builder/useBuilderStore";
import { getUserData } from "../../functions/getUserData"; import { getUserData } from "../../functions/getUserData";
import WallAssetGroup from "./wallAsset/wallAssetGroup";
export default function Builder() { export default function Builder() {
const state = useThree<Types.ThreeState>(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. const state = useThree<Types.ThreeState>();
const plane = useRef<THREE.Mesh>(null);
const csgRef = useRef<any>(null);
// Assigning the scene and camera from the Three.js state to the references. const { toggleView } = useToggleView();
const plane = useRef<THREE.Mesh>(null); // Reference for a plane object for raycaster reference.
const grid = useRef() as any; // Reference for a grid object for raycaster reference.
const { toggleView } = useToggleView(); // State for toggling between 2D and 3D.
const { setToolMode } = useToolMode(); const { setToolMode } = useToolMode();
const { setRoofVisibility } = useRoofVisibility(); const { setRoofVisibility } = useRoofVisibility();
const { setWallVisibility } = useWallVisibility(); const { setWallVisibility } = useWallVisibility();
@@ -59,14 +57,6 @@ export default function Builder() {
const { setHoveredPoint, setHoveredLine } = useBuilderStore(); const { setHoveredPoint, setHoveredLine } = useBuilderStore();
const { userId, organization } = getUserData(); const { userId, organization } = getUserData();
// 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);
////////// All Toggle's //////////
useEffect(() => { useEffect(() => {
if (!toggleView) { if (!toggleView) {
setHoveredLine(null); setHoveredLine(null);
@@ -91,29 +81,31 @@ export default function Builder() {
fetchVisibility(); fetchVisibility();
}, []); }, []);
////////// Return ////////// useFrame(() => {
if (csgRef.current) {
csgRef.current.update();
}
})
return ( return (
<> <>
<Ground grid={grid} plane={plane} /> <Ground plane={plane} />
<SocketResponses /> <SocketResponses />
{/* <WallsAndWallItems
CSGGroup={CSGGroup}
setSelectedItemsIndex={setSelectedItemsIndex}
selectedItemsIndex={selectedItemsIndex}
currentWallItem={currentWallItem}
csg={csg}
lines={lines}
hoveredDeletableWallItem={hoveredDeletableWallItem}
/> */}
<AssetsGroup plane={plane} /> <AssetsGroup plane={plane} />
<AislesGroup /> <mesh name='Walls-And-WallAssets-Group'>
<Geometry ref={csgRef} useGroups>
<WallGroup /> <WallGroup />
<WallAssetGroup />
</Geometry>
</mesh>
<AislesGroup />
<FloorGroup /> <FloorGroup />

View File

@@ -154,7 +154,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
<MeshDiscardMaterial /> <MeshDiscardMaterial />
{wall.decals.map((decal) => ( {wall.decals.map((decal) => (
<DecalInstance visible={visible} key={decal.decalUuid} decal={decal} /> <DecalInstance zPosition={wall.wallThickness / 2 + 0.001} visible={visible} key={decal.decalUuid} decal={decal} />
))} ))}
</mesh> </mesh>
</mesh> </mesh>

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { DoubleSide, RepeatWrapping, Shape, SRGBColorSpace, TextureLoader, Vector2, Vector3 } from 'three'; import { DoubleSide, RepeatWrapping, Shape, SRGBColorSpace, TextureLoader, Vector2, Vector3 } from 'three';
import { Geometry } from '@react-three/csg';
import { Html, Extrude } from '@react-three/drei'; import { Html, Extrude } from '@react-three/drei';
import { useLoader } from '@react-three/fiber'; import { useLoader } from '@react-three/fiber';
import { useSceneContext } from '../../../scene/sceneContext'; import { useSceneContext } from '../../../scene/sceneContext';
@@ -44,13 +43,9 @@ function WallInstances() {
<> <>
{!toggleView && walls.length > 1 && ( {!toggleView && walls.length > 1 && (
<> <>
<mesh name='Walls-Group'> {walls.map((wall) => (
<Geometry useGroups> <WallInstance key={wall.wallUuid} wall={wall} />
{walls.map((wall) => ( ))}
<WallInstance key={wall.wallUuid} wall={wall} />
))}
</Geometry>
</mesh>
<group name='Wall-Floors-Group'> <group name='Wall-Floors-Group'>
{rooms.map((room, index) => ( {rooms.map((room, index) => (

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

View File

@@ -29,7 +29,6 @@ function ZoneGroup() {
useEffect(() => { useEffect(() => {
if (projectId && selectedVersion) { if (projectId && selectedVersion) {
getZonesApi(projectId, selectedVersion?.versionId || '').then((zones) => { getZonesApi(projectId, selectedVersion?.versionId || '').then((zones) => {
console.log('zones: ', zones);
if (zones && zones.length > 0) { if (zones && zones.length > 0) {
setZones(zones); setZones(zones);
} else { } else {

View File

@@ -1,14 +1,13 @@
import { useTileDistance, useToggleView } from "../../../store/builder/store"; import { useTileDistance, useToggleView } from "../../../store/builder/store";
import * as CONSTANTS from "../../../types/world/worldConstants"; import * as CONSTANTS from "../../../types/world/worldConstants";
const Ground = ({ grid, plane }: any) => { const Ground = ({ plane }: any) => {
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const { planeValue, gridValue } = useTileDistance(); const { planeValue, gridValue } = useTileDistance();
return ( return (
<mesh name="Ground"> <mesh name="Ground">
<mesh <mesh
ref={grid}
name="Grid" name="Grid"
position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D} position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D}
> >

View File

@@ -6,7 +6,7 @@ export const upsertZoneApi = async (
zoneData: Zone zoneData: Zone
) => { ) => {
try { try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/UpsertZone`, { const response = await fetch(`${url_Backend_dwinzo}/api/V1/upsertZone`, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: "Bearer <access_token>", Authorization: "Bearer <access_token>",

View File

@@ -51,9 +51,11 @@ type Assets = Asset[];
interface WallAsset { interface WallAsset {
modelUuid: string; modelUuid: string;
modelName: string; modelName: string;
wallAssetType: string;
assetId: string; assetId: string;
wallUuid: string; wallUuid: string;
position: [number, number, number]; position: [number, number, number];
rotation: [number, number, number];
isLocked: boolean; isLocked: boolean;
isVisible: boolean; isVisible: boolean;
opacity: number; opacity: number;