sline points

Replaced hardcoded spline points with first two raycast intersection points on click.

Computed the middle control point using the incenter of a triangle formed with the midpoint as the third vertex, creating a dynamic Bézier curve.
This commit is contained in:
2025-08-01 15:57:11 +05:30
parent 1eac23607f
commit 3394c44ed1
9 changed files with 592 additions and 208 deletions

View File

@@ -22,8 +22,8 @@ import { getAssetIksApi } from '../../../../../services/simulation/ik/getAssetIK
import { ModelAnimator } from './animator/modelAnimator';
import ConveyorCollider from './conveyorCollider';
import RibbonCollider from './ribbonCollider';
import ConveyorCollider from '../../../../scene/physics/conveyor/conveyorCollider';
import RibbonCollider from '../../../../scene/physics/conveyor/ribbonCollider';
function Model({ asset, isRendered }: { readonly asset: Asset, isRendered: boolean }) {
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
@@ -52,11 +52,11 @@ function Model({ asset, isRendered }: { readonly asset: Asset, isRendered: boole
const isRightMouseDown = useRef(false);
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
const [boundingBox, setBoundingBox] = useState<THREE.Box3 | null>(null);
const [conveyorPlaneSize, setConveyorPlaneSize] = useState<[number, number] | null>(null);
const groupRef = useRef<THREE.Group>(null);
const rigidBodyRef = useRef<RapierRigidBody>(null);
const [isSelected, setIsSelected] = useState(false);
const [ikData, setIkData] = useState<any>();
const [ribbonData, setRibbonData] = useState<ConveyorPoints>();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { userId, organization } = getUserData();
@@ -89,6 +89,31 @@ function Model({ asset, isRendered }: { readonly asset: Asset, isRendered: boole
}
}, [asset.modelUuid, ikData])
useEffect(() => {
if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
console.log('asset: ', asset);
if (asset.assetId === '7dc04e36882e4debbc1a8e3d') {
setRibbonData({
type: 'normal',
points: [
[-2.4697049405553173e-9, 0.8729155659675598, -2.6850852955950217],
[-2.4697049405553173e-9, 0.8729155659675598, 2.6950024154767225]
]
})
}
if (asset.assetId === '7a4de28658830e2e42abc06d') {
setRibbonData({
type: 'curved',
points: [
[-0.08963948491646367, 1.2324171172287208, 0.0013611617557632294],
[2.745753362991343, 1.2324171172287208, -0.20188181291400256],
[3.0696383388490056, 1.2324171172287208, -3.044220906761294],
],
})
}
}
}, [asset.modelUuid, asset.eventData, ribbonData, boundingBox])
useEffect(() => {
if (gltfScene) {
gltfScene.traverse((child: any) => {
@@ -183,11 +208,6 @@ function Model({ asset, isRendered }: { readonly asset: Asset, isRendered: boole
const calculateBoundingBox = (scene: THREE.Object3D) => {
const box = new THREE.Box3().setFromObject(scene);
setBoundingBox(box);
if (asset.eventData?.type === 'Conveyor') {
const size = box.getSize(new THREE.Vector3());
setConveyorPlaneSize([size.x, size.z]);
}
};
loadModel();
@@ -470,16 +490,19 @@ function Model({ asset, isRendered }: { readonly asset: Asset, isRendered: boole
<AssetBoundingBox name='Asset Fallback' boundingBox={boundingBox} color='gray' lineWidth={2.5} />
)}
{/* <ConveyorCollider boundingBox={boundingBox}
asset={asset}
conveyorPlaneSize={conveyorPlaneSize}
/> */}
<RibbonCollider boundingBox={boundingBox}
asset={asset}
conveyorPlaneSize={conveyorPlaneSize}
/>
{/* <ConveyorCollider
boundingBox={boundingBox}
asset={asset}
/> */}
{ribbonData &&
<RibbonCollider
key={asset.modelUuid}
boundingBox={boundingBox}
ribbonData={ribbonData}
asset={asset}
/>
}
{isSelected &&
<AssetBoundingBox name='Asset BBox' boundingBox={boundingBox} color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"} lineWidth={2.7} />
}

View File

@@ -1,182 +0,0 @@
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';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../../simulation/products/productContext';
function RibbonCollider({ boundingBox, asset, conveyorPlaneSize }: {
boundingBox: THREE.Box3 | null,
asset: Asset,
conveyorPlaneSize: [number, number] | null,
}) {
const { productStore } = useSceneContext();
const { getEventByModelUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const conveyorRef = useRef<any>(null);
const [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
const conveyorSpeed = 2;
const [geometryKey, setGeometryKey] = useState(0);
const event = getEventByModelUuid(
selectedProduct.productUuid,
asset.modelUuid
) as ConveyorEventSchema | undefined;
useEffect(() => {
if (!boundingBox || !conveyorPlaneSize) return;
const [width, depth] = conveyorPlaneSize;
if (width < depth) {
conveyorDirection.current.set(0, 0, 1);
} else {
conveyorDirection.current.set(1, 0, 0);
}
const rotation = new THREE.Euler().fromArray(asset.rotation || [0, 0, 0]);
conveyorDirection.current.applyEuler(rotation);
}, [boundingBox, conveyorPlaneSize, 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(() => {
if (!event?.points || event.points.length < 2) return;
const curve = new THREE.CatmullRomCurve3(
event.points.map(p => new THREE.Vector3(...p.position))
);
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 inverseAssetQuat = assetQuat.clone().invert();
objectsOnConveyor.forEach(rigidBody => {
if (!rigidBody) return;
const worldPos = new THREE.Vector3().copy(rigidBody.translation());
const localPos = worldPos.clone().sub(assetPos).applyQuaternion(inverseAssetQuat);
const curvePoints = curve.getPoints(100);
let closestIndex = 0;
let minDist = Infinity;
for (let i = 0; i < curvePoints.length; i++) {
const dist = curvePoints[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 tangentLocal = new THREE.Vector3().subVectors(next, prev).normalize();
const sideLocal = new THREE.Vector3().crossVectors(tangentLocal, new THREE.Vector3(0, 1, 0)).normalize();
const relative = new THREE.Vector3().subVectors(localPos, point);
const sideOffset = relative.dot(sideLocal);
const centeringStrength = 10;
const centeringForceLocal = sideLocal.clone().multiplyScalar(-sideOffset * centeringStrength);
const forwardForceLocal = tangentLocal.clone().multiplyScalar(conveyorSpeed);
const totalForceLocal = forwardForceLocal.add(centeringForceLocal);
const totalForceWorld = totalForceLocal.applyQuaternion(assetQuat);
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
rigidBody.setLinvel(totalForceWorld, true);
});
});
const geometry = useMemo(() => {
if (asset.eventData?.type !== 'Conveyor' || !conveyorPlaneSize) return null;
const width = 1;
const segments = 30;
const vertices: number[] = [];
const indices: number[] = [];
if (!event || !event.points || event.points.length < 2) return null;
const points = event.points.map(p => new THREE.Vector3(p.position[0], p.position[1], p.position[2]));
if (points.length < 2) return null;
const curve = new THREE.CatmullRomCurve3(points);
const curvePoints = curve.getPoints((points.length - 1) * segments);
for (let i = 0; i < curvePoints.length; i++) {
const point = curvePoints[i];
const prev = curvePoints[i - 1] || curvePoints[i];
const next = curvePoints[i + 1] || curvePoints[i];
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 = 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());
}
const totalSegments = curvePoints.length - 1;
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;
}, [asset.eventData, event]);
return (
<>
{asset.eventData?.type === 'Conveyor' && conveyorPlaneSize && 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} opacity={0.5} transparent />
</mesh>
</RigidBody>
)}
</>
);
}
export default RibbonCollider;

View File

@@ -3,18 +3,19 @@ import { CollisionPayload, RigidBody } from '@react-three/rapier';
import { useEffect, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize }: {
function ConveyorCollider({ boundingBox, asset }: {
boundingBox: THREE.Box3 | null,
asset: Asset,
conveyorPlaneSize: [number, number] | null,
}) {
const conveyorRef = useRef<any>(null);
const [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
const conveyorSpeed = 2;
const size = boundingBox?.getSize(new THREE.Vector3());
useEffect(() => {
if (!boundingBox || !conveyorPlaneSize) return;
const [width, depth] = conveyorPlaneSize;
if (!boundingBox || !size) return;
const [width, depth] = [size.x, size.z];
if (width < depth) {
conveyorDirection.current.set(0, 0, 1);
} else {
@@ -22,7 +23,7 @@ function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize }: {
}
const rotation = new THREE.Euler().fromArray(asset.rotation || [0, 0, 0]);
conveyorDirection.current.applyEuler(rotation);
}, [boundingBox, conveyorPlaneSize, asset.rotation]);
}, [boundingBox, asset.rotation]);
const handleMaterialEnter = (e: CollisionPayload) => {
if (e.other.rigidBody) {
@@ -107,7 +108,7 @@ function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize }: {
return (
<>
{asset.eventData?.type === 'Conveyor' && conveyorPlaneSize && (
{asset.eventData?.type === 'Conveyor' && boundingBox && size && (
<RigidBody
ref={conveyorRef}
type="fixed"
@@ -120,7 +121,7 @@ function ConveyorCollider({ boundingBox, asset, conveyorPlaneSize }: {
colliders="cuboid"
>
<mesh>
<planeGeometry args={conveyorPlaneSize} />
<planeGeometry args={[size.x, size.z]} />
<meshBasicMaterial
color="green"
transparent

View File

@@ -0,0 +1,35 @@
import * as THREE from 'three';
import NormalConveyorCollider from './types/normalConveyorCollider';
import CurvedConveyorCollider from './types/curvedConveyorCollider';
function RibbonCollider({ ribbonData, boundingBox, asset }: {
ribbonData: ConveyorPoints,
boundingBox: THREE.Box3 | null,
asset: Asset,
}) {
return (
<>
{ribbonData.type === 'normal' &&
<NormalConveyorCollider
points={ribbonData.points}
boundingBox={boundingBox}
asset={asset}
forward={false}
isPaused={false}
/>
}
{ribbonData.type === 'curved' &&
<CurvedConveyorCollider
points={ribbonData.points}
boundingBox={boundingBox}
asset={asset}
forward={false}
isPaused={false}
/>
}
</>
);
}
export default RibbonCollider;

View File

@@ -0,0 +1,154 @@
import * as THREE from 'three'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { RigidBody } from '@react-three/rapier'
import { CameraControls, Sphere } from '@react-three/drei'
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)
useFrame(() => {
if (draggingRef.current !== null) {
const i = draggingRef.current
const y = points[i]?.y ?? 0
plane.constant = -y
raycaster.setFromCamera(pointer, camera)
const intersectionPoint = new THREE.Vector3()
const position = raycaster.ray.intersectPlane(plane, intersectionPoint) || intersectionPoint
setPoints(prev => {
const next = [...prev]
next[i] = new THREE.Vector3(position.x, y, position.z)
return next
})
}
})
const geometry = useMemo(() => {
const width = 1
const segments = 20
const vertices: number[] = []
const indices: number[] = []
if (points.length < 3) return null
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 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 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())
}
}
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])
useEffect(() => {
const canvas = gl.domElement
const onMouseUp = (evt: MouseEvent) => {
(controls as CameraControls).enabled = true
if (evt.button === 0 && draggingRef.current !== null) {
draggingRef.current = null
setGeometryKey(k => k + 1)
}
}
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)
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
const mid = new THREE.Vector3().addVectors(p0, p2).multiplyScalar(0.5)
return [p0, mid, p2]
}
return prev
})
}
}
const canvas = gl.domElement
canvas.addEventListener('click', handleClick)
return () => canvas.removeEventListener('click', handleClick)
}, [points, raycaster, pointer, camera, gl, scene])
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>
))}
{geometry && (
<RigidBody key={geometryKey} type="fixed" colliders="trimesh">
<mesh geometry={geometry}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} />
</mesh>
</RigidBody>
)}
</>
)
}

View File

@@ -0,0 +1,184 @@
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 CurvedConveyorCollider({ 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 [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;
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 vectorPoints = points.map(p => new THREE.Vector3(...p));
if (!forward) {
vectorPoints.reverse();
}
const segments = 20;
const bezierPoints: 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];
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);
}
}
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();
objectsOnConveyor.forEach(rigidBody => {
const worldPos = new THREE.Vector3().copy(rigidBody.translation());
const localPos = worldPos.clone().sub(assetPos).applyQuaternion(inverseQuat);
let closestIndex = 0;
let minDist = Infinity;
for (let i = 0; i < bezierPoints.length; i++) {
const dist = bezierPoints[i].distanceToSquared(localPos);
if (dist < minDist) {
minDist = dist;
closestIndex = i;
}
}
const point = bezierPoints[closestIndex];
const prev = bezierPoints[closestIndex - 1] || point;
const next = bezierPoints[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 geometry = useMemo(() => {
const width = 1
const segments = 20
const vertices: number[] = []
const indices: number[] = []
const vectorPoint = points.map((point) => new THREE.Vector3(...point))
if (vectorPoint.length < 3) return null
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)
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)
vertices.push(...left.toArray())
vertices.push(...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 ribbonGeometry = new THREE.BufferGeometry()
ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
ribbonGeometry.setIndex(indices)
ribbonGeometry.computeVertexNormals()
setGeometryKey(k => k + 1)
return ribbonGeometry
}, [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>
)
);
}
export default CurvedConveyorCollider;

View File

@@ -0,0 +1,151 @@
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 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 [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;
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 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();
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();
}
let closestIndex = 0;
let minDist = Infinity;
for (let i = 0; i < curvePoints.length; i++) {
const dist = curvePoints[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 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 geometry = useMemo(() => {
if (points.length < 2) return null;
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[] = [];
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();
setGeometryKey(k => k + 1);
return geo;
}, [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>
)
);
}
export default NormalConveyorCollider;

View File

@@ -1,5 +1,6 @@
import MaterialSpawner from './materialSpawner'
import ColliderCreator from './colliders/colliderCreator'
import { SplineCreator } from './conveyor/splineCreator'
function PhysicsSimulator() {
return (
@@ -15,14 +16,15 @@ function PhysicsSimulator() {
spawnInterval={1000}
spawnCount={5}
/>
<MaterialSpawner
{/* <MaterialSpawner
position={[6, 3, -6]}
spawnInterval={1000}
spawnCount={5}
/>
/> */}
<ColliderCreator />
{/* <SplineCreator /> */}
</>
)
}

View File

@@ -47,6 +47,22 @@ interface Asset {
type Assets = Asset[];
type NormalConveyor = {
type: 'normal';
points: [number, number, number][];
}
type YJunctionConveyor = {
type: 'y-junction';
points: [number, number, number][][];
}
type CurvedConveyor = {
type: 'curved';
points: [number, number, number][];
}
type ConveyorPoints = NormalConveyor | YJunctionConveyor | CurvedConveyor;
// Wall-Asset