Merge remote-tracking branch 'origin/main-dev' into main-demo

This commit is contained in:
2025-08-28 15:54:24 +05:30
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;