feat: Implement wall asset management features including creation, instances, and rendering; enhance wall properties input validation
This commit is contained in:
@@ -133,11 +133,17 @@ const SelectedWallProperties = () => {
|
||||
<InputWithDropDown
|
||||
label="Height"
|
||||
value={height}
|
||||
min={1}
|
||||
max={25}
|
||||
step={1}
|
||||
onChange={handleHeightChange}
|
||||
/>
|
||||
<InputWithDropDown
|
||||
label="Thickness"
|
||||
value={thickness}
|
||||
min={0.1}
|
||||
max={2}
|
||||
step={0.1}
|
||||
onChange={handleThicknessChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -63,11 +63,17 @@ const WallProperties = () => {
|
||||
<InputWithDropDown
|
||||
label="Height"
|
||||
value={`${wallHeight}`}
|
||||
min={1}
|
||||
max={25}
|
||||
step={1}
|
||||
onChange={(val) => handleHeightChange(val)}
|
||||
/>
|
||||
<InputWithDropDown
|
||||
label="Thickness"
|
||||
value={`${wallThickness}`}
|
||||
min={0.1}
|
||||
max={2}
|
||||
step={0.1}
|
||||
onChange={(val) => handleThicknessChange(val)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useBuilderStore } from '../../../store/builder/useBuilderStore';
|
||||
import defaultMaterial from '../../../assets/textures/floor/wall-tex.png';
|
||||
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 { togglView } = useToggleView();
|
||||
const { activeModule } = useModuleStore();
|
||||
@@ -17,7 +17,7 @@ function DecalInstance({ visible = true, decal }: { visible?: boolean, decal: De
|
||||
<Decal
|
||||
// debug
|
||||
visible={visible}
|
||||
position={decal.decalPosition}
|
||||
position={[decal.decalPosition[0], decal.decalPosition[1], zPosition]}
|
||||
rotation={[0, 0, decal.decalRotation]}
|
||||
scale={[decal.decalScale, decal.decalScale, 0.01]}
|
||||
userData={decal}
|
||||
|
||||
@@ -269,7 +269,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
const onDrop = (event: any) => {
|
||||
const onDrop = (event: DragEvent) => {
|
||||
if (!event.dataTransfer?.files[0]) return;
|
||||
|
||||
if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category !== 'Fenestration') {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { Bvh } from "@react-three/drei";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { Geometry } from "@react-three/csg";
|
||||
|
||||
////////// Zustand State Imports //////////
|
||||
|
||||
@@ -39,16 +39,14 @@ import ZoneGroup from "./zone/zoneGroup";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useBuilderStore } from "../../store/builder/useBuilderStore";
|
||||
import { getUserData } from "../../functions/getUserData";
|
||||
import WallAssetGroup from "./wallAsset/wallAssetGroup";
|
||||
|
||||
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 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 { toggleView } = useToggleView();
|
||||
const { setToolMode } = useToolMode();
|
||||
const { setRoofVisibility } = useRoofVisibility();
|
||||
const { setWallVisibility } = useWallVisibility();
|
||||
@@ -59,14 +57,6 @@ export default function Builder() {
|
||||
const { setHoveredPoint, setHoveredLine } = useBuilderStore();
|
||||
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(() => {
|
||||
if (!toggleView) {
|
||||
setHoveredLine(null);
|
||||
@@ -91,30 +81,32 @@ export default function Builder() {
|
||||
fetchVisibility();
|
||||
}, []);
|
||||
|
||||
////////// Return //////////
|
||||
useFrame(() => {
|
||||
if (csgRef.current) {
|
||||
csgRef.current.update();
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Ground grid={grid} plane={plane} />
|
||||
<Ground plane={plane} />
|
||||
|
||||
<SocketResponses />
|
||||
|
||||
{/* <WallsAndWallItems
|
||||
CSGGroup={CSGGroup}
|
||||
setSelectedItemsIndex={setSelectedItemsIndex}
|
||||
selectedItemsIndex={selectedItemsIndex}
|
||||
currentWallItem={currentWallItem}
|
||||
csg={csg}
|
||||
lines={lines}
|
||||
hoveredDeletableWallItem={hoveredDeletableWallItem}
|
||||
/> */}
|
||||
|
||||
<AssetsGroup plane={plane} />
|
||||
|
||||
<AislesGroup />
|
||||
<mesh name='Walls-And-WallAssets-Group'>
|
||||
<Geometry ref={csgRef} useGroups>
|
||||
|
||||
<WallGroup />
|
||||
|
||||
<WallAssetGroup />
|
||||
|
||||
</Geometry>
|
||||
</mesh>
|
||||
|
||||
<AislesGroup />
|
||||
|
||||
<FloorGroup />
|
||||
|
||||
<ZoneGroup />
|
||||
|
||||
@@ -154,7 +154,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||
<MeshDiscardMaterial />
|
||||
|
||||
{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>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { DoubleSide, RepeatWrapping, Shape, SRGBColorSpace, TextureLoader, Vector2, Vector3 } from 'three';
|
||||
import { Geometry } from '@react-three/csg';
|
||||
import { Html, Extrude } from '@react-three/drei';
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
import { useSceneContext } from '../../../scene/sceneContext';
|
||||
@@ -44,13 +43,9 @@ function WallInstances() {
|
||||
<>
|
||||
{!toggleView && walls.length > 1 && (
|
||||
<>
|
||||
<mesh name='Walls-Group'>
|
||||
<Geometry useGroups>
|
||||
{walls.map((wall) => (
|
||||
<WallInstance key={wall.wallUuid} wall={wall} />
|
||||
))}
|
||||
</Geometry>
|
||||
</mesh>
|
||||
|
||||
<group name='Wall-Floors-Group'>
|
||||
{rooms.map((room, index) => (
|
||||
|
||||
@@ -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
|
||||
@@ -29,7 +29,6 @@ function ZoneGroup() {
|
||||
useEffect(() => {
|
||||
if (projectId && selectedVersion) {
|
||||
getZonesApi(projectId, selectedVersion?.versionId || '').then((zones) => {
|
||||
console.log('zones: ', zones);
|
||||
if (zones && zones.length > 0) {
|
||||
setZones(zones);
|
||||
} else {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useTileDistance, useToggleView } from "../../../store/builder/store";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
|
||||
const Ground = ({ grid, plane }: any) => {
|
||||
const Ground = ({ plane }: any) => {
|
||||
const { toggleView } = useToggleView();
|
||||
const { planeValue, gridValue } = useTileDistance();
|
||||
|
||||
return (
|
||||
<mesh name="Ground">
|
||||
<mesh
|
||||
ref={grid}
|
||||
name="Grid"
|
||||
position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,7 @@ export const upsertZoneApi = async (
|
||||
zoneData: Zone
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(`${url_Backend_dwinzo}/api/V1/UpsertZone`, {
|
||||
const response = await fetch(`${url_Backend_dwinzo}/api/V1/upsertZone`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: "Bearer <access_token>",
|
||||
|
||||
2
app/src/types/builderTypes.d.ts
vendored
2
app/src/types/builderTypes.d.ts
vendored
@@ -51,9 +51,11 @@ type Assets = Asset[];
|
||||
interface WallAsset {
|
||||
modelUuid: string;
|
||||
modelName: string;
|
||||
wallAssetType: string;
|
||||
assetId: string;
|
||||
wallUuid: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
isLocked: boolean;
|
||||
isVisible: boolean;
|
||||
opacity: number;
|
||||
|
||||
Reference in New Issue
Block a user