feat: implement aisle management features with creator and instances

This commit is contained in:
2025-05-27 18:11:26 +05:30
parent 42a3d7285e
commit bb46eeb3cc
13 changed files with 720 additions and 22 deletions

View File

@@ -0,0 +1,24 @@
import { useEffect } from 'react';
import { useAisleStore } from '../../../../store/builder/useAisleStore';
import AisleInstance from './instance/aisleInstance';
function AisleInstances() {
const { aisles } = useAisleStore();
useEffect(() => {
console.log('aisles: ', aisles);
}, [aisles]);
return (
<>
{aisles.map((aisle) =>
<AisleInstance aisle={aisle} key={aisle.uuid} />
)}
</>
)
}
export default AisleInstances

View File

@@ -0,0 +1,19 @@
import DashedAisle from './aisleTypes/dashedAisle';
import SolidAisle from './aisleTypes/solidAisle';
function AisleInstance({ aisle }: { readonly aisle: Aisle }) {
return (
<>
{aisle.type.aisleType === 'solid-aisle' && (
<SolidAisle aisle={aisle} />
)}
{aisle.type.aisleType === 'dashed-aisle' && (
<DashedAisle aisle={aisle} />
)}
</>
);
}
export default AisleInstance;

View File

@@ -0,0 +1,71 @@
import * as THREE from 'three';
import { useMemo } from 'react';
import { Extrude } from '@react-three/drei';
import * as Constants from '../../../../../../types/world/worldConstants';
function DashedAisle({ aisle }: { readonly aisle: Aisle }) {
const shapes = useMemo(() => {
if (aisle.points.length < 2) return [];
const start = new THREE.Vector3(...aisle.points[0].position);
const end = new THREE.Vector3(...aisle.points[1].position);
const width = aisle.type.width || 0.1;
const dashLength = 0.5;
const gapLength = 0.3;
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
const totalLength = new THREE.Vector3().subVectors(end, start).length();
const segmentCount = Math.floor(totalLength / (dashLength + gapLength));
const shapes = [];
const directionNormalized = new THREE.Vector3().subVectors(end, start).normalize();
for (let i = 0; i < segmentCount; i++) {
const segmentStart = new THREE.Vector3().copy(start).addScaledVector(directionNormalized, i * (dashLength + gapLength));
const segmentEnd = new THREE.Vector3().copy(segmentStart).addScaledVector(directionNormalized, dashLength);
const leftStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, width / 2);
const rightStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, -width / 2);
const leftEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, width / 2);
const rightEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, -width / 2);
const shape = new THREE.Shape();
shape.moveTo(leftStart.x, leftStart.z);
shape.lineTo(leftEnd.x, leftEnd.z);
shape.lineTo(rightEnd.x, rightEnd.z);
shape.lineTo(rightStart.x, rightStart.z);
shape.closePath();
shapes.push(shape);
}
return shapes;
}, [aisle]);
if (shapes.length === 0) return null;
return (
<group
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
rotation={[Math.PI / 2, 0, 0]}
>
{shapes.map((shape, index) => (
<Extrude
key={index}
args={[shape, { depth: 0.01, bevelEnabled: false }]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.color || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
))}
</group>
);
}
export default DashedAisle;

View File

@@ -0,0 +1,53 @@
import * as THREE from 'three';
import { useMemo } from 'react';
import { Extrude } from '@react-three/drei';
import * as Constants from '../../../../../../types/world/worldConstants';
function SolidAisle({ aisle }: { readonly aisle: Aisle }) {
const shape = useMemo(() => {
if (aisle.points.length < 2) return null;
const start = new THREE.Vector3(...aisle.points[0].position);
const end = new THREE.Vector3(...aisle.points[1].position);
const width = aisle.type.width || 0.1;
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
const leftStart = new THREE.Vector3().copy(start).addScaledVector(perp, width / 2);
const rightStart = new THREE.Vector3().copy(start).addScaledVector(perp, -width / 2);
const leftEnd = new THREE.Vector3().copy(end).addScaledVector(perp, width / 2);
const rightEnd = new THREE.Vector3().copy(end).addScaledVector(perp, -width / 2);
const shape = new THREE.Shape();
shape.moveTo(leftStart.x, leftStart.z);
shape.lineTo(leftEnd.x, leftEnd.z);
shape.lineTo(rightEnd.x, rightEnd.z);
shape.lineTo(rightStart.x, rightStart.z);
shape.closePath();
return shape;
}, [aisle]);
if (!shape) return null;
return (
<group
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
rotation={[Math.PI / 2, 0, 0]}
>
<Extrude
args={[shape, { depth: 0.01, bevelEnabled: false }]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.color || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
</group>
);
}
export default SolidAisle;

View File

@@ -0,0 +1,196 @@
import * as THREE from 'three'
import { useEffect, useMemo, useState } from 'react'
import { useThree } from '@react-three/fiber';
import { useActiveLayer, useSocketStore, useToggleView, useToolMode } from '../../../../store/builder/store';
import { useAisleStore } from '../../../../store/builder/useAisleStore';
import * as Constants from '../../../../types/world/worldConstants';
import ReferenceAisle from './referenceAisle';
function AisleCreator() {
const { scene, camera, raycaster, gl, pointer } = useThree();
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
const { toggleView } = useToggleView();
const { toolMode } = useToolMode();
const { activeLayer } = useActiveLayer();
const { socket } = useSocketStore();
const { aisles, addAisle } = useAisleStore();
const [tempPoints, setTempPoints] = useState<Point[]>([]);
const [isCreating, setIsCreating] = useState(false);
const [aisleType, setAisleType] = useState<'solid-aisle' | 'dashed-aisle' | 'stripped-aisle' | 'dotted-aisle' | 'arrow-aisle' | 'arrows-aisle' | 'arc-aisle' | 'circle-aisle' | 'junction-aisle'>('dashed-aisle');
useEffect(() => {
if (tempPoints.length > 0) {
setTempPoints([]);
setIsCreating(false);
}
}, [aisleType]);
const allPoints = useMemo(() => {
const points: Point[] = [];
const seenUuids = new Set<string>();
// Add points from existing aisles
aisles.forEach(aisle => {
aisle.points.forEach(point => {
if (!seenUuids.has(point.uuid)) {
seenUuids.add(point.uuid);
points.push(point);
}
});
});
// Add temporary points
tempPoints.forEach(point => {
if (!seenUuids.has(point.uuid)) {
seenUuids.add(point.uuid);
points.push(point);
}
});
return points;
}, [aisles, tempPoints]);
useEffect(() => {
const canvasElement = gl.domElement;
let drag = false;
let isLeftMouseDown = false;
const onMouseDown = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown = true;
drag = false;
}
};
const onMouseUp = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown = false;
}
};
const onMouseMove = () => {
if (isLeftMouseDown) {
drag = true;
}
};
const onMouseClick = () => {
if (drag || !toggleView) return;
raycaster.setFromCamera(pointer, camera);
const intersectionPoint = new THREE.Vector3();
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (!point) return;
if (['solid-aisle', 'dashed-aisle', 'stripped-aisle', 'dotted-aisle', 'arrows-aisle'].includes(aisleType)) {
const newPoint: Point = {
uuid: THREE.MathUtils.generateUUID(),
position: [point.x, point.y, point.z],
layer: activeLayer
};
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
} else {
const aisle: Aisle = {
uuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
typeName: 'Aisle',
material: 'default',
aisleType: aisleType,
color: Constants.aisleConfig.defaultColor,
width: Constants.aisleConfig.width
}
};
addAisle(aisle);
setTempPoints([newPoint]);
}
} else if (['arc-aisle', 'circle-aisle', 'arrow-aisle', 'junction-aisle'].includes(aisleType)) {
const newPoint: Point = {
uuid: THREE.MathUtils.generateUUID(),
position: [point.x, point.y, point.z],
layer: activeLayer
};
if (tempPoints.length === 0) {
setTempPoints([newPoint]);
setIsCreating(true);
} else {
const aisle: Aisle = {
uuid: THREE.MathUtils.generateUUID(),
points: [tempPoints[0], newPoint],
type: {
typeName: 'Aisle',
material: 'default',
aisleType: aisleType,
color: Constants.aisleConfig.defaultColor,
width: Constants.aisleConfig.width
}
};
addAisle(aisle);
setTempPoints([]);
setIsCreating(false);
}
}
};
const onContext = (event: any) => {
event.preventDefault();
if (isCreating) {
setTempPoints([]);
setIsCreating(false);
}
};
if (toolMode === "Aisle" && toggleView) {
canvasElement.addEventListener("mousedown", onMouseDown);
canvasElement.addEventListener("mouseup", onMouseUp);
canvasElement.addEventListener("mousemove", onMouseMove);
canvasElement.addEventListener("click", onMouseClick);
canvasElement.addEventListener("contextmenu", onContext);
} else {
setTempPoints([]);
setIsCreating(false);
}
return () => {
canvasElement.removeEventListener("mousedown", onMouseDown);
canvasElement.removeEventListener("mouseup", onMouseUp);
canvasElement.removeEventListener("mousemove", onMouseMove);
canvasElement.removeEventListener("click", onMouseClick);
canvasElement.removeEventListener("contextmenu", onContext);
};
}, [gl, camera, scene, raycaster, pointer, plane, toggleView, toolMode, activeLayer, socket, tempPoints, isCreating, addAisle, aisleType]);
return (
<>
<group >
{allPoints.map((point) => (
<mesh
key={point.uuid}
position={new THREE.Vector3(...point.position)}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshBasicMaterial
color={0xffff00}
/>
</mesh>
))}
</group>
<ReferenceAisle tempPoints={tempPoints} aisleType={aisleType} />
</>
);
}
export default AisleCreator;

View File

@@ -0,0 +1,193 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import * as THREE from 'three';
import { useFrame, useThree } from '@react-three/fiber';
import { useActiveLayer, useToolMode, useToggleView } from '../../../../store/builder/store';
import * as Constants from '../../../../types/world/worldConstants';
import { Extrude } from '@react-three/drei';
interface ReferenceAisleProps {
tempPoints: Point[];
aisleType: 'solid-aisle' | 'dashed-aisle' | 'stripped-aisle' | 'dotted-aisle' | 'arrow-aisle' | 'arrows-aisle' | 'arc-aisle' | 'circle-aisle' | 'junction-aisle';
}
function ReferenceAisle({ tempPoints, aisleType }: Readonly<ReferenceAisleProps>) {
const { pointer, raycaster, camera } = useThree();
const { toolMode } = useToolMode();
const { toggleView } = useToggleView();
const { activeLayer } = useActiveLayer();
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const [tempAisle, setTempAisle] = useState<Aisle | null>(null);
const mousePosRef = useRef<THREE.Vector3>(new THREE.Vector3());
useFrame(() => {
if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) {
raycaster.setFromCamera(pointer, camera);
const intersectionPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
if (intersectionPoint) {
mousePosRef.current.copy(intersectionPoint);
setTempAisle({
uuid: 'temp-aisle',
points: [
tempPoints[0],
{
uuid: 'temp-point',
position: [mousePosRef.current.x, mousePosRef.current.y, mousePosRef.current.z],
layer: activeLayer
}
],
type: {
typeName: 'Aisle',
material: 'default',
aisleType: aisleType,
color: Constants.aisleConfig.defaultColor,
width: Constants.aisleConfig.width
}
});
}
} else if (tempAisle !== null) {
setTempAisle(null);
}
});
useEffect(() => {
setTempAisle(null);
}, [toolMode, toggleView, tempPoints.length, aisleType]);
if (!tempAisle) return null;
const renderAisle = () => {
switch (aisleType) {
case 'solid-aisle':
return <SolidAisle aisle={tempAisle} />;
case 'dashed-aisle':
return <DashedAisle aisle={tempAisle} />;
default:
return null;
}
};
return (
<group>
{renderAisle()}
</group>
);
}
export default ReferenceAisle;
function SolidAisle({ aisle }: { readonly aisle: Aisle }) {
const shape = useMemo(() => {
if (aisle.points.length < 2) return null;
const start = new THREE.Vector3(...aisle.points[0].position);
const end = new THREE.Vector3(...aisle.points[1].position);
const width = aisle.type.width || 0.1;
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
const leftStart = new THREE.Vector3().copy(start).addScaledVector(perp, width / 2);
const rightStart = new THREE.Vector3().copy(start).addScaledVector(perp, -width / 2);
const leftEnd = new THREE.Vector3().copy(end).addScaledVector(perp, width / 2);
const rightEnd = new THREE.Vector3().copy(end).addScaledVector(perp, -width / 2);
const shape = new THREE.Shape();
shape.moveTo(leftStart.x, leftStart.z);
shape.lineTo(leftEnd.x, leftEnd.z);
shape.lineTo(rightEnd.x, rightEnd.z);
shape.lineTo(rightStart.x, rightStart.z);
shape.closePath();
return shape;
}, [aisle]);
if (!shape) return null;
return (
<group
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
rotation={[Math.PI / 2, 0, 0]}
>
<Extrude
args={[shape, { depth: 0.01, bevelEnabled: false }]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.color || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
</group>
);
}
function DashedAisle({ aisle }: { readonly aisle: Aisle }) {
const shapes = useMemo(() => {
if (aisle.points.length < 2) return [];
const start = new THREE.Vector3(...aisle.points[0].position);
const end = new THREE.Vector3(...aisle.points[1].position);
const width = aisle.type.width || 0.1;
const dashLength = 0.5;
const gapLength = 0.3;
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
const totalLength = new THREE.Vector3().subVectors(end, start).length();
const segmentCount = Math.floor(totalLength / (dashLength + gapLength));
const shapes = [];
const directionNormalized = new THREE.Vector3().subVectors(end, start).normalize();
for (let i = 0; i < segmentCount; i++) {
const segmentStart = new THREE.Vector3().copy(start).addScaledVector(directionNormalized, i * (dashLength + gapLength));
const segmentEnd = new THREE.Vector3().copy(segmentStart).addScaledVector(directionNormalized, dashLength);
const leftStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, width / 2);
const rightStart = new THREE.Vector3().copy(segmentStart).addScaledVector(perp, -width / 2);
const leftEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, width / 2);
const rightEnd = new THREE.Vector3().copy(segmentEnd).addScaledVector(perp, -width / 2);
const shape = new THREE.Shape();
shape.moveTo(leftStart.x, leftStart.z);
shape.lineTo(leftEnd.x, leftEnd.z);
shape.lineTo(rightEnd.x, rightEnd.z);
shape.lineTo(rightStart.x, rightStart.z);
shape.closePath();
shapes.push(shape);
}
return shapes;
}, [aisle]);
if (shapes.length === 0) return null;
return (
<group
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
rotation={[Math.PI / 2, 0, 0]}
>
{shapes.map((shape, index) => (
<Extrude
key={index}
args={[shape, { depth: 0.01, bevelEnabled: false }]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.color || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
))}
</group>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react'
import AisleCreator from './aisleCreator/aisleCreator'
import AisleInstances from './Instances/aisleInstances'
function AislesGroup() {
return (
<>
<AisleCreator />
<AisleInstances />
</>
)
}
export default AislesGroup