diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx
index fc73e58..9d8e393 100644
--- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx
+++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx
@@ -6,8 +6,8 @@ import RenameInput from "../../../../../ui/inputs/RenameInput";
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
import Trigger from "../trigger/Trigger";
import ActionsList from "../components/ActionsList";
-import WorkerAction from "../actions/workerAction";
-import AssemblyAction from "../actions/assemblyAction";
+import WorkerAction from "../actions/WorkerAction";
+import AssemblyAction from "../actions/AssemblyAction";
import { useSelectedEventData, useSelectedAction } from "../../../../../../store/simulation/useSimulationStore";
import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi";
diff --git a/app/src/modules/scene/physics/conveyor/ribbonCollider.tsx b/app/src/modules/scene/physics/conveyor/ribbonCollider.tsx
index e67b72e..a4f5e35 100644
--- a/app/src/modules/scene/physics/conveyor/ribbonCollider.tsx
+++ b/app/src/modules/scene/physics/conveyor/ribbonCollider.tsx
@@ -2,45 +2,51 @@ import * as THREE from 'three';
import NormalConveyorCollider from './types/normalConveyorCollider';
import CurvedConveyorCollider from './types/curvedConveyorCollider';
import YSplitConveyorCollider from './types/ySplitConveyorCollider';
+import { useState } from 'react';
-function RibbonCollider({ ribbonData, boundingBox, asset }: {
+function RibbonCollider({
+ ribbonData,
+ boundingBox,
+ asset
+}: {
ribbonData: ConveyorPoints,
boundingBox: THREE.Box3 | null,
asset: Asset,
}) {
- // console.log('ribbonData: ', ribbonData);
+ const [forward, setForward] = useState(false);
return (
<>
- {ribbonData.type === 'normal' &&
+ {ribbonData.type === 'normal' && (
- }
- {ribbonData.type === 'curved' &&
+ )}
+ {ribbonData.type === 'curved' && (
- }
- {ribbonData.type === 'y-Split' &&
+ )}
+ {ribbonData.type === 'y-Split' && (
- }
+ )}
>
);
}
-export default RibbonCollider;
+export default RibbonCollider;
\ No newline at end of file
diff --git a/app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx b/app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx
index 4011173..8235a27 100644
--- a/app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx
+++ b/app/src/modules/scene/physics/conveyor/types/curvedConveyorCollider.tsx
@@ -3,195 +3,360 @@ 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
+function CurvedConveyorCollider({
+ points,
+ boundingBox,
+ asset,
+ forward: initialForward,
+ 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);
+ const conveyorRef = useRef(null);
+ const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set());
+ const conveyorDirection = useRef(new THREE.Vector3());
+ const [forward, setForward] = useState(initialForward);
+ const [showDirection, setShowDirection] = useState(false);
+ const [hoverState, setHoverState] = useState(false);
+ const conveyorSpeed = 2;
+ const lastClickTime = useRef(0);
+ const arrowRefs = useRef([]);
+ 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;
- });
+ // Toggle direction on double right click
+ useEffect(() => {
+ const handleClick = (e: MouseEvent) => {
+ if (e.button === 2 && hoverState) { // Right click and hovering over conveyor
+ const now = Date.now();
+ if (now - lastClickTime.current < 300) {
+ console.log("log");
+ setForward(prev => !prev);
}
+ lastClickTime.current = now;
+ }
};
- const handleMaterialExit = (e: CollisionPayload) => {
- if (e.other.rigidBody) {
- setObjectsOnConveyor(prev => {
- const newSet = new Set(prev);
- newSet.delete(e.other.rigidBody);
- return newSet;
- });
+ window.addEventListener('mousedown', handleClick);
+ return () => window.removeEventListener('mousedown', handleClick);
+ }, [forward, hoverState]);
+
+ 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, forward]);
+
+ 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;
+ });
+ }
+ };
+
+ const bezierPoints = useMemo(() => {
+ const segments = 20;
+ const allPoints: THREE.Vector3[] = [];
+
+ points.forEach(segment => {
+ let vectorPoints = segment.map(p => new THREE.Vector3(...p));
+ if (!forward) vectorPoints.reverse();
+
+ for (let group = 0; group + 2 < vectorPoints.length; group += 2) {
+ const p0 = vectorPoints[group];
+ const p1 = vectorPoints[group + 1];
+ const p2 = vectorPoints[group + 2];
+
+ for (let i = 0; i <= segments; i++) {
+ const t = i / segments;
+ const point = new THREE.Vector3()
+ .copy(p0)
+ .multiplyScalar((1 - t) ** 2)
+ .addScaledVector(p1, 2 * (1 - t) * t)
+ .addScaledVector(p2, t ** 2);
+ allPoints.push(point);
}
- };
-
- const bezierPoints = useMemo(() => {
- const segments = 20;
- const allPoints: THREE.Vector3[] = [];
-
- points.forEach(segment => {
- let vectorPoints = segment.map(p => new THREE.Vector3(...p));
- if (!forward) vectorPoints.reverse();
-
- for (let group = 0; group + 2 < vectorPoints.length; group += 2) {
- const p0 = vectorPoints[group];
- const p1 = vectorPoints[group + 1];
- const p2 = vectorPoints[group + 2];
-
- for (let i = 0; i <= segments; i++) {
- const t = i / segments;
- const point = new THREE.Vector3()
- .copy(p0)
- .multiplyScalar((1 - t) ** 2)
- .addScaledVector(p1, 2 * (1 - t) * t)
- .addScaledVector(p2, t ** 2);
- allPoints.push(point);
- }
- }
- });
-
- return allPoints;
- }, [points, forward]);
-
- useFrame(() => {
- const assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
- const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
- const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
- 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 geometries = useMemo(() => {
- const width = 1;
- const segments = 20;
- const geos: THREE.BufferGeometry[] = [];
+ return allPoints;
+ }, [points, forward]);
- points.forEach(segment => {
- const vertices: number[] = [];
- const indices: number[] = [];
+ useFrame(({ clock }) => {
+ if (isPaused) return;
+
+ // Physics simulation
+ 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 vectorPoint = segment.map(p => new THREE.Vector3(...p));
- if (vectorPoint.length < 3) return;
+ objectsOnConveyor.forEach(rigidBody => {
+ const worldPos = new THREE.Vector3().copy(rigidBody.translation());
+ const localPos = worldPos.clone().sub(assetPos).applyQuaternion(inverseQuat);
- for (let group = 0; group + 2 < vectorPoint.length; group += 2) {
- const p0 = vectorPoint[group];
- const p1 = vectorPoint[group + 1];
- const p2 = vectorPoint[group + 2];
+ 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;
+ }
+ }
- 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 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 tangent = new THREE.Vector3()
- .copy(p0)
- .multiplyScalar(-2 * (1 - t))
- .addScaledVector(p1, 2 - 4 * t)
- .addScaledVector(p2, 2 * t)
- .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 normal = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
- const left = new THREE.Vector3().copy(point).addScaledVector(normal, -width / 2);
- const right = new THREE.Vector3().copy(point).addScaledVector(normal, width / 2);
+ const centeringForce = side.clone().multiplyScalar(-sideOffset * 10);
+ const forwardForce = tangent.clone().multiplyScalar(conveyorSpeed);
+ const totalForce = forwardForce.add(centeringForce).applyQuaternion(assetQuat);
- vertices.push(...left.toArray(), ...right.toArray());
- }
+ rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
+ rigidBody.setLinvel(totalForce, true);
+ });
+
+ // Arrow animations
+ if (showDirection && arrowRefs.current.length > 0) {
+ const elapsedTime = clock.getElapsedTime();
+ arrowRefs.current.forEach((arrowGroup, index) => {
+ // Pulse animation
+ const pulseScale = 0.9 + 0.1 * Math.sin(elapsedTime * 5 + index * 0.5);
+ arrowGroup.scale.setScalar(pulseScale);
+
+ // Flow animation (color intensity)
+ const intensity = 0.7 + 0.3 * Math.sin(elapsedTime * 3 + index * 0.3);
+ arrowGroup.children.forEach(child => {
+ if (child instanceof THREE.Mesh) {
+ const material = child.material as THREE.MeshBasicMaterial;
+ if (forward) {
+ material.color.setRGB(0, intensity, 0);
+ } else {
+ material.color.setRGB(intensity, 0, 0);
}
-
- const totalSegments = ((vectorPoint.length - 1) / 2) * segments;
- for (let i = 0; i < totalSegments; i++) {
- const base = i * 2;
- indices.push(base, base + 1, base + 2);
- indices.push(base + 1, base + 3, base + 2);
- }
-
- const ribbonGeometry = new THREE.BufferGeometry();
- ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
- ribbonGeometry.setIndex(indices);
- ribbonGeometry.computeVertexNormals();
- geos.push(ribbonGeometry);
+ }
});
+ });
+ }
+ });
- setGeometryKey(k => k + 1);
- return geos;
- }, [points, asset.position, asset.rotation]);
+ const geometries = useMemo(() => {
+ const width = 1;
+ const segments = 20;
+ const geos: THREE.BufferGeometry[] = [];
- return (
- <>
- {geometries.length > 0 && (
-
- {geometries.map((geometry, index) => (
-
-
-
- ))}
-
- )}
- >
- );
+ points.forEach(segment => {
+ const vertices: number[] = [];
+ const indices: number[] = [];
+
+ const vectorPoint = segment.map(p => new THREE.Vector3(...p));
+ if (vectorPoint.length < 3) return;
+
+ for (let group = 0; group + 2 < vectorPoint.length; group += 2) {
+ const p0 = vectorPoint[group];
+ const p1 = vectorPoint[group + 1];
+ const p2 = vectorPoint[group + 2];
+
+ for (let 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(), ...right.toArray());
+ }
+ }
+
+ const totalSegments = ((vectorPoint.length - 1) / 2) * segments;
+ for (let i = 0; i < totalSegments; i++) {
+ const base = i * 2;
+ indices.push(base, base + 1, base + 2);
+ indices.push(base + 1, base + 3, base + 2);
+ }
+
+ const ribbonGeometry = new THREE.BufferGeometry();
+ ribbonGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
+ ribbonGeometry.setIndex(indices);
+ ribbonGeometry.computeVertexNormals();
+ geos.push(ribbonGeometry);
+ });
+
+ setGeometryKey(k => k + 1);
+ return geos;
+ }, [points, asset.position, asset.rotation]);
+
+ // Create curved direction indicators
+ const directionArrows = useMemo(() => {
+ if (!showDirection) return null;
+
+ const arrows: THREE.Group[] = [];
+ const arrowHeight = 0.2;
+ const arrowRadius = 0.05;
+ const segments = 8; // Fewer arrows for curved conveyors
+
+ points.forEach(segment => {
+ let vectorPoints = segment.map(p => new THREE.Vector3(...p));
+ if (!forward) vectorPoints.reverse();
+
+ 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);
+
+ const tangent = new THREE.Vector3()
+ .copy(p0)
+ .multiplyScalar(-2 * (1 - t))
+ .addScaledVector(p1, 2 - 4 * t)
+ .addScaledVector(p2, 2 * t)
+ .normalize();
+
+ // Create arrow group
+ const arrowGroup = new THREE.Group();
+
+ // Arrow shaft (cylinder)
+ const shaftLength = arrowHeight * 0.7;
+ const shaftGeometry = new THREE.CylinderGeometry(arrowRadius * 0.3, arrowRadius * 0.3, shaftLength, 8);
+ const shaftMaterial = new THREE.MeshBasicMaterial({
+ color: forward ? 0x00ff00 : 0xff0000
+ });
+ const shaft = new THREE.Mesh(shaftGeometry, shaftMaterial);
+ shaft.position.y = shaftLength / 2;
+ shaft.rotation.x = Math.PI / 2;
+
+ // Arrow head (cone)
+ const headGeometry = new THREE.ConeGeometry(arrowRadius, arrowHeight * 0.3, 8);
+ const headMaterial = new THREE.MeshBasicMaterial({
+ color: forward ? 0x00ff00 : 0xff0000
+ });
+ const head = new THREE.Mesh(headGeometry, headMaterial);
+ head.position.y = shaftLength;
+
+ // Position and orient the entire arrow
+ arrowGroup.add(shaft);
+ arrowGroup.add(head);
+ arrowGroup.position.copy(point);
+ arrowGroup.position.y += 0.1; // Slightly above conveyor
+ arrowGroup.quaternion.setFromUnitVectors(
+ new THREE.Vector3(0, 1, 0),
+ new THREE.Vector3(tangent.x, 0.1, tangent.z)
+ );
+
+ arrows.push(arrowGroup);
+ }
+ }
+ });
+
+ arrowRefs.current = arrows;
+ return arrows;
+ }, [points, showDirection, forward]);
+
+ return (
+ {
+ setShowDirection(true);
+ setHoverState(true);
+ }}
+ onPointerOut={() => {
+ setShowDirection(false);
+ setHoverState(false);
+ }}
+ >
+ {/* Conveyor surface */}
+ {geometries.length > 0 && (
+
+ {geometries.map((geometry, index) => (
+
+
+
+ ))}
+
+ )}
+
+ {/* Direction indicators */}
+ {showDirection && directionArrows?.map((arrow, i) => (
+
+ ))}
+
+ {/* Hover highlight */}
+ {hoverState && (
+
+ {geometries.map((geometry, index) => (
+
+
+
+ ))}
+
+ )}
+
+ );
}
-export default CurvedConveyorCollider;
+export default CurvedConveyorCollider;
\ No newline at end of file
diff --git a/app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx b/app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx
index eb3487d..227736a 100644
--- a/app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx
+++ b/app/src/modules/scene/physics/conveyor/types/normalConveyorCollider.tsx
@@ -9,27 +9,49 @@ function NormalConveyorCollider({
asset,
forward,
isPaused,
+ onDirectionChange
}: {
points: [number, number, number][][];
boundingBox: THREE.Box3 | null;
asset: Asset;
forward: boolean;
isPaused: boolean;
+ onDirectionChange?: (newDirection: boolean) => void;
}) {
- const conveyorRefs = useRef([]);
+ const conveyorRefs = useRef<(any)[]>([]);
const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set());
const conveyorDirection = useRef(new THREE.Vector3());
+ const [showDirection, setShowDirection] = useState(false);
const conveyorSpeed = 2;
+ const lastClickTime = useRef(0);
+ const [hoverState, setHoverState] = useState(false);
+
+ // Toggle direction on double right click
+ useEffect(() => {
+ const handleClick = (e: MouseEvent) => {
+ if (e.button === 2) { // Right click
+ const now = Date.now();
+ if (now - lastClickTime.current < 300) { // Double click within 300ms
+ if (onDirectionChange) {
+ onDirectionChange(!forward);
+ }
+ }
+ lastClickTime.current = now;
+ }
+ };
+
+ window.addEventListener('mousedown', handleClick);
+ return () => window.removeEventListener('mousedown', handleClick);
+ }, [forward]);
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]);
+ }, [boundingBox, asset.rotation, forward]);
const handleMaterialEnter = (e: CollisionPayload) => {
if (e.other.rigidBody) {
@@ -52,6 +74,8 @@ function NormalConveyorCollider({
};
useFrame(() => {
+ if (isPaused) return;
+
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);
@@ -60,7 +84,6 @@ function NormalConveyorCollider({
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);
@@ -74,7 +97,6 @@ function NormalConveyorCollider({
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++) {
@@ -91,12 +113,11 @@ function NormalConveyorCollider({
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);
});
@@ -107,7 +128,6 @@ function NormalConveyorCollider({
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[] = [];
@@ -117,13 +137,10 @@ function NormalConveyorCollider({
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());
}
@@ -138,11 +155,48 @@ function NormalConveyorCollider({
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
- }).filter(Boolean);
+ }).filter((geo): geo is THREE.BufferGeometry => geo !== null);
}, [points, asset.position, asset.rotation]);
+ // Create direction indicators
+ const directionArrows = useMemo(() => {
+
+ if (!showDirection) return null;
+
+ const arrows: THREE.Mesh[] = [];
+ const arrowGeometry = new THREE.ConeGeometry(0.05, 0.2, 8);
+ const arrowMaterial = new THREE.MeshBasicMaterial({ color: forward ? 0x00ff00 : 0xff0000 });
+
+ points.forEach(segment => {
+ if (segment.length < 2) return;
+
+ const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
+ const curvePoints = curve.getPoints(10); // Fewer points for arrows
+
+ for (let i = 0; i < curvePoints.length; i++) {
+ const point = curvePoints[i];
+ const next = curvePoints[i + 1] || point;
+ const direction = new THREE.Vector3().subVectors(next, point).normalize();
+
+ const arrow = new THREE.Mesh(arrowGeometry, arrowMaterial);
+ arrow.position.copy(point);
+ arrow.quaternion.setFromUnitVectors(
+ new THREE.Vector3(0, forward ? 1 : -1, 0),
+ new THREE.Vector3(direction.x, 0.1, direction.z)
+ );
+ arrows.push(arrow);
+ //
+ }
+ });
+
+ return arrows;
+ }, [points, showDirection, forward]);
+
return (
- <>
+ { setShowDirection(true); setHoverState(true); }}
+ onPointerLeave={() => { setShowDirection(false); setHoverState(false); }}
+ >
{geometries.map((geometry, index) => (
-
-
+
+
))}
- >
+
+ {showDirection && directionArrows?.map((arrow, i) => (
+
+ ))}
+
+ {/* Hover highlight */}
+ {hoverState && (
+
+ {geometries.map((geometry, index) => (
+
+
+
+ ))}
+
+ )}
+
);
}
-export default NormalConveyorCollider;
+export default NormalConveyorCollider;
\ No newline at end of file
diff --git a/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx b/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx
index b337fe5..2b52ca9 100644
--- a/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx
+++ b/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx
@@ -3,33 +3,61 @@ 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,
-}: {
+interface YSplitConveyorColliderProps {
points: [number, number, number][][];
boundingBox: THREE.Box3 | null;
asset: Asset;
forward: boolean;
isPaused: boolean;
-}) {
- const conveyorRefs = useRef([]);
+ onDirectionChange?: (newDirection: boolean) => void;
+}
+
+function YSplitConveyorCollider({
+ points,
+ boundingBox,
+ asset,
+ forward: initialForward,
+ isPaused,
+ onDirectionChange
+}: YSplitConveyorColliderProps) {
+ const conveyorRefs = useRef<(any | null)[]>([]);
const [objectsOnConveyor, setObjectsOnConveyor] = useState>(new Set());
const conveyorDirection = useRef(new THREE.Vector3());
+ const [forward, setForward] = useState(initialForward);
+ const [showDirection, setShowDirection] = useState(false);
+ const [hoverState, setHoverState] = useState(false);
const conveyorSpeed = 2;
+ const lastClickTime = useRef(0);
+ const arrowRefs = useRef([]);
+
+ // Toggle direction on double right click
+ useEffect(() => {
+ const handleClick = (e: MouseEvent) => {
+ if (e.button === 2 && hoverState) { // Right click and hovering over conveyor
+ const now = Date.now();
+ if (now - lastClickTime.current < 300) { // Double click within 300ms
+ const newDirection = !forward;
+ setForward(newDirection);
+ if (onDirectionChange) {
+ onDirectionChange(newDirection);
+ }
+ }
+ lastClickTime.current = now;
+ }
+ };
+
+ window.addEventListener('mousedown', handleClick);
+ return () => window.removeEventListener('mousedown', handleClick);
+ }, [forward, hoverState, onDirectionChange]);
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]);
+ }, [boundingBox, asset.rotation, forward]);
const handleMaterialEnter = (e: CollisionPayload) => {
if (e.other.rigidBody) {
@@ -51,21 +79,18 @@ function YSplitConveyorCollider({
}
};
- useFrame(() => {
+ useFrame(({ clock }) => {
+ if (isPaused) return;
+ // Update physics
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);
+ allCurvePoints.push(...curve.getPoints((segment.length - 1) * 30));
});
if (!forward) allCurvePoints.reverse();
@@ -74,7 +99,6 @@ function YSplitConveyorCollider({
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++) {
@@ -91,15 +115,37 @@ function YSplitConveyorCollider({
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);
});
+
+ // Animate direction arrows
+ if (showDirection && arrowRefs.current.length > 0) {
+ const elapsedTime = clock.getElapsedTime();
+ arrowRefs.current.forEach((arrowGroup, index) => {
+ // Pulse animation
+ const pulseScale = 0.9 + 0.1 * Math.sin(elapsedTime * 5 + index * 0.5);
+ arrowGroup.scale.setScalar(pulseScale);
+
+ // Flow animation (color intensity)
+ const intensity = 0.7 + 0.3 * Math.sin(elapsedTime * 3 + index * 0.3);
+ arrowGroup.children.forEach(child => {
+ if (child instanceof THREE.Mesh) {
+ const material = child.material as THREE.MeshBasicMaterial;
+ if (forward) {
+ material.color.setRGB(0, intensity, 0);
+ } else {
+ material.color.setRGB(intensity, 0, 0);
+ }
+ }
+ });
+ });
+ }
});
const geometries = useMemo(() => {
@@ -107,7 +153,6 @@ function YSplitConveyorCollider({
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[] = [];
@@ -117,13 +162,10 @@ function YSplitConveyorCollider({
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());
}
@@ -138,14 +180,82 @@ function YSplitConveyorCollider({
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
- }).filter(Boolean);
+ }).filter((geo): geo is THREE.BufferGeometry => geo !== null);
}, [points, asset.position, asset.rotation]);
+ // Create direction indicators
+ const directionArrows = useMemo(() => {
+ if (!showDirection) return null;
+
+ const arrows: THREE.Group[] = [];
+ const arrowHeight = 0.2;
+ const arrowRadius = 0.05;
+
+ points.forEach(segment => {
+ if (segment.length < 2) return;
+
+ const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
+ const curvePoints = curve.getPoints(8); // Fewer points for arrows
+
+ for (let i = 0; i < curvePoints.length; i++) {
+ const point = curvePoints[i];
+ const next = curvePoints[i + 1] || point;
+ const direction = new THREE.Vector3().subVectors(next, point).normalize();
+
+ // Create arrow group
+ const arrowGroup = new THREE.Group();
+
+ // Arrow shaft (cylinder)
+ const shaftLength = arrowHeight * 0.7;
+ const shaftGeometry = new THREE.CylinderGeometry(arrowRadius * 0.3, arrowRadius * 0.3, shaftLength, 8);
+ const shaftMaterial = new THREE.MeshBasicMaterial({
+ color: forward ? 0x00ff00 : 0xff0000
+ });
+ const shaft = new THREE.Mesh(shaftGeometry, shaftMaterial);
+ shaft.position.y = shaftLength / 2;
+ shaft.rotation.x = Math.PI / 2;
+
+ // Arrow head (cone)
+ const headGeometry = new THREE.ConeGeometry(arrowRadius, arrowHeight * 0.3, 8);
+ const headMaterial = new THREE.MeshBasicMaterial({
+ color: forward ? 0x00ff00 : 0xff0000
+ });
+ const head = new THREE.Mesh(headGeometry, headMaterial);
+ head.position.y = shaftLength;
+
+ // Position and orient the entire arrow
+ arrowGroup.add(shaft);
+ arrowGroup.add(head);
+ arrowGroup.position.copy(point);
+ arrowGroup.position.y += 0.1; // Slightly above conveyor
+ arrowGroup.quaternion.setFromUnitVectors(
+ new THREE.Vector3(0, 1, 0),
+ new THREE.Vector3(direction.x, 0.1, direction.z)
+ );
+
+ arrows.push(arrowGroup);
+ }
+ });
+
+ arrowRefs.current = arrows;
+ return arrows;
+ }, [points, showDirection, forward]);
+
return (
- <>
+ {
+ setShowDirection(true);
+ setHoverState(true);
+ }}
+ onPointerOut={() => {
+ setShowDirection(false);
+ setHoverState(false);
+ }}
+ >
+ {/* Conveyor surface */}
{geometries.map((geometry, index) => (
(conveyorRefs.current[index] = el)}
type="fixed"
position={[0, 0.001, 0]}
@@ -154,14 +264,45 @@ function YSplitConveyorCollider({
onCollisionExit={handleMaterialExit}
colliders="trimesh"
>
-
-
+
+
))}
- >
+
+ {/* Direction indicators */}
+ {showDirection && directionArrows?.map((arrow, i) => (
+
+ ))}
+
+ {/* Hover highlight */}
+ {hoverState && (
+
+ {geometries.map((geometry, index) => (
+
+
+
+ ))}
+
+ )}
+
);
}
-export default YSplitConveyorCollider;
-
+export default YSplitConveyorCollider;
\ No newline at end of file
diff --git a/app/src/modules/scene/physics/materialSpawner.tsx b/app/src/modules/scene/physics/materialSpawner.tsx
index 022d738..ea342ae 100644
--- a/app/src/modules/scene/physics/materialSpawner.tsx
+++ b/app/src/modules/scene/physics/materialSpawner.tsx
@@ -4,7 +4,7 @@ import { useLoadingProgress } from '../../../store/builder/store';
import { MaterialModel } from '../../simulation/materials/instances/material/materialModel';
import { useThree } from '@react-three/fiber';
import * as THREE from 'three';
-import { CameraControls } from '@react-three/drei';
+import { CameraControls, TransformControls } from '@react-three/drei';
import { generateUniqueId } from '../../../functions/generateUniqueId';
type MaterialSpawnerProps = {
@@ -13,7 +13,7 @@ type MaterialSpawnerProps = {
spawnCount: number;
};
-function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawnerProps) {
+function MaterialSpawner({ position: initialPos, spawnInterval, spawnCount }: MaterialSpawnerProps) {
const { loadingProgress } = useLoadingProgress();
const [spawned, setSpawned] = useState<{
id: string;
@@ -28,6 +28,9 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
const dragOffset = useRef(new THREE.Vector3());
const initialDepth = useRef(0);
const materialTypes = ['Default material', 'Material 1', 'Material 2', 'Material 3'];
+ const [boxPosition, setBoxPosition] = useState<[number, number, number]>(initialPos);
+ const spawnerRef = useRef(null!);
+ const newPositionRef = useRef<[number, number, number]>([...initialPos]);
useEffect(() => {
if (loadingProgress !== 0) return;
@@ -46,13 +49,16 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
}
spawnedCount.current++;
- const randomMaterialType = materialTypes[Math.floor(Math.random() * materialTypes.length)];
+ const randomMaterialType =
+ materialTypes[Math.floor(Math.random() * materialTypes.length)];
+
+ console.log('boxPosition: ', boxPosition);
return [
...prev,
{
id: generateUniqueId(),
- position,
+ position: [...boxPosition] as [number, number, number], // use latest position state
ref: React.createRef(),
materialType: randomMaterialType,
}
@@ -85,7 +91,9 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
stopSpawning();
document.removeEventListener('visibilitychange', handleVisibility);
};
- }, [loadingProgress, spawnInterval, spawnCount, position, spawningPaused]);
+
+ }, [loadingProgress, spawnInterval, spawnCount, spawningPaused, boxPosition]);
+
const handleSleep = (id: string) => {
setSpawned(prev => prev.filter(obj => obj.id !== id));
@@ -180,16 +188,37 @@ function MaterialSpawner({ position, spawnInterval, spawnCount }: MaterialSpawne
const handleBoxContextMenu = () => {
};
+
return (
<>
- {
+ if (controls) (controls as CameraControls).enabled = false;
+ }}
+ onMouseUp={() => {
+ if (controls) (controls as CameraControls).enabled = true;
+ setBoxPosition(newPositionRef.current);
+ }}
+ onObjectChange={() => {
+ if (spawnerRef.current) {
+ const pos = spawnerRef.current.position;
+ newPositionRef.current = [pos.x, pos.y, pos.z]; // Save latest
+ }
+ }}
>
-
-
-
+
+
+
+
+
{spawned.map(({ id, position, materialType, ref }) => (
-
{/* */}
+
{/*
-
+ {/* */}
>
)
}
diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx
index 7a10131..9d51f4a 100644
--- a/app/src/modules/scene/scene.tsx
+++ b/app/src/modules/scene/scene.tsx
@@ -75,8 +75,8 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
>
- {/* */}
-
+
+ {/* */}
diff --git a/app/src/modules/visualization/zone/DisplayZone.tsx b/app/src/modules/visualization/zone/DisplayZone.tsx
index 42fe4c4..eb57c66 100644
--- a/app/src/modules/visualization/zone/DisplayZone.tsx
+++ b/app/src/modules/visualization/zone/DisplayZone.tsx
@@ -181,6 +181,7 @@ const DisplayZone: React.FC = ({
// setSelectedChartId(null);
let response = await getSelect2dZoneData(zoneUuid, organization, projectId, selectedVersion?.versionId || '');
+ console.log('response: ', response);
//
let res = await getFloatingZoneData(zoneUuid, organization, projectId, selectedVersion?.versionId || '');