feat: Implement Y-Split conveyor collider and update related types; refactor conveyor handling across components
This commit is contained in:
@@ -91,7 +91,6 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
|
if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
|
||||||
// console.log('asset: ', asset);
|
|
||||||
getAssetConveyorPoints(asset.assetId).then((data) => {
|
getAssetConveyorPoints(asset.assetId).then((data) => {
|
||||||
console.log('asset.assetI: ', asset.assetId);
|
console.log('asset.assetI: ', asset.assetId);
|
||||||
console.log('data: ', data);
|
console.log('data: ', data);
|
||||||
@@ -102,6 +101,43 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
|
|||||||
|
|
||||||
}
|
}
|
||||||
}, [asset.modelUuid, asset.eventData, ribbonData, boundingBox])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (gltfScene) {
|
if (gltfScene) {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import NormalConveyorCollider from './types/normalConveyorCollider';
|
import NormalConveyorCollider from './types/normalConveyorCollider';
|
||||||
import CurvedConveyorCollider from './types/curvedConveyorCollider';
|
import CurvedConveyorCollider from './types/curvedConveyorCollider';
|
||||||
|
import YSplitConveyorCollider from './types/ySplitConveyorCollider';
|
||||||
|
|
||||||
function RibbonCollider({ ribbonData, boundingBox, asset }: {
|
function RibbonCollider({ ribbonData, boundingBox, asset }: {
|
||||||
ribbonData: ConveyorPoints,
|
ribbonData: ConveyorPoints,
|
||||||
boundingBox: THREE.Box3 | null,
|
boundingBox: THREE.Box3 | null,
|
||||||
asset: Asset,
|
asset: Asset,
|
||||||
}) {
|
}) {
|
||||||
|
// console.log('ribbonData: ', ribbonData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -28,6 +30,15 @@ function RibbonCollider({ ribbonData, boundingBox, asset }: {
|
|||||||
isPaused={false}
|
isPaused={false}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{ribbonData.type === 'y-Split' &&
|
||||||
|
<YSplitConveyorCollider
|
||||||
|
points={ribbonData.points}
|
||||||
|
boundingBox={boundingBox}
|
||||||
|
asset={asset}
|
||||||
|
forward={false}
|
||||||
|
isPaused={false}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,99 +8,92 @@ export function SplineCreator() {
|
|||||||
const { raycaster, camera, gl, pointer, controls, scene } = useThree()
|
const { raycaster, camera, gl, pointer, controls, scene } = useThree()
|
||||||
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), [])
|
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), [])
|
||||||
|
|
||||||
const [points, setPoints] = useState<THREE.Vector3[]>([])
|
const [splines, setSplines] = useState<THREE.Vector3[][]>([])
|
||||||
const draggingRef = useRef<number | null>(null)
|
const [_, setTempPoints] = useState<THREE.Vector3[]>([])
|
||||||
const [geometryKey, setGeometryKey] = useState(0)
|
const draggingRef = useRef<{ splineIndex: number, pointIndex: number } | null>(null)
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
if (draggingRef.current !== null) {
|
if (draggingRef.current !== null) {
|
||||||
const i = draggingRef.current
|
const { splineIndex, pointIndex } = draggingRef.current
|
||||||
const y = points[i]?.y ?? 0
|
const y = splines[splineIndex][pointIndex]?.y ?? 0
|
||||||
|
|
||||||
plane.constant = -y
|
plane.constant = -y
|
||||||
|
|
||||||
raycaster.setFromCamera(pointer, camera)
|
raycaster.setFromCamera(pointer, camera)
|
||||||
const intersectionPoint = new THREE.Vector3()
|
const intersectionPoint = new THREE.Vector3()
|
||||||
const position = raycaster.ray.intersectPlane(plane, intersectionPoint) || intersectionPoint
|
const position = raycaster.ray.intersectPlane(plane, intersectionPoint) || intersectionPoint
|
||||||
|
|
||||||
setPoints(prev => {
|
setSplines(prev => {
|
||||||
const next = [...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
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const geometry = useMemo(() => {
|
const geometries = useMemo(() => {
|
||||||
const width = 1
|
const width = 1
|
||||||
const segments = 20
|
const segments = 20
|
||||||
const vertices: number[] = []
|
const groupGeometries: THREE.BufferGeometry[] = []
|
||||||
const indices: number[] = []
|
|
||||||
|
|
||||||
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) {
|
for (let j = 0; j <= segments; j++) {
|
||||||
const p0 = points[group]
|
const t = j / segments
|
||||||
const p1 = points[group + 1]
|
const point = new THREE.Vector3()
|
||||||
const p2 = points[group + 2]
|
.copy(p0).multiplyScalar((1 - t) ** 2)
|
||||||
|
.addScaledVector(p1, 2 * (1 - t) * t)
|
||||||
|
.addScaledVector(p2, t ** 2)
|
||||||
|
|
||||||
for (let i = 0; i <= segments; i++) {
|
const tangent = new THREE.Vector3()
|
||||||
const t = i / segments
|
.copy(p0).multiplyScalar(-2 * (1 - t))
|
||||||
const point = new THREE.Vector3()
|
.addScaledVector(p1, 2 - 4 * t)
|
||||||
.copy(p0)
|
.addScaledVector(p2, 2 * t)
|
||||||
.multiplyScalar((1 - t) ** 2)
|
.normalize()
|
||||||
.addScaledVector(p1, 2 * (1 - t) * t)
|
|
||||||
.addScaledVector(p2, t ** 2)
|
|
||||||
|
|
||||||
const tangent = new THREE.Vector3()
|
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize()
|
||||||
.copy(p0)
|
const left = new THREE.Vector3().copy(point).addScaledVector(normal, -width / 2)
|
||||||
.multiplyScalar(-2 * (1 - t))
|
const right = new THREE.Vector3().copy(point).addScaledVector(normal, width / 2)
|
||||||
.addScaledVector(p1, 2 - 4 * t)
|
|
||||||
.addScaledVector(p2, 2 * t)
|
|
||||||
.normalize()
|
|
||||||
|
|
||||||
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize()
|
vertices.push(...left.toArray(), ...right.toArray())
|
||||||
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())
|
for (let j = 0; j < segments; j++) {
|
||||||
vertices.push(...right.toArray())
|
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
|
return groupGeometries
|
||||||
for (let i = 0; i < totalSegments; i++) {
|
}, [splines])
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
const canvas = gl.domElement
|
const canvas = gl.domElement
|
||||||
|
const onMouseUp = () => {
|
||||||
const onMouseUp = (evt: MouseEvent) => {
|
|
||||||
(controls as CameraControls).enabled = true
|
(controls as CameraControls).enabled = true
|
||||||
if (evt.button === 0 && draggingRef.current !== null) {
|
draggingRef.current = null
|
||||||
draggingRef.current = null
|
|
||||||
setGeometryKey(k => k + 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener('mouseup', onMouseUp)
|
canvas.addEventListener('mouseup', onMouseUp)
|
||||||
return () => canvas.removeEventListener('mouseup', onMouseUp)
|
return () => canvas.removeEventListener('mouseup', onMouseUp)
|
||||||
}, [camera])
|
}, [camera])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
if (points.length >= 3) return
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
raycaster.setFromCamera(pointer, camera)
|
raycaster.setFromCamera(pointer, camera)
|
||||||
const intersections = raycaster.intersectObject(scene, true)
|
const intersections = raycaster.intersectObject(scene, true)
|
||||||
@@ -108,47 +101,55 @@ export function SplineCreator() {
|
|||||||
if (intersections.length > 0) {
|
if (intersections.length > 0) {
|
||||||
const hitPoint = intersections[0].point.clone()
|
const hitPoint = intersections[0].point.clone()
|
||||||
|
|
||||||
setPoints(prev => {
|
setTempPoints(temp => {
|
||||||
if (prev.length === 0) return [hitPoint]
|
const nextTemp = [...temp, hitPoint]
|
||||||
if (prev.length === 1) {
|
if (nextTemp.length === 2) {
|
||||||
const p0 = prev[0]
|
const [p0, p2] = nextTemp
|
||||||
const p2 = hitPoint
|
|
||||||
const mid = new THREE.Vector3().addVectors(p0, p2).multiplyScalar(0.5)
|
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
|
const canvas = gl.domElement
|
||||||
canvas.addEventListener('click', handleClick)
|
canvas.addEventListener('dblclick', handleClick)
|
||||||
return () => canvas.removeEventListener('click', handleClick)
|
return () => canvas.removeEventListener('dblclick', handleClick)
|
||||||
}, [points, raycaster, pointer, camera, gl, scene])
|
}, [raycaster, pointer, camera, gl, scene])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
console.log(splines);
|
||||||
|
}, [splines])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{points.map((p, i) => (
|
{splines.map((spline, splineIndex) =>
|
||||||
<Sphere
|
spline.map((point, pointIndex) => (
|
||||||
key={i}
|
<Sphere
|
||||||
position={p}
|
key={`${splineIndex}-${pointIndex}`}
|
||||||
args={[0.1, 16, 16]}
|
position={point}
|
||||||
onPointerDown={() => {
|
args={[0.1, 16, 16]}
|
||||||
draggingRef.current = i;
|
onPointerDown={() => {
|
||||||
(controls as CameraControls).enabled = false
|
draggingRef.current = { splineIndex, pointIndex };
|
||||||
}}
|
(controls as CameraControls).enabled = false
|
||||||
>
|
}}
|
||||||
<meshStandardMaterial color={'red'} />
|
>
|
||||||
</Sphere>
|
<meshStandardMaterial color="red" />
|
||||||
))}
|
</Sphere>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
{geometry && (
|
{geometries.map((geom, idx) => (
|
||||||
<RigidBody key={geometryKey} type="fixed" colliders="trimesh">
|
<RigidBody key={idx} type="fixed" colliders="trimesh">
|
||||||
<mesh geometry={geometry}>
|
<mesh geometry={geom}>
|
||||||
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} />
|
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</RigidBody>
|
</RigidBody>
|
||||||
)}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
|
||||||
function CurvedConveyorCollider({ points, boundingBox, asset, forward, isPaused }: {
|
function CurvedConveyorCollider({ points, boundingBox, asset, forward, isPaused }: {
|
||||||
points: [number, number, number][],
|
points: [number, number, number][][],
|
||||||
boundingBox: THREE.Box3 | null,
|
boundingBox: THREE.Box3 | null,
|
||||||
asset: Asset,
|
asset: Asset,
|
||||||
forward: boolean
|
forward: boolean
|
||||||
@@ -46,30 +46,35 @@ function CurvedConveyorCollider({ points, boundingBox, asset, forward, isPaused
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useFrame(() => {
|
const bezierPoints = useMemo(() => {
|
||||||
const vectorPoints = points.map(p => new THREE.Vector3(...p));
|
|
||||||
if (!forward) {
|
|
||||||
vectorPoints.reverse();
|
|
||||||
}
|
|
||||||
const segments = 20;
|
const segments = 20;
|
||||||
const bezierPoints: THREE.Vector3[] = [];
|
const allPoints: THREE.Vector3[] = [];
|
||||||
|
|
||||||
for (let group = 0; group + 2 < vectorPoints.length; group += 2) {
|
points.forEach(segment => {
|
||||||
const p0 = vectorPoints[group];
|
let vectorPoints = segment.map(p => new THREE.Vector3(...p));
|
||||||
const p1 = vectorPoints[group + 1];
|
if (!forward) vectorPoints.reverse();
|
||||||
const p2 = vectorPoints[group + 2];
|
|
||||||
|
|
||||||
for (let i = 0; i <= segments; i++) {
|
for (let group = 0; group + 2 < vectorPoints.length; group += 2) {
|
||||||
const t = i / segments;
|
const p0 = vectorPoints[group];
|
||||||
const point = new THREE.Vector3()
|
const p1 = vectorPoints[group + 1];
|
||||||
.copy(p0)
|
const p2 = vectorPoints[group + 2];
|
||||||
.multiplyScalar((1 - t) ** 2)
|
|
||||||
.addScaledVector(p1, 2 * (1 - t) * t)
|
for (let i = 0; i <= segments; i++) {
|
||||||
.addScaledVector(p2, t ** 2);
|
const t = i / segments;
|
||||||
bezierPoints.push(point);
|
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 assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
|
||||||
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
|
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
|
||||||
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
|
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
|
||||||
@@ -107,77 +112,85 @@ function CurvedConveyorCollider({ points, boundingBox, asset, forward, isPaused
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const geometry = useMemo(() => {
|
const geometries = useMemo(() => {
|
||||||
const width = 1
|
const width = 1;
|
||||||
const segments = 20
|
const segments = 20;
|
||||||
const vertices: number[] = []
|
const geos: THREE.BufferGeometry[] = [];
|
||||||
const indices: number[] = []
|
|
||||||
|
|
||||||
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) {
|
for (let group = 0; group + 2 < vectorPoint.length; group += 2) {
|
||||||
const p0 = vectorPoint[group]
|
const p0 = vectorPoint[group];
|
||||||
const p1 = vectorPoint[group + 1]
|
const p1 = vectorPoint[group + 1];
|
||||||
const p2 = vectorPoint[group + 2]
|
const p2 = vectorPoint[group + 2];
|
||||||
|
|
||||||
for (let i = 0; i <= segments; i++) {
|
for (let i = 0; i <= segments; i++) {
|
||||||
const t = i / segments
|
const t = i / segments;
|
||||||
const point = new THREE.Vector3()
|
const point = new THREE.Vector3()
|
||||||
.copy(p0)
|
.copy(p0)
|
||||||
.multiplyScalar((1 - t) ** 2)
|
.multiplyScalar((1 - t) ** 2)
|
||||||
.addScaledVector(p1, 2 * (1 - t) * t)
|
.addScaledVector(p1, 2 * (1 - t) * t)
|
||||||
.addScaledVector(p2, t ** 2)
|
.addScaledVector(p2, t ** 2);
|
||||||
|
|
||||||
const tangent = new THREE.Vector3()
|
const tangent = new THREE.Vector3()
|
||||||
.copy(p0)
|
.copy(p0)
|
||||||
.multiplyScalar(-2 * (1 - t))
|
.multiplyScalar(-2 * (1 - t))
|
||||||
.addScaledVector(p1, 2 - 4 * t)
|
.addScaledVector(p1, 2 - 4 * t)
|
||||||
.addScaledVector(p2, 2 * t)
|
.addScaledVector(p2, 2 * t)
|
||||||
.normalize()
|
.normalize();
|
||||||
|
|
||||||
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).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 left = new THREE.Vector3().copy(point).addScaledVector(normal, -width / 2);
|
||||||
const right = 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(...left.toArray(), ...right.toArray());
|
||||||
vertices.push(...right.toArray())
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const totalSegments = ((points.length - 1) / 2) * segments
|
const totalSegments = ((vectorPoint.length - 1) / 2) * segments;
|
||||||
for (let i = 0; i < totalSegments; i++) {
|
for (let i = 0; i < totalSegments; i++) {
|
||||||
const base = i * 2
|
const base = i * 2;
|
||||||
indices.push(base, base + 1, base + 2)
|
indices.push(base, base + 1, base + 2);
|
||||||
indices.push(base + 1, base + 3, base + 2)
|
indices.push(base + 1, base + 3, base + 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ribbonGeometry = new THREE.BufferGeometry()
|
const ribbonGeometry = new THREE.BufferGeometry();
|
||||||
ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
|
ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
||||||
ribbonGeometry.setIndex(indices)
|
ribbonGeometry.setIndex(indices);
|
||||||
ribbonGeometry.computeVertexNormals()
|
ribbonGeometry.computeVertexNormals();
|
||||||
setGeometryKey(k => k + 1)
|
geos.push(ribbonGeometry);
|
||||||
return ribbonGeometry
|
});
|
||||||
|
|
||||||
|
setGeometryKey(k => k + 1);
|
||||||
|
return geos;
|
||||||
}, [points, asset.position, asset.rotation]);
|
}, [points, asset.position, asset.rotation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
geometry && (
|
<>
|
||||||
<RigidBody
|
{geometries.length > 0 && (
|
||||||
key={geometryKey}
|
<RigidBody
|
||||||
ref={conveyorRef}
|
key={geometryKey}
|
||||||
type="fixed"
|
ref={conveyorRef}
|
||||||
position={[0, 0.001, 0]}
|
type="fixed"
|
||||||
userData={{ isConveyor: true }}
|
position={[0, 0.001, 0]}
|
||||||
onCollisionEnter={handleMaterialEnter}
|
userData={{ isConveyor: true }}
|
||||||
onCollisionExit={handleMaterialExit}
|
onCollisionEnter={handleMaterialEnter}
|
||||||
colliders="trimesh"
|
onCollisionExit={handleMaterialExit}
|
||||||
>
|
colliders="trimesh"
|
||||||
<mesh geometry={geometry}>
|
>
|
||||||
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
|
{geometries.map((geometry, index) => (
|
||||||
</mesh>
|
<mesh key={index} geometry={geometry}>
|
||||||
</RigidBody>
|
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
|
||||||
)
|
</mesh>
|
||||||
|
))}
|
||||||
|
</RigidBody>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,23 @@ import { CollisionPayload, RigidBody } from '@react-three/rapier';
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
|
||||||
function NormalConveyorCollider({ points, boundingBox, asset, forward, isPaused }: {
|
function NormalConveyorCollider({
|
||||||
points: [number, number, number][],
|
points,
|
||||||
boundingBox: THREE.Box3 | null,
|
boundingBox,
|
||||||
asset: Asset,
|
asset,
|
||||||
forward: boolean
|
forward,
|
||||||
isPaused: boolean
|
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 [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
|
||||||
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
|
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
|
||||||
const conveyorSpeed = 2;
|
const conveyorSpeed = 2;
|
||||||
const [geometryKey, setGeometryKey] = useState(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!boundingBox) return;
|
if (!boundingBox) return;
|
||||||
@@ -47,35 +52,42 @@ function NormalConveyorCollider({ points, boundingBox, asset, forward, isPaused
|
|||||||
};
|
};
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
const curve = new THREE.CatmullRomCurve3(points.map(p => new THREE.Vector3(...p)));
|
|
||||||
|
|
||||||
const assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
|
const assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
|
||||||
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
|
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
|
||||||
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
|
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
|
||||||
const inverseQuat = assetQuat.clone().invert();
|
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 => {
|
objectsOnConveyor.forEach(rigidBody => {
|
||||||
const worldPos = new THREE.Vector3().copy(rigidBody.translation());
|
const worldPos = new THREE.Vector3().copy(rigidBody.translation());
|
||||||
const localPos = worldPos.clone().sub(assetPos).applyQuaternion(inverseQuat);
|
const localPos = worldPos.clone().sub(assetPos).applyQuaternion(inverseQuat);
|
||||||
|
|
||||||
const curvePoints = curve.getPoints(100);
|
// Find closest point on full conveyor
|
||||||
if (!forward) {
|
|
||||||
curvePoints.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
let closestIndex = 0;
|
let closestIndex = 0;
|
||||||
let minDist = Infinity;
|
let minDist = Infinity;
|
||||||
for (let i = 0; i < curvePoints.length; i++) {
|
for (let i = 0; i < allCurvePoints.length; i++) {
|
||||||
const dist = curvePoints[i].distanceToSquared(localPos);
|
const dist = allCurvePoints[i].distanceToSquared(localPos);
|
||||||
if (dist < minDist) {
|
if (dist < minDist) {
|
||||||
minDist = dist;
|
minDist = dist;
|
||||||
closestIndex = i;
|
closestIndex = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const point = curvePoints[closestIndex];
|
const point = allCurvePoints[closestIndex];
|
||||||
const prev = curvePoints[closestIndex - 1] || point;
|
const prev = allCurvePoints[closestIndex - 1] || point;
|
||||||
const next = curvePoints[closestIndex + 1] || point;
|
const next = allCurvePoints[closestIndex + 1] || point;
|
||||||
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
|
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
|
||||||
const side = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
|
const side = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
|
||||||
const relative = new THREE.Vector3().subVectors(localPos, point);
|
const relative = new THREE.Vector3().subVectors(localPos, point);
|
||||||
@@ -90,61 +102,64 @@ function NormalConveyorCollider({ points, boundingBox, asset, forward, isPaused
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const geometry = useMemo(() => {
|
const geometries = useMemo(() => {
|
||||||
if (points.length < 2) return null;
|
|
||||||
|
|
||||||
const width = 1;
|
const width = 1;
|
||||||
const segments = 30;
|
const segments = 30;
|
||||||
const curve = new THREE.CatmullRomCurve3(points.map(p => new THREE.Vector3(...p)));
|
return points.map(segment => {
|
||||||
const curvePoints = curve.getPoints((points.length - 1) * segments);
|
if (segment.length < 2) return null;
|
||||||
const vertices: number[] = [];
|
|
||||||
const indices: number[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < curvePoints.length; i++) {
|
const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
|
||||||
const point = curvePoints[i];
|
const curvePoints = curve.getPoints((segment.length - 1) * segments);
|
||||||
const prev = curvePoints[i - 1] || point;
|
const vertices: number[] = [];
|
||||||
const next = curvePoints[i + 1] || point;
|
const indices: number[] = [];
|
||||||
|
|
||||||
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
|
for (let i = 0; i < curvePoints.length; i++) {
|
||||||
const normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
|
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 tangent = new THREE.Vector3().subVectors(next, prev).normalize();
|
||||||
const right = point.clone().addScaledVector(normal, width / 2);
|
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++) {
|
vertices.push(...left.toArray(), ...right.toArray());
|
||||||
const base = i * 2;
|
}
|
||||||
indices.push(base, base + 1, base + 2);
|
|
||||||
indices.push(base + 1, base + 3, base + 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const geo = new THREE.BufferGeometry();
|
for (let i = 0; i < curvePoints.length - 1; i++) {
|
||||||
geo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
const base = i * 2;
|
||||||
geo.setIndex(indices);
|
indices.push(base, base + 1, base + 2);
|
||||||
geo.computeVertexNormals();
|
indices.push(base + 1, base + 3, base + 2);
|
||||||
setGeometryKey(k => k + 1);
|
}
|
||||||
return geo;
|
|
||||||
|
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]);
|
}, [points, asset.position, asset.rotation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
geometry && (
|
<>
|
||||||
<RigidBody
|
{geometries.map((geometry, index) => (
|
||||||
key={geometryKey}
|
<RigidBody
|
||||||
ref={conveyorRef}
|
key={index}
|
||||||
type="fixed"
|
ref={el => (conveyorRefs.current[index] = el)}
|
||||||
position={[0, 0.001, 0]}
|
type="fixed"
|
||||||
userData={{ isConveyor: true }}
|
position={[0, 0.001, 0]}
|
||||||
onCollisionEnter={handleMaterialEnter}
|
userData={{ isConveyor: true }}
|
||||||
onCollisionExit={handleMaterialExit}
|
onCollisionEnter={handleMaterialEnter}
|
||||||
colliders="trimesh"
|
onCollisionExit={handleMaterialExit}
|
||||||
>
|
colliders="trimesh"
|
||||||
<mesh geometry={geometry}>
|
>
|
||||||
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
|
<mesh geometry={geometry!}>
|
||||||
</mesh>
|
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} transparent opacity={0.5} />
|
||||||
</RigidBody>
|
</mesh>
|
||||||
)
|
</RigidBody>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ function PhysicsSimulator() {
|
|||||||
|
|
||||||
<ColliderCreator />
|
<ColliderCreator />
|
||||||
|
|
||||||
{/* <SplineCreator /> */}
|
<SplineCreator />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
6
app/src/types/builderTypes.d.ts
vendored
6
app/src/types/builderTypes.d.ts
vendored
@@ -49,17 +49,17 @@ type Assets = Asset[];
|
|||||||
|
|
||||||
type NormalConveyor = {
|
type NormalConveyor = {
|
||||||
type: 'normal';
|
type: 'normal';
|
||||||
points: [number, number, number][];
|
points: [number, number, number][][];
|
||||||
}
|
}
|
||||||
|
|
||||||
type YJunctionConveyor = {
|
type YJunctionConveyor = {
|
||||||
type: 'y-junction';
|
type: 'y-Split';
|
||||||
points: [number, number, number][][];
|
points: [number, number, number][][];
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurvedConveyor = {
|
type CurvedConveyor = {
|
||||||
type: 'curved';
|
type: 'curved';
|
||||||
points: [number, number, number][];
|
points: [number, number, number][][];
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConveyorPoints = NormalConveyor | YJunctionConveyor | CurvedConveyor;
|
type ConveyorPoints = NormalConveyor | YJunctionConveyor | CurvedConveyor;
|
||||||
|
|||||||
Reference in New Issue
Block a user