From 614c265071dc77c9b62c0bdc5135d609d22c86d6 Mon Sep 17 00:00:00 2001 From: Jerald-Golden-B Date: Fri, 26 Sep 2025 12:27:40 +0530 Subject: [PATCH] decal boundary added based on the wall and floor movement --- .../Decal/decalInstance/decalInstance.tsx | 14 +- .../eventHandler/useDecalEventHandlers.ts | 172 +++++++++++++++++- .../Instances/Instance/floorInstance.tsx | 55 +++++- .../builder/wall/Instances/instance/wall.tsx | 45 +++-- app/src/store/builder/useFloorStore.ts | 9 + app/src/store/builder/useWallStore.ts | 9 + 6 files changed, 272 insertions(+), 32 deletions(-) diff --git a/app/src/modules/builder/Decal/decalInstance/decalInstance.tsx b/app/src/modules/builder/Decal/decalInstance/decalInstance.tsx index 602ac25..fd4c149 100644 --- a/app/src/modules/builder/Decal/decalInstance/decalInstance.tsx +++ b/app/src/modules/builder/Decal/decalInstance/decalInstance.tsx @@ -1,5 +1,6 @@ import * as THREE from "three"; import { Decal } from "@react-three/drei"; +import { useEffect, useRef, useState } from "react"; import { useToggleView, useToolMode } from "../../../../store/builder/store"; import { useBuilderStore } from "../../../../store/builder/useBuilderStore"; import { retrieveImage, storeImage } from "../../../../utils/indexDB/idbUtils"; @@ -7,11 +8,16 @@ import deepEqual from "../../../../functions/objectDeepEqual"; import defaultMaterial from "../../../../assets/image/fallback/fallback decal 1.png"; import useModuleStore from "../../../../store/ui/useModuleStore"; -import { useEffect, useRef, useState } from "react"; import { useDecalEventHandlers } from "../eventHandler/useDecalEventHandlers"; -function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalPosition[2] }: Readonly<{ parent: Wall | Floor; visible?: boolean; decal: Decal; zPosition?: number }>) { +function DecalInstance({ + parent, + visible = true, + decal, + zPosition = decal.decalPosition[2], + overWritePosition = null, +}: Readonly<{ parent: Wall | Floor; visible?: boolean; decal: Decal; zPosition?: number; overWritePosition?: [number, number, number] | null }>) { const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; const { selectedDecal, deletableDecal, setSelectedDecal, setDeletableDecal } = useBuilderStore(); const { toolMode } = useToolMode(); @@ -155,10 +161,10 @@ function DecalInstance({ parent, visible = true, decal, zPosition = decal.decalP return ( i.object.userData?.wallUuid); const floorIntersect = intersects.find((i) => i.object.userData?.floorUuid); - let offset = decalDragState.dragOffset || new THREE.Vector3(0, 0, 0); + let offset = decalDragState.dragOffset || new Vector3(0, 0, 0); if (wallIntersect) { const wallUuid = wallIntersect.object.userData.wallUuid; @@ -59,7 +61,30 @@ export function useDecalEventHandlers({ parent, decal, visible }: { parent: Wall } if ("wallUuid" in parent && parent.wallUuid === wallUuid && decal.decalType.type === "Wall") { - updateDecalPositionInWall(decal.decalUuid, [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]]); + const wall = getWallById(wallUuid); + const decalData = getDecalOnWall(decal.decalUuid); + if (!decalData || !wall) return; + const wallFlipped = isWallFlipped(wall); + const [rawStart, rawEnd] = wall.points; + const [startPoint, endPoint] = wallFlipped ? [rawStart, rawEnd] : [rawEnd, rawStart]; + + const startX = startPoint.position[0]; + const startZ = startPoint.position[2]; + const endX = endPoint.position[0]; + const endZ = endPoint.position[2]; + + const wallLength = Math.sqrt((endX - startX) ** 2 + (endZ - startZ) ** 2); + + function clampDecalPosition(decal: Decal, wallLength: number, wallHeight: number) { + const localPos = new Vector3(...decal.decalPosition); + localPos.x = MathUtils.clamp(localPos.x, -wallLength / 2, wallLength / 2); + localPos.y = MathUtils.clamp(localPos.y, -wallHeight / 2, wallHeight / 2); + return localPos.toArray() as [number, number, number]; + } + + const clampedPosition = clampDecalPosition({ ...decalData, decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]] }, wallLength, wall.wallHeight); + + updateDecalPositionInWall(decalData.decalUuid, clampedPosition); } else if (decal.decalType.type === "Wall" && wallUuid) { deleteDecal(decal.decalUuid, parent); @@ -100,7 +125,60 @@ export function useDecalEventHandlers({ parent, decal, visible }: { parent: Wall } if ("floorUuid" in parent && parent.floorUuid === floorUuid && decal.decalType.type === "Floor") { - updateDecalPositionInFloor(decal.decalUuid, [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]]); + function isPointInPolygon(point: Vector2, polygon: Vector2[]): boolean { + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x, + yi = polygon[i].y; + const xj = polygon[j].x, + yj = polygon[j].y; + + const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + return inside; + } + + function clampToPolygon(point: Vector2, polygon: Vector2[]): Vector2 { + let closestPoint = point.clone(); + let minDist = Infinity; + + for (let i = 0; i < polygon.length; i++) { + const a = polygon[i]; + const b = polygon[(i + 1) % polygon.length]; + + const ab = new Vector2().subVectors(b, a); + const t = Math.max(0, Math.min(1, point.clone().sub(a).dot(ab) / ab.lengthSq())); + const proj = a.clone().add(ab.multiplyScalar(t)); + + const dist = proj.distanceTo(point); + if (dist < minDist) { + minDist = dist; + closestPoint = proj; + } + } + return closestPoint; + } + + function clampDecalPosition(decal: Decal, floor: Floor): [number, number, number] { + const pos = new Vector3(...decal.decalPosition); + const polygon2D = floor.points.map((p) => new Vector2(p.position[0], p.position[2])); + const p2 = new Vector2(pos.x, pos.y); + + let final2D: Vector2; + if (isPointInPolygon(p2, polygon2D)) { + final2D = p2; + } else { + final2D = clampToPolygon(p2, polygon2D); + } + + const clampedPos = new Vector3(final2D.x, final2D.y, pos.z); + return clampedPos.toArray() as [number, number, number]; + } + + const clampedPosition = clampDecalPosition({ ...decal, decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]] }, parent); + + updateDecalPositionInFloor(decal.decalUuid, clampedPosition); } else if (decal.decalType.type === "Floor" && floorUuid) { deleteDecal(decal.decalUuid, parent); @@ -299,7 +377,87 @@ export function useDecalEventHandlers({ parent, decal, visible }: { parent: Wall 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); + + let clampedDecalPosition; + + if (decal.decalType.type === "Wall" && "wallUuid" in parent) { + const wallFlipped = isWallFlipped(parent); + const [rawStart, rawEnd] = parent.points; + const [startPoint, endPoint] = wallFlipped ? [rawStart, rawEnd] : [rawEnd, rawStart]; + + const startX = startPoint.position[0]; + const startZ = startPoint.position[2]; + const endX = endPoint.position[0]; + const endZ = endPoint.position[2]; + + const wallLength = Math.sqrt((endX - startX) ** 2 + (endZ - startZ) ** 2); + + function clampDecalPosition(decal: Decal, wallLength: number, wallHeight: number) { + const localPos = new Vector3(...decal.decalPosition); + localPos.x = MathUtils.clamp(localPos.x, -wallLength / 2, wallLength / 2); + localPos.y = MathUtils.clamp(localPos.y, -wallHeight / 2, wallHeight / 2); + return localPos.toArray() as [number, number, number]; + } + + clampedDecalPosition = clampDecalPosition(decal, wallLength, parent.wallHeight); + } else if (decal.decalType.type === "Floor" && "floorUuid" in parent) { + function isPointInPolygon(point: Vector2, polygon: Vector2[]): boolean { + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x, + yi = polygon[i].y; + const xj = polygon[j].x, + yj = polygon[j].y; + + const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + return inside; + } + + function clampToPolygon(point: Vector2, polygon: Vector2[]): Vector2 { + let closestPoint = point.clone(); + let minDist = Infinity; + + for (let i = 0; i < polygon.length; i++) { + const a = polygon[i]; + const b = polygon[(i + 1) % polygon.length]; + + const ab = new Vector2().subVectors(b, a); + const t = Math.max(0, Math.min(1, point.clone().sub(a).dot(ab) / ab.lengthSq())); + const proj = a.clone().add(ab.multiplyScalar(t)); + + const dist = proj.distanceTo(point); + if (dist < minDist) { + minDist = dist; + closestPoint = proj; + } + } + return closestPoint; + } + + function clampDecalPosition(decal: Decal, floor: Floor): [number, number, number] { + const pos = new Vector3(...decal.decalPosition); + const polygon2D = floor.points.map((p) => new Vector2(p.position[0], p.position[2])); + const p2 = new Vector2(pos.x, pos.y); + + let final2D: Vector2; + if (isPointInPolygon(p2, polygon2D)) { + final2D = p2; + } else { + final2D = clampToPolygon(p2, polygon2D); + } + + const clampedPos = new Vector3(final2D.x, final2D.y, pos.z); + return clampedPos.toArray() as [number, number, number]; + } + + clampedDecalPosition = clampDecalPosition(decal, parent); + } else { + clampedDecalPosition = decal.decalPosition; + } + + let dragOffset = new Vector3(clampedDecalPosition[0] - localIntersect.x, clampedDecalPosition[1] - localIntersect.y, 0); setDecalDragState(true, decal.decalUuid, dragOffset); } } diff --git a/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx b/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx index 1d73f6a..c5d2c5f 100644 --- a/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx +++ b/app/src/modules/builder/floor/Instances/Instance/floorInstance.tsx @@ -1,4 +1,4 @@ -import { DoubleSide, TextureLoader, RepeatWrapping, SRGBColorSpace, NoColorSpace } from "three"; +import { DoubleSide, TextureLoader, RepeatWrapping, SRGBColorSpace, NoColorSpace, Vector2, Vector3 } from "three"; import { useLoader } from "@react-three/fiber"; import useModuleStore from "../../../../../store/ui/useModuleStore"; import { useBuilderStore } from "../../../../../store/builder/useBuilderStore"; @@ -123,6 +123,57 @@ function FloorInstance({ floor }: { readonly floor: Floor }) { bevelThickness: 0.1, }; + function isPointInPolygon(point: Vector2, polygon: Vector2[]): boolean { + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x, + yi = polygon[i].y; + const xj = polygon[j].x, + yj = polygon[j].y; + + const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + return inside; + } + + function clampToPolygon(point: Vector2, polygon: Vector2[]): Vector2 { + let closestPoint = point.clone(); + let minDist = Infinity; + + for (let i = 0; i < polygon.length; i++) { + const a = polygon[i]; + const b = polygon[(i + 1) % polygon.length]; + + const ab = new Vector2().subVectors(b, a); + const t = Math.max(0, Math.min(1, point.clone().sub(a).dot(ab) / ab.lengthSq())); + const proj = a.clone().add(ab.multiplyScalar(t)); + + const dist = proj.distanceTo(point); + if (dist < minDist) { + minDist = dist; + closestPoint = proj; + } + } + return closestPoint; + } + + function clampDecalPosition(decal: Decal, floor: Floor): [number, number, number] { + const pos = new Vector3(...decal.decalPosition); + const polygon2D = floor.points.map((p) => new Vector2(p.position[0], p.position[2])); + const p2 = new Vector2(pos.x, pos.y); + + let final2D: Vector2; + if (isPointInPolygon(p2, polygon2D)) { + final2D = p2; + } else { + final2D = clampToPolygon(p2, polygon2D); + } + + const clampedPos = new Vector3(final2D.x, final2D.y, pos.z); + return clampedPos.toArray() as [number, number, number]; + } + return ( {floor.decals.map((decal) => ( - + ))} ); diff --git a/app/src/modules/builder/wall/Instances/instance/wall.tsx b/app/src/modules/builder/wall/Instances/instance/wall.tsx index 31990ed..55b6f38 100644 --- a/app/src/modules/builder/wall/Instances/instance/wall.tsx +++ b/app/src/modules/builder/wall/Instances/instance/wall.tsx @@ -1,4 +1,4 @@ -import * as THREE from "three"; +import { BoxGeometry, DoubleSide, MathUtils, MeshStandardMaterial, RepeatWrapping, SRGBColorSpace, TextureLoader, Vector3 } from "three"; import { Base } from "@react-three/csg"; import { useMemo, useRef, useState } from "react"; import { MeshDiscardMaterial } from "@react-three/drei"; @@ -48,47 +48,47 @@ function Wall({ wall }: { readonly wall: Wall }) { const centerZ = (startZ + endZ) / 2; const centerY = wall.wallHeight / 2; - const textureLoader = new THREE.TextureLoader(); + const textureLoader = new TextureLoader(); const [defaultWallTexture, material1WallTexture] = useMemo(() => { const inside = textureLoader.load(defaultMaterial); - inside.wrapS = inside.wrapT = THREE.RepeatWrapping; + inside.wrapS = inside.wrapT = RepeatWrapping; inside.repeat.set(wallLength / 10, wall.wallHeight / 10); - inside.colorSpace = THREE.SRGBColorSpace; + inside.colorSpace = SRGBColorSpace; const outside = textureLoader.load(material1); - outside.wrapS = outside.wrapT = THREE.RepeatWrapping; + outside.wrapS = outside.wrapT = RepeatWrapping; outside.repeat.set(wallLength / 10, wall.wallHeight / 10); - outside.colorSpace = THREE.SRGBColorSpace; + outside.colorSpace = SRGBColorSpace; return [inside, outside]; }, [wallLength, wall.wallHeight]); const materials = useMemo(() => { return [ - new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide, visible: visible, clipShadows: true }), // Left - new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide, visible: visible, clipShadows: true }), // Right - new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide, visible: visible, clipShadows: true }), // Top - new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide, visible: visible, clipShadows: true }), // Bottom - new THREE.MeshStandardMaterial({ + new MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: DoubleSide, visible: visible, clipShadows: true }), // Left + new MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: DoubleSide, visible: visible, clipShadows: true }), // Right + new MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: DoubleSide, visible: visible, clipShadows: true }), // Top + new MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: DoubleSide, visible: visible, clipShadows: true }), // Bottom + new MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, - side: THREE.DoubleSide, + side: DoubleSide, map: wall.insideMaterial === "Default Material" ? defaultWallTexture : material1WallTexture, }), - new THREE.MeshStandardMaterial({ + new MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, - side: THREE.DoubleSide, + side: DoubleSide, map: wall.outsideMaterial === "Default Material" ? defaultWallTexture : material1WallTexture, }), ]; }, [defaultWallTexture, material1WallTexture, wall, visible]); - const geometry = useMemo(() => new THREE.BoxGeometry(wallLength, wall.wallHeight, wall.wallThickness), [wallLength, wall.wallHeight, wall.wallThickness]); + const geometry = useMemo(() => new BoxGeometry(wallLength, wall.wallHeight, wall.wallThickness), [wallLength, wall.wallHeight, wall.wallThickness]); useFrame(() => { if (!meshRef.current) return; - const v = new THREE.Vector3(); - const u = new THREE.Vector3(); + const v = new Vector3(); + const u = new Vector3(); let nextVisible = true; @@ -108,6 +108,13 @@ function Wall({ wall }: { readonly wall: Wall }) { } }); + function clampDecalPosition(decal: Decal, wallLength: number, wallHeight: number) { + const localPos = new Vector3(...decal.decalPosition); + localPos.x = MathUtils.clamp(localPos.x, -wallLength / 2, wallLength / 2); + localPos.y = MathUtils.clamp(localPos.y, -wallHeight / 2, wallHeight / 2); + return localPos.toArray() as [number, number, number]; + } + return ( {assets.length > 0 || (walls[0].wallUuid === wall.wallUuid && wallAssets.length > 0) ? ( @@ -116,7 +123,7 @@ function Wall({ wall }: { readonly wall: Wall }) { castShadow receiveShadow ref={meshRef} - geometry={visible ? geometry : new THREE.BoxGeometry(0.00001, 0.00001)} + geometry={visible ? geometry : new BoxGeometry(0.00001, 0.00001)} position={[centerX, centerY, centerZ]} rotation={[0, -angle, 0]} userData={wall} @@ -158,7 +165,7 @@ function Wall({ wall }: { readonly wall: Wall }) { {wall.decals.map((decal) => ( - + ))} diff --git a/app/src/store/builder/useFloorStore.ts b/app/src/store/builder/useFloorStore.ts index 0b11f36..ccc1d29 100644 --- a/app/src/store/builder/useFloorStore.ts +++ b/app/src/store/builder/useFloorStore.ts @@ -33,6 +33,7 @@ interface FloorStore { getFloorsByPoints: (points: [Point, Point]) => Floor[] | []; getFloorPointById: (uuid: string) => Point | undefined; getConnectedPoints: (uuid: string) => Point[]; + getDecalById: (uuid: string) => Decal | undefined; } export const createFloorStore = () => { @@ -431,6 +432,14 @@ export const createFloorStore = () => { } return connected; }, + + getDecalById: (decalUuid: string): Decal | undefined => { + for (const floor of get().floors) { + const decal = floor.decals.find((d) => d.decalUuid === decalUuid); + if (decal) return JSON.parse(JSON.stringify(decal)); + } + return undefined; + }, })) ); }; diff --git a/app/src/store/builder/useWallStore.ts b/app/src/store/builder/useWallStore.ts index c77902b..8e48ffb 100644 --- a/app/src/store/builder/useWallStore.ts +++ b/app/src/store/builder/useWallStore.ts @@ -32,6 +32,7 @@ interface WallStore { getWallPointById: (wallUuid: string) => Point | undefined; getConnectedPoints: (wallUuid: string) => Point[]; getConnectedWallsByWallId: (wallUuid: string, skipSelf: boolean) => Walls; + getDecalById: (decalUuid: string) => Decal | undefined; } export const createWallStore = () => { @@ -312,6 +313,14 @@ export const createWallStore = () => { return w.points.some((p) => pointUuids.includes(p.pointUuid)); }); }, + + getDecalById: (decalUuid: string): Decal | undefined => { + for (const wall of get().walls) { + const decal = wall.decals.find((d) => d.decalUuid === decalUuid); + if (decal) return JSON.parse(JSON.stringify(decal)); + } + return undefined; + }, })) ); };