refactor: Enhance wall and comparison handling, improve state management in builder components

This commit is contained in:
Jerald-Golden-B 2025-06-05 11:55:46 +05:30
parent d57ee378aa
commit e4196eee8c
11 changed files with 296 additions and 143 deletions

View File

@ -6,21 +6,24 @@ import useModuleStore from '../../../store/useModuleStore';
import CompareLayOut from '../../ui/compareVersion/CompareLayOut';
import ComparisonResult from '../../ui/compareVersion/ComparisonResult';
import { useComparisonProduct } from '../../../store/simulation/useSimulationStore';
import { usePlayButtonStore } from '../../../store/usePlayButtonStore';
import { usePauseButtonStore, usePlayButtonStore } from '../../../store/usePlayButtonStore';
function ComparisonScene() {
const { isPlaying } = usePlayButtonStore();
const { isPlaying, setIsPlaying } = usePlayButtonStore();
const { products } = useProductStore();
const { isVersionSaved } = useSaveVersion();
const { activeModule } = useModuleStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { comparisonProduct, setComparisonProduct } = useComparisonProduct();
const { setIsPaused } = usePauseButtonStore();
const handleSelectLayout = (option: string) => {
const product = products.find((product) => product.productName === option);
if (product) {
setComparisonProduct(product.productId, product.productName);
setIsPlaying(true);
setIsPaused(true);
}
};
return (

View File

@ -18,7 +18,7 @@ import ControlsPlayer from '../controls/ControlsPlayer';
import SelectFloorPlan from '../../temporary/SelectFloorPlan';
import { createHandleDrop } from '../../../modules/visualization/functions/handleUiDrop';
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 { useProductStore } from '../../../store/simulation/useProductStore';
import RegularDropDown from '../../ui/inputs/RegularDropDown';
@ -38,6 +38,7 @@ function MainScene() {
const { visualizationSocket } = useSocketStore();
const { selectedZone } = useSelectedZoneStore();
const { setFloatingWidget } = useFloatingWidget();
const { comparisonProduct } = useComparisonProduct();
const handleSelectLayout = (option: string) => {
const product = products.find((product) => product.productName === option);
@ -64,8 +65,8 @@ function MainScene() {
{activeModule !== "market" && !isPlaying && !isVersionSaved && (
<Tools />
)}
{isPlaying && activeModule === "simulation" && <SimulationPlayer />}
{isPlaying && activeModule !== "simulation" && <ControlsPlayer />}
{(isPlaying || comparisonProduct !== null) && activeModule === "simulation" && <SimulationPlayer />}
{(isPlaying || comparisonProduct !== null) && activeModule !== "simulation" && <ControlsPlayer />}
{/* remove this later */}
{activeModule === "builder" && !toggleThreeD && <SelectFloorPlan />}

View File

@ -10,6 +10,7 @@ import OuterClick from "../../../utils/outerClick";
import { useProductStore } from "../../../store/simulation/useProductStore";
import Scene from "../../../modules/scene/scene";
import { useComparisonProduct } from "../../../store/simulation/useSimulationStore";
import { usePauseButtonStore, usePlayButtonStore } from "../../../store/usePlayButtonStore";
const CompareLayOut = () => {
const { comparisonProduct, setComparisonProduct, clearComparisonProduct } = useComparisonProduct();
@ -22,6 +23,8 @@ const CompareLayOut = () => {
const startWidthRef = useRef<number>(0);
const startXRef = useRef<number>(0);
const { setIsVersionSaved } = useSaveVersion();
const { setIsPlaying } = usePlayButtonStore();
const { setIsPaused } = usePauseButtonStore();
OuterClick({
contextClassName: ["displayLayouts-container", "selectLayout"],
@ -64,6 +67,7 @@ const CompareLayOut = () => {
setWidth("0px");
setIsVersionSaved(false);
clearComparisonProduct();
setIsPlaying(false);
} else {
setWidth(`${finalWidthVw}vw`);
}
@ -109,6 +113,8 @@ const CompareLayOut = () => {
if (product) {
setComparisonProduct(product.productId, product.productName);
setLoadingProgress(1);
setIsPlaying(true);
setIsPaused(true);
}
};

View File

@ -24,6 +24,7 @@ import ProductionCapacity from "../analysis/ThroughputSummary";
import ThroughputSummary from "../analysis/ProductionCapacity";
import ROISummary from "../analysis/ROISummary";
import { usePlayerStore } from "../../../store/useUIToggleStore";
import { useComparisonProduct } from "../../../store/simulation/useSimulationStore";
const SimulationPlayer: React.FC = () => {
const MAX_SPEED = 4; // Maximum speed
@ -31,7 +32,6 @@ const SimulationPlayer: React.FC = () => {
const isDragging = useRef(false);
const sliderRef = useRef<HTMLDivElement>(null);
const [expand, setExpand] = useState(true);
const [playSimulation, setPlaySimulation] = useState(false);
const { hidePlayer, setHidePlayer } = usePlayerStore();
const { speed, setSpeed } = useAnimationPlaySpeed();
@ -40,6 +40,7 @@ const SimulationPlayer: React.FC = () => {
const { isPaused, setIsPaused } = usePauseButtonStore();
const { isReset, setReset } = useResetButtonStore();
const { subModule } = useSubModuleStore();
const { clearComparisonProduct } = useComparisonProduct();
useEffect(() => {
if (isReset) {
@ -55,17 +56,15 @@ const SimulationPlayer: React.FC = () => {
setIsPaused(false);
setSpeed(1);
echo.info("Simulation RESET.....");
setPlaySimulation(false); // local state reset
};
const handlePlayStop = () => {
setIsPaused(!isPaused);
echo.warn(`Simulation is ${isPaused ? "Resumed" : "Paused"}`);
setPlaySimulation(!playSimulation);
};
const handleExit = () => {
setPlaySimulation(false);
setIsPlaying(false);
setIsPaused(false);
clearComparisonProduct();
setActiveTool("cursor");
echo.info("Exit Simulation");
};
@ -210,7 +209,7 @@ const SimulationPlayer: React.FC = () => {
{!hidePlayer && subModule !== "analysis" && (
<div className="header">
<InfoIcon />
{playSimulation
{isPaused
? "Paused - system idle."
: "Running simulation..."}
</div>
@ -237,7 +236,7 @@ const SimulationPlayer: React.FC = () => {
}}
>
<PlayStopIcon />
{playSimulation ? "Play" : "Stop"}
{isPaused ? "Play" : "Stop"}
</button>
)}
<button
@ -298,11 +297,10 @@ const SimulationPlayer: React.FC = () => {
</div>
{index < intervals.length - 1 && (
<div
className={`line ${
progress >= ((index + 1) / totalSegments) * 100
? "filled"
: ""
}`}
className={`line ${progress >= ((index + 1) / totalSegments) * 100
? "filled"
: ""
}`}
></div>
)}
</React.Fragment>
@ -342,9 +340,8 @@ const SimulationPlayer: React.FC = () => {
<div className="custom-slider">
<button
id="slider-handle"
className={`slider-handle ${
isDragging ? "dragging" : ""
}`}
className={`slider-handle ${isDragging ? "dragging" : ""
}`}
style={{ left: `${calculateHandlePosition()}%` }}
onMouseDown={handleMouseDown}
>

View File

@ -51,6 +51,7 @@ import DxfFile from "./dfx/LoadBlueprint";
import { useParams } from "react-router-dom";
import AislesGroup from "./aisle/aislesGroup";
import WallGroup from "./wall/wallGroup";
import { useBuilderStore } from "../../store/builder/useBuilderStore";
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.
@ -117,6 +118,7 @@ export default function Builder() {
const { setWalls } = useWalls();
const { refTextupdate, setRefTextUpdate } = useRefTextUpdate();
const { projectId } = useParams();
const { setHoveredPoint } = useBuilderStore();
// const loader = new GLTFLoader();
// const dracoLoader = new DRACOLoader();
@ -138,6 +140,7 @@ export default function Builder() {
dragPointControls
);
} else {
setHoveredPoint(null);
setToolMode(null);
setDeletePointOrLine(false);
loadWalls(lines, setWalls);

View File

@ -46,6 +46,9 @@ function Line({ points }: Readonly<LineProps>) {
return (
<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]}
>
<meshStandardMaterial color={colors.defaultLineColor} />

View File

@ -122,6 +122,12 @@ function Point({ point }: { readonly point: Point }) {
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
key={point.pointUuid}
uuid={point.pointUuid}
name='Aisle-Point'
name={`${point.pointType}-Point`}
position={new THREE.Vector3(...point.position)}
onClick={() => {
handlePointClick(point);

View File

@ -3,168 +3,263 @@ import * as THREE from 'three';
import * as turf from '@turf/turf';
export function useWallClassification(walls: Walls) {
// Find all rooms from the given walls
// Find all minimal rooms from the given walls
const findRooms = () => {
if (walls.length < 3) return [];
// Build a graph of point connections
const graph = new Map<string, string[]>();
const pointMap = new Map<string, Point>();
const connections = new Map<string, string[]>();
// Build connection graph
walls.forEach(wall => {
const [p1, p2] = wall.points;
if (!pointMap.has(p1.pointUuid)) pointMap.set(p1.pointUuid, p1);
if (!pointMap.has(p2.pointUuid)) pointMap.set(p2.pointUuid, p2);
pointMap.set(p1.pointUuid, p1);
pointMap.set(p2.pointUuid, p2);
if (!connections.has(p1.pointUuid)) connections.set(p1.pointUuid, []);
if (!connections.has(p2.pointUuid)) connections.set(p2.pointUuid, []);
// Add connection from p1 to p2
if (!graph.has(p1.pointUuid)) graph.set(p1.pointUuid, []);
graph.get(p1.pointUuid)?.push(p2.pointUuid);
connections.get(p1.pointUuid)?.push(p2.pointUuid);
connections.get(p2.pointUuid)?.push(p1.pointUuid);
// Add connection from p2 to p1
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 rooms: Point[][] = [];
// Modified DFS to find all possible cycles
const findAllCycles = (current: string, path: string[]) => {
visited.add(current);
path.push(current);
const findCycles = (startNode: string, currentNode: string, path: string[], depth = 0) => {
if (depth > 20) return; // Prevent infinite recursion
visited.add(currentNode);
path.push(currentNode);
const neighbors = graph.get(currentNode) || [];
const neighbors = connections.get(current) || [];
for (const neighbor of neighbors) {
if (path.length > 2 && neighbor === path[0]) {
// Found a cycle (potential room)
const roomPoints = [...path, neighbor].map(uuid => pointMap.get(uuid)!);
if (path.length > 2 && neighbor === startNode) {
// Found a cycle that returns to start
const cycle = [...path, neighbor];
// Check if this room is valid and not already found
if (isValidRoom(roomPoints) && !roomAlreadyExists(rooms, roomPoints)) {
rooms.push(roomPoints);
// Check if this is a new unique cycle
if (!cycleExists(allCycles, cycle)) {
allCycles.push(cycle);
}
continue;
}
if (!path.includes(neighbor)) {
findAllCycles(neighbor, [...path]);
findCycles(startNode, neighbor, [...path], depth + 1);
}
}
};
// Start from each point to find all possible cycles
for (const [pointUuid] of connections) {
if (!visited.has(pointUuid)) {
findAllCycles(pointUuid, []);
}
// Start cycle detection from each node
for (const [pointUuid] of graph) {
findCycles(pointUuid, 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
const isValidRoom = (roomPoints: Point[]) => {
if (roomPoints.length < 4) return false;
const coordinates = roomPoints.map(p => [p.position[0], p.position[2]]);
const polygon = turf.polygon([coordinates]);
return turf.booleanValid(polygon);
};
// Check if a cycle already exists in our list (considering different orders)
const cycleExists = (allCycles: string[][], newCycle: string[]) => {
const newSet = new Set(newCycle);
// Helper function to check for duplicate rooms
const roomAlreadyExists = (existingRooms: Point[][], newRoom: Point[]) => {
const newRoomIds = newRoom.map(p => p.pointUuid).sort().join('-');
return existingRooms.some(room => {
const roomIds = room.map(p => p.pointUuid).sort().join('-');
return roomIds === newRoomIds;
return allCycles.some(existingCycle => {
if (existingCycle.length !== newCycle.length) return false;
const existingSet = new Set(existingCycle);
return setsEqual(newSet, existingSet);
});
};
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
const getWallOrientation = (wall: Wall) => {
const [p1, p2] = wall.points;
const orientations: Array<{ isOutsideFacing: boolean }> = [];
// 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 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 roomP1 = room[i];
const roomP2 = room[i + 1];
const p1 = room[i];
const p2 = room[i + 1];
// Check both directions
if ((p1.pointUuid === roomP1.pointUuid && p2.pointUuid === roomP2.pointUuid) ||
(p1.pointUuid === roomP2.pointUuid && p2.pointUuid === roomP1.pointUuid)) {
// 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)
);
const roomPoints = room.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);
if (wall) roomWalls.add(wall);
}
});
const wallVector = new THREE.Vector3(
p2.position[0] - p1.position[0],
0,
p2.position[2] - p1.position[2]
);
return Array.from(roomWalls);
};
// Normal depends on wall direction
let normal = new THREE.Vector3(-wallVector.z, 0, wallVector.x).normalize();
if (p1.pointUuid === roomP2.pointUuid) {
normal = new THREE.Vector3(wallVector.z, 0, -wallVector.x).normalize();
}
// Determine wall orientation relative to room
const getWallOrientation = (wall: Wall) => {
const roomWalls = getRoomWalls();
const isRoomWall = roomWalls.includes(wall);
const testPoint = new THREE.Vector3(
(p1.position[0] + p2.position[0]) / 2 + normal.x,
0,
(p1.position[2] + p2.position[2]) / 2 + normal.z
);
if (!isRoomWall) {
return {
isRoomWall: false,
isOutsideFacing: false
};
}
const pointInside = turf.booleanPointInPolygon(
turf.point([testPoint.x, testPoint.z]),
turf.polygon([room.map(p => [p.position[0], p.position[2]])])
);
orientations.push({
isOutsideFacing: !pointInside
});
// Find which room this wall belongs to
const containingRoom = rooms.find(room => {
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) ||
(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 {
isRoomWall: orientations.length > 0,
orientations // Now tracks all orientations for walls in multiple rooms
isRoomWall: true,
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);
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
if (orientation.orientations.length === 2) {
// Wall is between two rooms - one side faces each room's interior
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' };
return orientation.isOutsideFacing
? { front: 'outside', back: 'inside' }
: { front: 'inside', back: 'outside' };
};
// 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 {
rooms,
getWallType,

View File

@ -41,7 +41,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
outside.colorSpace = THREE.SRGBColorSpace;
return [inside, outside];
}, [wallLength, wall.wallHeight, textureLoader]);
}, [wallLength, wall.wallHeight]);
const materials = useMemo(() => {
// 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

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useWallStore } from '../../../../store/builder/useWallStore'
import WallInstance from './instance/wallInstance';
import Line from '../../line/line';
@ -14,25 +14,47 @@ function WallInstances() {
// console.log('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 (
<>
<Geometry computeVertexNormals useGroups>
{walls.map((wall) => (
<WallInstance key={wall.wallUuid} wall={wall} />
))}
</Geometry>
<group name='Walls-Group'>
<Geometry computeVertexNormals useGroups>
{walls.map((wall) => (
<WallInstance key={wall.wallUuid} wall={wall} />
))}
</Geometry>
</group>
{toggleView && (
<>
{walls.map((wall) => (
<React.Fragment key={wall.wallUuid}>
<Point point={wall.points[0]} />
<Line points={wall.points} />
<Point point={wall.points[1]} />
</React.Fragment>
))}
<group name='Wall-Points-Group'>
{allPoints.map((point) => (
<Point key={point.pointUuid} point={point} />
))}
</group>
<group name='Wall-Lines-Group'>
{walls.map((wall) => (
<React.Fragment key={wall.wallUuid}>
<Line points={wall.points} />
</React.Fragment>
))}
</group>
</>
)}
</>

View File

@ -52,7 +52,7 @@ function WallCreator() {
let position = raycaster.ray.intersectPlane(plane, intersectionPoint);
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 = {
pointUuid: THREE.MathUtils.generateUUID(),
@ -137,7 +137,7 @@ function WallCreator() {
<>
{toggleView &&
<>
<group name='Aisle-Reference-Points-Group'>
<group name='Wall-Reference-Points-Group'>
{tempPoints.map((point) => (
<ReferencePoint key={point.pointUuid} point={point} />
))}