v3-ui #98
|
@ -6,21 +6,24 @@ import useModuleStore from '../../../store/useModuleStore';
|
||||||
import CompareLayOut from '../../ui/compareVersion/CompareLayOut';
|
import CompareLayOut from '../../ui/compareVersion/CompareLayOut';
|
||||||
import ComparisonResult from '../../ui/compareVersion/ComparisonResult';
|
import ComparisonResult from '../../ui/compareVersion/ComparisonResult';
|
||||||
import { useComparisonProduct } from '../../../store/simulation/useSimulationStore';
|
import { useComparisonProduct } from '../../../store/simulation/useSimulationStore';
|
||||||
import { usePlayButtonStore } from '../../../store/usePlayButtonStore';
|
import { usePauseButtonStore, usePlayButtonStore } from '../../../store/usePlayButtonStore';
|
||||||
|
|
||||||
function ComparisonScene() {
|
function ComparisonScene() {
|
||||||
const { isPlaying } = usePlayButtonStore();
|
const { isPlaying, setIsPlaying } = usePlayButtonStore();
|
||||||
const { products } = useProductStore();
|
const { products } = useProductStore();
|
||||||
const { isVersionSaved } = useSaveVersion();
|
const { isVersionSaved } = useSaveVersion();
|
||||||
const { activeModule } = useModuleStore();
|
const { activeModule } = useModuleStore();
|
||||||
const { selectedProductStore } = useProductContext();
|
const { selectedProductStore } = useProductContext();
|
||||||
const { selectedProduct } = selectedProductStore();
|
const { selectedProduct } = selectedProductStore();
|
||||||
const { comparisonProduct, setComparisonProduct } = useComparisonProduct();
|
const { comparisonProduct, setComparisonProduct } = useComparisonProduct();
|
||||||
|
const { setIsPaused } = usePauseButtonStore();
|
||||||
|
|
||||||
const handleSelectLayout = (option: string) => {
|
const handleSelectLayout = (option: string) => {
|
||||||
const product = products.find((product) => product.productName === option);
|
const product = products.find((product) => product.productName === option);
|
||||||
if (product) {
|
if (product) {
|
||||||
setComparisonProduct(product.productId, product.productName);
|
setComparisonProduct(product.productId, product.productName);
|
||||||
|
setIsPlaying(true);
|
||||||
|
setIsPaused(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -18,7 +18,7 @@ import ControlsPlayer from '../controls/ControlsPlayer';
|
||||||
import SelectFloorPlan from '../../temporary/SelectFloorPlan';
|
import SelectFloorPlan from '../../temporary/SelectFloorPlan';
|
||||||
import { createHandleDrop } from '../../../modules/visualization/functions/handleUiDrop';
|
import { createHandleDrop } from '../../../modules/visualization/functions/handleUiDrop';
|
||||||
import Scene from '../../../modules/scene/scene';
|
import Scene from '../../../modules/scene/scene';
|
||||||
import { useMainProduct } from '../../../store/simulation/useSimulationStore';
|
import { useComparisonProduct, useMainProduct } from '../../../store/simulation/useSimulationStore';
|
||||||
import { useProductContext } from '../../../modules/simulation/products/productContext';
|
import { useProductContext } from '../../../modules/simulation/products/productContext';
|
||||||
import { useProductStore } from '../../../store/simulation/useProductStore';
|
import { useProductStore } from '../../../store/simulation/useProductStore';
|
||||||
import RegularDropDown from '../../ui/inputs/RegularDropDown';
|
import RegularDropDown from '../../ui/inputs/RegularDropDown';
|
||||||
|
@ -38,6 +38,7 @@ function MainScene() {
|
||||||
const { visualizationSocket } = useSocketStore();
|
const { visualizationSocket } = useSocketStore();
|
||||||
const { selectedZone } = useSelectedZoneStore();
|
const { selectedZone } = useSelectedZoneStore();
|
||||||
const { setFloatingWidget } = useFloatingWidget();
|
const { setFloatingWidget } = useFloatingWidget();
|
||||||
|
const { comparisonProduct } = useComparisonProduct();
|
||||||
|
|
||||||
const handleSelectLayout = (option: string) => {
|
const handleSelectLayout = (option: string) => {
|
||||||
const product = products.find((product) => product.productName === option);
|
const product = products.find((product) => product.productName === option);
|
||||||
|
@ -64,8 +65,8 @@ function MainScene() {
|
||||||
{activeModule !== "market" && !isPlaying && !isVersionSaved && (
|
{activeModule !== "market" && !isPlaying && !isVersionSaved && (
|
||||||
<Tools />
|
<Tools />
|
||||||
)}
|
)}
|
||||||
{isPlaying && activeModule === "simulation" && <SimulationPlayer />}
|
{(isPlaying || comparisonProduct !== null) && activeModule === "simulation" && <SimulationPlayer />}
|
||||||
{isPlaying && activeModule !== "simulation" && <ControlsPlayer />}
|
{(isPlaying || comparisonProduct !== null) && activeModule !== "simulation" && <ControlsPlayer />}
|
||||||
|
|
||||||
{/* remove this later */}
|
{/* remove this later */}
|
||||||
{activeModule === "builder" && !toggleThreeD && <SelectFloorPlan />}
|
{activeModule === "builder" && !toggleThreeD && <SelectFloorPlan />}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import OuterClick from "../../../utils/outerClick";
|
||||||
import { useProductStore } from "../../../store/simulation/useProductStore";
|
import { useProductStore } from "../../../store/simulation/useProductStore";
|
||||||
import Scene from "../../../modules/scene/scene";
|
import Scene from "../../../modules/scene/scene";
|
||||||
import { useComparisonProduct } from "../../../store/simulation/useSimulationStore";
|
import { useComparisonProduct } from "../../../store/simulation/useSimulationStore";
|
||||||
|
import { usePauseButtonStore, usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||||
|
|
||||||
const CompareLayOut = () => {
|
const CompareLayOut = () => {
|
||||||
const { comparisonProduct, setComparisonProduct, clearComparisonProduct } = useComparisonProduct();
|
const { comparisonProduct, setComparisonProduct, clearComparisonProduct } = useComparisonProduct();
|
||||||
|
@ -22,6 +23,8 @@ const CompareLayOut = () => {
|
||||||
const startWidthRef = useRef<number>(0);
|
const startWidthRef = useRef<number>(0);
|
||||||
const startXRef = useRef<number>(0);
|
const startXRef = useRef<number>(0);
|
||||||
const { setIsVersionSaved } = useSaveVersion();
|
const { setIsVersionSaved } = useSaveVersion();
|
||||||
|
const { setIsPlaying } = usePlayButtonStore();
|
||||||
|
const { setIsPaused } = usePauseButtonStore();
|
||||||
|
|
||||||
OuterClick({
|
OuterClick({
|
||||||
contextClassName: ["displayLayouts-container", "selectLayout"],
|
contextClassName: ["displayLayouts-container", "selectLayout"],
|
||||||
|
@ -64,6 +67,7 @@ const CompareLayOut = () => {
|
||||||
setWidth("0px");
|
setWidth("0px");
|
||||||
setIsVersionSaved(false);
|
setIsVersionSaved(false);
|
||||||
clearComparisonProduct();
|
clearComparisonProduct();
|
||||||
|
setIsPlaying(false);
|
||||||
} else {
|
} else {
|
||||||
setWidth(`${finalWidthVw}vw`);
|
setWidth(`${finalWidthVw}vw`);
|
||||||
}
|
}
|
||||||
|
@ -109,6 +113,8 @@ const CompareLayOut = () => {
|
||||||
if (product) {
|
if (product) {
|
||||||
setComparisonProduct(product.productId, product.productName);
|
setComparisonProduct(product.productId, product.productName);
|
||||||
setLoadingProgress(1);
|
setLoadingProgress(1);
|
||||||
|
setIsPlaying(true);
|
||||||
|
setIsPaused(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import ProductionCapacity from "../analysis/ThroughputSummary";
|
||||||
import ThroughputSummary from "../analysis/ProductionCapacity";
|
import ThroughputSummary from "../analysis/ProductionCapacity";
|
||||||
import ROISummary from "../analysis/ROISummary";
|
import ROISummary from "../analysis/ROISummary";
|
||||||
import { usePlayerStore } from "../../../store/useUIToggleStore";
|
import { usePlayerStore } from "../../../store/useUIToggleStore";
|
||||||
|
import { useComparisonProduct } from "../../../store/simulation/useSimulationStore";
|
||||||
|
|
||||||
const SimulationPlayer: React.FC = () => {
|
const SimulationPlayer: React.FC = () => {
|
||||||
const MAX_SPEED = 4; // Maximum speed
|
const MAX_SPEED = 4; // Maximum speed
|
||||||
|
@ -31,7 +32,6 @@ const SimulationPlayer: React.FC = () => {
|
||||||
const isDragging = useRef(false);
|
const isDragging = useRef(false);
|
||||||
const sliderRef = useRef<HTMLDivElement>(null);
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
const [expand, setExpand] = useState(true);
|
const [expand, setExpand] = useState(true);
|
||||||
const [playSimulation, setPlaySimulation] = useState(false);
|
|
||||||
const { hidePlayer, setHidePlayer } = usePlayerStore();
|
const { hidePlayer, setHidePlayer } = usePlayerStore();
|
||||||
|
|
||||||
const { speed, setSpeed } = useAnimationPlaySpeed();
|
const { speed, setSpeed } = useAnimationPlaySpeed();
|
||||||
|
@ -40,6 +40,7 @@ const SimulationPlayer: React.FC = () => {
|
||||||
const { isPaused, setIsPaused } = usePauseButtonStore();
|
const { isPaused, setIsPaused } = usePauseButtonStore();
|
||||||
const { isReset, setReset } = useResetButtonStore();
|
const { isReset, setReset } = useResetButtonStore();
|
||||||
const { subModule } = useSubModuleStore();
|
const { subModule } = useSubModuleStore();
|
||||||
|
const { clearComparisonProduct } = useComparisonProduct();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReset) {
|
if (isReset) {
|
||||||
|
@ -55,17 +56,15 @@ const SimulationPlayer: React.FC = () => {
|
||||||
setIsPaused(false);
|
setIsPaused(false);
|
||||||
setSpeed(1);
|
setSpeed(1);
|
||||||
echo.info("Simulation RESET.....");
|
echo.info("Simulation RESET.....");
|
||||||
setPlaySimulation(false); // local state reset
|
|
||||||
};
|
};
|
||||||
const handlePlayStop = () => {
|
const handlePlayStop = () => {
|
||||||
setIsPaused(!isPaused);
|
setIsPaused(!isPaused);
|
||||||
echo.warn(`Simulation is ${isPaused ? "Resumed" : "Paused"}`);
|
echo.warn(`Simulation is ${isPaused ? "Resumed" : "Paused"}`);
|
||||||
setPlaySimulation(!playSimulation);
|
|
||||||
};
|
};
|
||||||
const handleExit = () => {
|
const handleExit = () => {
|
||||||
setPlaySimulation(false);
|
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setIsPaused(false);
|
setIsPaused(false);
|
||||||
|
clearComparisonProduct();
|
||||||
setActiveTool("cursor");
|
setActiveTool("cursor");
|
||||||
echo.info("Exit Simulation");
|
echo.info("Exit Simulation");
|
||||||
};
|
};
|
||||||
|
@ -210,7 +209,7 @@ const SimulationPlayer: React.FC = () => {
|
||||||
{!hidePlayer && subModule !== "analysis" && (
|
{!hidePlayer && subModule !== "analysis" && (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
{playSimulation
|
{isPaused
|
||||||
? "Paused - system idle."
|
? "Paused - system idle."
|
||||||
: "Running simulation..."}
|
: "Running simulation..."}
|
||||||
</div>
|
</div>
|
||||||
|
@ -237,7 +236,7 @@ const SimulationPlayer: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlayStopIcon />
|
<PlayStopIcon />
|
||||||
{playSimulation ? "Play" : "Stop"}
|
{isPaused ? "Play" : "Stop"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
@ -298,11 +297,10 @@ const SimulationPlayer: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
{index < intervals.length - 1 && (
|
{index < intervals.length - 1 && (
|
||||||
<div
|
<div
|
||||||
className={`line ${
|
className={`line ${progress >= ((index + 1) / totalSegments) * 100
|
||||||
progress >= ((index + 1) / totalSegments) * 100
|
? "filled"
|
||||||
? "filled"
|
: ""
|
||||||
: ""
|
}`}
|
||||||
}`}
|
|
||||||
></div>
|
></div>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -342,9 +340,8 @@ const SimulationPlayer: React.FC = () => {
|
||||||
<div className="custom-slider">
|
<div className="custom-slider">
|
||||||
<button
|
<button
|
||||||
id="slider-handle"
|
id="slider-handle"
|
||||||
className={`slider-handle ${
|
className={`slider-handle ${isDragging ? "dragging" : ""
|
||||||
isDragging ? "dragging" : ""
|
}`}
|
||||||
}`}
|
|
||||||
style={{ left: `${calculateHandlePosition()}%` }}
|
style={{ left: `${calculateHandlePosition()}%` }}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
>
|
>
|
||||||
|
|
|
@ -51,6 +51,7 @@ import DxfFile from "./dfx/LoadBlueprint";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import AislesGroup from "./aisle/aislesGroup";
|
import AislesGroup from "./aisle/aislesGroup";
|
||||||
import WallGroup from "./wall/wallGroup";
|
import WallGroup from "./wall/wallGroup";
|
||||||
|
import { useBuilderStore } from "../../store/builder/useBuilderStore";
|
||||||
|
|
||||||
export default function Builder() {
|
export default function Builder() {
|
||||||
const state = useThree<Types.ThreeState>(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements.
|
const state = useThree<Types.ThreeState>(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements.
|
||||||
|
@ -117,6 +118,7 @@ export default function Builder() {
|
||||||
const { setWalls } = useWalls();
|
const { setWalls } = useWalls();
|
||||||
const { refTextupdate, setRefTextUpdate } = useRefTextUpdate();
|
const { refTextupdate, setRefTextUpdate } = useRefTextUpdate();
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
|
const { setHoveredPoint } = useBuilderStore();
|
||||||
|
|
||||||
// const loader = new GLTFLoader();
|
// const loader = new GLTFLoader();
|
||||||
// const dracoLoader = new DRACOLoader();
|
// const dracoLoader = new DRACOLoader();
|
||||||
|
@ -138,6 +140,7 @@ export default function Builder() {
|
||||||
dragPointControls
|
dragPointControls
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
setHoveredPoint(null);
|
||||||
setToolMode(null);
|
setToolMode(null);
|
||||||
setDeletePointOrLine(false);
|
setDeletePointOrLine(false);
|
||||||
loadWalls(lines, setWalls);
|
loadWalls(lines, setWalls);
|
||||||
|
|
|
@ -46,6 +46,9 @@ function Line({ points }: Readonly<LineProps>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tube
|
<Tube
|
||||||
|
key={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||||
|
uuid={`${points[0].pointUuid}-${points[1].pointUuid}`}
|
||||||
|
userData={points}
|
||||||
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} />
|
||||||
|
|
|
@ -122,6 +122,12 @@ function Point({ point }: { readonly point: Point }) {
|
||||||
setHoveredPoint(null);
|
setHoveredPoint(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (point.pointType === 'Wall') {
|
||||||
|
const removedAisles = removeWallPoint(point.pointUuid);
|
||||||
|
if (removedAisles.length > 0) {
|
||||||
|
setHoveredPoint(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +151,7 @@ function Point({ point }: { readonly point: Point }) {
|
||||||
<mesh
|
<mesh
|
||||||
key={point.pointUuid}
|
key={point.pointUuid}
|
||||||
uuid={point.pointUuid}
|
uuid={point.pointUuid}
|
||||||
name='Aisle-Point'
|
name={`${point.pointType}-Point`}
|
||||||
position={new THREE.Vector3(...point.position)}
|
position={new THREE.Vector3(...point.position)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePointClick(point);
|
handlePointClick(point);
|
||||||
|
|
|
@ -3,168 +3,263 @@ 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) {
|
||||||
// Find all rooms from the given 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
|
||||||
|
const graph = new Map<string, string[]>();
|
||||||
const pointMap = new Map<string, Point>();
|
const pointMap = new Map<string, Point>();
|
||||||
const connections = new Map<string, string[]>();
|
|
||||||
|
|
||||||
// Build connection graph
|
|
||||||
walls.forEach(wall => {
|
walls.forEach(wall => {
|
||||||
const [p1, p2] = wall.points;
|
const [p1, p2] = wall.points;
|
||||||
if (!pointMap.has(p1.pointUuid)) pointMap.set(p1.pointUuid, p1);
|
pointMap.set(p1.pointUuid, p1);
|
||||||
if (!pointMap.has(p2.pointUuid)) pointMap.set(p2.pointUuid, p2);
|
pointMap.set(p2.pointUuid, p2);
|
||||||
|
|
||||||
if (!connections.has(p1.pointUuid)) connections.set(p1.pointUuid, []);
|
// Add connection from p1 to p2
|
||||||
if (!connections.has(p2.pointUuid)) connections.set(p2.pointUuid, []);
|
if (!graph.has(p1.pointUuid)) graph.set(p1.pointUuid, []);
|
||||||
|
graph.get(p1.pointUuid)?.push(p2.pointUuid);
|
||||||
|
|
||||||
connections.get(p1.pointUuid)?.push(p2.pointUuid);
|
// Add connection from p2 to p1
|
||||||
connections.get(p2.pointUuid)?.push(p1.pointUuid);
|
if (!graph.has(p2.pointUuid)) graph.set(p2.pointUuid, []);
|
||||||
|
graph.get(p2.pointUuid)?.push(p1.pointUuid);
|
||||||
});
|
});
|
||||||
console.log('connections: ', connections);
|
|
||||||
|
|
||||||
|
// Find all minimal cycles (rooms) in the graph
|
||||||
|
const allCycles: string[][] = [];
|
||||||
const visited = new Set<string>();
|
const visited = new Set<string>();
|
||||||
const rooms: Point[][] = [];
|
|
||||||
|
|
||||||
// Modified DFS to find all possible cycles
|
const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => {
|
||||||
const findAllCycles = (current: string, path: string[]) => {
|
if (depth > 20) return; // Prevent infinite recursion
|
||||||
visited.add(current);
|
|
||||||
path.push(current);
|
visited.add(currentNode);
|
||||||
|
path.push(currentNode);
|
||||||
|
|
||||||
|
const neighbors = graph.get(currentNode) || [];
|
||||||
|
|
||||||
const neighbors = connections.get(current) || [];
|
|
||||||
for (const neighbor of neighbors) {
|
for (const neighbor of neighbors) {
|
||||||
if (path.length > 2 && neighbor === path[0]) {
|
if (path.length > 2 && neighbor === startNode) {
|
||||||
// Found a cycle (potential room)
|
// Found a cycle that returns to start
|
||||||
const roomPoints = [...path, neighbor].map(uuid => pointMap.get(uuid)!);
|
const cycle = [...path, neighbor];
|
||||||
|
|
||||||
// Check if this room is valid and not already found
|
// Check if this is a new unique cycle
|
||||||
if (isValidRoom(roomPoints) && !roomAlreadyExists(rooms, roomPoints)) {
|
if (!cycleExists(allCycles, cycle)) {
|
||||||
rooms.push(roomPoints);
|
allCycles.push(cycle);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path.includes(neighbor)) {
|
if (!path.includes(neighbor)) {
|
||||||
findAllCycles(neighbor, [...path]);
|
findCycles(startNode, neighbor, [...path], depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start from each point to find all possible cycles
|
// Start cycle detection from each node
|
||||||
for (const [pointUuid] of connections) {
|
for (const [pointUuid] of graph) {
|
||||||
if (!visited.has(pointUuid)) {
|
findCycles(pointUuid, pointUuid, []);
|
||||||
findAllCycles(pointUuid, []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rooms;
|
// Convert cycles to Point arrays and validate them
|
||||||
|
const potentialRooms = allCycles
|
||||||
|
.map(cycle => cycle.map(uuid => pointMap.get(uuid)!))
|
||||||
|
.filter(room => isValidRoom(room));
|
||||||
|
|
||||||
|
// Filter out duplicate rooms (same set of points in different orders)
|
||||||
|
const uniqueRooms = removeDuplicateRooms(potentialRooms);
|
||||||
|
|
||||||
|
return uniqueRooms;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to check if room is valid
|
// Check if a cycle already exists in our list (considering different orders)
|
||||||
const isValidRoom = (roomPoints: Point[]) => {
|
const cycleExists = (allCycles: string[][], newCycle: string[]) => {
|
||||||
if (roomPoints.length < 4) return false;
|
const newSet = new Set(newCycle);
|
||||||
const coordinates = roomPoints.map(p => [p.position[0], p.position[2]]);
|
|
||||||
const polygon = turf.polygon([coordinates]);
|
|
||||||
return turf.booleanValid(polygon);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to check for duplicate rooms
|
return allCycles.some(existingCycle => {
|
||||||
const roomAlreadyExists = (existingRooms: Point[][], newRoom: Point[]) => {
|
if (existingCycle.length !== newCycle.length) return false;
|
||||||
const newRoomIds = newRoom.map(p => p.pointUuid).sort().join('-');
|
const existingSet = new Set(existingCycle);
|
||||||
return existingRooms.some(room => {
|
return setsEqual(newSet, existingSet);
|
||||||
const roomIds = room.map(p => p.pointUuid).sort().join('-');
|
|
||||||
return roomIds === newRoomIds;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const rooms = useMemo(() => findRooms(), [walls]);
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
// Modified to track all rooms a wall belongs to
|
// Remove duplicate rooms (same set of points in different orders)
|
||||||
const getWallOrientation = (wall: Wall) => {
|
const removeDuplicateRooms = (rooms: Point[][]) => {
|
||||||
const [p1, p2] = wall.points;
|
const uniqueRooms: Point[][] = [];
|
||||||
const orientations: Array<{ isOutsideFacing: boolean }> = [];
|
const roomHashes = new Set<string>();
|
||||||
|
|
||||||
for (const room of rooms) {
|
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 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++) {
|
for (let i = 0; i < room.length - 1; i++) {
|
||||||
const roomP1 = room[i];
|
const p1 = room[i];
|
||||||
const roomP2 = room[i + 1];
|
const p2 = room[i + 1];
|
||||||
|
|
||||||
// Check both directions
|
// Find the wall that connects these two points
|
||||||
if ((p1.pointUuid === roomP1.pointUuid && p2.pointUuid === roomP2.pointUuid) ||
|
const wall = walls.find(w =>
|
||||||
(p1.pointUuid === roomP2.pointUuid && p2.pointUuid === roomP1.pointUuid)) {
|
(w.points[0].pointUuid === p1.pointUuid && w.points[1].pointUuid === p2.pointUuid) ||
|
||||||
|
(w.points[0].pointUuid === p2.pointUuid && w.points[1].pointUuid === p1.pointUuid)
|
||||||
|
);
|
||||||
|
|
||||||
const roomPoints = room.map(p => new THREE.Vector3(p.position[0], 0, p.position[2]));
|
if (wall) roomWalls.add(wall);
|
||||||
const centroid = new THREE.Vector3();
|
}
|
||||||
roomPoints.forEach(p => centroid.add(p));
|
});
|
||||||
centroid.divideScalar(roomPoints.length);
|
|
||||||
|
|
||||||
const wallVector = new THREE.Vector3(
|
return Array.from(roomWalls);
|
||||||
p2.position[0] - p1.position[0],
|
};
|
||||||
0,
|
|
||||||
p2.position[2] - p1.position[2]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Normal depends on wall direction
|
// Determine wall orientation relative to room
|
||||||
let normal = new THREE.Vector3(-wallVector.z, 0, wallVector.x).normalize();
|
const getWallOrientation = (wall: Wall) => {
|
||||||
if (p1.pointUuid === roomP2.pointUuid) {
|
const roomWalls = getRoomWalls();
|
||||||
normal = new THREE.Vector3(wallVector.z, 0, -wallVector.x).normalize();
|
const isRoomWall = roomWalls.includes(wall);
|
||||||
}
|
|
||||||
|
|
||||||
const testPoint = new THREE.Vector3(
|
if (!isRoomWall) {
|
||||||
(p1.position[0] + p2.position[0]) / 2 + normal.x,
|
return {
|
||||||
0,
|
isRoomWall: false,
|
||||||
(p1.position[2] + p2.position[2]) / 2 + normal.z
|
isOutsideFacing: false
|
||||||
);
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const pointInside = turf.booleanPointInPolygon(
|
// Find which room this wall belongs to
|
||||||
turf.point([testPoint.x, testPoint.z]),
|
const containingRoom = rooms.find(room => {
|
||||||
turf.polygon([room.map(p => [p.position[0], p.position[2]])])
|
for (let i = 0; i < room.length - 1; i++) {
|
||||||
);
|
const p1 = room[i];
|
||||||
|
const p2 = room[i + 1];
|
||||||
orientations.push({
|
if ((wall.points[0].pointUuid === p1.pointUuid && wall.points[1].pointUuid === p2.pointUuid) ||
|
||||||
isOutsideFacing: !pointInside
|
(wall.points[0].pointUuid === p2.pointUuid && wall.points[1].pointUuid === p1.pointUuid)) {
|
||||||
});
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!containingRoom) {
|
||||||
|
return {
|
||||||
|
isRoomWall: false,
|
||||||
|
isOutsideFacing: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
return {
|
||||||
isRoomWall: orientations.length > 0,
|
isRoomWall: true,
|
||||||
orientations // Now tracks all orientations for walls in multiple rooms
|
isOutsideFacing: isSameOrder ? !pointInside : pointInside
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWallMaterialSide = (wall: Wall) => {
|
// Rest of the functions remain the same...
|
||||||
|
const getWallType = (wall: Wall): 'room' | 'segment' => {
|
||||||
|
return getWallOrientation(wall).isRoomWall ? 'room' : 'segment';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRoomWall = (wall: Wall): boolean => {
|
||||||
|
return getWallOrientation(wall).isRoomWall;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSegmentWall = (wall: Wall): boolean => {
|
||||||
|
return !getWallOrientation(wall).isRoomWall;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWallMaterialSide = (wall: Wall): { front: 'inside' | 'outside', back: 'inside' | 'outside' } => {
|
||||||
const orientation = getWallOrientation(wall);
|
const orientation = getWallOrientation(wall);
|
||||||
|
|
||||||
if (!orientation.isRoomWall) {
|
if (!orientation.isRoomWall) {
|
||||||
return { front: 'inside', back: 'inside' }; // Both sides same for segment walls
|
return { front: 'inside', back: 'outside' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// For walls in multiple rooms, we need to determine which side faces which room
|
return orientation.isOutsideFacing
|
||||||
if (orientation.orientations.length === 2) {
|
? { front: 'outside', back: 'inside' }
|
||||||
// Wall is between two rooms - one side faces each room's interior
|
: { front: 'inside', back: 'outside' };
|
||||||
return {
|
|
||||||
front: 'inside',
|
|
||||||
back: 'inside'
|
|
||||||
};
|
|
||||||
} else if (orientation.orientations.length === 1) {
|
|
||||||
// Wall is part of only one room (exterior wall)
|
|
||||||
return orientation.orientations[0].isOutsideFacing
|
|
||||||
? { front: 'outside', back: 'inside' }
|
|
||||||
: { front: 'inside', back: 'outside' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default case (shouldn't normally happen)
|
|
||||||
return { front: 'inside', back: 'inside' };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rest of the functions remain the same
|
|
||||||
const getWallType = (wall: Wall) => getWallOrientation(wall).isRoomWall ? 'room' : 'segment';
|
|
||||||
const isRoomWall = (wall: Wall) => getWallOrientation(wall).isRoomWall;
|
|
||||||
const isSegmentWall = (wall: Wall) => !getWallOrientation(wall).isRoomWall;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rooms,
|
rooms,
|
||||||
getWallType,
|
getWallType,
|
||||||
|
|
|
@ -41,7 +41,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
outside.colorSpace = THREE.SRGBColorSpace;
|
outside.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
return [inside, outside];
|
return [inside, outside];
|
||||||
}, [wallLength, wall.wallHeight, textureLoader]);
|
}, [wallLength, wall.wallHeight]);
|
||||||
|
|
||||||
const materials = useMemo(() => {
|
const materials = useMemo(() => {
|
||||||
// For segment walls (not in a room), use inside material on both sides
|
// For segment walls (not in a room), use inside material on both sides
|
||||||
|
@ -89,4 +89,21 @@ function Wall({ wall }: { readonly wall: Wall }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Wall;
|
export default Wall;
|
||||||
|
|
||||||
|
|
||||||
|
// A--------------------------B--------------G
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// F--------------------------C--------------H
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// E--------------------------D
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useWallStore } from '../../../../store/builder/useWallStore'
|
import { useWallStore } from '../../../../store/builder/useWallStore'
|
||||||
import WallInstance from './instance/wallInstance';
|
import WallInstance from './instance/wallInstance';
|
||||||
import Line from '../../line/line';
|
import Line from '../../line/line';
|
||||||
|
@ -14,25 +14,47 @@ function WallInstances() {
|
||||||
// console.log('walls: ', walls);
|
// console.log('walls: ', walls);
|
||||||
}, [walls])
|
}, [walls])
|
||||||
|
|
||||||
|
const allPoints = useMemo(() => {
|
||||||
|
const points: Point[] = [];
|
||||||
|
const seenUuids = new Set<string>();
|
||||||
|
|
||||||
|
walls.forEach(aisle => {
|
||||||
|
aisle.points.forEach(point => {
|
||||||
|
if (!seenUuids.has(point.pointUuid)) {
|
||||||
|
seenUuids.add(point.pointUuid);
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}, [walls]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<group name='Walls-Group'>
|
||||||
<Geometry computeVertexNormals useGroups>
|
<Geometry computeVertexNormals useGroups>
|
||||||
{walls.map((wall) => (
|
{walls.map((wall) => (
|
||||||
<WallInstance key={wall.wallUuid} wall={wall} />
|
<WallInstance key={wall.wallUuid} wall={wall} />
|
||||||
))}
|
))}
|
||||||
</Geometry>
|
</Geometry>
|
||||||
|
</group>
|
||||||
|
|
||||||
{toggleView && (
|
{toggleView && (
|
||||||
<>
|
<>
|
||||||
{walls.map((wall) => (
|
<group name='Wall-Points-Group'>
|
||||||
<React.Fragment key={wall.wallUuid}>
|
{allPoints.map((point) => (
|
||||||
<Point point={wall.points[0]} />
|
<Point key={point.pointUuid} point={point} />
|
||||||
<Line points={wall.points} />
|
))}
|
||||||
<Point point={wall.points[1]} />
|
</group>
|
||||||
</React.Fragment>
|
|
||||||
))}
|
<group name='Wall-Lines-Group'>
|
||||||
|
{walls.map((wall) => (
|
||||||
|
<React.Fragment key={wall.wallUuid}>
|
||||||
|
<Line points={wall.points} />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -52,7 +52,7 @@ 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 === 'Aisle-Point');
|
const intersects = raycaster.intersectObjects(scene.children).find((intersect) => intersect.object.name === 'Wall-Point');
|
||||||
|
|
||||||
const newPoint: Point = {
|
const newPoint: Point = {
|
||||||
pointUuid: THREE.MathUtils.generateUUID(),
|
pointUuid: THREE.MathUtils.generateUUID(),
|
||||||
|
@ -137,7 +137,7 @@ function WallCreator() {
|
||||||
<>
|
<>
|
||||||
{toggleView &&
|
{toggleView &&
|
||||||
<>
|
<>
|
||||||
<group name='Aisle-Reference-Points-Group'>
|
<group name='Wall-Reference-Points-Group'>
|
||||||
{tempPoints.map((point) => (
|
{tempPoints.map((point) => (
|
||||||
<ReferencePoint key={point.pointUuid} point={point} />
|
<ReferencePoint key={point.pointUuid} point={point} />
|
||||||
))}
|
))}
|
||||||
|
|
Loading…
Reference in New Issue