added decal dropping to scene, decal movement from one wall to another wall, one floor to another floor, one wall to floor and one floor to wall

This commit is contained in:
2025-08-28 15:50:45 +05:30
parent d129a86885
commit 8ff609b85c
10 changed files with 568 additions and 296 deletions

View File

@@ -1,22 +1,129 @@
import { MathUtils } from 'three';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useThree } from '@react-three/fiber'; import { useThree } from '@react-three/fiber';
import { useDroppedDecal } from '../../../../store/builder/store'; import { useParams } from 'react-router-dom';
import { useDroppedDecal, useSocketStore } from '../../../../store/builder/store';
import useModuleStore from '../../../../store/useModuleStore'; import useModuleStore from '../../../../store/useModuleStore';
import { useSceneContext } from '../../../scene/sceneContext';
import { useVersionContext } from '../../version/versionContext';
import { getUserData } from '../../../../functions/getUserData';
// import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi';
// import { upsertFloorApi } from '../../../../services/factoryBuilder/floor/upsertFloorApi';
function DecalCreator() { function DecalCreator() {
const { wallStore, floorStore } = useSceneContext();
const { addDecal: addDecalOnWall, getWallById } = wallStore();
const { addDecal : addDecalOnFloor, getFloorById } = floorStore();
const { droppedDecal } = useDroppedDecal(); const { droppedDecal } = useDroppedDecal();
const { activeModule } = useModuleStore(); const { activeModule } = useModuleStore();
const { userId, organization } = getUserData();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
const { socket } = useSocketStore();
const { controls, gl, pointer, camera, raycaster, scene } = useThree(); const { controls, gl, pointer, camera, raycaster, scene } = useThree();
useEffect(() => { useEffect(() => {
const canvasElement = gl.domElement; const canvasElement = gl.domElement;
const onDrop = (event: DragEvent) => { const onDrop = (event: DragEvent) => {
console.log('event: ', event);
console.log('droppedDecal: ', droppedDecal);
if (!event.dataTransfer?.files[0]) return;
if (droppedDecal) { if (droppedDecal) {
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 wallIntersect = intersects.find(i => i.object.userData && i.object.userData.wallUuid);
const floorIntersect = intersects.find(i => i.object.userData && i.object.userData.floorUuid);
if (wallIntersect) {
const wall = getWallById(wallIntersect.object.userData.wallUuid);
if (!wall) return;
const point = wallIntersect.object.worldToLocal(wallIntersect.point.clone());
const decal: Decal = {
decalUuid: MathUtils.generateUUID(),
decalName: droppedDecal.decalName,
decalId: droppedDecal.decalId,
decalType: {
type: 'Wall',
wallUuid: wallIntersect.object.userData.wallUuid,
},
decalPosition: [point.x, point.y, wall.wallThickness / 2 + 0.001],
decalRotation: 0,
decalOpacity: 1,
decalScale: 1,
}
addDecalOnWall(wallIntersect.object.userData.wallUuid, decal);
setTimeout(() => {
const updatedWall = getWallById(wallIntersect.object.userData.wallUuid);
if (updatedWall) {
if (projectId && updatedWall) {
// API
// upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall);
// SOCKET
const data = {
wallData: updatedWall,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Wall:add', data);
}
}
}, 0)
} else if (floorIntersect) {
const floor = getFloorById(floorIntersect.object.userData.floorUuid);
if (!floor) return;
const point = floorIntersect.object.worldToLocal(floorIntersect.point.clone());
const decal: Decal = {
decalUuid: MathUtils.generateUUID(),
decalName: droppedDecal.decalName,
decalId: droppedDecal.decalId,
decalType: {
type: 'Floor',
floorUuid: floorIntersect.object.userData.floorUuid,
},
decalPosition: [point.x, point.y, -0.001],
decalRotation: 0,
decalOpacity: 1,
decalScale: 1,
}
addDecalOnFloor(floorIntersect.object.userData.floorUuid, decal);
setTimeout(() => {
const updatedFloor = getFloorById(floorIntersect.object.userData.floorUuid);
if (projectId && updatedFloor) {
// API
// upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedFloor);
// SOCKET
const data = {
floorData: updatedFloor,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Floor:add', data);
}
}, 0)
}
} }
}; };

View File

@@ -1,40 +1,38 @@
import * as THREE from 'three'; import * as THREE from 'three';
import { CameraControls, Decal } from '@react-three/drei' import { Decal } from '@react-three/drei'
import { useThree } from '@react-three/fiber'; import { useToggleView, useToolMode } from '../../../../store/builder/store';
import { useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
import { useBuilderStore } from '../../../../store/builder/useBuilderStore'; import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
import { retrieveImage, storeImage } from '../../../../utils/indexDB/idbUtils'; import { retrieveImage, storeImage } from '../../../../utils/indexDB/idbUtils';
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';
import { useSceneContext } from '../../../scene/sceneContext';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { getUserData } from '../../../../functions/getUserData'; import { useDecalEventHandlers } from '../eventHandler/useDecalEventHandlers';
import { useVersionContext } from '../../version/versionContext';
import { useParams } from 'react-router-dom';
// import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi'; // import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi';
// import { upsertFloorApi } from '../../../../services/factoryBuilder/floor/upsertFloorApi'; // import { upsertFloorApi } from '../../../../services/factoryBuilder/floor/upsertFloorApi';
function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalPosition[2] }: { parent: Wall | Floor; visible?: boolean, decal: Decal, zPosition?: number }) { function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalPosition[2] }: { parent: Wall | Floor; visible?: boolean, decal: Decal, zPosition?: number }) {
const { setSelectedWall, setSelectedFloor, selectedDecal, deletableDecal, setSelectedDecal, setDeletableDecal } = useBuilderStore(); const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const { wallStore, floorStore } = useSceneContext(); const { selectedDecal, deletableDecal, setSelectedDecal, setDeletableDecal } = useBuilderStore();
const { removeDecal: removeDecalInWall, updateDecalPosition: updateDecalPositionInWall, getWallById } = wallStore();
const { removeDecal: removeDecalInFloor } = floorStore();
const { toolMode } = useToolMode(); const { toolMode } = useToolMode();
const { toggleView } = useToggleView(); const { toggleView } = useToggleView();
const { activeModule } = useModuleStore(); const { activeModule } = useModuleStore();
const { userId, organization } = getUserData(); const decalRef = useRef<any>(null);
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore(); useEffect(() => {
const { projectId } = useParams(); if (selectedDecal?.decalData.decalUuid === decal.decalUuid && !selectedDecal.decalMesh) {
const { socket } = useSocketStore(); setSelectedDecal({ decalData: selectedDecal.decalData, decalMesh: decalRef.current });
const { raycaster, pointer, camera, scene, gl, controls } = useThree(); }
const isDraggingRef = useRef(false); }, [selectedDecal])
const dragOffsetRef = useRef<THREE.Vector3 | null>(null);
const { handlePointerMissed, handlePointerLeave, handleClick, handlePointerDown, handlePointerEnter } = useDecalEventHandlers({ parent, decal, visible });
const [texture, setTexture] = useState<THREE.Texture | null>(null); const [texture, setTexture] = useState<THREE.Texture | null>(null);
const [isLoading, setIsLoading] = useState(true);
const logDecalStatus = (decalId: string, status: string) => {
// console.log(decalId, status);
}
const loadDefaultTexture = () => { const loadDefaultTexture = () => {
const textureLoader = new THREE.TextureLoader(); const textureLoader = new THREE.TextureLoader();
@@ -43,24 +41,22 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
(fallbackTex) => { (fallbackTex) => {
fallbackTex.name = "default-decal"; fallbackTex.name = "default-decal";
setTexture(fallbackTex); setTexture(fallbackTex);
setIsLoading(false); logDecalStatus(decal.decalId, 'default-loaded');
}, },
undefined, undefined,
(error) => { (error) => {
console.error("Error loading default decal texture:", error); console.error("Error loading default decal texture:", error);
setIsLoading(false);
} }
); );
}; };
const loadDecalTexture = async (decalId: string) => { const loadDecalTexture = async (decalId: string) => {
setIsLoading(true);
try { try {
const cachedTexture = THREE.Cache.get(decalId); const cachedTexture = THREE.Cache.get(decalId);
if (cachedTexture) { if (cachedTexture) {
setTexture(cachedTexture); setTexture(cachedTexture);
setIsLoading(false); logDecalStatus(decalId, 'cache-loaded');
return; return;
} }
@@ -75,7 +71,7 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
tex.name = decalId; tex.name = decalId;
THREE.Cache.add(decalId, tex); THREE.Cache.add(decalId, tex);
setTexture(tex); setTexture(tex);
setIsLoading(false); logDecalStatus(decalId, 'indexedDB-loaded');
}, },
undefined, undefined,
(error) => { (error) => {
@@ -95,8 +91,8 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
}; };
const loadFromBackend = (decalId: string) => { const loadFromBackend = (decalId: string) => {
console.log('decalId: ', decalId);
const textureUrl = `${process.env.REACT_APP_SERVER_MARKETPLACE_URL}/api/v2/DecalFile/${decalId}`; const textureUrl = `${url_Backend_dwinzo}/api/v1/DecalImage/${decalId}`;
const textureLoader = new THREE.TextureLoader(); const textureLoader = new THREE.TextureLoader();
textureLoader.load( textureLoader.load(
@@ -105,7 +101,7 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
tex.name = decalId; tex.name = decalId;
THREE.Cache.add(decalId, tex); THREE.Cache.add(decalId, tex);
setTexture(tex); setTexture(tex);
setIsLoading(false); logDecalStatus(decalId, 'backend-loaded');
try { try {
const response = await fetch(textureUrl); const response = await fetch(textureUrl);
@@ -145,183 +141,20 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
} }
}, [toggleView, toolMode, activeModule, selectedDecal, deletableDecal]); }, [toggleView, toolMode, activeModule, selectedDecal, deletableDecal]);
useEffect(() => {
const canvasElement = gl.domElement;
const handlePointerMove = (e: PointerEvent) => {
if (!isDraggingRef.current || !selectedDecal || selectedDecal.decalData.decalUuid !== decal.decalUuid) return;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
const wallIntersect = intersects.find(i => i.object.userData && 'wallUuid' in parent && i.object.userData.wallUuid === parent.wallUuid);
if (wallIntersect) {
const point = wallIntersect.object.worldToLocal(wallIntersect.point.clone());
let offset = dragOffsetRef.current || new THREE.Vector3(0, 0, 0);
updateDecalPositionInWall(decal.decalUuid, [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]]);
}
};
const handlePointerUp = (e: PointerEvent) => {
if (controls) {
(controls as CameraControls).enabled = true;
}
if (isDraggingRef.current) {
isDraggingRef.current = false;
dragOffsetRef.current = null;
if ('wallUuid' in parent) {
setTimeout(() => {
const updatedWall = getWallById(parent.wallUuid);
if (updatedWall) {
if (projectId && updatedWall) {
// API
// upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall);
// SOCKET
const data = {
wallData: updatedWall,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Wall:add', data);
}
}
}, 0)
}
}
};
if (activeModule === 'builder' && !toggleView) {
canvasElement.addEventListener('pointermove', handlePointerMove);
canvasElement.addEventListener('pointerup', handlePointerUp);
}
return () => {
canvasElement.removeEventListener('pointermove', handlePointerMove);
canvasElement.removeEventListener('pointerup', handlePointerUp);
};
}, [gl, camera, scene, raycaster, selectedDecal, decal, parent, activeModule, toggleView, projectId, selectedVersion, userId, organization, socket]);
const deleteDecal = (decalUuid: string, parent: Wall | Floor) => {
if ('wallUuid' in parent) {
const updatedWall = removeDecalInWall(decalUuid);
if (projectId && updatedWall) {
// API
// upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall);
// SOCKET
const data = {
wallData: updatedWall,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Wall:add', data);
}
} else if ('floorUuid' in parent) {
const updatedFloor = removeDecalInFloor(decalUuid);
if (projectId && updatedFloor) {
// API
// upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedFloor);
// SOCKET
const data = {
floorData: updatedFloor,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Floor:add', data);
}
}
}
return ( return (
<Decal <Decal
// debug // debug
visible={visible} visible={visible}
ref={decalRef}
position={[decal.decalPosition[0], decal.decalPosition[1], zPosition]} position={[decal.decalPosition[0], decal.decalPosition[1], zPosition]}
rotation={[0, 0, decal.decalRotation * (Math.PI / 180)]} rotation={[0, 0, decal.decalRotation * (Math.PI / 180)]}
scale={[decal.decalScale, decal.decalScale, 0.01]} scale={[(decal.decalType.type === 'Floor' || zPosition < 0) ? -decal.decalScale : decal.decalScale, decal.decalScale, 0.01]}
userData={decal} userData={decal}
onPointerDown={(e) => { onPointerDown={(e) => { if (e.button === 0) handlePointerDown(e) }}
if (visible && !toggleView && activeModule === 'builder') { onClick={(e) => { handleClick(e) }}
if (e.object.userData.decalUuid && toolMode === 'cursor') { onPointerEnter={(e) => { handlePointerEnter(e) }}
e.stopPropagation(); onPointerLeave={(e) => { handlePointerLeave(e) }}
isDraggingRef.current = true; onPointerMissed={() => handlePointerMissed()}
if (controls) {
(controls as CameraControls).enabled = false;
}
setSelectedDecal({ decalMesh: e.object, decalData: decal });
setSelectedWall(null);
setSelectedFloor(null);
const localIntersect = e.object.worldToLocal(e.point.clone());
dragOffsetRef.current = new THREE.Vector3(decal.decalPosition[0] - localIntersect.x, decal.decalPosition[1] - localIntersect.y, 0);
}
}
}}
onClick={(e) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === 'cursor') {
setSelectedDecal({ decalMesh: e.object, decalData: decal });
setSelectedWall(null);
setSelectedFloor(null);
} else if (toolMode === '3D-Delete') {
deleteDecal(e.object.userData.decalUuid, parent);
}
}
}
}}
onPointerEnter={(e) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === '3D-Delete') {
setDeletableDecal(e.object);
}
}
}
}}
onPointerLeave={(e) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === '3D-Delete' && deletableDecal && deletableDecal?.userData.decalUuid === e.object.userData.decalUuid) {
setDeletableDecal(null);
}
}
}
}}
onPointerMissed={() => {
if (selectedDecal && selectedDecal.decalMesh.userData.decalUuid === decal.decalUuid) {
setSelectedDecal(null);
}
}}
> >
<meshBasicMaterial <meshBasicMaterial
map={texture} map={texture}

View File

@@ -0,0 +1,301 @@
import * as THREE from 'three';
import { CameraControls } from '@react-three/drei';
import { ThreeEvent, useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef } from 'react';
import { useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
import useModuleStore from '../../../../store/useModuleStore';
import { getUserData } from '../../../../functions/getUserData';
import { useVersionContext } from '../../version/versionContext';
import { useParams } from 'react-router-dom';
import { useSceneContext } from '../../../scene/sceneContext';
// import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi';
// import { upsertFloorApi } from '../../../../services/factoryBuilder/floor/upsertFloorApi';
export function useDecalEventHandlers({
parent,
decal,
visible,
}: {
parent: Wall | Floor;
decal: Decal;
visible: boolean;
}) {
const { wallStore, floorStore } = useSceneContext();
const { removeDecal: removeDecalInWall, updateDecalPosition: updateDecalPositionInWall, getWallById, addDecal: addDecalToWall } = wallStore();
const { removeDecal: removeDecalInFloor, updateDecalPosition: updateDecalPositionInFloor, getFloorById, addDecal: addDecalToFloor } = floorStore();
const { setSelectedWall, setSelectedFloor, setSelectedDecal, setDeletableDecal, deletableDecal, selectedDecal, setDecalDragState, decalDragState } = useBuilderStore();
const { toolMode } = useToolMode();
const { toggleView } = useToggleView();
const { activeModule } = useModuleStore();
const { userId, organization } = getUserData();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
const { socket } = useSocketStore();
const { raycaster, pointer, camera, scene, gl, controls } = useThree();
useFrame(() => {
if (activeModule !== 'builder' || toggleView || !decalDragState.isDragging || !selectedDecal || selectedDecal.decalData.decalUuid !== decal.decalUuid) return;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
const wallIntersect = intersects.find(i => i.object.userData?.wallUuid);
const floorIntersect = intersects.find(i => i.object.userData?.floorUuid);
let offset = decalDragState.dragOffset || new THREE.Vector3(0, 0, 0);
if (wallIntersect) {
const wallUuid = wallIntersect.object.userData.wallUuid;
const point = wallIntersect.object.worldToLocal(wallIntersect.point.clone());
if ("wallUuid" in parent && parent.wallUuid === wallUuid && decal.decalType.type === 'Wall') {
updateDecalPositionInWall(decal.decalUuid, [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]]);
} else if (decal.decalType.type === 'Wall' && wallUuid) {
deleteDecal(decal.decalUuid, parent);
const addedDecal = addDecalToWall(wallUuid, {
...decal,
decalPosition: [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]],
decalType: { type: 'Wall', wallUuid: wallUuid }
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal })
}
} else if (decal.decalType.type === 'Floor' && wallUuid) {
deleteDecal(decal.decalUuid, parent);
const wall = getWallById(wallUuid);
if (!wall) return;
const addedDecal = addDecalToWall(wallUuid, {
...decal,
decalPosition: [point.x + offset.x, point.y + offset.y, wall.wallThickness / 2 + 0.001],
decalType: { type: 'Wall', wallUuid: wallUuid }
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal })
}
}
} else if (floorIntersect) {
const floorUuid = floorIntersect.object.userData.floorUuid;
const point = floorIntersect.object.worldToLocal(floorIntersect.point.clone());
if ("floorUuid" in parent && parent.floorUuid === floorUuid && decal.decalType.type === 'Floor') {
updateDecalPositionInFloor(decal.decalUuid, [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]]);
} else if (decal.decalType.type === 'Floor' && floorUuid) {
deleteDecal(decal.decalUuid, parent);
const addedDecal = addDecalToFloor(floorUuid, {
...decal,
decalPosition: [point.x + offset.x, point.y + offset.y, decal.decalPosition[2]],
decalType: { type: 'Floor', floorUuid: floorUuid }
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal })
}
} else if (decal.decalType.type === 'Wall' && floorUuid) {
deleteDecal(decal.decalUuid, parent);
const floor = getFloorById(floorUuid);
if (!floor) return;
const addedDecal = addDecalToFloor(floorUuid, {
...decal,
decalPosition: [point.x + offset.x, point.y + offset.y, -0.001],
decalType: { type: 'Floor', floorUuid: floorUuid }
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal })
}
}
}
});
const handlePointerUp = (e: PointerEvent) => {
if (controls) {
(controls as CameraControls).enabled = true;
}
if (decalDragState.isDragging) {
setDecalDragState(false, null, null);
if ('wallUuid' in parent) {
setTimeout(() => {
const updatedWall = getWallById(parent.wallUuid);
if (updatedWall) {
if (projectId && updatedWall) {
// API
// upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall);
// SOCKET
const data = {
wallData: updatedWall,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Wall:add', data);
}
}
}, 0)
} else if ('floorUuid' in parent) {
setTimeout(() => {
const updatedFloor = parent;
if (projectId && updatedFloor) {
// API
// upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedFloor);
// SOCKET
const data = {
floorData: updatedFloor,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Floor:add', data);
}
}, 0)
}
}
};
const deleteDecal = (decalUuid: string, parent: Wall | Floor) => {
if ('wallUuid' in parent) {
const updatedWall = removeDecalInWall(decalUuid);
if (projectId && updatedWall) {
// API
// upsertWallApi(projectId, selectedVersion?.versionId || '', updatedWall);
// SOCKET
const data = {
wallData: updatedWall,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Wall:add', data);
}
} else if ('floorUuid' in parent) {
const updatedFloor = removeDecalInFloor(decalUuid);
if (projectId && updatedFloor) {
// API
// upsertFloorApi(projectId, selectedVersion?.versionId || '', updatedFloor);
// SOCKET
const data = {
floorData: updatedFloor,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:model-Floor:add', data);
}
}
}
const handlePointerDown = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid && toolMode === 'cursor') {
e.stopPropagation();
setDecalDragState(true, decal.decalUuid, null);
if (controls) {
(controls as CameraControls).enabled = false;
}
setSelectedDecal({ decalMesh: e.object, decalData: decal });
setSelectedWall(null);
setSelectedFloor(null);
const localIntersect = e.object.worldToLocal(e.point.clone());
let dragOffset = new THREE.Vector3(decal.decalPosition[0] - localIntersect.x, decal.decalPosition[1] - localIntersect.y, 0);
setDecalDragState(true, decal.decalUuid, dragOffset);
}
}
};
const handleClick = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === 'cursor') {
setSelectedDecal({ decalMesh: e.object, decalData: decal });
setSelectedWall(null);
setSelectedFloor(null);
} else if (toolMode === '3D-Delete') {
deleteDecal(e.object.userData.decalUuid, parent);
}
}
}
};
const handlePointerEnter = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === '3D-Delete') {
setDeletableDecal(e.object);
}
}
}
};
const handlePointerLeave = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === '3D-Delete' && deletableDecal && deletableDecal?.userData.decalUuid === e.object.userData.decalUuid) {
setDeletableDecal(null);
}
}
}
};
const handlePointerMissed = () => {
if (selectedDecal && selectedDecal.decalMesh && selectedDecal.decalMesh.userData.decalUuid === decal.decalUuid) {
setSelectedDecal(null);
}
};
useEffect(() => {
const canvasElement = gl.domElement;
if (activeModule === 'builder' && !toggleView && selectedDecal && selectedDecal.decalData.decalUuid === decal.decalUuid) {
canvasElement.addEventListener('pointerup', handlePointerUp);
}
return () => {
canvasElement.removeEventListener('pointerup', handlePointerUp);
};
}, [gl, activeModule, toggleView, selectedDecal, camera, controls, visible, parent, decal, decalDragState]);
return {
handlePointerDown,
handleClick,
handlePointerEnter,
handlePointerLeave,
handlePointerMissed,
deleteDecal
};
}

View File

@@ -88,6 +88,10 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
} }
}, [gltfScene]); }, [gltfScene]);
const logModelStatus = (modelId: string, status: string) => {
// console.log(modelId, status);
}
useEffect(() => { useEffect(() => {
// Calculate Bounding Box // Calculate Bounding Box
const calculateBoundingBox = (scene: THREE.Object3D) => { const calculateBoundingBox = (scene: THREE.Object3D) => {
@@ -103,6 +107,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
clone.animations = cachedModel.animations || []; clone.animations = cachedModel.animations || [];
setGltfScene(clone); setGltfScene(clone);
calculateBoundingBox(clone); calculateBoundingBox(clone);
logModelStatus(assetId, 'cache-loaded');
return; return;
} }
@@ -118,6 +123,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
THREE.Cache.add(assetId, gltf); THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone()); setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene); calculateBoundingBox(gltf.scene);
logModelStatus(assetId, 'indexedDB-loaded');
}, },
undefined, undefined,
(error) => { (error) => {
@@ -140,6 +146,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
THREE.Cache.add(assetId, gltf); THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone()); setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene); calculateBoundingBox(gltf.scene);
logModelStatus(assetId, 'backend-loaded');
}) })
.catch((error) => { .catch((error) => {
console.error( console.error(

View File

@@ -1,12 +1,13 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Shape, Vector2, DoubleSide, TextureLoader, RepeatWrapping, SRGBColorSpace, NoColorSpace, } from "three"; import { Shape, Vector2, DoubleSide, TextureLoader, RepeatWrapping, SRGBColorSpace, NoColorSpace, ExtrudeGeometry, Vector3, Euler, } from "three";
import { useLoader } from "@react-three/fiber"; import { useLoader } from "@react-three/fiber";
import { Extrude } from "@react-three/drei";
import useModuleStore from "../../../../../store/useModuleStore"; import useModuleStore from "../../../../../store/useModuleStore";
import { useBuilderStore } from "../../../../../store/builder/useBuilderStore"; import { useBuilderStore } from "../../../../../store/builder/useBuilderStore";
import { useToggleView } from "../../../../../store/builder/store"; import { useToggleView } from "../../../../../store/builder/store";
import * as Constants from "../../../../../types/world/worldConstants"; import * as Constants from "../../../../../types/world/worldConstants";
import DecalInstance from "../../../Decal/decalInstance/decalInstance";
import texturePath from "../../../../../assets/textures/floor/white.png"; import texturePath from "../../../../../assets/textures/floor/white.png";
import texturePathDark from "../../../../../assets/textures/floor/black.png"; import texturePathDark from "../../../../../assets/textures/floor/black.png";
import material1 from "../../../../../assets/textures/floor/factory wall texture.jpg"; import material1 from "../../../../../assets/textures/floor/factory wall texture.jpg";
@@ -67,20 +68,26 @@ function FloorInstance({ floor }: { floor: Floor }) {
}, },
}; };
const shape = useMemo(() => { const shapeData = useMemo(() => {
const shape = new Shape();
const points = floor.points.map((p) => new Vector2(p.position[0], p.position[2])); const points = floor.points.map((p) => new Vector2(p.position[0], p.position[2]));
if (points.length < 3) return null; if (points.length < 3) return null;
shape.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) { const centroidX = points.reduce((sum, p) => sum + p.x, 0) / points.length;
shape.lineTo(points[i].x, points[i].y); const centroidY = points.reduce((sum, p) => sum + p.y, 0) / points.length;
const relativePoints = points.map((p) => new Vector2(p.x - centroidX, p.y - centroidY));
const shape = new Shape();
shape.moveTo(relativePoints[0].x, relativePoints[0].y);
for (let i = 1; i < relativePoints.length; i++) {
shape.lineTo(relativePoints[i].x, relativePoints[i].y);
} }
return shape;
return { shape, center: [centroidX, centroidY] };
}, [floor]); }, [floor]);
const textureScale = Constants.floorConfig.textureScale; const textureScale = Constants.floorConfig.textureScale;
// Helper function to handle texture maps and filter out null values
function getMaterialMaps(material: any, defaultMap: any) { function getMaterialMaps(material: any, defaultMap: any) {
const materialMap = material.map || defaultMap; const materialMap = material.map || defaultMap;
const normalMap = material.normalMap || null; const normalMap = material.normalMap || null;
@@ -90,26 +97,18 @@ function FloorInstance({ floor }: { floor: Floor }) {
return [materialMap, normalMap, roughnessMap, metalnessMap].filter((texture): texture is string => texture !== null); return [materialMap, normalMap, roughnessMap, metalnessMap].filter((texture): texture is string => texture !== null);
} }
// Default material map
const defaultMaterialMap = materials["Default Material"].map; const defaultMaterialMap = materials["Default Material"].map;
// Get top and side material maps
const topMaterial = materials[floor.topMaterial]; const topMaterial = materials[floor.topMaterial];
const sideMaterial = materials[floor.sideMaterial]; const sideMaterial = materials[floor.sideMaterial];
// Get the filtered lists for top and side textures
const topTexturesList = getMaterialMaps(topMaterial, defaultMaterialMap); const topTexturesList = getMaterialMaps(topMaterial, defaultMaterialMap);
const sideTexturesList = getMaterialMaps(sideMaterial, defaultMaterialMap); const sideTexturesList = getMaterialMaps(sideMaterial, defaultMaterialMap);
// Use loader to load top and side textures
const [topTexture, topNormalTexture, topRoughnessTexture, topMetalicTexture] = useLoader(TextureLoader, topTexturesList); const [topTexture, topNormalTexture, topRoughnessTexture, topMetalicTexture] = useLoader(TextureLoader, topTexturesList);
const [sideTexture, sideNormalTexture, sideRoughnessTexture, sideMetalicTexture] = useLoader(TextureLoader, sideTexturesList); const [sideTexture, sideNormalTexture, sideRoughnessTexture, sideMetalicTexture] = useLoader(TextureLoader, sideTexturesList);
// Early exit if materials are missing
if (!materials[floor.topMaterial] || !materials[floor.sideMaterial]) return null;
// Combine and pair textures with their corresponding material
const textureMaterialMap = [ const textureMaterialMap = [
{ {
textures: [ textures: [
@@ -131,7 +130,6 @@ function FloorInstance({ floor }: { floor: Floor }) {
}, },
]; ];
// Apply texture settings
textureMaterialMap.forEach(({ textures, materialKey }) => { textureMaterialMap.forEach(({ textures, materialKey }) => {
const tileScale = materials[materialKey]?.textureTileScale ?? [ const tileScale = materials[materialKey]?.textureTileScale ?? [
textureScale, textureScale,
@@ -143,20 +141,36 @@ function FloorInstance({ floor }: { floor: Floor }) {
tex.wrapS = tex.wrapT = RepeatWrapping; tex.wrapS = tex.wrapT = RepeatWrapping;
tex.repeat.set(tileScale[0], tileScale[1]); tex.repeat.set(tileScale[0], tileScale[1]);
tex.anisotropy = 16; tex.anisotropy = 16;
// First texture is always the color map (use SRGB), others should be linear
tex.colorSpace = idx < 1 ? SRGBColorSpace : NoColorSpace; tex.colorSpace = idx < 1 ? SRGBColorSpace : NoColorSpace;
}); });
}); });
if (!shape) return null; const geometry = useMemo(() => {
if (!shapeData) return null;
return new ExtrudeGeometry(shapeData.shape, {
depth: !floor.isBeveled ? floor.floorDepth : floor.floorDepth - 0.1,
bevelEnabled: floor.isBeveled,
bevelSegments: floor.bevelStrength,
bevelOffset: -0.1,
bevelSize: 0.1,
bevelThickness: 0.1,
});
}, [shapeData, floor]);
if (!geometry) return null;
return ( return (
<mesh <mesh
castShadow castShadow
receiveShadow receiveShadow
geometry={geometry}
name={`Floor-${floor.floorUuid}`} name={`Floor-${floor.floorUuid}`}
rotation={[Math.PI / 2, 0, 0]} rotation={[Math.PI / 2, 0, 0]}
position={[0, !floor.isBeveled ? floor.floorDepth - 0.1 : floor.floorDepth - 0.2, 0,]} position={[
shapeData?.center[0] ?? 0,
!floor.isBeveled ? floor.floorDepth - 0.1 : floor.floorDepth - 0.2,
shapeData?.center[1] ?? 0,
]}
userData={floor} userData={floor}
onDoubleClick={(e) => { onDoubleClick={(e) => {
if (!toggleView && activeModule === "builder") { if (!toggleView && activeModule === "builder") {
@@ -172,20 +186,6 @@ function FloorInstance({ floor }: { floor: Floor }) {
setSelectedFloor(null); setSelectedFloor(null);
} }
}} }}
>
<Extrude
name={`Floor-${floor.floorUuid}`}
args={[shape,
{
depth: !floor.isBeveled ? floor.floorDepth : floor.floorDepth - 0.1,
bevelEnabled: floor.isBeveled,
bevelSegments: floor.bevelStrength,
bevelOffset: -0.1,
bevelSize: 0.1,
bevelThickness: 0.1,
},
]}
userData={floor}
> >
<meshPhysicalMaterial <meshPhysicalMaterial
attach="material-0" attach="material-0"
@@ -205,9 +205,14 @@ function FloorInstance({ floor }: { floor: Floor }) {
roughnessMap={sideRoughnessTexture?.clone()} roughnessMap={sideRoughnessTexture?.clone()}
metalnessMap={sideMetalicTexture?.clone()} metalnessMap={sideMetalicTexture?.clone()}
normalMap={sideNormalTexture?.clone()} normalMap={sideNormalTexture?.clone()}
roughness={1.5}
metalness={1.0}
side={DoubleSide} side={DoubleSide}
/> />
</Extrude>
{floor.decals.map((decal) => (
<DecalInstance parent={floor} key={decal.decalUuid} decal={decal} />
))}
</mesh> </mesh>
); );
} }

View File

@@ -17,7 +17,7 @@ import material1 from '../../../../../assets/textures/floor/factory wall texture
function Wall({ wall }: { readonly wall: Wall }) { function Wall({ wall }: { readonly wall: Wall }) {
const { wallStore, wallAssetStore } = useSceneContext(); const { wallStore, wallAssetStore } = useSceneContext();
const { walls, addDecal } = wallStore(); const { walls } = wallStore();
const { wallAssets, getWallAssetsByWall, setVisibility } = wallAssetStore(); const { wallAssets, getWallAssetsByWall, setVisibility } = wallAssetStore();
const assets = getWallAssetsByWall(wall.wallUuid); const assets = getWallAssetsByWall(wall.wallUuid);
const { selectedWall, setSelectedWall, setSelectedDecal } = useBuilderStore(); const { selectedWall, setSelectedWall, setSelectedDecal } = useBuilderStore();
@@ -118,6 +118,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
> >
{(assets.length > 0 || (walls[0].wallUuid === wall.wallUuid && wallAssets.length > 0)) ? {(assets.length > 0 || (walls[0].wallUuid === wall.wallUuid && wallAssets.length > 0)) ?
<Base <Base
name={`BaseWall${wall.wallUuid}`}
castShadow castShadow
receiveShadow receiveShadow
ref={meshRef} ref={meshRef}
@@ -159,19 +160,6 @@ function Wall({ wall }: { readonly wall: Wall }) {
e.stopPropagation(); e.stopPropagation();
setSelectedWall(e.object); setSelectedWall(e.object);
setSelectedDecal(null); setSelectedDecal(null);
if (wall.decals.length > 0) return;
const decal: Decal = {
decalUuid: THREE.MathUtils.generateUUID(),
decalName: 'Decal',
decalId: "68abe2f771863f0888b0d35d",
decalPosition: [0, 0, wall.wallThickness / 2 + 0.001],
decalRotation: 0,
decalOpacity: 1,
decalScale: 1,
decalType: { type: 'Wall', wallUuid: wall.wallUuid }
}
addDecal(wall.wallUuid, decal);
} }
} }
}} }}

View File

@@ -165,7 +165,7 @@ export default function PostProcessing() {
)} )}
{selectedDecal && ( {selectedDecal && (
<Outline <Outline
selection={selectedDecal.decalMesh} selection={selectedDecal.decalMesh || undefined}
selectionLayer={10} selectionLayer={10}
width={2000} width={2000}
blendFunction={BlendFunction.ALPHA} blendFunction={BlendFunction.ALPHA}

View File

@@ -1,4 +1,4 @@
import { Object3D } from 'three'; import { Object3D, Vector3 } from 'three';
import { create } from 'zustand'; import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer'; import { immer } from 'zustand/middleware/immer';
@@ -38,8 +38,13 @@ interface BuilderState {
zoneColor: string; zoneColor: string;
// Decal Settings // Decal Settings
selectedDecal: { decalMesh: Object3D, decalData: Decal } | null; selectedDecal: { decalMesh: Object3D | null, decalData: Decal } | null;
deletableDecal: Object3D | null; deletableDecal: Object3D | null;
decalDragState: {
isDragging: boolean,
draggingDecalUuid: string | null,
dragOffset: Vector3 | null,
},
// Aisle General // Aisle General
selectedAisle: Object3D | null; selectedAisle: Object3D | null;
@@ -87,8 +92,9 @@ interface BuilderState {
setZoneColor: (color: string) => void; setZoneColor: (color: string) => void;
// Setters - Decal // Setters - Decal
setSelectedDecal: (decal: { decalMesh: Object3D, decalData: Decal } | null) => void; setSelectedDecal: (decal: { decalMesh: Object3D | null, decalData: Decal } | null) => void;
setDeletableDecal: (decal: Object3D | null) => void; setDeletableDecal: (decal: Object3D | null) => void;
setDecalDragState: (isDragging: boolean, draggingDecalUuid: string | null, dragOffset: Vector3 | null) => void;
// Setters - Aisle General // Setters - Aisle General
setSelectedAisle: (aisle: Object3D | null) => void; setSelectedAisle: (aisle: Object3D | null) => void;
@@ -143,6 +149,11 @@ export const useBuilderStore = create<BuilderState>()(
selectedDecal: null, selectedDecal: null,
deletableDecal: null, deletableDecal: null,
decalDragState: {
isDragging: false,
draggingDecalUuid: null,
dragOffset: null,
},
selectedAisle: null, selectedAisle: null,
aisleType: 'solid-aisle', aisleType: 'solid-aisle',
@@ -290,7 +301,7 @@ export const useBuilderStore = create<BuilderState>()(
// === Setters: Decal === // === Setters: Decal ===
setSelectedDecal: (decal: { decalMesh: Object3D, decalData: Decal } | null) => { setSelectedDecal: (decal: { decalMesh: Object3D | null, decalData: Decal } | null) => {
set((state) => { set((state) => {
state.selectedDecal = decal; state.selectedDecal = decal;
}) })
@@ -302,6 +313,16 @@ export const useBuilderStore = create<BuilderState>()(
}) })
}, },
setDecalDragState: (isDragging: boolean, draggingDecalUuid: string | null, dragOffset: Vector3 | null) => {
set((state) => {
state.decalDragState = {
isDragging,
draggingDecalUuid,
dragOffset,
}
})
},
// === Setters: Aisle General === // === Setters: Aisle General ===
setSelectedAisle: (aisle: Object3D | null) => { setSelectedAisle: (aisle: Object3D | null) => {

View File

@@ -19,8 +19,8 @@ interface FloorStore {
setBevelStrength: (uuid: string, strength: number) => void; setBevelStrength: (uuid: string, strength: number) => void;
setDepth: (uuid: string, depth: number) => void; setDepth: (uuid: string, depth: number) => void;
setMaterial: (uuid: string, sideMaterial: string, topMaterial: string) => void; setMaterial: (uuid: string, sideMaterial: string, topMaterial: string) => void;
addDecal: (floors: string, decal: Decal) => void; addDecal: (floors: string, decal: Decal) => Decal | undefined;
updateDecal: (decalUuid: string, decal: Decal) => Floor | undefined; updateDecal: (decalUuid: string, decal: Partial<Decal>) => Floor | undefined;
removeDecal: (decalUuid: string) => Floor | undefined; removeDecal: (decalUuid: string) => Floor | undefined;
updateDecalPosition: (decalUuid: string, position: [number, number, number]) => void; updateDecalPosition: (decalUuid: string, position: [number, number, number]) => void;
updateDecalRotation: (decalUuid: string, rotation: number) => void; updateDecalRotation: (decalUuid: string, rotation: number) => void;
@@ -196,12 +196,17 @@ export const createFloorStore = () => {
} }
}), }),
addDecal: (floorUuid, decal) => set(state => { addDecal: (floorUuid, decal) => {
let addedDecal: Decal | undefined;
set(state => {
const floor = state.floors.find(f => f.floorUuid === floorUuid); const floor = state.floors.find(f => f.floorUuid === floorUuid);
if (floor) { if (floor) {
floor.decals.push(decal); floor.decals.push(decal);
addedDecal = JSON.parse(JSON.stringify(decal));
} }
}), })
return addedDecal;
},
updateDecal: (decalUuid, updatedDecal) => { updateDecal: (decalUuid, updatedDecal) => {
let affectedFloor: Floor | undefined; let affectedFloor: Floor | undefined;
@@ -209,12 +214,12 @@ export const createFloorStore = () => {
for (const floor of state.floors) { for (const floor of state.floors) {
const index = floor.decals.findIndex(d => d.decalUuid === decalUuid); const index = floor.decals.findIndex(d => d.decalUuid === decalUuid);
if (index !== -1) { if (index !== -1) {
floor.decals[index] = updatedDecal; Object.assign(floor.decals[index], updatedDecal);
affectedFloor = JSON.parse(JSON.stringify(floor)); affectedFloor = JSON.parse(JSON.stringify(floor));
break; break;
} }
} }
}) });
return affectedFloor; return affectedFloor;
}, },

View File

@@ -9,8 +9,8 @@ interface WallStore {
removeWall: (uuid: string) => void; removeWall: (uuid: string) => void;
clearWalls: () => void; clearWalls: () => void;
removeWallByPoints: (Points: [Point, Point]) => Wall | undefined; removeWallByPoints: (Points: [Point, Point]) => Wall | undefined;
addDecal: (wallUuid: string, decal: Decal) => void; addDecal: (wallUuid: string, decal: Decal) => Decal | undefined;
updateDecal: (decalUuid: string, decal: Decal) => Wall | undefined; updateDecal: (decalUuid: string, decal: Partial<Decal>) => Wall | undefined;
removeDecal: (decalUuid: string) => Wall | undefined; removeDecal: (decalUuid: string) => Wall | undefined;
updateDecalPosition: (decalUuid: string, position: [number, number, number]) => Wall | undefined; updateDecalPosition: (decalUuid: string, position: [number, number, number]) => Wall | undefined;
updateDecalRotation: (decalUuid: string, rotation: number) => void; updateDecalRotation: (decalUuid: string, rotation: number) => void;
@@ -83,12 +83,17 @@ export const createWallStore = () => {
return removedWall; return removedWall;
}, },
addDecal: (wallUuid, decal) => set((state) => { addDecal: (wallUuid, decal) => {
let addedDecal: Decal | undefined;
set((state) => {
const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid); const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid);
if (wallToUpdate) { if (wallToUpdate) {
wallToUpdate.decals.push(decal); wallToUpdate.decals.push(decal);
addedDecal = JSON.parse(JSON.stringify(decal));
} }
}), })
return addedDecal;
},
updateDecal: (decalUuid, decal) => { updateDecal: (decalUuid, decal) => {
let affectedWall: Wall | undefined; let affectedWall: Wall | undefined;