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 { 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 { 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() {
const { wallStore, floorStore } = useSceneContext();
const { addDecal: addDecalOnWall, getWallById } = wallStore();
const { addDecal : addDecalOnFloor, getFloorById } = floorStore();
const { droppedDecal } = useDroppedDecal();
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();
useEffect(() => {
const canvasElement = gl.domElement;
const onDrop = (event: DragEvent) => {
console.log('event: ', event);
console.log('droppedDecal: ', droppedDecal);
if (!event.dataTransfer?.files[0]) return;
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 { CameraControls, Decal } from '@react-three/drei'
import { useThree } from '@react-three/fiber';
import { useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
import { Decal } from '@react-three/drei'
import { useToggleView, useToolMode } from '../../../../store/builder/store';
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
import { retrieveImage, storeImage } from '../../../../utils/indexDB/idbUtils';
import defaultMaterial from '../../../../assets/textures/floor/wall-tex.png';
import useModuleStore from '../../../../store/useModuleStore';
import { useSceneContext } from '../../../scene/sceneContext';
import { useEffect, useRef, useState } from 'react';
import { getUserData } from '../../../../functions/getUserData';
import { useVersionContext } from '../../version/versionContext';
import { useParams } from 'react-router-dom';
import { useDecalEventHandlers } from '../eventHandler/useDecalEventHandlers';
// import { upsertWallApi } from '../../../../services/factoryBuilder/wall/upsertWallApi';
// 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 }) {
const { setSelectedWall, setSelectedFloor, selectedDecal, deletableDecal, setSelectedDecal, setDeletableDecal } = useBuilderStore();
const { wallStore, floorStore } = useSceneContext();
const { removeDecal: removeDecalInWall, updateDecalPosition: updateDecalPositionInWall, getWallById } = wallStore();
const { removeDecal: removeDecalInFloor } = floorStore();
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const { selectedDecal, deletableDecal, setSelectedDecal, setDeletableDecal } = 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();
const isDraggingRef = useRef(false);
const dragOffsetRef = useRef<THREE.Vector3 | null>(null);
const decalRef = useRef<any>(null);
useEffect(() => {
if (selectedDecal?.decalData.decalUuid === decal.decalUuid && !selectedDecal.decalMesh) {
setSelectedDecal({ decalData: selectedDecal.decalData, decalMesh: decalRef.current });
}
}, [selectedDecal])
const { handlePointerMissed, handlePointerLeave, handleClick, handlePointerDown, handlePointerEnter } = useDecalEventHandlers({ parent, decal, visible });
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 textureLoader = new THREE.TextureLoader();
@@ -43,24 +41,22 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
(fallbackTex) => {
fallbackTex.name = "default-decal";
setTexture(fallbackTex);
setIsLoading(false);
logDecalStatus(decal.decalId, 'default-loaded');
},
undefined,
(error) => {
console.error("Error loading default decal texture:", error);
setIsLoading(false);
}
);
};
const loadDecalTexture = async (decalId: string) => {
setIsLoading(true);
try {
const cachedTexture = THREE.Cache.get(decalId);
if (cachedTexture) {
setTexture(cachedTexture);
setIsLoading(false);
logDecalStatus(decalId, 'cache-loaded');
return;
}
@@ -75,7 +71,7 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
tex.name = decalId;
THREE.Cache.add(decalId, tex);
setTexture(tex);
setIsLoading(false);
logDecalStatus(decalId, 'indexedDB-loaded');
},
undefined,
(error) => {
@@ -95,8 +91,8 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
};
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();
textureLoader.load(
@@ -105,7 +101,7 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
tex.name = decalId;
THREE.Cache.add(decalId, tex);
setTexture(tex);
setIsLoading(false);
logDecalStatus(decalId, 'backend-loaded');
try {
const response = await fetch(textureUrl);
@@ -145,183 +141,20 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP
}
}, [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 (
<Decal
// debug
visible={visible}
ref={decalRef}
position={[decal.decalPosition[0], decal.decalPosition[1], zPosition]}
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}
onPointerDown={(e) => {
if (visible && !toggleView && activeModule === 'builder') {
if (e.object.userData.decalUuid && toolMode === 'cursor') {
e.stopPropagation();
isDraggingRef.current = true;
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);
}
}}
onPointerDown={(e) => { if (e.button === 0) handlePointerDown(e) }}
onClick={(e) => { handleClick(e) }}
onPointerEnter={(e) => { handlePointerEnter(e) }}
onPointerLeave={(e) => { handlePointerLeave(e) }}
onPointerMissed={() => handlePointerMissed()}
>
<meshBasicMaterial
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]);
const logModelStatus = (modelId: string, status: string) => {
// console.log(modelId, status);
}
useEffect(() => {
// Calculate Bounding Box
const calculateBoundingBox = (scene: THREE.Object3D) => {
@@ -103,6 +107,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
clone.animations = cachedModel.animations || [];
setGltfScene(clone);
calculateBoundingBox(clone);
logModelStatus(assetId, 'cache-loaded');
return;
}
@@ -118,6 +123,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene);
logModelStatus(assetId, 'indexedDB-loaded');
},
undefined,
(error) => {
@@ -140,6 +146,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene);
logModelStatus(assetId, 'backend-loaded');
})
.catch((error) => {
console.error(

View File

@@ -1,12 +1,13 @@
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 { Extrude } from "@react-three/drei";
import useModuleStore from "../../../../../store/useModuleStore";
import { useBuilderStore } from "../../../../../store/builder/useBuilderStore";
import { useToggleView } from "../../../../../store/builder/store";
import * as Constants from "../../../../../types/world/worldConstants";
import DecalInstance from "../../../Decal/decalInstance/decalInstance";
import texturePath from "../../../../../assets/textures/floor/white.png";
import texturePathDark from "../../../../../assets/textures/floor/black.png";
import material1 from "../../../../../assets/textures/floor/factory wall texture.jpg";
@@ -67,20 +68,26 @@ function FloorInstance({ floor }: { floor: Floor }) {
},
};
const shape = useMemo(() => {
const shape = new Shape();
const shapeData = useMemo(() => {
const points = floor.points.map((p) => new Vector2(p.position[0], p.position[2]));
if (points.length < 3) return null;
shape.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
shape.lineTo(points[i].x, points[i].y);
const centroidX = points.reduce((sum, p) => sum + p.x, 0) / points.length;
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]);
const textureScale = Constants.floorConfig.textureScale;
// Helper function to handle texture maps and filter out null values
function getMaterialMaps(material: any, defaultMap: any) {
const materialMap = material.map || defaultMap;
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);
}
// Default material map
const defaultMaterialMap = materials["Default Material"].map;
// Get top and side material maps
const topMaterial = materials[floor.topMaterial];
const sideMaterial = materials[floor.sideMaterial];
// Get the filtered lists for top and side textures
const topTexturesList = getMaterialMaps(topMaterial, defaultMaterialMap);
const sideTexturesList = getMaterialMaps(sideMaterial, defaultMaterialMap);
// Use loader to load top and side textures
const [topTexture, topNormalTexture, topRoughnessTexture, topMetalicTexture] = useLoader(TextureLoader, topTexturesList);
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 = [
{
textures: [
@@ -131,7 +130,6 @@ function FloorInstance({ floor }: { floor: Floor }) {
},
];
// Apply texture settings
textureMaterialMap.forEach(({ textures, materialKey }) => {
const tileScale = materials[materialKey]?.textureTileScale ?? [
textureScale,
@@ -143,20 +141,36 @@ function FloorInstance({ floor }: { floor: Floor }) {
tex.wrapS = tex.wrapT = RepeatWrapping;
tex.repeat.set(tileScale[0], tileScale[1]);
tex.anisotropy = 16;
// First texture is always the color map (use SRGB), others should be linear
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 (
<mesh
castShadow
receiveShadow
geometry={geometry}
name={`Floor-${floor.floorUuid}`}
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}
onDoubleClick={(e) => {
if (!toggleView && activeModule === "builder") {
@@ -173,41 +187,32 @@ function FloorInstance({ floor }: { floor: Floor }) {
}
}}
>
<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
attach="material-0"
color={Constants.floorConfig.defaultColor}
map={topTexture}
roughnessMap={topRoughnessTexture}
metalnessMap={topMetalicTexture}
normalMap={topNormalTexture}
roughness={1.5}
metalness={1.0}
side={DoubleSide}
/>
<meshStandardMaterial
attach="material-1"
color={Constants.floorConfig.defaultColor}
map={sideTexture?.clone()}
roughnessMap={sideRoughnessTexture?.clone()}
metalnessMap={sideMetalicTexture?.clone()}
normalMap={sideNormalTexture?.clone()}
side={DoubleSide}
/>
</Extrude>
<meshPhysicalMaterial
attach="material-0"
color={Constants.floorConfig.defaultColor}
map={topTexture}
roughnessMap={topRoughnessTexture}
metalnessMap={topMetalicTexture}
normalMap={topNormalTexture}
roughness={1.5}
metalness={1.0}
side={DoubleSide}
/>
<meshStandardMaterial
attach="material-1"
color={Constants.floorConfig.defaultColor}
map={sideTexture?.clone()}
roughnessMap={sideRoughnessTexture?.clone()}
metalnessMap={sideMetalicTexture?.clone()}
normalMap={sideNormalTexture?.clone()}
roughness={1.5}
metalness={1.0}
side={DoubleSide}
/>
{floor.decals.map((decal) => (
<DecalInstance parent={floor} key={decal.decalUuid} decal={decal} />
))}
</mesh>
);
}

View File

@@ -17,7 +17,7 @@ import material1 from '../../../../../assets/textures/floor/factory wall texture
function Wall({ wall }: { readonly wall: Wall }) {
const { wallStore, wallAssetStore } = useSceneContext();
const { walls, addDecal } = wallStore();
const { walls } = wallStore();
const { wallAssets, getWallAssetsByWall, setVisibility } = wallAssetStore();
const assets = getWallAssetsByWall(wall.wallUuid);
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)) ?
<Base
name={`BaseWall${wall.wallUuid}`}
castShadow
receiveShadow
ref={meshRef}
@@ -159,19 +160,6 @@ function Wall({ wall }: { readonly wall: Wall }) {
e.stopPropagation();
setSelectedWall(e.object);
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 && (
<Outline
selection={selectedDecal.decalMesh}
selection={selectedDecal.decalMesh || undefined}
selectionLayer={10}
width={2000}
blendFunction={BlendFunction.ALPHA}

View File

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

View File

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

View File

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