feat: Implement Y-Split conveyor collider and update related types; refactor conveyor handling across components

This commit is contained in:
2025-08-07 09:11:06 +05:30
parent 33d960cedd
commit 6de1cacf8d
8 changed files with 473 additions and 230 deletions

View File

@@ -91,7 +91,6 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
useEffect(() => {
if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
// console.log('asset: ', asset);
getAssetConveyorPoints(asset.assetId).then((data) => {
console.log('asset.assetI: ', asset.assetId);
console.log('data: ', data);
@@ -102,6 +101,43 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
}
}, [asset.modelUuid, asset.eventData, ribbonData, boundingBox])
// useEffect(() => {
// if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
// console.log('asset: ', asset);
// if (asset.assetId === "97e037828ce57fa7bd1cc615") {
// setRibbonData({
// type: 'normal',
// points: [
// [
// [-2.4697049405553173e-9, 0.8729155659675598, -2.6850852955950217],
// [-2.4697049405553173e-9, 0.8729155659675598, 2.6950024154767225]
// ],
// [
// [-2.4697049405553173e-9, 1, -2.6850852955950217],
// [-2.4697049405553173e-9, 1, 2.6950024154767225]
// ]
// ]
// })
// }
// if (asset.assetId === "7ce992f669caaf2d384b9e76") {
// setRibbonData({
// type: 'curved',
// points: [
// [
// [-0.08963948491646367, 1.2324171172287208, 0.0013611617557632294],
// [2.745753362991343, 1.2324171172287208, -0.20188181291400256],
// [3.0696383388490056, 1.2324171172287208, -3.044220906761294],
// ],
// [
// [-0.08963948491646367, 2, 0.0013611617557632294],
// [2.745753362991343, 2, -0.20188181291400256],
// [3.0696383388490056, 2, -3.044220906761294],
// ]
// ],
// })
// }
// }
// }, [asset.modelUuid, asset.eventData, ribbonData, boundingBox])
useEffect(() => {
if (gltfScene) {

View File

@@ -1,12 +1,14 @@
import * as THREE from 'three';
import NormalConveyorCollider from './types/normalConveyorCollider';
import CurvedConveyorCollider from './types/curvedConveyorCollider';
import YSplitConveyorCollider from './types/ySplitConveyorCollider';
function RibbonCollider({ ribbonData, boundingBox, asset }: {
ribbonData: ConveyorPoints,
boundingBox: THREE.Box3 | null,
asset: Asset,
}) {
// console.log('ribbonData: ', ribbonData);
return (
<>
@@ -28,6 +30,15 @@ function RibbonCollider({ ribbonData, boundingBox, asset }: {
isPaused={false}
/>
}
{ribbonData.type === 'y-Split' &&
<YSplitConveyorCollider
points={ribbonData.points}
boundingBox={boundingBox}
asset={asset}
forward={false}
isPaused={false}
/>
}
</>
);
}

View File

@@ -8,99 +8,92 @@ export function SplineCreator() {
const { raycaster, camera, gl, pointer, controls, scene } = useThree()
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), [])
const [points, setPoints] = useState<THREE.Vector3[]>([])
const draggingRef = useRef<number | null>(null)
const [geometryKey, setGeometryKey] = useState(0)
const [splines, setSplines] = useState<THREE.Vector3[][]>([])
const [_, setTempPoints] = useState<THREE.Vector3[]>([])
const draggingRef = useRef<{ splineIndex: number, pointIndex: number } | null>(null)
useFrame(() => {
if (draggingRef.current !== null) {
const i = draggingRef.current
const y = points[i]?.y ?? 0
const { splineIndex, pointIndex } = draggingRef.current
const y = splines[splineIndex][pointIndex]?.y ?? 0
plane.constant = -y
raycaster.setFromCamera(pointer, camera)
const intersectionPoint = new THREE.Vector3()
const position = raycaster.ray.intersectPlane(plane, intersectionPoint) || intersectionPoint
setPoints(prev => {
setSplines(prev => {
const next = [...prev]
next[i] = new THREE.Vector3(position.x, y, position.z)
const spline = [...next[splineIndex]]
spline[pointIndex] = new THREE.Vector3(position.x, y, position.z)
next[splineIndex] = spline
return next
})
}
})
const geometry = useMemo(() => {
const geometries = useMemo(() => {
const width = 1
const segments = 20
const vertices: number[] = []
const indices: number[] = []
const groupGeometries: THREE.BufferGeometry[] = []
if (points.length < 3) return null
for (const spline of splines) {
for (let i = 0; i + 2 < spline.length; i += 3) {
const p0 = spline[i]
const p1 = spline[i + 1]
const p2 = spline[i + 2]
const vertices: number[] = []
const indices: number[] = []
for (let group = 0; group + 2 < points.length; group += 2) {
const p0 = points[group]
const p1 = points[group + 1]
const p2 = points[group + 2]
for (let j = 0; j <= segments; j++) {
const t = j / segments
const point = new THREE.Vector3()
.copy(p0).multiplyScalar((1 - t) ** 2)
.addScaledVector(p1, 2 * (1 - t) * t)
.addScaledVector(p2, t ** 2)
for (let i = 0; i <= segments; i++) {
const t = i / segments
const point = new THREE.Vector3()
.copy(p0)
.multiplyScalar((1 - t) ** 2)
.addScaledVector(p1, 2 * (1 - t) * t)
.addScaledVector(p2, t ** 2)
const tangent = new THREE.Vector3()
.copy(p0).multiplyScalar(-2 * (1 - t))
.addScaledVector(p1, 2 - 4 * t)
.addScaledVector(p2, 2 * t)
.normalize()
const tangent = new THREE.Vector3()
.copy(p0)
.multiplyScalar(-2 * (1 - t))
.addScaledVector(p1, 2 - 4 * t)
.addScaledVector(p2, 2 * t)
.normalize()
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize()
const left = new THREE.Vector3().copy(point).addScaledVector(normal, -width / 2)
const right = new THREE.Vector3().copy(point).addScaledVector(normal, width / 2)
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize()
const left = new THREE.Vector3().copy(point).addScaledVector(normal, -width / 2)
const right = new THREE.Vector3().copy(point).addScaledVector(normal, width / 2)
vertices.push(...left.toArray(), ...right.toArray())
}
vertices.push(...left.toArray())
vertices.push(...right.toArray())
for (let j = 0; j < segments; j++) {
const base = j * 2
indices.push(base, base + 1, base + 2)
indices.push(base + 1, base + 3, base + 2)
}
const ribbonGeometry = new THREE.BufferGeometry()
ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
ribbonGeometry.setIndex(indices)
ribbonGeometry.computeVertexNormals()
groupGeometries.push(ribbonGeometry)
}
}
const totalSegments = ((points.length - 1) / 2) * segments
for (let i = 0; i < totalSegments; i++) {
const base = i * 2
indices.push(base, base + 1, base + 2)
indices.push(base + 1, base + 3, base + 2)
}
const ribbonGeometry = new THREE.BufferGeometry()
ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
ribbonGeometry.setIndex(indices)
ribbonGeometry.computeVertexNormals()
setGeometryKey(k => k + 1)
return ribbonGeometry
}, [points])
return groupGeometries
}, [splines])
useEffect(() => {
const canvas = gl.domElement
const onMouseUp = (evt: MouseEvent) => {
const onMouseUp = () => {
(controls as CameraControls).enabled = true
if (evt.button === 0 && draggingRef.current !== null) {
draggingRef.current = null
setGeometryKey(k => k + 1)
}
draggingRef.current = null
}
canvas.addEventListener('mouseup', onMouseUp)
return () => canvas.removeEventListener('mouseup', onMouseUp)
}, [camera])
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (points.length >= 3) return
e.preventDefault()
raycaster.setFromCamera(pointer, camera)
const intersections = raycaster.intersectObject(scene, true)
@@ -108,47 +101,55 @@ export function SplineCreator() {
if (intersections.length > 0) {
const hitPoint = intersections[0].point.clone()
setPoints(prev => {
if (prev.length === 0) return [hitPoint]
if (prev.length === 1) {
const p0 = prev[0]
const p2 = hitPoint
setTempPoints(temp => {
const nextTemp = [...temp, hitPoint]
if (nextTemp.length === 2) {
const [p0, p2] = nextTemp
const mid = new THREE.Vector3().addVectors(p0, p2).multiplyScalar(0.5)
return [p0, mid, p2]
const newSpline = [p0, mid, p2]
setSplines(prev => [...prev, newSpline])
return []
}
return prev
return nextTemp
})
}
}
const canvas = gl.domElement
canvas.addEventListener('click', handleClick)
return () => canvas.removeEventListener('click', handleClick)
}, [points, raycaster, pointer, camera, gl, scene])
canvas.addEventListener('dblclick', handleClick)
return () => canvas.removeEventListener('dblclick', handleClick)
}, [raycaster, pointer, camera, gl, scene])
useEffect(() => {
console.log(splines);
}, [splines])
return (
<>
{points.map((p, i) => (
<Sphere
key={i}
position={p}
args={[0.1, 16, 16]}
onPointerDown={() => {
draggingRef.current = i;
(controls as CameraControls).enabled = false
}}
>
<meshStandardMaterial color={'red'} />
</Sphere>
))}
{splines.map((spline, splineIndex) =>
spline.map((point, pointIndex) => (
<Sphere
key={`${splineIndex}-${pointIndex}`}
position={point}
args={[0.1, 16, 16]}
onPointerDown={() => {
draggingRef.current = { splineIndex, pointIndex };
(controls as CameraControls).enabled = false
}}
>
<meshStandardMaterial color="red" />
</Sphere>
))
)}
{geometry && (
<RigidBody key={geometryKey} type="fixed" colliders="trimesh">
<mesh geometry={geometry}>
{geometries.map((geom, idx) => (
<RigidBody key={idx} type="fixed" colliders="trimesh">
<mesh geometry={geom}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} />
</mesh>
</RigidBody>
)}
))}
</>
)
}

View File

@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
function CurvedConveyorCollider({ points, boundingBox, asset, forward, isPaused }: {
points: [number, number, number][],
points: [number, number, number][][],
boundingBox: THREE.Box3 | null,
asset: Asset,
forward: boolean
@@ -46,30 +46,35 @@ function CurvedConveyorCollider({ points, boundingBox, asset, forward, isPaused
}
};
useFrame(() => {
const vectorPoints = points.map(p => new THREE.Vector3(...p));
if (!forward) {
vectorPoints.reverse();
}
const bezierPoints = useMemo(() => {
const segments = 20;
const bezierPoints: THREE.Vector3[] = [];
const allPoints: THREE.Vector3[] = [];
for (let group = 0; group + 2 < vectorPoints.length; group += 2) {
const p0 = vectorPoints[group];
const p1 = vectorPoints[group + 1];
const p2 = vectorPoints[group + 2];
points.forEach(segment => {
let vectorPoints = segment.map(p => new THREE.Vector3(...p));
if (!forward) vectorPoints.reverse();
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const point = new THREE.Vector3()
.copy(p0)
.multiplyScalar((1 - t) ** 2)
.addScaledVector(p1, 2 * (1 - t) * t)
.addScaledVector(p2, t ** 2);
bezierPoints.push(point);
for (let group = 0; group + 2 < vectorPoints.length; group += 2) {
const p0 = vectorPoints[group];
const p1 = vectorPoints[group + 1];
const p2 = vectorPoints[group + 2];
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const point = new THREE.Vector3()
.copy(p0)
.multiplyScalar((1 - t) ** 2)
.addScaledVector(p1, 2 * (1 - t) * t)
.addScaledVector(p2, t ** 2);
allPoints.push(point);
}
}
}
});
return allPoints;
}, [points, forward]);
useFrame(() => {
const assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
@@ -107,77 +112,85 @@ function CurvedConveyorCollider({ points, boundingBox, asset, forward, isPaused
});
});
const geometry = useMemo(() => {
const width = 1
const segments = 20
const vertices: number[] = []
const indices: number[] = []
const geometries = useMemo(() => {
const width = 1;
const segments = 20;
const geos: THREE.BufferGeometry[] = [];
const vectorPoint = points.map((point) => new THREE.Vector3(...point))
points.forEach(segment => {
const vertices: number[] = [];
const indices: number[] = [];
if (vectorPoint.length < 3) return null
const vectorPoint = segment.map(p => new THREE.Vector3(...p));
if (vectorPoint.length < 3) return;
for (let group = 0; group + 2 < vectorPoint.length; group += 2) {
const p0 = vectorPoint[group]
const p1 = vectorPoint[group + 1]
const p2 = vectorPoint[group + 2]
for (let group = 0; group + 2 < vectorPoint.length; group += 2) {
const p0 = vectorPoint[group];
const p1 = vectorPoint[group + 1];
const p2 = vectorPoint[group + 2];
for (let i = 0; i <= segments; i++) {
const t = i / segments
const point = new THREE.Vector3()
.copy(p0)
.multiplyScalar((1 - t) ** 2)
.addScaledVector(p1, 2 * (1 - t) * t)
.addScaledVector(p2, t ** 2)
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const point = new THREE.Vector3()
.copy(p0)
.multiplyScalar((1 - t) ** 2)
.addScaledVector(p1, 2 * (1 - t) * t)
.addScaledVector(p2, t ** 2);
const tangent = new THREE.Vector3()
.copy(p0)
.multiplyScalar(-2 * (1 - t))
.addScaledVector(p1, 2 - 4 * t)
.addScaledVector(p2, 2 * t)
.normalize()
const tangent = new THREE.Vector3()
.copy(p0)
.multiplyScalar(-2 * (1 - t))
.addScaledVector(p1, 2 - 4 * t)
.addScaledVector(p2, 2 * t)
.normalize();
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize()
const left = new THREE.Vector3().copy(point).addScaledVector(normal, -width / 2)
const right = new THREE.Vector3().copy(point).addScaledVector(normal, width / 2)
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
const left = new THREE.Vector3().copy(point).addScaledVector(normal, -width / 2);
const right = new THREE.Vector3().copy(point).addScaledVector(normal, width / 2);
vertices.push(...left.toArray())
vertices.push(...right.toArray())
vertices.push(...left.toArray(), ...right.toArray());
}
}
}
const totalSegments = ((points.length - 1) / 2) * segments
for (let i = 0; i < totalSegments; i++) {
const base = i * 2
indices.push(base, base + 1, base + 2)
indices.push(base + 1, base + 3, base + 2)
}
const totalSegments = ((vectorPoint.length - 1) / 2) * segments;
for (let i = 0; i < totalSegments; i++) {
const base = i * 2;
indices.push(base, base + 1, base + 2);
indices.push(base + 1, base + 3, base + 2);
}
const ribbonGeometry = new THREE.BufferGeometry()
ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
ribbonGeometry.setIndex(indices)
ribbonGeometry.computeVertexNormals()
setGeometryKey(k => k + 1)
return ribbonGeometry
const ribbonGeometry = new THREE.BufferGeometry();
ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
ribbonGeometry.setIndex(indices);
ribbonGeometry.computeVertexNormals();
geos.push(ribbonGeometry);
});
setGeometryKey(k => k + 1);
return geos;
}, [points, asset.position, asset.rotation]);
return (
geometry && (
<RigidBody
key={geometryKey}
ref={conveyorRef}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={handleMaterialEnter}
onCollisionExit={handleMaterialExit}
colliders="trimesh"
>
<mesh geometry={geometry}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
</mesh>
</RigidBody>
)
<>
{geometries.length > 0 && (
<RigidBody
key={geometryKey}
ref={conveyorRef}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={handleMaterialEnter}
onCollisionExit={handleMaterialExit}
colliders="trimesh"
>
{geometries.map((geometry, index) => (
<mesh key={index} geometry={geometry}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
</mesh>
))}
</RigidBody>
)}
</>
);
}

View File

@@ -3,18 +3,23 @@ import { CollisionPayload, RigidBody } from '@react-three/rapier';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
function NormalConveyorCollider({ points, boundingBox, asset, forward, isPaused }: {
points: [number, number, number][],
boundingBox: THREE.Box3 | null,
asset: Asset,
forward: boolean
isPaused: boolean
function NormalConveyorCollider({
points,
boundingBox,
asset,
forward,
isPaused,
}: {
points: [number, number, number][][];
boundingBox: THREE.Box3 | null;
asset: Asset;
forward: boolean;
isPaused: boolean;
}) {
const conveyorRef = useRef<any>(null);
const conveyorRefs = useRef<any[]>([]);
const [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
const conveyorSpeed = 2;
const [geometryKey, setGeometryKey] = useState(0);
useEffect(() => {
if (!boundingBox) return;
@@ -47,35 +52,42 @@ function NormalConveyorCollider({ points, boundingBox, asset, forward, isPaused
};
useFrame(() => {
const curve = new THREE.CatmullRomCurve3(points.map(p => new THREE.Vector3(...p)));
const assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
const inverseQuat = assetQuat.clone().invert();
const allCurvePoints: THREE.Vector3[] = [];
const segmentCurves: THREE.Vector3[][] = [];
// Build all curve points across segments
points.forEach(segment => {
const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
const curvePoints = curve.getPoints((segment.length - 1) * 30);
segmentCurves.push(curvePoints);
allCurvePoints.push(...curvePoints);
});
if (!forward) allCurvePoints.reverse();
objectsOnConveyor.forEach(rigidBody => {
const worldPos = new THREE.Vector3().copy(rigidBody.translation());
const localPos = worldPos.clone().sub(assetPos).applyQuaternion(inverseQuat);
const curvePoints = curve.getPoints(100);
if (!forward) {
curvePoints.reverse();
}
// Find closest point on full conveyor
let closestIndex = 0;
let minDist = Infinity;
for (let i = 0; i < curvePoints.length; i++) {
const dist = curvePoints[i].distanceToSquared(localPos);
for (let i = 0; i < allCurvePoints.length; i++) {
const dist = allCurvePoints[i].distanceToSquared(localPos);
if (dist < minDist) {
minDist = dist;
closestIndex = i;
}
}
const point = curvePoints[closestIndex];
const prev = curvePoints[closestIndex - 1] || point;
const next = curvePoints[closestIndex + 1] || point;
const point = allCurvePoints[closestIndex];
const prev = allCurvePoints[closestIndex - 1] || point;
const next = allCurvePoints[closestIndex + 1] || point;
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
const side = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
const relative = new THREE.Vector3().subVectors(localPos, point);
@@ -90,61 +102,64 @@ function NormalConveyorCollider({ points, boundingBox, asset, forward, isPaused
});
});
const geometry = useMemo(() => {
if (points.length < 2) return null;
const geometries = useMemo(() => {
const width = 1;
const segments = 30;
const curve = new THREE.CatmullRomCurve3(points.map(p => new THREE.Vector3(...p)));
const curvePoints = curve.getPoints((points.length - 1) * segments);
const vertices: number[] = [];
const indices: number[] = [];
return points.map(segment => {
if (segment.length < 2) return null;
for (let i = 0; i < curvePoints.length; i++) {
const point = curvePoints[i];
const prev = curvePoints[i - 1] || point;
const next = curvePoints[i + 1] || point;
const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
const curvePoints = curve.getPoints((segment.length - 1) * segments);
const vertices: number[] = [];
const indices: number[] = [];
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
for (let i = 0; i < curvePoints.length; i++) {
const point = curvePoints[i];
const prev = curvePoints[i - 1] || point;
const next = curvePoints[i + 1] || point;
const left = point.clone().addScaledVector(normal, -width / 2);
const right = point.clone().addScaledVector(normal, width / 2);
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
vertices.push(...left.toArray(), ...right.toArray());
}
const left = point.clone().addScaledVector(normal, -width / 2);
const right = point.clone().addScaledVector(normal, width / 2);
for (let i = 0; i < curvePoints.length - 1; i++) {
const base = i * 2;
indices.push(base, base + 1, base + 2);
indices.push(base + 1, base + 3, base + 2);
}
vertices.push(...left.toArray(), ...right.toArray());
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geo.setIndex(indices);
geo.computeVertexNormals();
setGeometryKey(k => k + 1);
return geo;
for (let i = 0; i < curvePoints.length - 1; i++) {
const base = i * 2;
indices.push(base, base + 1, base + 2);
indices.push(base + 1, base + 3, base + 2);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
}).filter(Boolean);
}, [points, asset.position, asset.rotation]);
return (
geometry && (
<RigidBody
key={geometryKey}
ref={conveyorRef}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={handleMaterialEnter}
onCollisionExit={handleMaterialExit}
colliders="trimesh"
>
<mesh geometry={geometry}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
</mesh>
</RigidBody>
)
<>
{geometries.map((geometry, index) => (
<RigidBody
key={index}
ref={el => (conveyorRefs.current[index] = el)}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={handleMaterialEnter}
onCollisionExit={handleMaterialExit}
colliders="trimesh"
>
<mesh geometry={geometry!}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
</mesh>
</RigidBody>
))}
</>
);
}

View File

@@ -0,0 +1,167 @@
import * as THREE from 'three';
import { CollisionPayload, RigidBody } from '@react-three/rapier';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
function YSplitConveyorCollider({
points,
boundingBox,
asset,
forward,
isPaused,
}: {
points: [number, number, number][][];
boundingBox: THREE.Box3 | null;
asset: Asset;
forward: boolean;
isPaused: boolean;
}) {
const conveyorRefs = useRef<any[]>([]);
const [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
const conveyorSpeed = 2;
useEffect(() => {
if (!boundingBox) return;
const size = boundingBox.getSize(new THREE.Vector3());
const [width, depth] = [size.x, size.z];
conveyorDirection.current.set(width < depth ? 0 : 1, 0, width < depth ? 1 : 0);
const rotation = new THREE.Euler().fromArray(asset.rotation || [0, 0, 0]);
conveyorDirection.current.applyEuler(rotation);
}, [boundingBox, asset.rotation]);
const handleMaterialEnter = (e: CollisionPayload) => {
if (e.other.rigidBody) {
setObjectsOnConveyor(prev => {
const newSet = new Set(prev);
newSet.add(e.other.rigidBody);
return newSet;
});
}
};
const handleMaterialExit = (e: CollisionPayload) => {
if (e.other.rigidBody) {
setObjectsOnConveyor(prev => {
const newSet = new Set(prev);
newSet.delete(e.other.rigidBody);
return newSet;
});
}
};
useFrame(() => {
const assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
const inverseQuat = assetQuat.clone().invert();
const allCurvePoints: THREE.Vector3[] = [];
const segmentCurves: THREE.Vector3[][] = [];
// Build all curve points across segments
points.forEach(segment => {
const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
const curvePoints = curve.getPoints((segment.length - 1) * 30);
segmentCurves.push(curvePoints);
allCurvePoints.push(...curvePoints);
});
if (!forward) allCurvePoints.reverse();
objectsOnConveyor.forEach(rigidBody => {
const worldPos = new THREE.Vector3().copy(rigidBody.translation());
const localPos = worldPos.clone().sub(assetPos).applyQuaternion(inverseQuat);
// Find closest point on full conveyor
let closestIndex = 0;
let minDist = Infinity;
for (let i = 0; i < allCurvePoints.length; i++) {
const dist = allCurvePoints[i].distanceToSquared(localPos);
if (dist < minDist) {
minDist = dist;
closestIndex = i;
}
}
const point = allCurvePoints[closestIndex];
const prev = allCurvePoints[closestIndex - 1] || point;
const next = allCurvePoints[closestIndex + 1] || point;
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
const side = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
const relative = new THREE.Vector3().subVectors(localPos, point);
const sideOffset = relative.dot(side);
const centeringForce = side.clone().multiplyScalar(-sideOffset * 10);
const forwardForce = tangent.clone().multiplyScalar(conveyorSpeed);
const totalForce = forwardForce.add(centeringForce).applyQuaternion(assetQuat);
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
rigidBody.setLinvel(totalForce, true);
});
});
const geometries = useMemo(() => {
const width = 1;
const segments = 30;
return points.map(segment => {
if (segment.length < 2) return null;
const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
const curvePoints = curve.getPoints((segment.length - 1) * segments);
const vertices: number[] = [];
const indices: number[] = [];
for (let i = 0; i < curvePoints.length; i++) {
const point = curvePoints[i];
const prev = curvePoints[i - 1] || point;
const next = curvePoints[i + 1] || point;
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
const left = point.clone().addScaledVector(normal, -width / 2);
const right = point.clone().addScaledVector(normal, width / 2);
vertices.push(...left.toArray(), ...right.toArray());
}
for (let i = 0; i < curvePoints.length - 1; i++) {
const base = i * 2;
indices.push(base, base + 1, base + 2);
indices.push(base + 1, base + 3, base + 2);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
}).filter(Boolean);
}, [points, asset.position, asset.rotation]);
return (
<>
{geometries.map((geometry, index) => (
<RigidBody
key={index}
ref={el => (conveyorRefs.current[index] = el)}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={handleMaterialEnter}
onCollisionExit={handleMaterialExit}
colliders="trimesh"
>
<mesh geometry={geometry!}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
</mesh>
</RigidBody>
))}
</>
);
}
export default YSplitConveyorCollider;

View File

@@ -24,7 +24,7 @@ function PhysicsSimulator() {
<ColliderCreator />
{/* <SplineCreator /> */}
<SplineCreator />
</>
)
}

View File

@@ -49,17 +49,17 @@ type Assets = Asset[];
type NormalConveyor = {
type: 'normal';
points: [number, number, number][];
points: [number, number, number][][];
}
type YJunctionConveyor = {
type: 'y-junction';
type: 'y-Split';
points: [number, number, number][][];
}
type CurvedConveyor = {
type: 'curved';
points: [number, number, number][];
points: [number, number, number][][];
}
type ConveyorPoints = NormalConveyor | YJunctionConveyor | CurvedConveyor;