Refactor wall snapping and classification logic; remove unused aisle snapping helper
- Updated Line component to include point type in Tube name and modified userData structure. - Deleted unused useAisleDragSnap helper and refactored usePointSnapping to handle both aisle and wall snapping. - Updated Point component to utilize new snapping methods and improved snapping logic for aisles and walls. - Enhanced wall classification logic to streamline room detection and wall flipping checks. - Refactored Wall component to utilize new material handling and wall flipping logic. - Updated WallCreator to manage wall creation and snapping more effectively, including handling intersections with existing walls. - Modified useBuilderStore and useWallStore to support new wall material properties and retrieval methods. - Adjusted TypeScript definitions for Wall interface to reflect changes in material properties.
This commit is contained in:
parent
00100dd19c
commit
462bae72a4
|
@ -25,7 +25,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
|
||||||
const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
|
const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
|
||||||
|
|
||||||
const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null);
|
const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null);
|
||||||
const { checkSnapForAisle } = usePointSnapping({ uuid: 'temp-aisle', pointType: 'Aisle', position: directionalSnap.position || [0, 0, 0] });
|
const { snapAislePoint } = usePointSnapping({ uuid: 'temp-aisle', pointType: 'Aisle', position: directionalSnap.position || [0, 0, 0] });
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) {
|
if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) {
|
||||||
|
@ -36,7 +36,7 @@ function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
|
||||||
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||||
|
|
||||||
if (intersectionPoint) {
|
if (intersectionPoint) {
|
||||||
const snapped = checkSnapForAisle([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
const snapped = snapAislePoint([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||||
|
|
||||||
if (snapped.isSnapped && snapped.snappedPoint) {
|
if (snapped.isSnapped && snapped.snappedPoint) {
|
||||||
finalPosition.current = snapped.position;
|
finalPosition.current = snapped.position;
|
||||||
|
|
|
@ -5,11 +5,11 @@ import * as Types from "../../../../types/world/worldTypes";
|
||||||
function getClosestIntersection(
|
function getClosestIntersection(
|
||||||
intersects: Types.Vector3Array,
|
intersects: Types.Vector3Array,
|
||||||
point: Types.Vector3
|
point: Types.Vector3
|
||||||
): Types.Vector3 | null {
|
): Types.Vector3 {
|
||||||
|
|
||||||
////////// A function that finds which point is closest from the intersects points that is given, Used in finding which point in a line is closest when clicked on a line during drawing //////////
|
////////// A function that finds which point is closest from the intersects points that is given, Used in finding which point in a line is closest when clicked on a line during drawing //////////
|
||||||
|
|
||||||
let closestNewPoint: THREE.Vector3 | null = null;
|
let closestNewPoint: THREE.Vector3 = point;
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
|
|
||||||
for (const intersect of intersects) {
|
for (const intersect of intersects) {
|
||||||
|
|
|
@ -46,9 +46,10 @@ function Line({ points }: Readonly<LineProps>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tube
|
<Tube
|
||||||
|
name={`${points[0].pointType}-Line`}
|
||||||
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||||
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||||
userData={points}
|
userData={{ points, path }}
|
||||||
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
|
args={[path, Constants.lineConfig.tubularSegments, Constants.lineConfig.radius, Constants.lineConfig.radialSegments, false]}
|
||||||
>
|
>
|
||||||
<meshStandardMaterial color={colors.defaultLineColor} />
|
<meshStandardMaterial color={colors.defaultLineColor} />
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
import { useCallback } from 'react';
|
|
||||||
import * as THREE from 'three';
|
|
||||||
import { useAisleStore } from '../../../../store/builder/useAisleStore';
|
|
||||||
|
|
||||||
const SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
|
||||||
|
|
||||||
const CAN_SNAP = true; // Whether snapping is enabled or not
|
|
||||||
|
|
||||||
export function useAislePointSnapping(point: Point) {
|
|
||||||
const { getConnectedPoints } = useAisleStore();
|
|
||||||
|
|
||||||
const snapPosition = useCallback((newPosition: [number, number, number]): {
|
|
||||||
position: [number, number, number],
|
|
||||||
isSnapped: boolean,
|
|
||||||
snapSources: THREE.Vector3[]
|
|
||||||
} => {
|
|
||||||
if (!CAN_SNAP) return { position: newPosition, isSnapped: false, snapSources: [] };
|
|
||||||
|
|
||||||
const connectedPoints = getConnectedPoints(point.pointUuid);
|
|
||||||
if (connectedPoints.length === 0) {
|
|
||||||
return {
|
|
||||||
position: newPosition,
|
|
||||||
isSnapped: false,
|
|
||||||
snapSources: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPos = new THREE.Vector3(...newPosition);
|
|
||||||
let closestX: { pos: THREE.Vector3, dist: number } | null = null;
|
|
||||||
let closestZ: { pos: THREE.Vector3, dist: number } | null = null;
|
|
||||||
|
|
||||||
for (const connectedPoint of connectedPoints) {
|
|
||||||
const cPos = new THREE.Vector3(...connectedPoint.position);
|
|
||||||
|
|
||||||
const xDist = Math.abs(newPos.x - cPos.x);
|
|
||||||
const zDist = Math.abs(newPos.z - cPos.z);
|
|
||||||
|
|
||||||
if (xDist < SNAP_DISTANCE_THRESHOLD) {
|
|
||||||
if (!closestX || xDist < closestX.dist) {
|
|
||||||
closestX = { pos: cPos, dist: xDist };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zDist < SNAP_DISTANCE_THRESHOLD) {
|
|
||||||
if (!closestZ || zDist < closestZ.dist) {
|
|
||||||
closestZ = { pos: cPos, dist: zDist };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const snappedPos = newPos.clone();
|
|
||||||
const snapSources: THREE.Vector3[] = [];
|
|
||||||
|
|
||||||
if (closestX) {
|
|
||||||
snappedPos.x = closestX.pos.x;
|
|
||||||
snapSources.push(closestX.pos.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closestZ) {
|
|
||||||
snappedPos.z = closestZ.pos.z;
|
|
||||||
snapSources.push(closestZ.pos.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSnapped = snapSources.length > 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
position: [snappedPos.x, snappedPos.y, snappedPos.z],
|
|
||||||
isSnapped,
|
|
||||||
snapSources
|
|
||||||
};
|
|
||||||
}, [point.pointUuid, getConnectedPoints]);
|
|
||||||
|
|
||||||
return { snapPosition };
|
|
||||||
}
|
|
|
@ -1,15 +1,106 @@
|
||||||
|
import * as THREE from 'three';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useAisleStore } from '../../../../store/builder/useAisleStore';
|
import { useAisleStore } from '../../../../store/builder/useAisleStore';
|
||||||
import * as THREE from 'three';
|
|
||||||
import { useWallStore } from '../../../../store/builder/useWallStore';
|
import { useWallStore } from '../../../../store/builder/useWallStore';
|
||||||
|
|
||||||
const SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
const POINT_SNAP_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
||||||
|
|
||||||
const CAN_SNAP = true; // Whether snapping is enabled or not
|
const CAN_POINT_SNAP = true; // Whether snapping is enabled or not
|
||||||
|
|
||||||
|
const ANGLE_SNAP_DISTANCE_THRESHOLD = 0.5; // Distance threshold for snapping in meters
|
||||||
|
|
||||||
|
const CAN_ANGLE_SNAP = true; // Whether snapping is enabled or not
|
||||||
|
|
||||||
export const usePointSnapping = (currentPoint: { uuid: string, pointType: string, position: [number, number, number] } | null) => {
|
export const usePointSnapping = (currentPoint: { uuid: string, pointType: string, position: [number, number, number] } | null) => {
|
||||||
const { aisles } = useAisleStore();
|
const { aisles, getConnectedPoints: getConnectedAislePoints } = useAisleStore();
|
||||||
const { walls } = useWallStore();
|
const { walls, getConnectedPoints: getConnectedWallPoints } = useWallStore();
|
||||||
|
|
||||||
|
// Wall Snapping
|
||||||
|
|
||||||
|
const getAllOtherWallPoints = useCallback(() => {
|
||||||
|
if (!currentPoint) return [];
|
||||||
|
return walls.flatMap(wall =>
|
||||||
|
wall.points.filter(point => point.pointUuid !== currentPoint.uuid)
|
||||||
|
);
|
||||||
|
}, [walls, currentPoint]);
|
||||||
|
|
||||||
|
const snapWallPoint = useCallback((position: [number, number, number]) => {
|
||||||
|
if (!currentPoint || !CAN_POINT_SNAP) return { position: position, isSnapped: false, snappedPoint: null };
|
||||||
|
|
||||||
|
const otherPoints = getAllOtherWallPoints();
|
||||||
|
const currentVec = new THREE.Vector3(...position);
|
||||||
|
for (const point of otherPoints) {
|
||||||
|
const pointVec = new THREE.Vector3(...point.position);
|
||||||
|
const distance = currentVec.distanceTo(pointVec);
|
||||||
|
if (distance <= POINT_SNAP_THRESHOLD && currentPoint.pointType === 'Wall') {
|
||||||
|
return { position: point.position, isSnapped: true, snappedPoint: point };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { position: position, isSnapped: false, snappedPoint: null };
|
||||||
|
}, [currentPoint, getAllOtherWallPoints]);
|
||||||
|
|
||||||
|
const snapWallAngle = useCallback((newPosition: [number, number, number]): {
|
||||||
|
position: [number, number, number],
|
||||||
|
isSnapped: boolean,
|
||||||
|
snapSources: THREE.Vector3[]
|
||||||
|
} => {
|
||||||
|
if (!currentPoint || !CAN_ANGLE_SNAP) return { position: newPosition, isSnapped: false, snapSources: [] };
|
||||||
|
|
||||||
|
const connectedPoints = getConnectedWallPoints(currentPoint.uuid);
|
||||||
|
if (connectedPoints.length === 0) {
|
||||||
|
return {
|
||||||
|
position: newPosition,
|
||||||
|
isSnapped: false,
|
||||||
|
snapSources: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPos = new THREE.Vector3(...newPosition);
|
||||||
|
let closestX: { pos: THREE.Vector3, dist: number } | null = null;
|
||||||
|
let closestZ: { pos: THREE.Vector3, dist: number } | null = null;
|
||||||
|
|
||||||
|
for (const connectedPoint of connectedPoints) {
|
||||||
|
const cPos = new THREE.Vector3(...connectedPoint.position);
|
||||||
|
|
||||||
|
const xDist = Math.abs(newPos.x - cPos.x);
|
||||||
|
const zDist = Math.abs(newPos.z - cPos.z);
|
||||||
|
|
||||||
|
if (xDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
|
||||||
|
if (!closestX || xDist < closestX.dist) {
|
||||||
|
closestX = { pos: cPos, dist: xDist };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
|
||||||
|
if (!closestZ || zDist < closestZ.dist) {
|
||||||
|
closestZ = { pos: cPos, dist: zDist };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const snappedPos = newPos.clone();
|
||||||
|
const snapSources: THREE.Vector3[] = [];
|
||||||
|
|
||||||
|
if (closestX) {
|
||||||
|
snappedPos.x = closestX.pos.x;
|
||||||
|
snapSources.push(closestX.pos.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closestZ) {
|
||||||
|
snappedPos.z = closestZ.pos.z;
|
||||||
|
snapSources.push(closestZ.pos.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSnapped = snapSources.length > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: [snappedPos.x, snappedPos.y, snappedPos.z],
|
||||||
|
isSnapped,
|
||||||
|
snapSources
|
||||||
|
};
|
||||||
|
}, [currentPoint, getConnectedAislePoints]);
|
||||||
|
|
||||||
|
// Aisle Snapping
|
||||||
|
|
||||||
const getAllOtherAislePoints = useCallback(() => {
|
const getAllOtherAislePoints = useCallback(() => {
|
||||||
if (!currentPoint) return [];
|
if (!currentPoint) return [];
|
||||||
|
@ -19,24 +110,15 @@ export const usePointSnapping = (currentPoint: { uuid: string, pointType: string
|
||||||
);
|
);
|
||||||
}, [aisles, currentPoint]);
|
}, [aisles, currentPoint]);
|
||||||
|
|
||||||
const getAllOtherWallPoints = useCallback(() => {
|
const snapAislePoint = useCallback((position: [number, number, number]) => {
|
||||||
if (!currentPoint) return [];
|
if (!currentPoint || !CAN_POINT_SNAP) return { position: position, isSnapped: false, snappedPoint: null };
|
||||||
return walls.flatMap(wall =>
|
|
||||||
wall.points.filter(point => point.pointUuid !== currentPoint.uuid)
|
|
||||||
);
|
|
||||||
}, [walls, currentPoint]);
|
|
||||||
|
|
||||||
const checkSnapForAisle = useCallback((position: [number, number, number]) => {
|
|
||||||
if (!currentPoint || !CAN_SNAP) return { position: position, isSnapped: false, snappedPoint: null };
|
|
||||||
|
|
||||||
const otherPoints = getAllOtherAislePoints();
|
const otherPoints = getAllOtherAislePoints();
|
||||||
const currentVec = new THREE.Vector3(...position);
|
const currentVec = new THREE.Vector3(...position);
|
||||||
|
|
||||||
for (const point of otherPoints) {
|
for (const point of otherPoints) {
|
||||||
const pointVec = new THREE.Vector3(...point.position);
|
const pointVec = new THREE.Vector3(...point.position);
|
||||||
const distance = currentVec.distanceTo(pointVec);
|
const distance = currentVec.distanceTo(pointVec);
|
||||||
|
if (distance <= POINT_SNAP_THRESHOLD && currentPoint.pointType === 'Aisle') {
|
||||||
if (distance <= SNAP_THRESHOLD && currentPoint.pointType === 'Aisle') {
|
|
||||||
return { position: point.position, isSnapped: true, snappedPoint: point };
|
return { position: point.position, isSnapped: true, snappedPoint: point };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,23 +126,71 @@ export const usePointSnapping = (currentPoint: { uuid: string, pointType: string
|
||||||
return { position: position, isSnapped: false, snappedPoint: null };
|
return { position: position, isSnapped: false, snappedPoint: null };
|
||||||
}, [currentPoint, getAllOtherAislePoints]);
|
}, [currentPoint, getAllOtherAislePoints]);
|
||||||
|
|
||||||
const checkSnapForWall = useCallback((position: [number, number, number]) => {
|
const snapAisleAngle = useCallback((newPosition: [number, number, number]): {
|
||||||
if (!currentPoint || !CAN_SNAP) return { position: position, isSnapped: false, snappedPoint: null };
|
position: [number, number, number],
|
||||||
|
isSnapped: boolean,
|
||||||
|
snapSources: THREE.Vector3[]
|
||||||
|
} => {
|
||||||
|
if (!currentPoint || !CAN_ANGLE_SNAP) return { position: newPosition, isSnapped: false, snapSources: [] };
|
||||||
|
|
||||||
const otherPoints = getAllOtherWallPoints();
|
const connectedPoints = getConnectedAislePoints(currentPoint.uuid);
|
||||||
const currentVec = new THREE.Vector3(...position);
|
if (connectedPoints.length === 0) {
|
||||||
for (const point of otherPoints) {
|
return {
|
||||||
const pointVec = new THREE.Vector3(...point.position);
|
position: newPosition,
|
||||||
const distance = currentVec.distanceTo(pointVec);
|
isSnapped: false,
|
||||||
if (distance <= SNAP_THRESHOLD && currentPoint.pointType === 'Wall') {
|
snapSources: []
|
||||||
return { position: point.position, isSnapped: true, snappedPoint: point };
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPos = new THREE.Vector3(...newPosition);
|
||||||
|
let closestX: { pos: THREE.Vector3, dist: number } | null = null;
|
||||||
|
let closestZ: { pos: THREE.Vector3, dist: number } | null = null;
|
||||||
|
|
||||||
|
for (const connectedPoint of connectedPoints) {
|
||||||
|
const cPos = new THREE.Vector3(...connectedPoint.position);
|
||||||
|
|
||||||
|
const xDist = Math.abs(newPos.x - cPos.x);
|
||||||
|
const zDist = Math.abs(newPos.z - cPos.z);
|
||||||
|
|
||||||
|
if (xDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
|
||||||
|
if (!closestX || xDist < closestX.dist) {
|
||||||
|
closestX = { pos: cPos, dist: xDist };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { position: position, isSnapped: false, snappedPoint: null };
|
|
||||||
}, [currentPoint, getAllOtherWallPoints]);
|
if (zDist < ANGLE_SNAP_DISTANCE_THRESHOLD) {
|
||||||
|
if (!closestZ || zDist < closestZ.dist) {
|
||||||
|
closestZ = { pos: cPos, dist: zDist };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const snappedPos = newPos.clone();
|
||||||
|
const snapSources: THREE.Vector3[] = [];
|
||||||
|
|
||||||
|
if (closestX) {
|
||||||
|
snappedPos.x = closestX.pos.x;
|
||||||
|
snapSources.push(closestX.pos.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closestZ) {
|
||||||
|
snappedPos.z = closestZ.pos.z;
|
||||||
|
snapSources.push(closestZ.pos.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSnapped = snapSources.length > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
checkSnapForAisle,
|
position: [snappedPos.x, snappedPos.y, snappedPos.z],
|
||||||
checkSnapForWall,
|
isSnapped,
|
||||||
|
snapSources
|
||||||
|
};
|
||||||
|
}, [currentPoint, getConnectedAislePoints]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
snapAislePoint,
|
||||||
|
snapAisleAngle,
|
||||||
|
snapWallPoint,
|
||||||
|
snapWallAngle,
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -7,7 +7,6 @@ import { useAisleStore } from '../../../store/builder/useAisleStore';
|
||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from '@react-three/fiber';
|
||||||
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
|
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
|
||||||
import { usePointSnapping } from './helpers/usePointSnapping';
|
import { usePointSnapping } from './helpers/usePointSnapping';
|
||||||
import { useAislePointSnapping } from './helpers/useAisleDragSnap';
|
|
||||||
import { useWallStore } from '../../../store/builder/useWallStore';
|
import { useWallStore } from '../../../store/builder/useWallStore';
|
||||||
import { deleteAisleApi } from '../../../services/factoryBuilder/aisle/deleteAisleApi';
|
import { deleteAisleApi } from '../../../services/factoryBuilder/aisle/deleteAisleApi';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
@ -21,8 +20,7 @@ function Point({ point }: { readonly point: Point }) {
|
||||||
const { toolMode } = useToolMode();
|
const { toolMode } = useToolMode();
|
||||||
const { setPosition: setAislePosition, removePoint: removeAislePoint, getAislesByPointId } = useAisleStore();
|
const { setPosition: setAislePosition, removePoint: removeAislePoint, getAislesByPointId } = useAisleStore();
|
||||||
const { setPosition: setWallPosition, removePoint: removeWallPoint } = useWallStore();
|
const { setPosition: setWallPosition, removePoint: removeWallPoint } = useWallStore();
|
||||||
const { snapPosition } = useAislePointSnapping(point);
|
const { snapAislePoint, snapAisleAngle, snapWallPoint, snapWallAngle } = usePointSnapping({ uuid: point.pointUuid, pointType: point.pointType, position: point.position });
|
||||||
const { checkSnapForAisle } = usePointSnapping({ uuid: point.pointUuid, pointType: point.pointType, position: point.position });
|
|
||||||
const { hoveredPoint, setHoveredPoint } = useBuilderStore();
|
const { hoveredPoint, setHoveredPoint } = useBuilderStore();
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const boxScale: [number, number, number] = Constants.pointConfig.boxScale;
|
const boxScale: [number, number, number] = Constants.pointConfig.boxScale;
|
||||||
|
@ -96,8 +94,8 @@ function Point({ point }: { readonly point: Point }) {
|
||||||
if (point.pointType === 'Aisle') {
|
if (point.pointType === 'Aisle') {
|
||||||
if (position) {
|
if (position) {
|
||||||
const newPosition: [number, number, number] = [position.x, position.y, position.z];
|
const newPosition: [number, number, number] = [position.x, position.y, position.z];
|
||||||
const aisleSnappedPosition = snapPosition(newPosition);
|
const aisleSnappedPosition = snapAisleAngle(newPosition);
|
||||||
const finalSnappedPosition = checkSnapForAisle(aisleSnappedPosition.position);
|
const finalSnappedPosition = snapAislePoint(aisleSnappedPosition.position);
|
||||||
|
|
||||||
setAislePosition(point.pointUuid, finalSnappedPosition.position);
|
setAislePosition(point.pointUuid, finalSnappedPosition.position);
|
||||||
|
|
||||||
|
@ -105,8 +103,10 @@ function Point({ point }: { readonly point: Point }) {
|
||||||
} else if (point.pointType === 'Wall') {
|
} else if (point.pointType === 'Wall') {
|
||||||
if (position) {
|
if (position) {
|
||||||
const newPosition: [number, number, number] = [position.x, position.y, position.z];
|
const newPosition: [number, number, number] = [position.x, position.y, position.z];
|
||||||
|
const wallSnappedPosition = snapWallAngle(newPosition);
|
||||||
|
const finalSnappedPosition = snapWallPoint(wallSnappedPosition.position);
|
||||||
|
|
||||||
setWallPosition(point.pointUuid, newPosition);
|
setWallPosition(point.pointUuid, finalSnappedPosition.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ function Point({ point }: { readonly point: Point }) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPointerOut={() => {
|
onPointerOut={() => {
|
||||||
if (hoveredPoint && hoveredPoint.pointUuid === point.pointUuid) {
|
if (hoveredPoint) {
|
||||||
setHoveredPoint(null);
|
setHoveredPoint(null);
|
||||||
}
|
}
|
||||||
setIsHovered(false)
|
setIsHovered(false)
|
||||||
|
|
|
@ -2,218 +2,130 @@ import { useMemo } from 'react';
|
||||||
import * as turf from '@turf/turf';
|
import * as turf from '@turf/turf';
|
||||||
|
|
||||||
export function useWallClassification(walls: Walls) {
|
export function useWallClassification(walls: Walls) {
|
||||||
// Find all minimal rooms from the given walls
|
|
||||||
const findRooms = () => {
|
const findRooms = () => {
|
||||||
if (walls.length < 3) return [];
|
if (walls.length < 3) return [];
|
||||||
|
|
||||||
// Build a graph of point connections
|
// Map pointUuid to list of connected line segments
|
||||||
const graph = new Map<string, string[]>();
|
const pointMap = new Map<string, Wall[]>();
|
||||||
const pointMap = new Map<string, Point>();
|
|
||||||
|
|
||||||
walls.forEach(wall => {
|
for (const wall of walls) {
|
||||||
const [p1, p2] = wall.points;
|
for (const point of wall.points) {
|
||||||
pointMap.set(p1.pointUuid, p1);
|
const list = pointMap.get(point.pointUuid) || [];
|
||||||
pointMap.set(p2.pointUuid, p2);
|
list.push(wall);
|
||||||
|
pointMap.set(point.pointUuid, list);
|
||||||
// Add connection from p1 to p2
|
|
||||||
if (!graph.has(p1.pointUuid)) graph.set(p1.pointUuid, []);
|
|
||||||
graph.get(p1.pointUuid)?.push(p2.pointUuid);
|
|
||||||
|
|
||||||
// Add connection from p2 to p1
|
|
||||||
if (!graph.has(p2.pointUuid)) graph.set(p2.pointUuid, []);
|
|
||||||
graph.get(p2.pointUuid)?.push(p1.pointUuid);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find all minimal cycles (rooms) in the graph
|
|
||||||
const allCycles: string[][] = [];
|
|
||||||
|
|
||||||
const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => {
|
|
||||||
if (depth > 20) return; // Prevent infinite recursion
|
|
||||||
|
|
||||||
path.push(currentNode);
|
|
||||||
|
|
||||||
const neighbors = graph.get(currentNode) || [];
|
|
||||||
|
|
||||||
for (const neighbor of neighbors) {
|
|
||||||
if (path.length > 2 && neighbor === startNode) {
|
|
||||||
// Found a cycle that returns to start
|
|
||||||
const cycle = [...path, neighbor];
|
|
||||||
|
|
||||||
// Check if this is a new unique cycle
|
|
||||||
if (!cycleExists(allCycles, cycle)) {
|
|
||||||
allCycles.push(cycle);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path.includes(neighbor)) {
|
|
||||||
findCycles(startNode, neighbor, [...path], depth + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Start cycle detection from each node
|
// Create graph of connected walls using pointUuid
|
||||||
for (const [pointUuid] of graph) {
|
const visited = new Set<string>();
|
||||||
findCycles(pointUuid, pointUuid, []);
|
const mergedLineStrings = [];
|
||||||
|
|
||||||
|
const wallKey = (p1: Point, p2: Point) => `${p1.pointUuid}-${p2.pointUuid}`;
|
||||||
|
|
||||||
|
for (const wall of walls) {
|
||||||
|
const key = wallKey(wall.points[0], wall.points[1]);
|
||||||
|
if (visited.has(key) || visited.has(wallKey(wall.points[1], wall.points[0]))) continue;
|
||||||
|
|
||||||
|
let line: Point[] = [...wall.points];
|
||||||
|
visited.add(key);
|
||||||
|
|
||||||
|
// Try to extend the line forward and backward by matching endpoints
|
||||||
|
let extended = true;
|
||||||
|
while (extended) {
|
||||||
|
extended = false;
|
||||||
|
|
||||||
|
const last = line[line.length - 1];
|
||||||
|
const nextWalls = pointMap.get(last.pointUuid) || [];
|
||||||
|
|
||||||
|
for (const next of nextWalls) {
|
||||||
|
const [n1, n2] = next.points;
|
||||||
|
const nextKey = wallKey(n1, n2);
|
||||||
|
if (visited.has(nextKey) || visited.has(wallKey(n2, n1))) continue;
|
||||||
|
|
||||||
|
if (n1.pointUuid === last.pointUuid && n2.pointUuid !== line[line.length - 2]?.pointUuid) {
|
||||||
|
line.push(n2);
|
||||||
|
visited.add(nextKey);
|
||||||
|
extended = true;
|
||||||
|
break;
|
||||||
|
} else if (n2.pointUuid === last.pointUuid && n1.pointUuid !== line[line.length - 2]?.pointUuid) {
|
||||||
|
line.push(n1);
|
||||||
|
visited.add(nextKey);
|
||||||
|
extended = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert cycles to Point arrays and validate them
|
const first = line[0];
|
||||||
const potentialRooms = allCycles
|
const prevWalls = pointMap.get(first.pointUuid) || [];
|
||||||
.map(cycle => cycle.map(uuid => pointMap.get(uuid)!))
|
|
||||||
.filter(room => isValidRoom(room));
|
|
||||||
|
|
||||||
const uniqueRooms = removeDuplicateRooms(potentialRooms);
|
for (const prev of prevWalls) {
|
||||||
|
const [p1, p2] = prev.points;
|
||||||
|
const prevKey = wallKey(p1, p2);
|
||||||
|
if (visited.has(prevKey) || visited.has(wallKey(p2, p1))) continue;
|
||||||
|
|
||||||
// ✅ New logic that only removes redundant supersets
|
if (p1.pointUuid === first.pointUuid && p2.pointUuid !== line[1]?.pointUuid) {
|
||||||
const filteredRooms = removeSupersetLikeRooms(uniqueRooms, walls);
|
line.unshift(p2);
|
||||||
|
visited.add(prevKey);
|
||||||
|
extended = true;
|
||||||
|
break;
|
||||||
|
} else if (p2.pointUuid === first.pointUuid && p1.pointUuid !== line[1]?.pointUuid) {
|
||||||
|
line.unshift(p1);
|
||||||
|
visited.add(prevKey);
|
||||||
|
extended = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return filteredRooms;
|
// Create merged LineString
|
||||||
|
const coords = line.map(p => [p.position[0], p.position[2]]);
|
||||||
|
mergedLineStrings.push(turf.lineString(coords, {
|
||||||
|
pointUuids: line.map(p => p.pointUuid)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
};
|
const lineStrings = turf.featureCollection(mergedLineStrings);
|
||||||
|
|
||||||
const removeSupersetLikeRooms = (rooms: Point[][], walls: Wall[]): Point[][] => {
|
// Now polygonize merged line strings
|
||||||
const toRemove = new Set<number>();
|
const polygons = turf.polygonize(lineStrings);
|
||||||
|
|
||||||
const getPolygon = (points: Point[]) =>
|
const rooms: Point[][] = [];
|
||||||
turf.polygon([points.map(p => [p.position[0], p.position[2]])]);
|
|
||||||
|
|
||||||
const getWallSet = (room: Point[]) => {
|
polygons.features.forEach(feature => {
|
||||||
const set = new Set<string>();
|
if (feature.geometry.type === 'Polygon') {
|
||||||
for (let i = 0; i < room.length - 1; i++) {
|
const coordinates = feature.geometry.coordinates[0];
|
||||||
const p1 = room[i].pointUuid;
|
const roomPoints: Point[] = [];
|
||||||
const p2 = room[i + 1].pointUuid;
|
|
||||||
|
|
||||||
const wall = walls.find(w =>
|
for (const [x, z] of coordinates) {
|
||||||
(w.points[0].pointUuid === p1 && w.points[1].pointUuid === p2) ||
|
const matchingPoint = walls.flatMap(wall => wall.points)
|
||||||
(w.points[0].pointUuid === p2 && w.points[1].pointUuid === p1)
|
.find(p =>
|
||||||
|
p.position[0].toFixed(10) === x.toFixed(10) &&
|
||||||
|
p.position[2].toFixed(10) === z.toFixed(10)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (wall) {
|
if (matchingPoint) {
|
||||||
set.add(wall.wallUuid);
|
roomPoints.push(matchingPoint);
|
||||||
}
|
|
||||||
}
|
|
||||||
return set;
|
|
||||||
};
|
|
||||||
|
|
||||||
const roomPolygons = rooms.map(getPolygon);
|
|
||||||
const roomAreas = roomPolygons.map(poly => turf.area(poly));
|
|
||||||
const wallSets = rooms.map(getWallSet);
|
|
||||||
|
|
||||||
// First, identify all rooms that are completely contained within others
|
|
||||||
for (let i = 0; i < rooms.length; i++) {
|
|
||||||
for (let j = 0; j < rooms.length; j++) {
|
|
||||||
if (i === j) continue;
|
|
||||||
|
|
||||||
// If room i completely contains room j
|
|
||||||
if (turf.booleanContains(roomPolygons[i], roomPolygons[j])) {
|
|
||||||
// Check if the contained room shares most of its walls with the containing room
|
|
||||||
const sharedWalls = [...wallSets[j]].filter(w => wallSets[i].has(w));
|
|
||||||
const shareRatio = sharedWalls.length / wallSets[j].size;
|
|
||||||
|
|
||||||
// If they share more than 50% of walls, mark the larger one for removal
|
|
||||||
// UNLESS the smaller one is significantly smaller (likely a real room)
|
|
||||||
if (shareRatio > 0.5 && (roomAreas[i] / roomAreas[j] > 2)) {
|
|
||||||
toRemove.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: handle cases where a room is divided by segmented walls
|
if (roomPoints.length > 0 &&
|
||||||
for (let i = 0; i < rooms.length; i++) {
|
roomPoints[0].pointUuid !== roomPoints[roomPoints.length - 1].pointUuid) {
|
||||||
if (toRemove.has(i)) continue;
|
roomPoints.push(roomPoints[0]);
|
||||||
|
|
||||||
for (let j = 0; j < rooms.length; j++) {
|
|
||||||
if (i === j || toRemove.has(j)) continue;
|
|
||||||
|
|
||||||
// Check if these rooms share a significant portion of walls
|
|
||||||
const sharedWalls = [...wallSets[i]].filter(w => wallSets[j].has(w));
|
|
||||||
const shareRatio = Math.max(
|
|
||||||
sharedWalls.length / wallSets[i].size,
|
|
||||||
sharedWalls.length / wallSets[j].size
|
|
||||||
);
|
|
||||||
|
|
||||||
// If they share more than 30% of walls and one is much larger
|
|
||||||
if (shareRatio > 0.3) {
|
|
||||||
const areaRatio = roomAreas[i] / roomAreas[j];
|
|
||||||
if (areaRatio > 2) {
|
|
||||||
// The larger room is likely the undivided version
|
|
||||||
toRemove.add(i);
|
|
||||||
} else if (areaRatio < 0.5) {
|
|
||||||
// The smaller room might be an artifact
|
|
||||||
toRemove.add(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rooms.filter((_, idx) => !toRemove.has(idx));
|
if (roomPoints.length >= 4) {
|
||||||
};
|
rooms.push(roomPoints);
|
||||||
|
}
|
||||||
// Check if a cycle already exists in our list (considering different orders)
|
}
|
||||||
const cycleExists = (allCycles: string[][], newCycle: string[]) => {
|
|
||||||
const newSet = new Set(newCycle);
|
|
||||||
|
|
||||||
return allCycles.some(existingCycle => {
|
|
||||||
if (existingCycle.length !== newCycle.length) return false;
|
|
||||||
const existingSet = new Set(existingCycle);
|
|
||||||
return setsEqual(newSet, existingSet);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return rooms;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if two sets are equal
|
|
||||||
const setsEqual = <T,>(a: Set<T>, b: Set<T>) => {
|
|
||||||
if (a.size !== b.size) return false;
|
|
||||||
for (const item of a) if (!b.has(item)) return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove duplicate rooms (same set of points in different orders)
|
|
||||||
const removeDuplicateRooms = (rooms: Point[][]) => {
|
|
||||||
const uniqueRooms: Point[][] = [];
|
|
||||||
const roomHashes = new Set<string>();
|
|
||||||
|
|
||||||
for (const room of rooms) {
|
|
||||||
// Create a consistent hash for the room regardless of point order
|
|
||||||
const hash = room
|
|
||||||
.map(p => p.pointUuid)
|
|
||||||
.sort()
|
|
||||||
.join('-');
|
|
||||||
|
|
||||||
if (!roomHashes.has(hash)) {
|
|
||||||
roomHashes.add(hash);
|
|
||||||
uniqueRooms.push(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniqueRooms;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if a room is valid (closed, non-self-intersecting polygon)
|
|
||||||
const isValidRoom = (points: Point[]): boolean => {
|
|
||||||
// Must have at least 4 points (first and last are same)
|
|
||||||
if (points.length < 4) return false;
|
|
||||||
|
|
||||||
// Must be a closed loop
|
|
||||||
if (points[0].pointUuid !== points[points.length - 1].pointUuid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const coordinates = points.map(p => [p.position[0], p.position[2]]);
|
|
||||||
const polygon = turf.polygon([coordinates]);
|
|
||||||
return turf.booleanValid(polygon);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rest of the implementation remains the same...
|
|
||||||
const rooms = useMemo(() => findRooms(), [walls]);
|
const rooms = useMemo(() => findRooms(), [walls]);
|
||||||
|
|
||||||
// Determine wall orientation relative to room
|
|
||||||
const findWallType = (wall: Wall) => {
|
const findWallType = (wall: Wall) => {
|
||||||
// Find all rooms that contain this wall
|
|
||||||
const containingRooms = rooms.filter(room => {
|
const containingRooms = rooms.filter(room => {
|
||||||
for (let i = 0; i < room.length - 1; i++) {
|
for (let i = 0; i < room.length - 1; i++) {
|
||||||
const p1 = room[i];
|
const p1 = room[i];
|
||||||
|
@ -244,11 +156,7 @@ export function useWallClassification(walls: Walls) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the other functions to use the new return type
|
const getWallType = (wall: Wall) => {
|
||||||
const getWallType = (wall: Wall): {
|
|
||||||
type: string;
|
|
||||||
rooms: Point[][];
|
|
||||||
} => {
|
|
||||||
return findWallType(wall);
|
return findWallType(wall);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -261,11 +169,32 @@ export function useWallClassification(walls: Walls) {
|
||||||
return findWallType(wall).type === 'segment';
|
return findWallType(wall).type === 'segment';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isWallFlipped = (wall: Wall): boolean => {
|
||||||
|
const wallType = findWallType(wall);
|
||||||
|
if (wallType.type === 'segment') return false;
|
||||||
|
|
||||||
|
for (const room of wallType.rooms) {
|
||||||
|
for (let i = 0; i < room.length - 1; i++) {
|
||||||
|
const p1 = room[i];
|
||||||
|
const p2 = room[i + 1];
|
||||||
|
if (wall.points[0].pointUuid === p1.pointUuid && wall.points[1].pointUuid === p2.pointUuid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (wall.points[0].pointUuid === p2.pointUuid && wall.points[1].pointUuid === p1.pointUuid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rooms,
|
rooms,
|
||||||
getWallType,
|
getWallType,
|
||||||
isRoomWall,
|
isRoomWall,
|
||||||
isSegmentWall,
|
isSegmentWall,
|
||||||
findRooms,
|
findRooms,
|
||||||
|
isWallFlipped
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -2,8 +2,8 @@ import * as THREE from 'three';
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import * as Constants from '../../../../../types/world/worldConstants';
|
import * as Constants from '../../../../../types/world/worldConstants';
|
||||||
|
|
||||||
import insideMaterial from '../../../../../assets/textures/floor/wall-tex.png';
|
import defaultMaterial from '../../../../../assets/textures/floor/wall-tex.png';
|
||||||
import outsideMaterial from '../../../../../assets/textures/floor/factory wall texture.jpg';
|
import material1 from '../../../../../assets/textures/floor/factory wall texture.jpg';
|
||||||
import useWallGeometry from './helpers/useWallGeometry';
|
import useWallGeometry from './helpers/useWallGeometry';
|
||||||
import { useWallStore } from '../../../../../store/builder/useWallStore';
|
import { useWallStore } from '../../../../../store/builder/useWallStore';
|
||||||
import { useWallClassification } from './helpers/useWallClassification';
|
import { useWallClassification } from './helpers/useWallClassification';
|
||||||
|
@ -14,14 +14,18 @@ import { Base } from '@react-three/csg';
|
||||||
|
|
||||||
function Wall({ wall }: { readonly wall: Wall }) {
|
function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
const { walls } = useWallStore();
|
const { walls } = useWallStore();
|
||||||
const { getWallType } = useWallClassification(walls);
|
const { getWallType, isWallFlipped } = useWallClassification(walls);
|
||||||
const wallType = getWallType(wall);
|
const wallType = getWallType(wall);
|
||||||
const [startPoint, endPoint] = wall.points;
|
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
const { wallVisibility } = useWallVisibility();
|
const { wallVisibility } = useWallVisibility();
|
||||||
const meshRef = useRef<any>();
|
const meshRef = useRef<any>();
|
||||||
const { camera } = useThree();
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
const wallFlipped = isWallFlipped(wall);
|
||||||
|
|
||||||
|
const [rawStart, rawEnd] = wall.points;
|
||||||
|
const [startPoint, endPoint] = wallFlipped ? [rawStart, rawEnd] : [rawEnd, rawStart];
|
||||||
|
|
||||||
const startX = startPoint.position[0];
|
const startX = startPoint.position[0];
|
||||||
const startZ = startPoint.position[2];
|
const startZ = startPoint.position[2];
|
||||||
const endX = endPoint.position[0];
|
const endX = endPoint.position[0];
|
||||||
|
@ -36,13 +40,13 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
|
|
||||||
const textureLoader = new THREE.TextureLoader();
|
const textureLoader = new THREE.TextureLoader();
|
||||||
|
|
||||||
const [insideWallTexture, outsideWallTexture] = useMemo(() => {
|
const [defaultWallTexture, material1WallTexture] = useMemo(() => {
|
||||||
const inside = textureLoader.load(insideMaterial);
|
const inside = textureLoader.load(defaultMaterial);
|
||||||
inside.wrapS = inside.wrapT = THREE.RepeatWrapping;
|
inside.wrapS = inside.wrapT = THREE.RepeatWrapping;
|
||||||
inside.repeat.set(wallLength / 10, wall.wallHeight / 10);
|
inside.repeat.set(wallLength / 10, wall.wallHeight / 10);
|
||||||
inside.colorSpace = THREE.SRGBColorSpace;
|
inside.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
const outside = textureLoader.load(outsideMaterial);
|
const outside = textureLoader.load(material1);
|
||||||
outside.wrapS = outside.wrapT = THREE.RepeatWrapping;
|
outside.wrapS = outside.wrapT = THREE.RepeatWrapping;
|
||||||
outside.repeat.set(wallLength / 10, wall.wallHeight / 10);
|
outside.repeat.set(wallLength / 10, wall.wallHeight / 10);
|
||||||
outside.colorSpace = THREE.SRGBColorSpace;
|
outside.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
@ -51,27 +55,24 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
}, [wallLength, wall.wallHeight]);
|
}, [wallLength, wall.wallHeight]);
|
||||||
|
|
||||||
const materials = useMemo(() => {
|
const materials = useMemo(() => {
|
||||||
const frontMaterial = insideWallTexture;
|
|
||||||
|
|
||||||
const backMaterial = outsideWallTexture;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new THREE.MeshStandardMaterial({
|
new THREE.MeshStandardMaterial({
|
||||||
color: Constants.wallConfig.defaultColor,
|
color: Constants.wallConfig.defaultColor,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
map: frontMaterial
|
map: wall.insideMaterial === 'Default Material' ? defaultWallTexture : material1WallTexture,
|
||||||
}),
|
}),
|
||||||
new THREE.MeshStandardMaterial({
|
new THREE.MeshStandardMaterial({
|
||||||
color: Constants.wallConfig.defaultColor,
|
color: Constants.wallConfig.defaultColor,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
map: backMaterial
|
map: wall.outsideMaterial === 'Default Material' ? defaultWallTexture : material1WallTexture,
|
||||||
}),
|
}),
|
||||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Left
|
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Left
|
||||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Right
|
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Right
|
||||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Top
|
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }), // Top
|
||||||
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }) // Bottom
|
new THREE.MeshStandardMaterial({ color: Constants.wallConfig.defaultColor, side: THREE.DoubleSide }) // Bottom
|
||||||
];
|
];
|
||||||
}, [insideWallTexture, outsideWallTexture, wall]);
|
}, [defaultWallTexture, material1WallTexture, wall]);
|
||||||
|
|
||||||
const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness);
|
const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness);
|
||||||
|
|
||||||
|
@ -83,6 +84,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
if (!wallVisibility && wallType.type === 'room') {
|
if (!wallVisibility && wallType.type === 'room') {
|
||||||
meshRef.current.getWorldDirection(v);
|
meshRef.current.getWorldDirection(v);
|
||||||
camera.getWorldDirection(u);
|
camera.getWorldDirection(u);
|
||||||
|
if (!u || !v) return;
|
||||||
setVisible((2 * v.dot(u)) <= 0.1);
|
setVisible((2 * v.dot(u)) <= 0.1);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -112,7 +114,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
scale={[decal.decalScale, decal.decalScale, 0.001]}
|
scale={[decal.decalScale, decal.decalScale, 0.001]}
|
||||||
>
|
>
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
map={outsideWallTexture}
|
map={material1WallTexture}
|
||||||
side={THREE.DoubleSide}
|
side={THREE.DoubleSide}
|
||||||
polygonOffset
|
polygonOffset
|
||||||
polygonOffsetFactor={-1}
|
polygonOffsetFactor={-1}
|
||||||
|
|
|
@ -26,7 +26,7 @@ function ReferenceWall({ tempPoints }: Readonly<ReferenceWallProps>) {
|
||||||
const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
|
const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
|
||||||
|
|
||||||
const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null);
|
const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null);
|
||||||
const { checkSnapForWall } = usePointSnapping({ uuid: 'temp-wall', pointType: 'Wall', position: directionalSnap.position || [0, 0, 0] });
|
const { snapWallPoint } = usePointSnapping({ uuid: 'temp-wall', pointType: 'Wall', position: directionalSnap.position || [0, 0, 0] });
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
if (toolMode === 'Wall' && toggleView && tempPoints.length === 1) {
|
if (toolMode === 'Wall' && toggleView && tempPoints.length === 1) {
|
||||||
|
@ -37,7 +37,7 @@ function ReferenceWall({ tempPoints }: Readonly<ReferenceWallProps>) {
|
||||||
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||||
|
|
||||||
if (intersectionPoint) {
|
if (intersectionPoint) {
|
||||||
const snapped = checkSnapForWall([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
const snapped = snapWallPoint([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
|
||||||
|
|
||||||
if (snapped.isSnapped && snapped.snappedPoint) {
|
if (snapped.isSnapped && snapped.snappedPoint) {
|
||||||
finalPosition.current = snapped.position;
|
finalPosition.current = snapped.position;
|
||||||
|
@ -68,8 +68,8 @@ function ReferenceWall({ tempPoints }: Readonly<ReferenceWallProps>) {
|
||||||
setTempWall({
|
setTempWall({
|
||||||
wallUuid: 'temp-wall',
|
wallUuid: 'temp-wall',
|
||||||
points: wallPoints,
|
points: wallPoints,
|
||||||
outSideMaterial: 'default',
|
outsideMaterial: 'default',
|
||||||
inSideMaterial: 'default',
|
insideMaterial: 'default',
|
||||||
wallThickness: wallThickness,
|
wallThickness: wallThickness,
|
||||||
wallHeight: wallHeight,
|
wallHeight: wallHeight,
|
||||||
decals: []
|
decals: []
|
||||||
|
|
|
@ -2,10 +2,12 @@ import * as THREE from 'three'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from '@react-three/fiber';
|
||||||
import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
|
import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
|
||||||
|
import * as Constants from '../../../../types/world/worldConstants';
|
||||||
import { useWallStore } from '../../../../store/builder/useWallStore';
|
import { useWallStore } from '../../../../store/builder/useWallStore';
|
||||||
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
||||||
import ReferencePoint from '../../point/reference/referencePoint';
|
import ReferencePoint from '../../point/reference/referencePoint';
|
||||||
import ReferenceWall from './referenceWall';
|
import ReferenceWall from './referenceWall';
|
||||||
|
import getClosestIntersection from '../../geomentries/lines/getClosestIntersection';
|
||||||
|
|
||||||
function WallCreator() {
|
function WallCreator() {
|
||||||
const { scene, camera, raycaster, gl, pointer } = useThree();
|
const { scene, camera, raycaster, gl, pointer } = useThree();
|
||||||
|
@ -14,10 +16,10 @@ function WallCreator() {
|
||||||
const { toolMode } = useToolMode();
|
const { toolMode } = useToolMode();
|
||||||
const { activeLayer } = useActiveLayer();
|
const { activeLayer } = useActiveLayer();
|
||||||
const { socket } = useSocketStore();
|
const { socket } = useSocketStore();
|
||||||
const { addWall, getWallPointById } = useWallStore();
|
const { addWall, getWallPointById, removeWall, getWallByPoints } = useWallStore();
|
||||||
const drag = useRef(false);
|
const drag = useRef(false);
|
||||||
const isLeftMouseDown = useRef(false);
|
const isLeftMouseDown = useRef(false);
|
||||||
const { wallThickness, wallHeight, snappedPosition, snappedPoint } = useBuilderStore();
|
const { wallThickness, wallHeight, insideMaterial, outsideMaterial, snappedPosition, snappedPoint } = useBuilderStore();
|
||||||
|
|
||||||
const [tempPoints, setTempPoints] = useState<Point[]>([]);
|
const [tempPoints, setTempPoints] = useState<Point[]>([]);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
@ -52,7 +54,96 @@ function WallCreator() {
|
||||||
let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||||
if (!position) return;
|
if (!position) return;
|
||||||
|
|
||||||
const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Point');
|
const pointIntersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Point');
|
||||||
|
|
||||||
|
const wallIntersect = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Line');
|
||||||
|
if (wallIntersect && !pointIntersects) {
|
||||||
|
const wall = getWallByPoints(wallIntersect.object.userData.points);
|
||||||
|
if (wall) {
|
||||||
|
const ThroughPoint = wallIntersect.object.userData.path.getPoints(Constants.lineConfig.lineIntersectionPoints);
|
||||||
|
let intersectionPoint = getClosestIntersection(ThroughPoint, wallIntersect.point);
|
||||||
|
const point1Vec = new THREE.Vector3(...wall.points[0].position);
|
||||||
|
const point2Vec = new THREE.Vector3(...wall.points[1].position);
|
||||||
|
|
||||||
|
const lineDir = new THREE.Vector3().subVectors(point2Vec, point1Vec).normalize();
|
||||||
|
|
||||||
|
const point1ToIntersect = new THREE.Vector3().subVectors(intersectionPoint, point1Vec);
|
||||||
|
|
||||||
|
const dotProduct = point1ToIntersect.dot(lineDir);
|
||||||
|
const projection = new THREE.Vector3().copy(lineDir).multiplyScalar(dotProduct).add(point1Vec);
|
||||||
|
|
||||||
|
const lineLength = point1Vec.distanceTo(point2Vec);
|
||||||
|
let t = point1Vec.distanceTo(projection) / lineLength;
|
||||||
|
t = Math.max(0, Math.min(1, t));
|
||||||
|
|
||||||
|
const closestPoint = new THREE.Vector3().lerpVectors(point1Vec, point2Vec, t);
|
||||||
|
|
||||||
|
removeWall(wall.wallUuid);
|
||||||
|
|
||||||
|
const point1: Point = {
|
||||||
|
pointUuid: wall.points[0].pointUuid,
|
||||||
|
pointType: 'Wall',
|
||||||
|
position: wall.points[0].position,
|
||||||
|
layer: wall.points[0].layer
|
||||||
|
};
|
||||||
|
|
||||||
|
const point2: Point = {
|
||||||
|
pointUuid: wall.points[1].pointUuid,
|
||||||
|
pointType: 'Wall',
|
||||||
|
position: wall.points[1].position,
|
||||||
|
layer: wall.points[1].layer
|
||||||
|
};
|
||||||
|
|
||||||
|
const newPoint: Point = {
|
||||||
|
pointUuid: THREE.MathUtils.generateUUID(),
|
||||||
|
pointType: 'Wall',
|
||||||
|
position: closestPoint.toArray(),
|
||||||
|
layer: activeLayer
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tempPoints.length === 0) {
|
||||||
|
setTempPoints([newPoint]);
|
||||||
|
setIsCreating(true);
|
||||||
|
} else {
|
||||||
|
const wall1: Wall = {
|
||||||
|
wallUuid: THREE.MathUtils.generateUUID(),
|
||||||
|
points: [tempPoints[0], newPoint],
|
||||||
|
outsideMaterial: insideMaterial,
|
||||||
|
insideMaterial: outsideMaterial,
|
||||||
|
wallThickness: wallThickness,
|
||||||
|
wallHeight: wallHeight,
|
||||||
|
decals: []
|
||||||
|
};
|
||||||
|
addWall(wall1);
|
||||||
|
|
||||||
|
const wall2: Wall = {
|
||||||
|
wallUuid: THREE.MathUtils.generateUUID(),
|
||||||
|
points: [point1, newPoint],
|
||||||
|
outsideMaterial: insideMaterial,
|
||||||
|
insideMaterial: outsideMaterial,
|
||||||
|
wallThickness: wallThickness,
|
||||||
|
wallHeight: wallHeight,
|
||||||
|
decals: []
|
||||||
|
}
|
||||||
|
addWall(wall2);
|
||||||
|
|
||||||
|
const wall3: Wall = {
|
||||||
|
wallUuid: THREE.MathUtils.generateUUID(),
|
||||||
|
points: [point2, newPoint],
|
||||||
|
outsideMaterial: insideMaterial,
|
||||||
|
insideMaterial: outsideMaterial,
|
||||||
|
wallThickness: wallThickness,
|
||||||
|
wallHeight: wallHeight,
|
||||||
|
decals: []
|
||||||
|
}
|
||||||
|
addWall(wall3);
|
||||||
|
|
||||||
|
setTempPoints([newPoint]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newPoint: Point = {
|
const newPoint: Point = {
|
||||||
pointUuid: THREE.MathUtils.generateUUID(),
|
pointUuid: THREE.MathUtils.generateUUID(),
|
||||||
|
@ -71,8 +162,8 @@ function WallCreator() {
|
||||||
newPoint.position = snappedPosition;
|
newPoint.position = snappedPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intersects && !snappedPoint) {
|
if (pointIntersects && !snappedPoint) {
|
||||||
const point = getWallPointById(intersects.object.uuid);
|
const point = getWallPointById(pointIntersects.object.uuid);
|
||||||
if (point) {
|
if (point) {
|
||||||
newPoint.pointUuid = point.pointUuid;
|
newPoint.pointUuid = point.pointUuid;
|
||||||
newPoint.position = point.position;
|
newPoint.position = point.position;
|
||||||
|
@ -87,8 +178,8 @@ function WallCreator() {
|
||||||
const wall: Wall = {
|
const wall: Wall = {
|
||||||
wallUuid: THREE.MathUtils.generateUUID(),
|
wallUuid: THREE.MathUtils.generateUUID(),
|
||||||
points: [tempPoints[0], newPoint],
|
points: [tempPoints[0], newPoint],
|
||||||
outSideMaterial: 'default',
|
outsideMaterial: insideMaterial,
|
||||||
inSideMaterial: 'default',
|
insideMaterial: outsideMaterial,
|
||||||
wallThickness: wallThickness,
|
wallThickness: wallThickness,
|
||||||
wallHeight: wallHeight,
|
wallHeight: wallHeight,
|
||||||
decals: []
|
decals: []
|
||||||
|
@ -114,10 +205,8 @@ function WallCreator() {
|
||||||
canvasElement.addEventListener("click", onMouseClick);
|
canvasElement.addEventListener("click", onMouseClick);
|
||||||
canvasElement.addEventListener("contextmenu", onContext);
|
canvasElement.addEventListener("contextmenu", onContext);
|
||||||
} else {
|
} else {
|
||||||
if (tempPoints.length > 0 || isCreating) {
|
|
||||||
setTempPoints([]);
|
setTempPoints([]);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
}
|
|
||||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||||
|
|
|
@ -13,9 +13,12 @@ interface BuilderState {
|
||||||
|
|
||||||
wallThickness: number;
|
wallThickness: number;
|
||||||
wallHeight: number;
|
wallHeight: number;
|
||||||
|
outsideMaterial: string;
|
||||||
|
insideMaterial: string;
|
||||||
|
|
||||||
setWallThickness: (thickness: number) => void;
|
setWallThickness: (thickness: number) => void;
|
||||||
setWallHeight: (height: number) => void;
|
setWallHeight: (height: number) => void;
|
||||||
|
setWallMaterial: (material: string, side: 'inside' | 'outside') => void;
|
||||||
|
|
||||||
// Aisle
|
// Aisle
|
||||||
|
|
||||||
|
@ -82,6 +85,8 @@ export const useBuilderStore = create<BuilderState>()(
|
||||||
|
|
||||||
wallThickness: 0.1,
|
wallThickness: 0.1,
|
||||||
wallHeight: 7,
|
wallHeight: 7,
|
||||||
|
outsideMaterial: 'Default Material',
|
||||||
|
insideMaterial: 'Default Material',
|
||||||
|
|
||||||
setWallThickness: (thickness: number) => {
|
setWallThickness: (thickness: number) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
@ -95,6 +100,13 @@ export const useBuilderStore = create<BuilderState>()(
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setWallMaterial: (material: string, side: 'inside' | 'outside') => {
|
||||||
|
set((state) => {
|
||||||
|
if (side === 'outside') state.outsideMaterial = material;
|
||||||
|
else state.insideMaterial = material;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Aisle
|
// Aisle
|
||||||
|
|
||||||
selectedAisle: null,
|
selectedAisle: null,
|
||||||
|
|
|
@ -19,6 +19,8 @@ interface WallStore {
|
||||||
setLayer: (pointUuid: string, layer: number) => void;
|
setLayer: (pointUuid: string, layer: number) => void;
|
||||||
|
|
||||||
getWallById: (uuid: string) => Wall | undefined;
|
getWallById: (uuid: string) => Wall | undefined;
|
||||||
|
getWallByPointId: (uuid: string) => Wall | undefined;
|
||||||
|
getWallByPoints: (points: Point[]) => Wall | undefined;
|
||||||
getWallPointById: (uuid: string) => Point | undefined;
|
getWallPointById: (uuid: string) => Point | undefined;
|
||||||
getConnectedPoints: (uuid: string) => Point[];
|
getConnectedPoints: (uuid: string) => Point[];
|
||||||
}
|
}
|
||||||
|
@ -135,6 +137,25 @@ export const useWallStore = create<WallStore>()(
|
||||||
return get().walls.find(w => w.wallUuid === uuid);
|
return get().walls.find(w => w.wallUuid === uuid);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getWallByPointId: (uuid) => {
|
||||||
|
for (const wall of get().walls) {
|
||||||
|
if (wall.points.some(p => p.pointUuid === uuid)) {
|
||||||
|
return wall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
getWallByPoints: (point) => {
|
||||||
|
for (const wall of get().walls) {
|
||||||
|
if (((wall.points[0].pointUuid === point[0].pointUuid) || (wall.points[1].pointUuid === point[0].pointUuid)) &&
|
||||||
|
((wall.points[0].pointUuid === point[1].pointUuid) || (wall.points[1].pointUuid === point[1].pointUuid))) {
|
||||||
|
return wall;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
getWallPointById: (uuid) => {
|
getWallPointById: (uuid) => {
|
||||||
for (const wall of get().walls) {
|
for (const wall of get().walls) {
|
||||||
const point = wall.points.find(p => p.pointUuid === uuid);
|
const point = wall.points.find(p => p.pointUuid === uuid);
|
||||||
|
|
|
@ -59,8 +59,8 @@ interface Decal {
|
||||||
interface Wall {
|
interface Wall {
|
||||||
wallUuid: string;
|
wallUuid: string;
|
||||||
points: [Point, Point];
|
points: [Point, Point];
|
||||||
outSideMaterial: string;
|
outsideMaterial: string;
|
||||||
inSideMaterial: string;
|
insideMaterial: string;
|
||||||
wallThickness: number;
|
wallThickness: number;
|
||||||
wallHeight: number;
|
wallHeight: number;
|
||||||
decals: Decal[]
|
decals: Decal[]
|
||||||
|
|
Loading…
Reference in New Issue