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:
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
35
app/src/modules/scene/physics/conveyor/ribbonCollider.tsx
Normal file
35
app/src/modules/scene/physics/conveyor/ribbonCollider.tsx
Normal 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;
|
||||
154
app/src/modules/scene/physics/conveyor/splineCreator.tsx
Normal file
154
app/src/modules/scene/physics/conveyor/splineCreator.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 /> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
16
app/src/types/builderTypes.d.ts
vendored
16
app/src/types/builderTypes.d.ts
vendored
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user