feat: enhance wall classification and add decal management in wall store
This commit is contained in:
parent
796a4ace45
commit
81664ba765
|
@ -1,5 +1,4 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import * as THREE from 'three';
|
|
||||||
import * as turf from '@turf/turf';
|
import * as turf from '@turf/turf';
|
||||||
|
|
||||||
export function useWallClassification(walls: Walls) {
|
export function useWallClassification(walls: Walls) {
|
||||||
|
@ -27,12 +26,10 @@ export function useWallClassification(walls: Walls) {
|
||||||
|
|
||||||
// Find all minimal cycles (rooms) in the graph
|
// Find all minimal cycles (rooms) in the graph
|
||||||
const allCycles: string[][] = [];
|
const allCycles: string[][] = [];
|
||||||
const visited = new Set<string>();
|
|
||||||
|
|
||||||
const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => {
|
const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => {
|
||||||
if (depth > 20) return; // Prevent infinite recursion
|
if (depth > 20) return; // Prevent infinite recursion
|
||||||
|
|
||||||
visited.add(currentNode);
|
|
||||||
path.push(currentNode);
|
path.push(currentNode);
|
||||||
|
|
||||||
const neighbors = graph.get(currentNode) || [];
|
const neighbors = graph.get(currentNode) || [];
|
||||||
|
@ -65,10 +62,92 @@ export function useWallClassification(walls: Walls) {
|
||||||
.map(cycle => cycle.map(uuid => pointMap.get(uuid)!))
|
.map(cycle => cycle.map(uuid => pointMap.get(uuid)!))
|
||||||
.filter(room => isValidRoom(room));
|
.filter(room => isValidRoom(room));
|
||||||
|
|
||||||
// Filter out duplicate rooms (same set of points in different orders)
|
|
||||||
const uniqueRooms = removeDuplicateRooms(potentialRooms);
|
const uniqueRooms = removeDuplicateRooms(potentialRooms);
|
||||||
|
|
||||||
return uniqueRooms;
|
// ✅ New logic that only removes redundant supersets
|
||||||
|
const filteredRooms = removeSupersetLikeRooms(uniqueRooms, walls);
|
||||||
|
|
||||||
|
return filteredRooms;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSupersetLikeRooms = (rooms: Point[][], walls: Wall[]): Point[][] => {
|
||||||
|
const toRemove = new Set<number>();
|
||||||
|
|
||||||
|
const getPolygon = (points: Point[]) =>
|
||||||
|
turf.polygon([points.map(p => [p.position[0], p.position[2]])]);
|
||||||
|
|
||||||
|
const getWallSet = (room: Point[]) => {
|
||||||
|
const set = new Set<string>();
|
||||||
|
for (let i = 0; i < room.length - 1; i++) {
|
||||||
|
const p1 = room[i].pointUuid;
|
||||||
|
const p2 = room[i + 1].pointUuid;
|
||||||
|
|
||||||
|
const wall = walls.find(w =>
|
||||||
|
(w.points[0].pointUuid === p1 && w.points[1].pointUuid === p2) ||
|
||||||
|
(w.points[0].pointUuid === p2 && w.points[1].pointUuid === p1)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wall) {
|
||||||
|
set.add(wall.wallUuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
for (let i = 0; i < rooms.length; i++) {
|
||||||
|
if (toRemove.has(i)) continue;
|
||||||
|
|
||||||
|
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));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if a cycle already exists in our list (considering different orders)
|
// Check if a cycle already exists in our list (considering different orders)
|
||||||
|
@ -132,48 +211,10 @@ export function useWallClassification(walls: Walls) {
|
||||||
// Rest of the implementation remains the same...
|
// Rest of the implementation remains the same...
|
||||||
const rooms = useMemo(() => findRooms(), [walls]);
|
const rooms = useMemo(() => findRooms(), [walls]);
|
||||||
|
|
||||||
const createPolygon = (points: Point[]) => {
|
|
||||||
const coordinates = points.map(p => [p.position[0], p.position[2]]);
|
|
||||||
return turf.polygon([coordinates]);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Get all walls that form part of any room
|
|
||||||
const getRoomWalls = (): Wall[] => {
|
|
||||||
const roomWalls = new Set<Wall>();
|
|
||||||
|
|
||||||
rooms.forEach(room => {
|
|
||||||
for (let i = 0; i < room.length - 1; i++) {
|
|
||||||
const p1 = room[i];
|
|
||||||
const p2 = room[i + 1];
|
|
||||||
|
|
||||||
// Find the wall that connects these two points
|
|
||||||
const wall = walls.find(w =>
|
|
||||||
(w.points[0].pointUuid === p1.pointUuid && w.points[1].pointUuid === p2.pointUuid) ||
|
|
||||||
(w.points[0].pointUuid === p2.pointUuid && w.points[1].pointUuid === p1.pointUuid)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (wall) roomWalls.add(wall);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(roomWalls);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine wall orientation relative to room
|
// Determine wall orientation relative to room
|
||||||
const getWallOrientation = (wall: Wall) => {
|
const findWallType = (wall: Wall) => {
|
||||||
const roomWalls = getRoomWalls();
|
// Find all rooms that contain this wall
|
||||||
const isRoomWall = roomWalls.includes(wall);
|
const containingRooms = rooms.filter(room => {
|
||||||
|
|
||||||
if (!isRoomWall) {
|
|
||||||
return {
|
|
||||||
isRoomWall: false,
|
|
||||||
isOutsideFacing: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find which room this wall belongs to
|
|
||||||
const containingRoom = rooms.find(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];
|
||||||
const p2 = room[i + 1];
|
const p2 = room[i + 1];
|
||||||
|
@ -185,79 +226,39 @@ export function useWallClassification(walls: Walls) {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!containingRoom) {
|
if (containingRooms.length === 0) {
|
||||||
return {
|
return {
|
||||||
isRoomWall: false,
|
type: 'segment',
|
||||||
isOutsideFacing: false
|
rooms: []
|
||||||
|
};
|
||||||
|
} else if (containingRooms.length === 1) {
|
||||||
|
return {
|
||||||
|
type: 'room',
|
||||||
|
rooms: containingRooms
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
type: 'rooms',
|
||||||
|
rooms: containingRooms
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the normal vector to determine outside direction
|
|
||||||
const roomPoints = containingRoom.map(p => new THREE.Vector3(p.position[0], 0, p.position[2]));
|
|
||||||
const centroid = new THREE.Vector3();
|
|
||||||
roomPoints.forEach(p => centroid.add(p));
|
|
||||||
centroid.divideScalar(roomPoints.length);
|
|
||||||
|
|
||||||
const [p1, p2] = wall.points;
|
|
||||||
const wallVector = new THREE.Vector3(
|
|
||||||
p2.position[0] - p1.position[0],
|
|
||||||
0,
|
|
||||||
p2.position[2] - p1.position[2]
|
|
||||||
);
|
|
||||||
const normal = new THREE.Vector3(-wallVector.z, 0, wallVector.x).normalize();
|
|
||||||
|
|
||||||
// Check if normal points away from centroid (outside)
|
|
||||||
const testPoint = new THREE.Vector3(
|
|
||||||
(p1.position[0] + p2.position[0]) / 2 + normal.x,
|
|
||||||
0,
|
|
||||||
(p1.position[2] + p2.position[2]) / 2 + normal.z
|
|
||||||
);
|
|
||||||
|
|
||||||
const pointInside = turf.booleanPointInPolygon(
|
|
||||||
turf.point([testPoint.x, testPoint.z]),
|
|
||||||
createPolygon(containingRoom)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Determine if the wall is in the same order as the room's edge
|
|
||||||
let isSameOrder = false;
|
|
||||||
for (let i = 0; i < containingRoom.length - 1; i++) {
|
|
||||||
const roomP1 = containingRoom[i];
|
|
||||||
const roomP2 = containingRoom[i + 1];
|
|
||||||
if (p1.pointUuid === roomP1.pointUuid && p2.pointUuid === roomP2.pointUuid) {
|
|
||||||
isSameOrder = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isRoomWall: true,
|
|
||||||
isOutsideFacing: isSameOrder ? !pointInside : pointInside
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rest of the functions remain the same...
|
// Update the other functions to use the new return type
|
||||||
const getWallType = (wall: Wall): 'room' | 'segment' => {
|
const getWallType = (wall: Wall): {
|
||||||
return getWallOrientation(wall).isRoomWall ? 'room' : 'segment';
|
type: string;
|
||||||
|
rooms: Point[][];
|
||||||
|
} => {
|
||||||
|
return findWallType(wall);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRoomWall = (wall: Wall): boolean => {
|
const isRoomWall = (wall: Wall): boolean => {
|
||||||
return getWallOrientation(wall).isRoomWall;
|
const type = findWallType(wall).type;
|
||||||
|
return type === 'room' || type === 'rooms';
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSegmentWall = (wall: Wall): boolean => {
|
const isSegmentWall = (wall: Wall): boolean => {
|
||||||
return !getWallOrientation(wall).isRoomWall;
|
return findWallType(wall).type === 'segment';
|
||||||
};
|
|
||||||
|
|
||||||
const getWallMaterialSide = (wall: Wall): { front: 'inside' | 'outside', back: 'inside' | 'outside' } => {
|
|
||||||
const orientation = getWallOrientation(wall);
|
|
||||||
|
|
||||||
if (!orientation.isRoomWall) {
|
|
||||||
return { front: 'inside', back: 'outside' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return orientation.isOutsideFacing
|
|
||||||
? { front: 'outside', back: 'inside' }
|
|
||||||
: { front: 'inside', back: 'outside' };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -265,7 +266,6 @@ export function useWallClassification(walls: Walls) {
|
||||||
getWallType,
|
getWallType,
|
||||||
isRoomWall,
|
isRoomWall,
|
||||||
isSegmentWall,
|
isSegmentWall,
|
||||||
getWallMaterialSide,
|
|
||||||
findRooms,
|
findRooms,
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useMemo } 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 insideMaterial from '../../../../../assets/textures/floor/wall-tex.png';
|
||||||
|
@ -7,13 +7,20 @@ import outsideMaterial from '../../../../../assets/textures/floor/factory wall t
|
||||||
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';
|
||||||
|
import { useFrame, useThree } from '@react-three/fiber';
|
||||||
|
import { useWallVisibility } from '../../../../../store/builder/store';
|
||||||
|
import { Decal } from '@react-three/drei';
|
||||||
|
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 { getWallMaterialSide, isRoomWall, rooms } = useWallClassification(walls);
|
const { getWallType } = useWallClassification(walls);
|
||||||
console.log('rooms: ', rooms);
|
const wallType = getWallType(wall);
|
||||||
const materialSide = getWallMaterialSide(wall);
|
|
||||||
const [startPoint, endPoint] = wall.points;
|
const [startPoint, endPoint] = wall.points;
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
const { wallVisibility } = useWallVisibility();
|
||||||
|
const meshRef = useRef<any>();
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
const startX = startPoint.position[0];
|
const startX = startPoint.position[0];
|
||||||
const startZ = startPoint.position[2];
|
const startZ = startPoint.position[2];
|
||||||
|
@ -44,14 +51,9 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
}, [wallLength, wall.wallHeight]);
|
}, [wallLength, wall.wallHeight]);
|
||||||
|
|
||||||
const materials = useMemo(() => {
|
const materials = useMemo(() => {
|
||||||
// For segment walls (not in a room), use inside material on both sides
|
const frontMaterial = insideWallTexture;
|
||||||
const frontMaterial = isRoomWall(wall)
|
|
||||||
? (materialSide.front === 'inside' ? insideWallTexture : outsideWallTexture)
|
|
||||||
: insideWallTexture;
|
|
||||||
|
|
||||||
const backMaterial = isRoomWall(wall)
|
const backMaterial = outsideWallTexture;
|
||||||
? (materialSide.back === 'inside' ? insideWallTexture : outsideWallTexture)
|
|
||||||
: insideWallTexture;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
new THREE.MeshStandardMaterial({
|
new THREE.MeshStandardMaterial({
|
||||||
|
@ -69,41 +71,58 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
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, materialSide, isRoomWall, wall]);
|
}, [insideWallTexture, outsideWallTexture, wall]);
|
||||||
|
|
||||||
const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness);
|
const geometry = useWallGeometry(wallLength, wall.wallHeight, wall.wallThickness);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const v = new THREE.Vector3();
|
||||||
|
const u = new THREE.Vector3();
|
||||||
|
|
||||||
|
if (!wallVisibility && wallType.type === 'room') {
|
||||||
|
meshRef.current.getWorldDirection(v);
|
||||||
|
camera.getWorldDirection(u);
|
||||||
|
setVisible((2 * v.dot(u)) <= 0.1);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group
|
||||||
name={`Wall-${wall.wallUuid}`}
|
name={`Wall-${wall.wallUuid}`}
|
||||||
userData={wall}
|
userData={wall}
|
||||||
position={[centerX, centerY, centerZ]}
|
position={[centerX, centerY, centerZ]}
|
||||||
rotation={[0, -angle, 0]}
|
rotation={[0, -angle, 0]}
|
||||||
|
visible={visible}
|
||||||
>
|
>
|
||||||
<mesh geometry={geometry}>
|
<Base ref={meshRef} geometry={geometry} visible>
|
||||||
{materials.map((material, index) => (
|
{materials.map((material, index) => (
|
||||||
<primitive key={index} object={material} attach={`material-${index}`} />
|
<primitive key={index} object={material} attach={`material-${index}`} />
|
||||||
))}
|
))}
|
||||||
</mesh>
|
|
||||||
|
{wall.decals.map((decal) => {
|
||||||
|
return (
|
||||||
|
<Decal
|
||||||
|
// debug
|
||||||
|
position={[decal.decalPosition[0], decal.decalPosition[1], wall.wallThickness / 2]}
|
||||||
|
rotation={[0, 0, decal.decalRotation]}
|
||||||
|
scale={[decal.decalScale, decal.decalScale, 0.001]}
|
||||||
|
>
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={outsideWallTexture}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
polygonOffset
|
||||||
|
polygonOffsetFactor={-1}
|
||||||
|
/>
|
||||||
|
</Decal>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Base>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Wall;
|
export default Wall;
|
||||||
|
|
||||||
|
|
||||||
// A--------------------------B--------------G
|
|
||||||
// | | |
|
|
||||||
// | | |
|
|
||||||
// | | |
|
|
||||||
// | | |
|
|
||||||
// | | |
|
|
||||||
// | | |
|
|
||||||
// F--------------------------C--------------H
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// E--------------------------D
|
|
|
@ -6,7 +6,6 @@ import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
||||||
import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/builder/store';
|
import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/builder/store';
|
||||||
import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping';
|
import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping';
|
||||||
import { usePointSnapping } from '../../point/helpers/usePointSnapping';
|
import { usePointSnapping } from '../../point/helpers/usePointSnapping';
|
||||||
import * as Constants from '../../../../types/world/worldConstants';
|
|
||||||
import ReferenceLine from '../../line/reference/referenceLine';
|
import ReferenceLine from '../../line/reference/referenceLine';
|
||||||
|
|
||||||
interface ReferenceWallProps {
|
interface ReferenceWallProps {
|
||||||
|
@ -73,6 +72,7 @@ function ReferenceWall({ tempPoints }: Readonly<ReferenceWallProps>) {
|
||||||
inSideMaterial: 'default',
|
inSideMaterial: 'default',
|
||||||
wallThickness: wallThickness,
|
wallThickness: wallThickness,
|
||||||
wallHeight: wallHeight,
|
wallHeight: wallHeight,
|
||||||
|
decals: []
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,8 @@ function WallCreator() {
|
||||||
outSideMaterial: 'default',
|
outSideMaterial: 'default',
|
||||||
inSideMaterial: 'default',
|
inSideMaterial: 'default',
|
||||||
wallThickness: wallThickness,
|
wallThickness: wallThickness,
|
||||||
wallHeight: wallHeight
|
wallHeight: wallHeight,
|
||||||
|
decals: []
|
||||||
};
|
};
|
||||||
addWall(wall);
|
addWall(wall);
|
||||||
setTempPoints([newPoint]);
|
setTempPoints([newPoint]);
|
||||||
|
|
|
@ -35,32 +35,32 @@ const SelectionControls: React.FC = () => {
|
||||||
const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]);
|
const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]);
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
|
|
||||||
|
const isDragging = useRef(false);
|
||||||
|
const isLeftMouseDown = useRef(false);
|
||||||
|
const isSelecting = useRef(false);
|
||||||
|
const isRightClick = useRef(false);
|
||||||
|
const rightClickMoved = useRef(false);
|
||||||
|
const isCtrlSelecting = useRef(false);
|
||||||
|
const isShiftSelecting = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!camera || !scene || toggleView) return;
|
if (!camera || !scene || toggleView) return;
|
||||||
|
|
||||||
const canvasElement = gl.domElement;
|
const canvasElement = gl.domElement;
|
||||||
canvasElement.tabIndex = 0;
|
canvasElement.tabIndex = 0;
|
||||||
|
|
||||||
let isDragging = false;
|
|
||||||
let isLeftMouseDown = false;
|
|
||||||
let isSelecting = false;
|
|
||||||
let isRightClick = false;
|
|
||||||
let rightClickMoved = false;
|
|
||||||
let isCtrlSelecting = false;
|
|
||||||
let isShiftSelecting = false;
|
|
||||||
|
|
||||||
const helper = new SelectionHelper(gl);
|
const helper = new SelectionHelper(gl);
|
||||||
|
|
||||||
const onPointerDown = (event: PointerEvent) => {
|
const onPointerDown = (event: PointerEvent) => {
|
||||||
if (event.button === 2) {
|
if (event.button === 2) {
|
||||||
isRightClick = true;
|
isRightClick.current = true;
|
||||||
rightClickMoved = false;
|
rightClickMoved.current = false;
|
||||||
} else if (event.button === 0) {
|
} else if (event.button === 0) {
|
||||||
isSelecting = false;
|
isSelecting.current = false;
|
||||||
isCtrlSelecting = event.ctrlKey;
|
isCtrlSelecting.current = event.ctrlKey;
|
||||||
isShiftSelecting = event.shiftKey;
|
isShiftSelecting.current = event.shiftKey;
|
||||||
isLeftMouseDown = true;
|
isLeftMouseDown.current = true;
|
||||||
isDragging = false;
|
isDragging.current = false;
|
||||||
if (event.ctrlKey && duplicatedObjects.length === 0) {
|
if (event.ctrlKey && duplicatedObjects.length === 0) {
|
||||||
if (controls) (controls as any).enabled = false;
|
if (controls) (controls as any).enabled = false;
|
||||||
selectionBox.startPoint.set(pointer.x, pointer.y, 0);
|
selectionBox.startPoint.set(pointer.x, pointer.y, 0);
|
||||||
|
@ -69,45 +69,46 @@ const SelectionControls: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
const onPointerMove = (event: PointerEvent) => {
|
||||||
if (isRightClick) {
|
if (isRightClick.current) {
|
||||||
rightClickMoved = true;
|
rightClickMoved.current = true;
|
||||||
}
|
}
|
||||||
if (isLeftMouseDown) {
|
if (isLeftMouseDown.current) {
|
||||||
isDragging = true;
|
isDragging.current = true;
|
||||||
}
|
}
|
||||||
isSelecting = true;
|
isSelecting.current = true;
|
||||||
if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting) {
|
if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting.current) {
|
||||||
selectionBox.endPoint.set(pointer.x, pointer.y, 0);
|
selectionBox.endPoint.set(pointer.x, pointer.y, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerUp = (event: PointerEvent) => {
|
const onPointerUp = (event: PointerEvent) => {
|
||||||
if (event.button === 2 && !event.ctrlKey && !event.shiftKey) {
|
if (event.button === 2 && !event.ctrlKey && !event.shiftKey) {
|
||||||
isRightClick = false;
|
isRightClick.current = false;
|
||||||
if (!rightClickMoved) {
|
rightClickMoved.current = false;
|
||||||
|
if (!rightClickMoved.current) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSelecting && isCtrlSelecting) {
|
if (isSelecting.current && isCtrlSelecting.current) {
|
||||||
isCtrlSelecting = false;
|
isCtrlSelecting.current = false;
|
||||||
isSelecting = false;
|
isSelecting.current = false;
|
||||||
if (event.ctrlKey && duplicatedObjects.length === 0) {
|
if (event.ctrlKey && duplicatedObjects.length === 0) {
|
||||||
selectAssets();
|
selectAssets();
|
||||||
}
|
}
|
||||||
} else if (!isSelecting && selectedAssets.length > 0 && ((!event.ctrlKey && !event.shiftKey && pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) {
|
} else if (!isSelecting.current && selectedAssets.length > 0 && ((!event.ctrlKey && !event.shiftKey && pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
helper.enabled = true;
|
helper.enabled = true;
|
||||||
isCtrlSelecting = false;
|
isCtrlSelecting.current = false;
|
||||||
} else if (controls) {
|
} else if (controls) {
|
||||||
(controls as any).enabled = true;
|
(controls as any).enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDragging && isLeftMouseDown && isShiftSelecting && event.shiftKey) {
|
if (!isDragging.current && isLeftMouseDown.current && isShiftSelecting.current && event.shiftKey) {
|
||||||
isShiftSelecting = false;
|
isShiftSelecting.current = false;
|
||||||
isLeftMouseDown = false;
|
isLeftMouseDown.current = false;
|
||||||
isDragging = false;
|
isDragging.current = false;
|
||||||
|
|
||||||
raycaster.setFromCamera(pointer, camera);
|
raycaster.setFromCamera(pointer, camera);
|
||||||
const intersects = raycaster.intersectObjects(scene.children, true)
|
const intersects = raycaster.intersectObjects(scene.children, true)
|
||||||
|
@ -167,7 +168,7 @@ const SelectionControls: React.FC = () => {
|
||||||
|
|
||||||
const onContextMenu = (event: MouseEvent) => {
|
const onContextMenu = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!rightClickMoved) {
|
if (!rightClickMoved.current) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -207,6 +208,7 @@ const SelectionControls: React.FC = () => {
|
||||||
}, [activeModule]);
|
}, [activeModule]);
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
|
console.log(rightClickMoved.current);
|
||||||
if (pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
if (pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
||||||
selectionGroup.current.position.set(0, 0, 0);
|
selectionGroup.current.position.set(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,12 @@ interface WallStore {
|
||||||
addWall: (wall: Wall) => void;
|
addWall: (wall: Wall) => void;
|
||||||
updateWall: (uuid: string, updated: Partial<Wall>) => void;
|
updateWall: (uuid: string, updated: Partial<Wall>) => void;
|
||||||
removeWall: (uuid: string) => void;
|
removeWall: (uuid: string) => void;
|
||||||
|
addDecal: (wallUuid: string, decal: Decal) => void;
|
||||||
|
updateDecal: (decalUuid: string, decal: Decal) => void;
|
||||||
|
removeDecal: (decalUuid: string) => void;
|
||||||
|
updateDecalPosition: (decalUuid: string, position: [number, number, number]) => void;
|
||||||
|
updateDecalRotation: (decalUuid: string, rotation: number) => void;
|
||||||
|
updateDecalScale: (decalUuid: string, scale: number) => void;
|
||||||
|
|
||||||
removePoint: (pointUuid: string) => Wall[];
|
removePoint: (pointUuid: string) => Wall[];
|
||||||
setPosition: (pointUuid: string, position: [number, number, number]) => void;
|
setPosition: (pointUuid: string, position: [number, number, number]) => void;
|
||||||
|
@ -40,6 +46,58 @@ export const useWallStore = create<WallStore>()(
|
||||||
state.walls = state.walls.filter(w => w.wallUuid !== uuid);
|
state.walls = state.walls.filter(w => w.wallUuid !== uuid);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
addDecal: (wallUuid, decal) => set((state) => {
|
||||||
|
const wallToUpdate = state.walls.find(w => w.wallUuid === wallUuid);
|
||||||
|
if (wallToUpdate) {
|
||||||
|
wallToUpdate.decals.push(decal);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateDecal: (decalUuid, decal) => set((state) => {
|
||||||
|
for (const wall of state.walls) {
|
||||||
|
const decalToUpdate = wall.decals.find(d => d.decalUuid === decalUuid);
|
||||||
|
if (decalToUpdate) {
|
||||||
|
Object.assign(decalToUpdate, decal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeDecal: (decalUuid) => set((state) => {
|
||||||
|
for (const wall of state.walls) {
|
||||||
|
wall.decals = wall.decals.filter(d => d.decalUuid !== decalUuid);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateDecalPosition: (decalUuid, position) => set((state) => {
|
||||||
|
for (const wall of state.walls) {
|
||||||
|
const decal = wall.decals.find(d => d.decalUuid === decalUuid);
|
||||||
|
if (decal) {
|
||||||
|
decal.decalPosition = position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateDecalRotation: (decalUuid, rotation) => set((state) => {
|
||||||
|
for (const wall of state.walls) {
|
||||||
|
const decal = wall.decals.find(d => d.decalUuid === decalUuid);
|
||||||
|
if (decal) {
|
||||||
|
decal.decalRotation = rotation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateDecalScale: (decalUuid, scale) => set((state) => {
|
||||||
|
for (const wall of state.walls) {
|
||||||
|
const decal = wall.decals.find(d => d.decalUuid === decalUuid);
|
||||||
|
if (decal) {
|
||||||
|
decal.decalScale = scale;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
removePoint: (pointUuid) => {
|
removePoint: (pointUuid) => {
|
||||||
const removedWalls: Wall[] = [];
|
const removedWalls: Wall[] = [];
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
// Asset
|
// Asset
|
||||||
|
|
||||||
interface Asset {
|
interface Asset {
|
||||||
modelUuid: string;
|
modelUuid: string;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
assetId: string;
|
assetId: string;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
rotation: [number, number, number];
|
rotation: [number, number, number];
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
isCollidable: boolean;
|
isCollidable: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
animations?: string[];
|
animations?: string[];
|
||||||
animationState?: {
|
animationState?: {
|
||||||
current: string;
|
current: string;
|
||||||
playing: boolean;
|
playing: boolean;
|
||||||
};
|
};
|
||||||
eventData?: {
|
eventData?: {
|
||||||
type: string;
|
type: string;
|
||||||
point?: {
|
point?: {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
position: [number, number, number];
|
position: [number, number, number];
|
||||||
rotation: [number, number, number];
|
rotation: [number, number, number];
|
||||||
|
};
|
||||||
|
points?: {
|
||||||
|
uuid: string;
|
||||||
|
position: [number, number, number];
|
||||||
|
rotation: [number, number, number];
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
points?: {
|
|
||||||
uuid: string;
|
|
||||||
position: [number, number, number];
|
|
||||||
rotation: [number, number, number];
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Assets = Asset[];
|
type Assets = Asset[];
|
||||||
|
@ -47,6 +47,15 @@ interface Point {
|
||||||
|
|
||||||
// Wall
|
// Wall
|
||||||
|
|
||||||
|
interface Decal {
|
||||||
|
decalUuid: string;
|
||||||
|
decalName: string;
|
||||||
|
decalId: string;
|
||||||
|
decalPosition: [number, number, number];
|
||||||
|
decalRotation: number;
|
||||||
|
decalScale: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface Wall {
|
interface Wall {
|
||||||
wallUuid: string;
|
wallUuid: string;
|
||||||
points: [Point, Point];
|
points: [Point, Point];
|
||||||
|
@ -54,6 +63,7 @@ interface Wall {
|
||||||
inSideMaterial: string;
|
inSideMaterial: string;
|
||||||
wallThickness: number;
|
wallThickness: number;
|
||||||
wallHeight: number;
|
wallHeight: number;
|
||||||
|
decals: Decal[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Walls = Wall[];
|
type Walls = Wall[];
|
||||||
|
|
Loading…
Reference in New Issue