decal boundary added based on the wall and floor movement

This commit is contained in:
2025-09-26 12:27:40 +05:30
parent 13d48ae9b6
commit 614c265071
6 changed files with 272 additions and 32 deletions

View File

@@ -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 (
<Decal
// debug
debug
visible={visible}
ref={decalRef}
position={[decal.decalPosition[0], decal.decalPosition[1], zPosition]}
position={overWritePosition ? [overWritePosition[0], overWritePosition[1], zPosition] : [decal.decalPosition[0], decal.decalPosition[1], zPosition]}
rotation={[0, 0, decal.decalRotation * (Math.PI / 180)]}
scale={[decal.decalType.type === "Floor" || zPosition < 0 ? -decal.decalScale : decal.decalScale, decal.decalScale, 0.01]}
userData={decal}

View File

@@ -1,4 +1,4 @@
import * as THREE from "three";
import { MathUtils, Vector2, Vector3 } from "three";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { CameraControls } from "@react-three/drei";
@@ -7,6 +7,7 @@ import { useToggleView, useToolMode } from "../../../../store/builder/store";
import { useSocketStore } from "../../../../store/socket/useSocketStore";
import { useBuilderStore } from "../../../../store/builder/useBuilderStore";
import { useSceneContext } from "../../../scene/sceneContext";
import { useWallClassification } from "../../wall/Instances/instance/helpers/useWallClassification";
import useModuleStore from "../../../../store/ui/useModuleStore";
import useWallResponseHandler from "../../../collaboration/responseHandler/useWallResponseHandler";
import useFloorResponseHandler from "../../../collaboration/responseHandler/useFloorResponseHandler";
@@ -20,10 +21,11 @@ import { upsertFloorApi } from "../../../../services/factoryBuilder/floor/upsert
export function useDecalEventHandlers({ parent, decal, visible }: { parent: Wall | Floor; decal: Decal; visible: boolean }) {
const { wallStore, floorStore, versionStore } = useSceneContext();
const { removeDecal: removeDecalInWall, updateDecalPosition: updateDecalPositionInWall, getWallById, addDecal: addDecalToWall } = wallStore();
const { removeDecal: removeDecalInFloor, updateDecalPosition: updateDecalPositionInFloor, getFloorById, addDecal: addDecalToFloor } = floorStore();
const { walls, removeDecal: removeDecalInWall, updateDecalPosition: updateDecalPositionInWall, getWallById, addDecal: addDecalToWall, getDecalById: getDecalOnWall } = wallStore();
const { removeDecal: removeDecalInFloor, updateDecalPosition: updateDecalPositionInFloor, getFloorById, addDecal: addDecalToFloor, getDecalById: getDecalOnFloor } = floorStore();
const { setSelectedWall, setSelectedFloor, setSelectedDecal, setDeletableDecal, deletableDecal, selectedDecal, setDecalDragState, decalDragState } = useBuilderStore();
const { updateWallInScene } = useWallResponseHandler();
const { isWallFlipped } = useWallClassification(walls);
const { updateFloorInScene } = useFloorResponseHandler();
const { toolMode } = useToolMode();
const { toggleView } = useToggleView();
@@ -44,7 +46,7 @@ export function useDecalEventHandlers({ parent, decal, visible }: { parent: Wall
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);
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);
}
}

View File

@@ -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 (
<ExtrudePolygon
castShadow
@@ -171,7 +222,7 @@ function FloorInstance({ floor }: { readonly floor: Floor }) {
/>
{floor.decals.map((decal) => (
<DecalInstance parent={floor} key={decal.decalUuid} decal={decal} />
<DecalInstance parent={floor} key={decal.decalUuid} decal={decal} overWritePosition={clampDecalPosition(decal, floor)} />
))}
</ExtrudePolygon>
);

View File

@@ -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 (
<mesh castShadow receiveShadow name={`Wall-${wall.wallUuid}`} key={wall.wallUuid} userData={wall}>
{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 }) {
<MeshDiscardMaterial />
{wall.decals.map((decal) => (
<DecalInstance parent={wall} visible={visible} key={decal.decalUuid} decal={decal} />
<DecalInstance parent={wall} visible={visible} key={decal.decalUuid} decal={decal} overWritePosition={clampDecalPosition(decal, wallLength, wall.wallHeight)} />
))}
</mesh>
</mesh>

View File

@@ -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;
},
}))
);
};

View File

@@ -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;
},
}))
);
};