diff --git a/app/src/components/layout/sidebarRight/properties/SelectedWallProperties.tsx b/app/src/components/layout/sidebarRight/properties/SelectedWallProperties.tsx index 517927c..794ede0 100644 --- a/app/src/components/layout/sidebarRight/properties/SelectedWallProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/SelectedWallProperties.tsx @@ -133,11 +133,17 @@ const SelectedWallProperties = () => { diff --git a/app/src/components/layout/sidebarRight/properties/WallProperties.tsx b/app/src/components/layout/sidebarRight/properties/WallProperties.tsx index ae17c1f..e158b44 100644 --- a/app/src/components/layout/sidebarRight/properties/WallProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/WallProperties.tsx @@ -63,11 +63,17 @@ const WallProperties = () => { handleHeightChange(val)} /> handleThicknessChange(val)} /> diff --git a/app/src/modules/builder/Decal/decalInstance.tsx b/app/src/modules/builder/Decal/decalInstance.tsx index dfdd622..1b68f3d 100644 --- a/app/src/modules/builder/Decal/decalInstance.tsx +++ b/app/src/modules/builder/Decal/decalInstance.tsx @@ -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 { 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') { diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index adea2c6..ab9434b 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -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(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. + const state = useThree(); + const plane = useRef(null); + const csgRef = useRef(null); - // Assigning the scene and camera from the Three.js state to the references. - - const plane = useRef(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,29 +81,31 @@ export default function Builder() { fetchVisibility(); }, []); - ////////// Return ////////// + useFrame(() => { + if (csgRef.current) { + csgRef.current.update(); + } + }) return ( <> - + - {/* */} - - + + - + + + + + + + + diff --git a/app/src/modules/builder/wall/Instances/instance/wall.tsx b/app/src/modules/builder/wall/Instances/instance/wall.tsx index 0498b5b..280167d 100644 --- a/app/src/modules/builder/wall/Instances/instance/wall.tsx +++ b/app/src/modules/builder/wall/Instances/instance/wall.tsx @@ -154,7 +154,7 @@ function Wall({ wall }: { readonly wall: Wall }) { {wall.decals.map((decal) => ( - + ))} diff --git a/app/src/modules/builder/wall/Instances/wallInstances.tsx b/app/src/modules/builder/wall/Instances/wallInstances.tsx index 98d00fc..c5e22bf 100644 --- a/app/src/modules/builder/wall/Instances/wallInstances.tsx +++ b/app/src/modules/builder/wall/Instances/wallInstances.tsx @@ -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 && ( <> - - - {walls.map((wall) => ( - - ))} - - + {walls.map((wall) => ( + + ))} {rooms.map((room, index) => ( diff --git a/app/src/modules/builder/wallAsset/Instances/Instances/wallAssetInstance.tsx b/app/src/modules/builder/wallAsset/Instances/Instances/wallAssetInstance.tsx new file mode 100644 index 0000000..6261be6 --- /dev/null +++ b/app/src/modules/builder/wallAsset/Instances/Instances/wallAssetInstance.tsx @@ -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(null); + const [boundingBox, setBoundingBox] = useState(null); + const groupRef = useRef(null); + const csgRef = useRef(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 ( + <> + + + + + + + {gltfScene && ( + { + console.log(wallAsset); + }} + > + + + )} + + + ) +} + +export default WallAssetInstance \ No newline at end of file diff --git a/app/src/modules/builder/wallAsset/Instances/wallAssetInstances.tsx b/app/src/modules/builder/wallAsset/Instances/wallAssetInstances.tsx new file mode 100644 index 0000000..581dfe7 --- /dev/null +++ b/app/src/modules/builder/wallAsset/Instances/wallAssetInstances.tsx @@ -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) => ( + + ))} + + )} + + + ) +} + +export default WallAssetInstances \ No newline at end of file diff --git a/app/src/modules/builder/wallAsset/wallAssetCreator.tsx b/app/src/modules/builder/wallAsset/wallAssetCreator.tsx new file mode 100644 index 0000000..a64d957 --- /dev/null +++ b/app/src/modules/builder/wallAsset/wallAssetCreator.tsx @@ -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 \ No newline at end of file diff --git a/app/src/modules/builder/wallAsset/wallAssetGroup.tsx b/app/src/modules/builder/wallAsset/wallAssetGroup.tsx new file mode 100644 index 0000000..d9ec309 --- /dev/null +++ b/app/src/modules/builder/wallAsset/wallAssetGroup.tsx @@ -0,0 +1,17 @@ +import WallAssetCreator from './wallAssetCreator' +import WallAssetInstances from './Instances/wallAssetInstances' + +function WallAssetGroup() { + + return ( + <> + + + + + + + ) +} + +export default WallAssetGroup \ No newline at end of file diff --git a/app/src/modules/builder/zone/zoneGroup.tsx b/app/src/modules/builder/zone/zoneGroup.tsx index fc9dc7f..46bddb7 100644 --- a/app/src/modules/builder/zone/zoneGroup.tsx +++ b/app/src/modules/builder/zone/zoneGroup.tsx @@ -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 { diff --git a/app/src/modules/scene/environment/ground.tsx b/app/src/modules/scene/environment/ground.tsx index ef51791..f6baeef 100644 --- a/app/src/modules/scene/environment/ground.tsx +++ b/app/src/modules/scene/environment/ground.tsx @@ -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 ( diff --git a/app/src/services/factoryBuilder/zone/upsertZoneApi.ts b/app/src/services/factoryBuilder/zone/upsertZoneApi.ts index ffa79c5..e19eae8 100644 --- a/app/src/services/factoryBuilder/zone/upsertZoneApi.ts +++ b/app/src/services/factoryBuilder/zone/upsertZoneApi.ts @@ -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 ", diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index d346547..de7586a 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -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;