Files
Dwinzo_Demo/app/src/modules/builder/aisle/aisleCreator/referenceAisle.tsx

774 lines
29 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, Html } from '@react-three/drei';
import { useBuilderStore } from '../../../../store/builder/useBuilderStore';
import { useDirectionalSnapping } from '../../point/helpers/useDirectionalSnapping';
import { usePointSnapping } from '../../point/helpers/usePointSnapping';
interface ReferenceAisleProps {
tempPoints: Point[];
}
function ReferenceAisle({ tempPoints }: Readonly<ReferenceAisleProps>) {
const { aisleType, aisleWidth, aisleColor, dashLength, gapLength, dotRadius, aisleLength, isFlipped, setSnappedPosition, setSnappedPoint } = 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 finalPosition = useRef<[number, number, number] | null>(null);
const [tempAisle, setTempAisle] = useState<Aisle | null>(null);
const [currentPosition, setCurrentPosition] = useState<[number, number, number]>(tempPoints[0]?.position);
const directionalSnap = useDirectionalSnapping(currentPosition, tempPoints[0]?.position || null);
const { snapAislePoint } = usePointSnapping({ uuid: 'temp-aisle', pointType: 'Aisle', position: directionalSnap.position || [0, 0, 0] });
useFrame(() => {
if (toolMode === "Aisle" && toggleView && tempPoints.length === 1) {
raycaster.setFromCamera(pointer, camera);
const intersectionPoint = new THREE.Vector3();
raycaster.ray.intersectPlane(plane, intersectionPoint);
setCurrentPosition([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
if (intersectionPoint) {
const snapped = snapAislePoint([intersectionPoint.x, intersectionPoint.y, intersectionPoint.z]);
if (snapped.isSnapped && snapped.snappedPoint) {
finalPosition.current = snapped.position;
setSnappedPosition(snapped.position);
setSnappedPoint(snapped.snappedPoint);
} else if (directionalSnap.isSnapped) {
finalPosition.current = directionalSnap.position;
setSnappedPosition(directionalSnap.position);
setSnappedPoint(null);
} else {
finalPosition.current = [intersectionPoint.x, intersectionPoint.y, intersectionPoint.z];
setSnappedPosition(null);
setSnappedPoint(null);
}
if (!finalPosition.current) return;
if (aisleType === 'solid-aisle') {
setTempAisle({
aisleUuid: 'temp-aisle',
points: [
tempPoints[0],
{
pointUuid: 'temp-point',
pointType: 'Aisle',
position: finalPosition.current,
layer: activeLayer
}
],
type: {
aisleType: aisleType,
aisleColor: aisleColor,
aisleWidth: aisleWidth
}
});
} else if (aisleType === 'dashed-aisle') {
setTempAisle({
aisleUuid: 'temp-aisle',
points: [
tempPoints[0],
{
pointUuid: 'temp-point',
pointType: 'Aisle',
position: finalPosition.current,
layer: activeLayer
}
],
type: {
aisleType: aisleType,
aisleColor: aisleColor,
aisleWidth: aisleWidth,
dashLength: dashLength,
gapLength: gapLength
}
});
} else if (aisleType === 'dotted-aisle') {
setTempAisle({
aisleUuid: 'temp-aisle',
points: [
tempPoints[0],
{
pointUuid: 'temp-point',
pointType: 'Aisle',
position: finalPosition.current,
layer: activeLayer
}
],
type: {
aisleType: aisleType,
aisleColor: aisleColor,
dotRadius: dotRadius,
gapLength: gapLength
}
});
} else if (aisleType === 'arrow-aisle' || aisleType === 'circle-aisle') {
setTempAisle({
aisleUuid: 'temp-aisle',
points: [
tempPoints[0],
{
pointUuid: 'temp-point',
pointType: 'Aisle',
position: finalPosition.current,
layer: activeLayer
}
],
type: {
aisleType: aisleType,
aisleColor: aisleColor,
aisleWidth: aisleWidth,
}
});
} else if (aisleType === 'arrows-aisle') {
setTempAisle({
aisleUuid: 'temp-aisle',
points: [
tempPoints[0],
{
pointUuid: 'temp-point',
pointType: 'Aisle',
position: finalPosition.current,
layer: activeLayer
}
],
type: {
aisleType: aisleType,
aisleColor: aisleColor,
aisleWidth: aisleWidth,
aisleLength: aisleLength,
gapLength: gapLength
}
});
} else if (aisleType === 'junction-aisle' || aisleType === 'arc-aisle') {
setTempAisle({
aisleUuid: 'temp-aisle',
points: [
tempPoints[0],
{
pointUuid: 'temp-point',
pointType: 'Aisle',
position: finalPosition.current,
layer: activeLayer
}
],
type: {
aisleType: aisleType,
aisleColor: aisleColor,
aisleWidth: aisleWidth,
isFlipped: isFlipped,
}
});
}
}
} else if (tempAisle !== null) {
setTempAisle(null);
}
});
useEffect(() => {
setTempAisle(null);
}, [toolMode, toggleView, tempPoints.length, aisleType, aisleWidth, aisleColor, activeLayer]);
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 'arrow-aisle':
return <ArrowAisle aisle={tempAisle} />;
case 'arrows-aisle':
return <ArrowsAisle aisle={tempAisle} />;
case 'circle-aisle':
return <CircleAisle aisle={tempAisle} />;
case 'junction-aisle':
return <JunctionAisle aisle={tempAisle} />;
case 'arc-aisle':
return <ArcAisle aisle={tempAisle} />
default:
return null;
}
};
const textPosition = new THREE.Vector3().addVectors(new THREE.Vector3(...tempAisle.points[0].position), new THREE.Vector3(...tempAisle.points[1].position)).divideScalar(2);
const distance = new THREE.Vector3(...tempAisle.points[0].position).distanceTo(new THREE.Vector3(...tempAisle.points[1].position));
const rendertext = () => {
return (
<>
{toggleView &&
<Html
key={tempAisle.aisleUuid}
userData={tempAisle}
position={[textPosition.x, 1, textPosition.z]}
wrapperClass="distance-text-wrapper"
className="distance-text"
zIndexRange={[1, 0]}
prepend
sprite
>
<div
key={tempAisle.aisleUuid}
className={`distance ${tempAisle.aisleUuid}`}
>
{distance.toFixed(2)} m
</div>
</Html>
}
</>
)
}
return (
<group name='Aisle-Reference-Group'>
{renderAisle()}
{rendertext()}
</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>
);
}
function ArrowAisle({ aisle }: { readonly aisle: Aisle }) {
const arrow = useMemo(() => {
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arrow-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);
const length = direction.length();
direction.normalize();
const shape = new THREE.Shape();
const arrowHeadLength = width * 2;
const shaftLength = length - arrowHeadLength;
if (shaftLength > 0) {
shape.moveTo(0, 0);
shape.lineTo(width, -arrowHeadLength);
shape.lineTo(width / 2, -arrowHeadLength);
shape.lineTo(width / 2, -length);
shape.lineTo(-width / 2, -length);
shape.lineTo(-width / 2, -arrowHeadLength);
shape.lineTo(-width, -arrowHeadLength);
shape.lineTo(0, 0);
} else {
shape.moveTo(0, 0);
shape.lineTo(width, -length);
shape.lineTo(-width, -length);
shape.lineTo(0, 0);
}
shape.closePath();
const position = end;
const angle = Math.atan2(direction.x, direction.z);
return { shape, position, rotationY: angle };
}, [aisle]);
if (!arrow) return null;
return (
<group
position={[0, (aisle.points[0].layer - 1) * Constants.wallConfig.height + 0.01, 0]}
rotation={[Math.PI / 2, 0, 0]}
>
<group position={[arrow.position.x, arrow.position.z, 0]} rotation={[0, 0, -arrow.rotationY]}>
<Extrude
args={[arrow.shape, { depth: 0.01, bevelEnabled: false }]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.aisleColor || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
</group>
</group>
);
}
function CircleAisle({ aisle }: { readonly aisle: Aisle }) {
const circle = useMemo(() => {
if (aisle.points.length < 2 || aisle.type.aisleType !== 'circle-aisle') return null;
const center = new THREE.Vector3(...aisle.points[0].position);
const widthCenter = new THREE.Vector3(...aisle.points[1].position);
const width = aisle.type.aisleWidth || 0.1;
const middleRadius = center.distanceTo(widthCenter);
const innerRadius = Math.max(0, middleRadius - width / 2);
const outerRadius = middleRadius + width / 2;
const shape = new THREE.Shape();
shape.absarc(center.x, center.z, outerRadius, 0, Math.PI * 2, false);
if (innerRadius > 0) {
const hole = new THREE.Path();
hole.absarc(center.x, center.z, innerRadius, 0, Math.PI * 2, true);
shape.holes.push(hole);
}
return {
shape,
rotationY: 0
};
}, [aisle]);
if (!circle) 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={[circle.shape, {
depth: 0.01,
bevelEnabled: false,
steps: 1,
curveSegments: 64
}]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.aisleColor || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
</group>
);
}
function JunctionAisle({ aisle }: { readonly aisle: Aisle }) {
const arrows = useMemo(() => {
if (aisle.points.length < 2 || aisle.type.aisleType !== 'junction-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 isFlipped = aisle.type.isFlipped || false;
const direction = new THREE.Vector3().subVectors(end, start);
const length = direction.length();
direction.normalize();
const mainShape = new THREE.Shape();
const arrowHeadLength = width * 2;
const shaftLength = length - arrowHeadLength;
if (shaftLength > 0) {
mainShape.moveTo(0, 0);
mainShape.lineTo(width, -arrowHeadLength);
mainShape.lineTo(width / 2, -arrowHeadLength);
mainShape.lineTo(width / 2, -length);
mainShape.lineTo(-width / 2, -length);
mainShape.lineTo(-width / 2, -arrowHeadLength);
mainShape.lineTo(-width, -arrowHeadLength);
mainShape.lineTo(0, 0);
} else {
mainShape.moveTo(0, 0);
mainShape.lineTo(width, -length);
mainShape.lineTo(-width, -length);
mainShape.lineTo(0, 0);
}
mainShape.closePath();
const secondaryLength = length / 4;
const secondaryShape = new THREE.Shape();
const secondaryHeadLength = width * 2;
const secondaryShaftLength = secondaryLength - secondaryHeadLength;
if (secondaryShaftLength > 0) {
secondaryShape.moveTo(0, 0);
secondaryShape.lineTo(width / 2, 0);
secondaryShape.lineTo(width / 2, secondaryShaftLength);
secondaryShape.lineTo(width, secondaryShaftLength);
secondaryShape.lineTo(0, secondaryLength);
secondaryShape.lineTo(-width, secondaryShaftLength);
secondaryShape.lineTo(-width / 2, secondaryShaftLength);
secondaryShape.lineTo(-width / 2, 0);
secondaryShape.lineTo(0, 0);
} else {
secondaryShape.moveTo(0, 0);
secondaryShape.lineTo(width, 0);
secondaryShape.lineTo(0, secondaryLength);
secondaryShape.lineTo(-width, 0);
secondaryShape.lineTo(0, 0);
}
secondaryShape.closePath();
const mainPosition = end;
const mainAngle = Math.atan2(direction.x, direction.z);
const perpendicularDirection = isFlipped
? new THREE.Vector3(direction.z, 0, -direction.x).normalize()
: new THREE.Vector3(-direction.z, 0, direction.x).normalize();
const secondaryAngle = Math.atan2(perpendicularDirection.x, perpendicularDirection.z);
const secondaryPosition = new THREE.Vector3().lerpVectors(start, end, 0.75);
return [
{ shape: mainShape, position: mainPosition, rotationY: mainAngle },
{
shape: secondaryShape,
position: secondaryPosition,
rotationY: secondaryAngle + Math.PI
}
];
}, [aisle]);
if (!arrows) 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((arrow, index) => (
<group key={index} position={[arrow.position.x, arrow.position.z, 0]} rotation={[0, 0, -arrow.rotationY]}>
<Extrude
args={[arrow.shape, { depth: 0.01, bevelEnabled: false }]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.aisleColor || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
</group>
))}
</group>
);
}
function ArcAisle({ aisle }: { readonly aisle: Aisle }) {
const arc = useMemo(() => {
if (aisle.points.length < 2 || aisle.type.aisleType !== 'arc-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.5;
const isFlipped = aisle.type.isFlipped || false;
const direction = new THREE.Vector3().subVectors(end, start);
const length = direction.length();
direction.normalize();
const perpendicular = new THREE.Vector3(-direction.z, 0, direction.x).normalize();
if (!isFlipped) perpendicular.negate();
const arcHeight = length * 0.25;
const midPoint = new THREE.Vector3().lerpVectors(start, end, 0.5);
const controlPoint = new THREE.Vector3().copy(midPoint).addScaledVector(perpendicular, arcHeight);
const widthOffset = perpendicular.clone().multiplyScalar(width / 2);
const p1 = new THREE.Vector3().copy(start).add(widthOffset);
const p2 = new THREE.Vector3().copy(end).add(widthOffset);
const p3 = new THREE.Vector3().copy(end).sub(widthOffset);
const p4 = new THREE.Vector3().copy(start).sub(widthOffset);
const shape = new THREE.Shape();
shape.moveTo(p1.x, p1.z);
shape.quadraticCurveTo(
controlPoint.x + widthOffset.x,
controlPoint.z + widthOffset.z,
p2.x, p2.z
);
shape.lineTo(p3.x, p3.z);
shape.quadraticCurveTo(
controlPoint.x - widthOffset.x,
controlPoint.z - widthOffset.z,
p4.x, p4.z
);
shape.lineTo(p1.x, p1.z);
return {
shape,
position: new THREE.Vector3(0, 0, 0),
rotationY: 0
};
}, [aisle]);
if (!arc) 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={[arc.shape, {
depth: 0.01,
bevelEnabled: false,
curveSegments: 32
}]}
receiveShadow
castShadow
>
<meshStandardMaterial
color={aisle.type.aisleColor || '#ffffff'}
side={THREE.DoubleSide}
/>
</Extrude>
</group>
);
}