From 3394c44ed1ad726d7752a42d5d931c7902e03841 Mon Sep 17 00:00:00 2001 From: Gomathi9520 Date: Fri, 1 Aug 2025 15:57:11 +0530 Subject: [PATCH] sline points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../builder/asset/models/model/model.tsx | 57 ++++-- .../asset/models/model/ribbonCollider.tsx | 182 ----------------- .../physics/conveyor}/conveyorCollider.tsx | 15 +- .../scene/physics/conveyor/ribbonCollider.tsx | 35 ++++ .../scene/physics/conveyor/splineCreator.tsx | 154 +++++++++++++++ .../conveyor/types/curvedConveyorCollider.tsx | 184 ++++++++++++++++++ .../conveyor/types/normalConveyorCollider.tsx | 151 ++++++++++++++ .../scene/physics/physicsSimulator.tsx | 6 +- app/src/types/builderTypes.d.ts | 16 ++ 9 files changed, 592 insertions(+), 208 deletions(-) delete mode 100644 app/src/modules/builder/asset/models/model/ribbonCollider.tsx rename app/src/modules/{builder/asset/models/model => scene/physics/conveyor}/conveyorCollider.tsx (92%) create mode 100644 app/src/modules/scene/physics/conveyor/ribbonCollider.tsx create mode 100644 app/src/modules/scene/physics/conveyor/splineCreator.tsx create mode 100644 app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx create mode 100644 app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx diff --git a/app/src/modules/builder/asset/models/model/model.tsx b/app/src/modules/builder/asset/models/model/model.tsx index e1332af..8cdf67f 100644 --- a/app/src/modules/builder/asset/models/model/model.tsx +++ b/app/src/modules/builder/asset/models/model/model.tsx @@ -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(null); const [boundingBox, setBoundingBox] = useState(null); - const [conveyorPlaneSize, setConveyorPlaneSize] = useState<[number, number] | null>(null); const groupRef = useRef(null); const rigidBodyRef = useRef(null); const [isSelected, setIsSelected] = useState(false); const [ikData, setIkData] = useState(); + const [ribbonData, setRibbonData] = useState(); 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 )} - {/* */} - - + {/* */} + {ribbonData && + + } {isSelected && } diff --git a/app/src/modules/builder/asset/models/model/ribbonCollider.tsx b/app/src/modules/builder/asset/models/model/ribbonCollider.tsx deleted file mode 100644 index 16fd83f..0000000 --- a/app/src/modules/builder/asset/models/model/ribbonCollider.tsx +++ /dev/null @@ -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(null); - const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set()); - const conveyorDirection = useRef(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 && ( - - - - - - )} - - ); -} - -export default RibbonCollider; \ No newline at end of file diff --git a/app/src/modules/builder/asset/models/model/conveyorCollider.tsx b/app/src/modules/scene/physics/conveyor/conveyorCollider.tsx similarity index 92% rename from app/src/modules/builder/asset/models/model/conveyorCollider.tsx rename to app/src/modules/scene/physics/conveyor/conveyorCollider.tsx index 26e4a29..b23df65 100644 --- a/app/src/modules/builder/asset/models/model/conveyorCollider.tsx +++ b/app/src/modules/scene/physics/conveyor/conveyorCollider.tsx @@ -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(null); const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set()); const conveyorDirection = useRef(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 && ( - + + {ribbonData.type === 'normal' && + + } + {ribbonData.type === 'curved' && + + } + + ); +} + +export default RibbonCollider; diff --git a/app/src/modules/scene/physics/conveyor/splineCreator.tsx b/app/src/modules/scene/physics/conveyor/splineCreator.tsx new file mode 100644 index 0000000..f767cf3 --- /dev/null +++ b/app/src/modules/scene/physics/conveyor/splineCreator.tsx @@ -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([]) + const draggingRef = useRef(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) => ( + { + draggingRef.current = i; + (controls as CameraControls).enabled = false + }} + > + + + ))} + + {geometry && ( + + + + + + )} + + ) +} diff --git a/app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx b/app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx new file mode 100644 index 0000000..1bf9d6d --- /dev/null +++ b/app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx @@ -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(null); + const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set()); + const conveyorDirection = useRef(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 && ( + + + + + + ) + ); +} + +export default CurvedConveyorCollider; diff --git a/app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx b/app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx new file mode 100644 index 0000000..dd797be --- /dev/null +++ b/app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx @@ -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(null); + const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set()); + const conveyorDirection = useRef(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 && ( + + + + + + ) + ); +} + +export default NormalConveyorCollider; diff --git a/app/src/modules/scene/physics/physicsSimulator.tsx b/app/src/modules/scene/physics/physicsSimulator.tsx index f6fc8a2..9030480 100644 --- a/app/src/modules/scene/physics/physicsSimulator.tsx +++ b/app/src/modules/scene/physics/physicsSimulator.tsx @@ -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} /> - + /> */} + {/* */} ) } diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index 318050a..fda8c43 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -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