386 lines
15 KiB
TypeScript
386 lines
15 KiB
TypeScript
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';
|
|
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
|
|
|
|
interface ReferenceAisleProps {
|
|
tempPoints: Point[];
|
|
}
|
|
|
|
function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
|
|
const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, } = useBuilderStore();
|
|
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);
|
|
|
|
if (aisleType === 'solid-aisle' || aisleType === 'stripped-aisle') {
|
|
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',
|
|
aisleType: aisleType,
|
|
aisleColor: aisleColor,
|
|
aisleWidth: aisleWidth
|
|
}
|
|
});
|
|
} else if (aisleType === 'dashed-aisle') {
|
|
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',
|
|
aisleType: aisleType,
|
|
aisleColor: aisleColor,
|
|
aisleWidth: aisleWidth,
|
|
dashLength: dashLength,
|
|
gapLength: gapLength
|
|
}
|
|
});
|
|
} else if (aisleType === 'dotted-aisle') {
|
|
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',
|
|
aisleType: aisleType,
|
|
aisleColor: aisleColor,
|
|
dotRadius: dotRadius,
|
|
gapLength: gapLength
|
|
}
|
|
});
|
|
} else if (aisleType === 'arrow-aisle') {
|
|
console.log();
|
|
} else if (aisleType === 'arrows-aisle') {
|
|
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',
|
|
aisleType: aisleType,
|
|
aisleColor: aisleColor,
|
|
aisleWidth: aisleWidth,
|
|
aisleLength: aisleLength,
|
|
gapLength: gapLength
|
|
}
|
|
});
|
|
} else if (aisleType === 'arc-aisle') {
|
|
console.log();
|
|
} else if (aisleType === 'circle-aisle') {
|
|
console.log();
|
|
} else if (aisleType === 'junction-aisle') {
|
|
console.log();
|
|
}
|
|
}
|
|
} else if (tempAisle !== null) {
|
|
setTempAisle(null);
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
setTempAisle(null);
|
|
}, [toolMode, toggleView, tempPoints.length, aisleType, aisleWidth, aisleColor]);
|
|
|
|
if (!tempAisle) return null;
|
|
|
|
const renderAisle = () => {
|
|
switch (aisleType) {
|
|
case 'solid-aisle':
|
|
return <SolidAisle aisle={tempAisle} />;
|
|
case 'dashed-aisle':
|
|
return <DashedAisle aisle={tempAisle} />;
|
|
case 'dotted-aisle':
|
|
return <DottedAisle aisle={tempAisle} />;
|
|
case 'arrows-aisle':
|
|
return <ArrowsAisle 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 || aisle.type.aisleType !== 'solid-aisle') return null;
|
|
|
|
const start = new THREE.Vector3(...aisle.points[0].position);
|
|
const end = new THREE.Vector3(...aisle.points[1].position);
|
|
const width = aisle.type.aisleWidth || 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.aisleColor || '#ffffff'}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</Extrude>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
function DashedAisle({ aisle }: { readonly aisle: Aisle }) {
|
|
const shapes = useMemo(() => {
|
|
if (aisle.points.length < 2 || aisle.type.aisleType !== 'dashed-aisle') return [];
|
|
|
|
const start = new THREE.Vector3(...aisle.points[0].position);
|
|
const end = new THREE.Vector3(...aisle.points[1].position);
|
|
const width = aisle.type.aisleWidth || 0.1;
|
|
const dashLength = aisle.type.dashLength || 0.5;
|
|
const gapLength = aisle.type.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 + gapLength) / (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.aisleColor || '#ffffff'}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</Extrude>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
function DottedAisle({ aisle }: { readonly aisle: Aisle }) {
|
|
const shapes = useMemo(() => {
|
|
if (aisle.points.length < 2 || aisle.type.aisleType !== 'dotted-aisle') return [];
|
|
|
|
const start = new THREE.Vector3(...aisle.points[0].position);
|
|
const end = new THREE.Vector3(...aisle.points[1].position);
|
|
const width = aisle.type.dotRadius || 0.1;
|
|
const dotSpacing = aisle.type.gapLength || 0.5;
|
|
const dotRadius = width * 0.6;
|
|
|
|
const totalLength = new THREE.Vector3().subVectors(end, start).length();
|
|
const dotCount = Math.floor((totalLength + (dotSpacing / 2)) / dotSpacing);
|
|
|
|
const shapes = [];
|
|
const directionNormalized = new THREE.Vector3().subVectors(end, start).normalize();
|
|
|
|
for (let i = 0; i < dotCount; i++) {
|
|
const dotCenter = new THREE.Vector3().copy(start).addScaledVector(directionNormalized, i * dotSpacing + dotSpacing / 2);
|
|
|
|
const shape = new THREE.Shape();
|
|
shape.absarc(dotCenter.x, dotCenter.z, dotRadius, 0, Math.PI * 2, false);
|
|
|
|
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.aisleColor || '#ffffff'}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</Extrude>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|
|
|
|
function ArrowsAisle({ aisle }: { readonly aisle: Aisle }) {
|
|
const arrows = useMemo(() => {
|
|
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arrows-aisle') return [];
|
|
|
|
const start = new THREE.Vector3(...aisle.points[0].position);
|
|
const end = new THREE.Vector3(...aisle.points[1].position);
|
|
const width = aisle.type.aisleWidth || 0.1;
|
|
const arrowLength = aisle.type.aisleLength || 0.6;
|
|
const spacing = aisle.type.gapLength || 0.6;
|
|
|
|
const direction = new THREE.Vector3().subVectors(end, start);
|
|
const length = direction.length();
|
|
direction.normalize();
|
|
|
|
const count = Math.floor((length + spacing) / (arrowLength + spacing));
|
|
|
|
const arrowShapes: { shape: THREE.Shape; position: THREE.Vector3; rotationY: number }[] = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const initialOffset = arrowLength;
|
|
const center = new THREE.Vector3().copy(start).addScaledVector(direction, initialOffset + i * (arrowLength + spacing));
|
|
|
|
const shape = new THREE.Shape();
|
|
const w = width * 0.8;
|
|
const h = arrowLength;
|
|
|
|
shape.moveTo(0, 0);
|
|
shape.lineTo(w, h * 0.6);
|
|
shape.lineTo(w * 0.4, h * 0.6);
|
|
shape.lineTo(w * 0.4, h);
|
|
shape.lineTo(-w * 0.4, h);
|
|
shape.lineTo(-w * 0.4, h * 0.6);
|
|
shape.lineTo(-w, h * 0.6);
|
|
shape.lineTo(0, 0);
|
|
|
|
const angle = Math.atan2(direction.x, direction.z) + Math.PI;
|
|
|
|
arrowShapes.push({ shape, position: center, rotationY: angle });
|
|
}
|
|
|
|
return arrowShapes;
|
|
}, [aisle]);
|
|
|
|
if (arrows.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]}
|
|
>
|
|
{arrows.map(({ shape, position, rotationY }, index) => (
|
|
<group key={index} position={[position.x, position.z, 0]} rotation={[0, 0, -rotationY]}>
|
|
<Extrude
|
|
args={[shape, { depth: 0.01, bevelEnabled: false }]}
|
|
receiveShadow
|
|
castShadow
|
|
>
|
|
<meshStandardMaterial
|
|
color={aisle.type.aisleColor || '#ffffff'}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</Extrude>
|
|
</group>
|
|
))}
|
|
</group>
|
|
);
|
|
}
|