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 || '');