Compare commits

...

30 Commits

Author SHA1 Message Date
a88c0e594b feat: enhance conveyor collider functionality and improve scene visibility toggle 2025-09-01 14:57:27 +05:30
f3ea48fcfd Merge branch 'main-demo' into dev-physics 2025-09-01 14:57:09 +05:30
26a80ba177 fix: update collider rotation to use Euler angles instead of raw quaternion values 2025-09-01 14:10:13 +05:30
6d557e8fde feat: enhance conveyor collider components with direction change functionality and refactor state management 2025-08-28 14:56:38 +05:30
987bc88dfa Merge branch 'main-demo' into dev-physics 2025-08-26 10:01:23 +05:30
4d124411d0 Update compose.yaml 2025-08-23 05:19:53 +00:00
2bc0921902 fix: Update frontend port mapping from 8400 to 8401 in docker-compose 2025-08-23 10:39:40 +05:30
e725610674 refactor: Clean up console logs and adjust MaterialSpawner properties 2025-08-23 09:37:47 +05:30
a830f03be9 feat: Integrate RibbonCollider for Conveyor asset handling and remove debug log 2025-08-21 18:14:55 +05:30
fcc8806ef4 Merge branch 'main-demo' into dev-physics 2025-08-21 18:14:26 +05:30
d1c78495ea feat: Implement collider functionality with drag-and-drop arrows and conditions
- Added Collider and ColliderCondition types to manage collider properties.
- Created a new useColliderStore for managing colliders and their arrows.
- Implemented ColliderArrow component for visual representation and interaction of arrows.
- Enhanced ColliderProperties UI for managing directions and conditions.
- Updated PhysicsSimulator and Scene components to integrate collider features.
- Refactored existing code for better organization and clarity.
2025-08-21 17:57:45 +05:30
57dffa0961 feat: Add initial ColliderProperties component with basic UI structure 2025-08-21 17:56:34 +05:30
324b7aa40c refactor: Clean up MaterialSpawner component and improve drag handling logic 2025-08-20 10:37:39 +05:30
7794e51d1f feat: Enhance conveyor collider functionality with direction toggle and visual indicators 2025-08-19 10:19:18 +05:30
361480578a fix: Update mesh material visibility for conveyor colliders and adjust MaterialSpawner settings in PhysicsSimulator 2025-08-13 10:34:16 +05:30
6de1cacf8d feat: Implement Y-Split conveyor collider and update related types; refactor conveyor handling across components 2025-08-07 09:11:06 +05:30
33d960cedd api updated for getassetConveyorPoints 2025-08-04 18:08:03 +05:30
298399b3e5 added getConveyorpoits function 2025-08-04 14:20:26 +05:30
be1a24a136 Merge branch 'main-dev' into dev-physics 2025-08-01 16:02:37 +05:30
3394c44ed1 sline points
Replaced hardcoded spline points with first two raycast intersection points on click.

Computed the middle control point using the incenter of a triangle formed with the midpoint as the third vertex, creating a dynamic Bézier curve.
2025-08-01 15:57:11 +05:30
1eac23607f commit 2025-08-01 13:03:38 +05:30
04573b86dd plane geometry added with physics 2025-07-30 18:16:01 +05:30
e001bfaa0a Merge branch 'main-dev' into dev-physics 2025-07-30 12:52:03 +05:30
d3697bb690 feat: Enhance ConveyorCollider with modelName and scene props, add CurvedPlane component, and update PhysicsSimulator to comment out MaterialSpawner 2025-07-30 10:02:55 +05:30
ec4a4247b2 feat: Implement ColliderCreator and ColliderInstance components, enhance PhysicsSimulator with multiple MaterialSpawner instances 2025-07-23 14:03:00 +05:30
d4d66d9d32 feat: Replace crypto.randomUUID with generateUniqueId in MaterialSpawner and reduce spawnCount to 10 2025-07-17 18:00:55 +05:30
8191e955ca refactor: Remove unused onReachEnd prop from ConveyorCollider component 2025-07-17 17:26:03 +05:30
bbe93a91d5 Merge branch 'main-dev' into dev-physics 2025-07-17 17:23:37 +05:30
1e314bc290 feat: Refactor conveyor collider logic, enhance material spawning, and remove unused colliders component 2025-07-17 17:15:46 +05:30
660e21edcc feat: Integrate @react-three/rapier for physics simulation and add conveyor collider functionality
- Added @react-three/rapier to package.json for physics support.
- Refactored AssetBoundingBox to utilize RigidBody for collision detection.
- Implemented ConveyorCollider to manage object movement on conveyor belts.
- Enhanced Model component to include rigid body references and bounding box calculations.
- Updated Ground component to use RigidBody for ground physics.
- Introduced Colliders component to manage material instances with physics interactions.
- Created SecondaryCamera for enhanced camera management and editing capabilities.
- Added secondary canvas for rendering secondary camera views.
- Updated selection controls to utilize bounding boxes for asset selection.
2025-07-17 14:31:43 +05:30
38 changed files with 3185 additions and 49 deletions

29
app/package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@react-three/drei": "^9.113.0",
"@react-three/fiber": "^8.17.7",
"@react-three/postprocessing": "^2.16.3",
"@react-three/rapier": "^1.5.0",
"@recast-navigation/core": "^0.39.0",
"@recast-navigation/three": "^0.39.0",
"@testing-library/jest-dom": "^5.17.0",
@@ -2372,6 +2373,12 @@
"ms": "^2.1.1"
}
},
"node_modules/@dimforge/rapier3d-compat": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.14.0.tgz",
"integrity": "sha512-/uHrUzS+CRQ+NQrrJCEDUkhwHlNsAAexbNXgbN9sHY+GwR+SFFAFrxRr8Llf5/AJZzqiLANdQIfJ63Cw4gJVqw==",
"license": "Apache-2.0"
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@@ -3814,6 +3821,21 @@
"three": ">=0.144.0"
}
},
"node_modules/@react-three/rapier": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@react-three/rapier/-/rapier-1.5.0.tgz",
"integrity": "sha512-gylk2KyCer9EoymFyTyc+g2IqyAq4mTbZgaHoSJi6gHoXlJsC2LVeN4jedvegvjUsXPExdE60wHjCPa+DS4iXw==",
"dependencies": {
"@dimforge/rapier3d-compat": "0.14.0",
"suspend-react": "^0.1.3",
"three-stdlib": "^2.29.4"
},
"peerDependencies": {
"@react-three/fiber": ">=8.9.0",
"react": ">=18.0.0",
"three": ">=0.139.0"
}
},
"node_modules/@recast-navigation/core": {
"version": "0.39.0",
"resolved": "https://registry.npmjs.org/@recast-navigation/core/-/core-0.39.0.tgz",
@@ -4191,10 +4213,9 @@
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",

View File

@@ -10,6 +10,7 @@
"@react-three/csg": "^3.2.0",
"@react-three/drei": "^9.113.0",
"@react-three/fiber": "^8.17.7",
"@react-three/rapier": "^1.5.0",
"@react-three/postprocessing": "^2.16.3",
"@recast-navigation/core": "^0.39.0",
"@recast-navigation/three": "^0.39.0",

View File

@@ -9,6 +9,7 @@ import Simulations from "./simulation/Simulations";
import useVersionHistoryVisibleStore, { useSaveVersion, useSelectedFloorItem, useToolMode } from "../../../store/builder/store";
import { useSelectedEventData, useSelectedEventSphere, } from "../../../store/simulation/useSimulationStore";
import { useBuilderStore } from "../../../store/builder/useBuilderStore";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import GlobalProperties from "./properties/GlobalProperties";
import AssetProperties from "./properties/AssetProperties";
import ZoneProperties from "./properties/ZoneProperties";
@@ -22,6 +23,7 @@ import SelectedFloorProperties from "./properties/SelectedFloorProperties";
import SelectedDecalProperties from "./properties/SelectedDecalProperties";
import SelectedAisleProperties from "./properties/SelectedAisleProperties";
import ResourceManagement from "./resourceManagement/ResourceManagement";
import ColliderProperties from "../../../modules/scene/physics/ui/ColliderProperties";
type DisplayComponent =
| "versionHistory"
@@ -40,6 +42,7 @@ type DisplayComponent =
| "analysis"
| "visualization"
| "resourceManagement"
| "colliderProperties"
| "none";
const SideBarRight: React.FC = () => {
@@ -54,6 +57,8 @@ const SideBarRight: React.FC = () => {
const { selectedEventSphere } = useSelectedEventSphere();
const { viewVersionHistory, setVersionHistoryVisible } = useVersionHistoryVisibleStore();
const { isVersionSaved } = useSaveVersion();
const { colliderStore } = useSceneContext();
const { selectedCollider } = colliderStore();
const [displayComponent, setDisplayComponent] = useState<DisplayComponent>("none");
@@ -107,6 +112,13 @@ const SideBarRight: React.FC = () => {
}
}
if (!isVersionSaved && activeModule === "builder") {
if (selectedCollider) {
setDisplayComponent("colliderProperties");
return;
}
}
if (subModule === "properties" && activeModule !== "visualization") {
if (selectedFloorItem) {
setDisplayComponent("assetProperties");
@@ -156,7 +168,7 @@ const SideBarRight: React.FC = () => {
}
setDisplayComponent("none");
}, [viewVersionHistory, activeModule, subModule, isVersionSaved, selectedFloorItem, selectedWall, selectedFloor, selectedAisle, toolMode, selectedDecal]);
}, [viewVersionHistory, activeModule, subModule, isVersionSaved, selectedFloorItem, selectedWall, selectedFloor, selectedAisle, toolMode, selectedDecal, selectedCollider]);
const renderComponent = () => {
switch (displayComponent) {
@@ -192,6 +204,8 @@ const SideBarRight: React.FC = () => {
return <Visualization />;
case "resourceManagement":
return <ResourceManagement />;
case "colliderProperties":
return <ColliderProperties />;
default:
return null;
}

View File

@@ -11,6 +11,7 @@ import { SkeletonUtils } from 'three-stdlib';
import { getAssetFieldApi } from '../../../../../services/factoryBuilder/asset/floorAsset/getAssetField';
import { ModelAnimator } from './animator/modelAnimator';
import { useModelEventHandlers } from './eventHandlers/useEventHandlers';
import RibbonCollider from '../../../../scene/physics/conveyor/ribbonCollider';
function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendered: boolean, loader: GLTFLoader }) {
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
@@ -225,12 +226,26 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
}
</>
)}
{/* <ConveyorCollider
boundingBox={boundingBox}
asset={asset}
/> */}
{asset.eventData && asset.eventData.type === 'Conveyor' && fieldData && activeModule === 'simulation' &&
<RibbonCollider
key={asset.modelUuid}
boundingBox={boundingBox}
ribbonData={fieldData}
asset={asset}
/>
}
{isSelected &&
<AssetBoundingBox name='Asset BBox' boundingBox={boundingBox} color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"} lineWidth={2.7} />
}
</>
)}
</group>
)} </group>
);
}

View File

@@ -36,10 +36,8 @@ const BoundingBox = ({ boundingBoxRef, isPerAsset = true }: BoundingBoxProps) =>
return selectedAssets.map((obj: THREE.Object3D) => {
const position = obj.position;
const rotation = obj.getWorldQuaternion(new THREE.Quaternion());
const clone = obj.clone();
clone.position.set(0, 0, 0);
clone.rotation.set(0, 0, 0);
const box = new THREE.Box3().setFromObject(clone);
const box: THREE.Box3 = obj.userData.boundingBox ?? new THREE.Box3().setFromObject(obj);
const size = new THREE.Vector3();
const center = new THREE.Vector3();
box.getSize(size);
@@ -57,12 +55,17 @@ const BoundingBox = ({ boundingBoxRef, isPerAsset = true }: BoundingBoxProps) =>
};
});
} else {
const box = new THREE.Box3();
selectedAssets.forEach((obj: any) => box.expandByObject(obj.clone()));
const unionBox = new THREE.Box3();
selectedAssets.forEach((obj: any) => {
const localBox: THREE.Box3 = obj.userData.boundingBox ?? new THREE.Box3().setFromObject(obj);
unionBox.union(localBox);
});
const size = new THREE.Vector3();
const center = new THREE.Vector3();
box.getSize(size);
box.getCenter(center);
unionBox.getSize(size);
unionBox.getCenter(center);
const halfSize = size.clone().multiplyScalar(0.5);
const min = center.clone().sub(halfSize);

View File

@@ -147,6 +147,11 @@ const CopyPasteControls3D = () => {
const copySelection = () => {
if (selectedAssets.length > 0) {
const newClones = selectedAssets.map((asset: any) => {
if (asset.userData.rigidBodyRef) {
let userData = { ...asset.userData };
delete userData.rigidBodyRef;
asset.userData = userData;
}
const clone = SkeletonUtils.clone(asset);
clone.position.copy(asset.position);
return clone;

View File

@@ -228,6 +228,11 @@ const DuplicationControls3D = () => {
const positions: Record<string, THREE.Vector3> = {};
const newDuplicatedObjects = selectedAssets.map((obj: any) => {
if (obj.userData.rigidBodyRef) {
let userData = { ...obj.userData };
delete userData.rigidBodyRef;
obj.userData = userData;
}
const clone = SkeletonUtils.clone(obj);
clone.userData.modelUuid = THREE.MathUtils.generateUUID();
positions[clone.userData.modelUuid] = new THREE.Vector3().copy(obj.position);

View File

@@ -1,3 +1,4 @@
import { RigidBody } from "@react-three/rapier";
import { useTileDistance, useToggleView } from "../../../store/builder/store";
import * as CONSTANTS from "../../../types/world/worldConstants";
@@ -6,7 +7,7 @@ const Ground = ({ plane }: any) => {
const { planeValue, gridValue } = useTileDistance();
return (
<mesh name="Ground">
<>
<mesh
name="Grid"
position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D}
@@ -20,6 +21,20 @@ const Ground = ({ plane }: any) => {
]}
/>
</mesh>
<RigidBody
type="fixed"
colliders='cuboid'
includeInvisible
>
<mesh
rotation-x={CONSTANTS.planeConfig.rotation}
position={[0, 0, 0]}
visible={false}
>
<planeGeometry args={[planeValue.width, planeValue.height]} />
</mesh>
</RigidBody>
<mesh
ref={plane}
rotation-x={CONSTANTS.planeConfig.rotation}
@@ -30,7 +45,7 @@ const Ground = ({ plane }: any) => {
<planeGeometry args={[planeValue.width, planeValue.height]} />
<meshBasicMaterial color={CONSTANTS.planeConfig.color} />
</mesh>
</mesh>
</>
);
};

View File

@@ -19,5 +19,5 @@ export default function StatsHelper() {
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
return visible ? <Perf position="bottom-left" className="scene-performance-stats"/> : null;
return visible ? <Perf position="bottom-left" className="scene-performance-stats" /> : null;
}

View File

@@ -0,0 +1,92 @@
import { useEffect, useRef } from 'react';
import { useThree } from '@react-three/fiber';
import ColliderInstance from './colliderInstance/colliderInstance';
import { useToggleView } from '../../../../store/builder/store';
import { useSceneContext } from '../../sceneContext';
function ColliderCreator() {
const { camera, gl, scene, raycaster, pointer } = useThree();
const { colliderStore } = useSceneContext();
const { colliders, addCollider, clearSelectedCollider } = colliderStore();
const drag = useRef(false);
const isLeftMouseDown = useRef(false);
const { toggleView } = useToggleView();
const handleCtrlClick = (e: MouseEvent) => {
if (!e.ctrlKey || drag.current || toggleView) return;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const point = intersects[0].point;
const spawnPosition: [number, number, number] = [point.x, point.y + 0.5, point.z];
addCollider({
id: Date.now().toString(),
position: spawnPosition,
rotation: [0, 0, 0],
arrows: [],
colliderCondition: {
conditionType: "material",
arrowCondition: []
}
});
}
};
// right click → deselect
const handleRightClick = (e: MouseEvent) => {
e.preventDefault();
clearSelectedCollider();
};
useEffect(() => {
const canvas = gl.domElement;
const onMouseDown = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown.current = true;
drag.current = false;
}
};
const onMouseUp = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown.current = false;
}
};
const onMouseMove = () => {
if (isLeftMouseDown.current) {
drag.current = true;
}
};
canvas.addEventListener('click', handleCtrlClick);
canvas.addEventListener('contextmenu', handleRightClick);
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('mousemove', onMouseMove);
return () => {
canvas.removeEventListener('click', handleCtrlClick);
canvas.removeEventListener('contextmenu', handleRightClick);
canvas.removeEventListener('mousedown', onMouseDown);
canvas.removeEventListener('mouseup', onMouseUp);
canvas.removeEventListener('mousemove', onMouseMove);
};
}, [colliders, camera]);
return (
<>
{colliders.map((collider) => (
<ColliderInstance key={collider.id} collider={collider} />
))}
</>
);
}
export default ColliderCreator;

View File

@@ -0,0 +1,115 @@
import { useRef, useMemo, useState, useCallback } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { useSceneContext } from "../../../sceneContext";
type ColliderArrowProps = {
arrowId: string;
colliderId: string;
startPosition: [number, number, number];
endPosition: [number, number, number];
colliderRotation: [number, number, number];
thickness?: number;
depth?: number;
color?: string;
};
export function ColliderArrow({
arrowId,
colliderId,
startPosition,
endPosition,
colliderRotation,
thickness = 0.05,
depth = 0.01,
color = "green",
}: ColliderArrowProps) {
const groupRef = useRef<THREE.Group>(null);
const { raycaster, pointer, controls } = useThree();
const { colliderStore } = useSceneContext();
const { updateArrow, selectedArrowId, clearSelectedArrow } = colliderStore();
const [dragging, setDragging] = useState(false);
const { dir, length } = useMemo(() => {
const start = new THREE.Vector3(...startPosition);
const end = new THREE.Vector3(...endPosition);
const d = new THREE.Vector3().subVectors(end, start);
return { dir: d.clone().normalize(), length: d.length() };
}, [startPosition, endPosition]);
const arrowShape = useMemo(() => {
const shaftWidth = thickness;
const headLength = Math.min(length * 0.3, 0.4);
const headWidth = thickness * 3;
const shape = new THREE.Shape();
shape.moveTo(0, -shaftWidth / 2);
shape.lineTo(length - headLength, -shaftWidth / 2);
shape.lineTo(length - headLength, -headWidth / 2);
shape.lineTo(length, 0);
shape.lineTo(length - headLength, headWidth / 2);
shape.lineTo(length - headLength, shaftWidth / 2);
shape.lineTo(0, shaftWidth / 2);
shape.closePath();
return shape;
}, [length, thickness]);
const extrudeSettings = useMemo(() => ({ depth, bevelEnabled: false, }), [depth]);
const geometry = useMemo(() => new THREE.ExtrudeGeometry(arrowShape, extrudeSettings), [arrowShape, extrudeSettings]);
const quaternion = useMemo(() => {
const q = new THREE.Quaternion();
q.setFromUnitVectors(new THREE.Vector3(1, 0, 0), dir);
const colliderQuat = new THREE.Quaternion().setFromEuler(
new THREE.Euler().fromArray(colliderRotation)
);
return colliderQuat.multiply(q);
}, [dir, colliderRotation]);
const handlePointerDown = useCallback((e: any) => {
e.stopPropagation();
setDragging(true);
(controls as any).enabled = false; // Disable controls while dragging
}, []);
const handlePointerUp = useCallback((e: any) => {
e.stopPropagation();
clearSelectedArrow()
setDragging(false);
(controls as any).enabled = true;
}, []);
useFrame(({ camera }) => {
if (dragging) {
if (selectedArrowId) {
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -startPosition[1]);
raycaster.setFromCamera(pointer, camera);
const hit = new THREE.Vector3();
if (raycaster.ray.intersectPlane(plane, hit)) {
updateArrow(colliderId, arrowId, {
position: [hit.x, startPosition[1], hit.z],
});
}
}
}
});
return (
<group ref={groupRef} quaternion={quaternion}>
<mesh geometry={geometry} rotation={[Math.PI / 2, 0, 0]}>
<meshStandardMaterial color={color} />
</mesh>
<mesh
position={[length, 0, 0]}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
>
<sphereGeometry args={[0.15, 16, 16]} />
<meshBasicMaterial transparent opacity={0.2} color="yellow" />
</mesh>
</group>
);
}

View File

@@ -0,0 +1,207 @@
import { CameraControls } from '@react-three/drei';
import { useThree, useFrame } from '@react-three/fiber';
import { CollisionPayload, RapierRigidBody, RigidBody } from '@react-three/rapier';
import { useEffect, useRef, useState } from 'react'
import * as THREE from 'three';
import { useSceneContext } from '../../../sceneContext';
import { ColliderArrow } from './colliderArrow';
function ColliderInstance({ collider }: { collider: Collider }) {
const { camera, gl, pointer, controls } = useThree();
const { colliderStore } = useSceneContext();
const { colliders, selectedCollider, setSelectedCollider, updateCollider, getArrowByArrowId } = colliderStore();
const [draggedId, setDraggedId] = useState<string | null>(null);
const dragOffset = useRef(new THREE.Vector3());
const initialDepth = useRef(0);
const ref = useRef<RapierRigidBody>(null);
const [objectsOnCollider, setObjectsOnCollider] = useState<Set<RapierRigidBody>>(new Set());
const isSelected = selectedCollider?.id === collider.id;
const objectCounterRef = useRef(0);
const handlePointerDown = (id: string) => {
if (controls) {
(controls as CameraControls).enabled = false;
}
setDraggedId(id);
const collider = colliders.find(c => c.id === id);
if (!collider || !ref.current) return;
const pos = ref.current.translation();
const screenPos = new THREE.Vector3(pos.x, pos.y, pos.z).project(camera);
dragOffset.current = new THREE.Vector3(screenPos.x - pointer.x, 0, screenPos.y - pointer.y);
initialDepth.current = new THREE.Vector3(pos.x, pos.y, pos.z).sub(camera.position).length();
ref.current.setGravityScale(0, true);
ref.current.setLinearDamping(10);
ref.current.setAngularDamping(10);
};
const handlePointerMove = (e: PointerEvent) => {
if (!draggedId) return;
const collider = colliders.find(c => c.id === draggedId);
if (!collider || !ref.current) return;
if (e.altKey) {
const rotation = ref.current.rotation();
const currentQuaternion = new THREE.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
const deltaQuaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), 0.01);
const newQuaternion = currentQuaternion.multiply(deltaQuaternion);
ref.current.setRotation({ x: newQuaternion.x, y: newQuaternion.y, z: newQuaternion.z, w: newQuaternion.w }, true);
return;
};
const screenTarget = new THREE.Vector3(pointer.x + dragOffset.current.x, 0, pointer.y + dragOffset.current.z);
const worldTarget = new THREE.Vector3(screenTarget.x, screenTarget.z, 0.5).unproject(camera);
const dir = worldTarget.clone().sub(camera.position).normalize();
const finalPos = camera.position.clone().add(dir.multiplyScalar(initialDepth.current));
ref.current.setTranslation({ x: finalPos.x, y: finalPos.y, z: finalPos.z }, true);
}
const handlePointerUp = () => {
if (controls) {
(controls as CameraControls).enabled = true;
}
if (!draggedId) return;
if (ref.current) {
ref.current.setGravityScale(1, true);
ref.current.setLinearDamping(0.5);
ref.current.setAngularDamping(0.5);
const pos = ref.current.translation();
const rot = ref.current.rotation();
const q = new THREE.Quaternion(rot.x, rot.y, rot.z, rot.w);
const euler = new THREE.Euler().setFromQuaternion(q, 'XYZ');
updateCollider(draggedId, {
position: [pos.x, pos.y, pos.z],
rotation: [euler.x, euler.y, euler.z],
});
}
setDraggedId(null);
};
useEffect(() => {
const canvas = gl.domElement;
canvas.addEventListener('pointermove', handlePointerMove);
canvas.addEventListener('pointerup', handlePointerUp);
return () => {
canvas.removeEventListener('pointermove', handlePointerMove);
canvas.removeEventListener('pointerup', handlePointerUp);
};
}, [colliders, draggedId, controls]);
const handleMaterialEnter = (e: CollisionPayload) => {
const body = e.other.rigidBody;
if (body && (body.userData as any)?.materialType) {
setObjectsOnCollider(prev => {
const newSet = new Set(prev);
newSet.add(body);
return newSet;
});
if (collider.colliderCondition.conditionType === "count") {
objectCounterRef.current += 1;
}
}
};
const handleMaterialExit = (e: CollisionPayload) => {
const body = e.other.rigidBody;
if (body) {
setObjectsOnCollider(prev => {
const newSet = new Set(prev);
newSet.delete(body);
return newSet;
});
}
};
useFrame(() => {
objectsOnCollider.forEach(rigidBody => {
if (!rigidBody) return;
if (collider.colliderCondition.conditionType === "material") {
const bodyMaterial = (rigidBody as any).userData.materialType;
if (!bodyMaterial) return;
let matchedCondition = collider.colliderCondition.arrowCondition?.find((material) => material.materialType === bodyMaterial);
if (!matchedCondition) {
matchedCondition = collider.colliderCondition.arrowCondition?.find((material) => material.materialType === "Any");
}
if (matchedCondition) {
let arrowDetails = getArrowByArrowId(matchedCondition.arrowId);
if (arrowDetails?.position && ref.current) {
const arrowPos = new THREE.Vector3(...arrowDetails.position);
const colliderPos = new THREE.Vector3(...(collider.position || [0, 0, 0]));
const direction = new THREE.Vector3();
direction.subVectors(arrowPos, colliderPos).normalize();
rigidBody.setLinvel({ x: direction.x, y: direction.y, z: direction.z }, true);
}
}
} else if (collider.colliderCondition.conditionType === "count") {
let { count, arrowsOrder } = collider.colliderCondition;
if (!arrowsOrder || arrowsOrder.length === 0) return;
const totalProcessed = objectCounterRef.current;
const arrowIndex = Math.floor(totalProcessed / count) % arrowsOrder.length;
const arrowId = arrowsOrder[arrowIndex];
const arrowDetails = getArrowByArrowId(arrowId);
if (arrowDetails?.position && ref.current) {
const arrowPos = new THREE.Vector3(...arrowDetails.position);
const colliderPos = new THREE.Vector3(...(collider.position || [0, 0, 0]));
const direction = new THREE.Vector3();
direction.subVectors(arrowPos, colliderPos).normalize();
rigidBody.setLinvel({ x: direction.x, y: direction.y, z: direction.z }, true);
}
}
});
});
return (
<RigidBody
name='Sensor-Collider'
key={collider.id}
ref={ref}
type="fixed"
sensor
position={collider.position}
rotation={collider.rotation}
colliders="cuboid"
includeInvisible
gravityScale={0}
onIntersectionEnter={handleMaterialEnter}
onIntersectionExit={handleMaterialExit}
>
<mesh
onPointerDown={(e) => {
e.stopPropagation();
handlePointerDown(collider.id);
}}
onClick={(e) => {
e.stopPropagation();
setSelectedCollider(collider);
}}
>
<boxGeometry args={[0.1, 1, 1]} />
<meshStandardMaterial color={isSelected ? 'green' : 'white'} transparent opacity={0.3} />
</mesh>
{isSelected && collider.arrows.map((arrow) => (
<ColliderArrow
key={arrow.arrowId}
startPosition={collider.position}
endPosition={arrow.position}
colliderRotation={collider.rotation}
colliderId={collider.id}
arrowId={arrow.arrowId}
/>
))}
</RigidBody>
);
}
export default ColliderInstance;

View File

@@ -0,0 +1,139 @@
import * as THREE from 'three';
import { CollisionPayload, RigidBody } from '@react-three/rapier';
import { useEffect, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
function ConveyorCollider({ boundingBox, asset }: {
boundingBox: THREE.Box3 | null,
asset: Asset,
}) {
const conveyorRef = useRef<any>(null);
const [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
const conveyorSpeed = 2;
const size = boundingBox?.getSize(new THREE.Vector3());
useEffect(() => {
if (!boundingBox || !size) return;
const [width, depth] = [size.x, size.z];
if (width < depth) {
conveyorDirection.current.set(0, 0, 1);
} else {
conveyorDirection.current.set(1, 0, 0);
}
const rotation = new THREE.Euler().fromArray(asset.rotation || [0, 0, 0]);
conveyorDirection.current.applyEuler(rotation);
}, [boundingBox, 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 forward = conveyorDirection.current.clone().normalize();
const side = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
const force = forward.clone().multiplyScalar(conveyorSpeed);
objectsOnConveyor.forEach(rigidBody => {
if (rigidBody) {
const position = rigidBody.translation();
const centerLine = conveyorRef.current.translation();
const relative = new THREE.Vector3().subVectors(position, centerLine);
const sideOffset = relative.dot(side);
const centeringStrength = 10;
const centeringForce = side.clone().multiplyScalar(-sideOffset * centeringStrength);
const totalForce = force.clone().add(centeringForce);
rigidBody.setAngvel(new THREE.Vector3(0, 0, 0), true);
rigidBody.setLinvel(totalForce, true);
}
});
});
// useFrame(() => {
// if (
// !boundingBox ||
// !conveyorPlaneSize ||
// !conveyorRef.current ||
// typeof conveyorRef.current.translation !== 'function'
// ) return;
// const forward = conveyorDirection.current.clone().normalize();
// const side = new THREE.Vector3().crossVectors(forward, new THREE.Vector3(0, 1, 0)).normalize();
// const force = forward.clone().multiplyScalar(conveyorSpeed);
// const start = conveyorRef.current.translation();
// const conveyorLength = Math.max(conveyorPlaneSize[0], conveyorPlaneSize[1]);
// const halfLength = conveyorLength / 2;
// objectsOnConveyor.forEach(rigidBody => {
// if (!rigidBody) return;
// const position = rigidBody.translation();
// const relative = new THREE.Vector3().subVectors(position, start);
// const forwardOffset = relative.dot(forward);
// const sideOffset = relative.dot(side);
// const atEnd = forwardOffset >= halfLength - 0.5;
// if (!atEnd) {
// const centeringStrength = 10;
// const centeringForce = side.clone().multiplyScalar(-sideOffset * centeringStrength);
// const totalForce = force.clone().add(centeringForce);
// rigidBody.setAngvel(new THREE.Vector3(0, 0, 0), true);
// rigidBody.setLinvel(totalForce, true);
// } else {
// rigidBody.setLinvel(new THREE.Vector3(0, 0, 0), true);
// rigidBody.setAngvel(new THREE.Vector3(0, 0, 0), true);
// }
// });
// });
return (
<>
{asset.eventData?.type === 'Conveyor' && boundingBox && size && (
<RigidBody
ref={conveyorRef}
type="fixed"
position={[0, (boundingBox?.max.y ?? 0) + 0.1, 0]}
rotation={[Math.PI / 2, 0, 0]}
sensor
userData={{ isConveyor: true }}
onIntersectionEnter={handleMaterialEnter}
onIntersectionExit={handleMaterialExit}
colliders="cuboid"
>
<mesh>
<planeGeometry args={[size.x, size.z]} />
<meshBasicMaterial
color="green"
transparent
opacity={0.3}
visible={true}
side={THREE.DoubleSide}
/>
</mesh>
</RigidBody>
)}
</>
);
}
export default ConveyorCollider;

View File

@@ -0,0 +1,58 @@
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
}: {
ribbonData: ConveyorPoints,
boundingBox: THREE.Box3 | null,
asset: Asset,
}) {
const [forward, setForward] = useState(false);
// console.log('ribbonData: ', ribbonData);
return (
<>
{ribbonData.type === 'normal' && (
<NormalConveyorCollider
key={asset.modelUuid}
points={ribbonData.points}
boundingBox={boundingBox}
asset={asset}
forward={forward}
isPaused={false}
onDirectionChange={setForward}
/>
)}
{ribbonData.type === 'curved' && (
<CurvedConveyorCollider
key={asset.modelUuid}
points={ribbonData.points}
boundingBox={boundingBox}
asset={asset}
forward={forward}
isPaused={false}
onDirectionChange={setForward}
/>
)}
{ribbonData.type === 'y-Split' && (
<YSplitConveyorCollider
key={asset.modelUuid}
points={ribbonData.points}
boundingBox={boundingBox}
asset={asset}
forward={forward}
isPaused={false}
onDirectionChange={setForward}
/>
)}
</>
);
}
export default RibbonCollider;

View File

@@ -0,0 +1,155 @@
import * as THREE from 'three'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { RigidBody } from '@react-three/rapier'
import { CameraControls, Sphere } from '@react-three/drei'
export function SplineCreator() {
const { raycaster, camera, gl, pointer, controls, scene } = useThree()
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), [])
const [splines, setSplines] = useState<THREE.Vector3[][]>([])
const [_, setTempPoints] = useState<THREE.Vector3[]>([])
const draggingRef = useRef<{ splineIndex: number, pointIndex: number } | null>(null)
useFrame(() => {
if (draggingRef.current !== null) {
const { splineIndex, pointIndex } = draggingRef.current
const y = splines[splineIndex][pointIndex]?.y ?? 0
plane.constant = -y
raycaster.setFromCamera(pointer, camera)
const intersectionPoint = new THREE.Vector3()
const position = raycaster.ray.intersectPlane(plane, intersectionPoint) || intersectionPoint
setSplines(prev => {
const next = [...prev]
const spline = [...next[splineIndex]]
spline[pointIndex] = new THREE.Vector3(position.x, y, position.z)
next[splineIndex] = spline
return next
})
}
})
const geometries = useMemo(() => {
const width = 1
const segments = 20
const groupGeometries: THREE.BufferGeometry[] = []
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 j = 0; j <= segments; j++) {
const t = j / 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())
}
for (let j = 0; j < segments; j++) {
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)
}
}
return groupGeometries
}, [splines])
useEffect(() => {
const canvas = gl.domElement
const onMouseUp = () => {
(controls as CameraControls).enabled = true
draggingRef.current = null
}
canvas.addEventListener('mouseup', onMouseUp)
return () => canvas.removeEventListener('mouseup', onMouseUp)
}, [camera])
useEffect(() => {
const handleClick = (e: MouseEvent) => {
e.preventDefault()
raycaster.setFromCamera(pointer, camera)
const intersections = raycaster.intersectObject(scene, true)
if (intersections.length > 0) {
const hitPoint = intersections[0].point.clone()
setTempPoints(temp => {
const nextTemp = [...temp, hitPoint]
if (nextTemp.length === 2) {
const [p0, p2] = nextTemp
const mid = new THREE.Vector3().addVectors(p0, p2).multiplyScalar(0.5)
const newSpline = [p0, mid, p2]
setSplines(prev => [...prev, newSpline])
return []
}
return nextTemp
})
}
}
const canvas = gl.domElement
canvas.addEventListener('dblclick', handleClick)
return () => canvas.removeEventListener('dblclick', handleClick)
}, [raycaster, pointer, camera, gl, scene])
useEffect(() => {
console.log(splines);
}, [splines])
return (
<>
{splines.map((spline, splineIndex) =>
spline.map((point, pointIndex) => (
<Sphere
key={`${splineIndex}-${pointIndex}`}
position={point}
args={[0.1, 16, 16]}
onPointerDown={() => {
draggingRef.current = { splineIndex, pointIndex };
(controls as CameraControls).enabled = false
}}
>
<meshStandardMaterial color="red" />
</Sphere>
))
)}
{geometries.map((geom, idx) => (
<RigidBody key={idx} type="fixed" colliders="trimesh">
<mesh geometry={geom}>
<meshStandardMaterial color="skyblue" side={THREE.DoubleSide} />
</mesh>
</RigidBody>
))}
</>
)
}

View File

@@ -0,0 +1,373 @@
import * as THREE from 'three';
import { CollisionPayload, RigidBody } from '@react-three/rapier';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
function CurvedConveyorCollider({
points,
boundingBox,
asset,
forward,
isPaused,
onDirectionChange
}: {
points: [number, number, number][][];
boundingBox: THREE.Box3 | null;
asset: Asset;
forward: boolean;
isPaused: boolean;
onDirectionChange?: (newDirection: boolean) => void;
}) {
const conveyorRef = useRef<any>(null);
const [objectsOnConveyor, setObjectsOnConveyor] = useState<Set<any>>(new Set());
const conveyorDirection = useRef<THREE.Vector3>(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<THREE.Group[]>([]);
const [geometryKey, setGeometryKey] = useState(0);
// 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) {
if (onDirectionChange) {
console.log('forwardcurve: ', forward);
onDirectionChange(!forward);
}
}
lastClickTime.current = now;
}
};
window.addEventListener('mousedown', handleClick);
return () => window.removeEventListener('mousedown', handleClick);
}, [forward, hoverState]);
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]);
const geometries = useMemo(() => {
const width = 1;
const segments = 20;
const geos: THREE.BufferGeometry[] = [];
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]);
useEffect(() => {
if (bezierPoints.length >= 2) {
const start = bezierPoints[0];
const end = bezierPoints[bezierPoints.length - 1];
conveyorDirection.current.copy(end).sub(start).normalize();
const rotation = new THREE.Euler().fromArray(asset.rotation || [0, 0, 0]);
conveyorDirection.current.applyEuler(rotation);
}
}, [bezierPoints, forward, 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(({ 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();
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);
});
// 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);
}
}
});
});
}
});
// 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 (
<group
onPointerOver={() => {
setShowDirection(true);
setHoverState(true);
}}
onPointerOut={() => {
setShowDirection(false);
setHoverState(false);
}}
>
{/* Conveyor surface */}
{geometries.length > 0 && (
<RigidBody
key={geometryKey}
ref={conveyorRef}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={handleMaterialEnter}
onCollisionExit={handleMaterialExit}
colliders="trimesh"
>
{geometries.map((geometry, index) => (
<mesh key={index} geometry={geometry}>
<meshStandardMaterial
color={forward ? "#64b5f6" : "#f48fb1"}
side={THREE.DoubleSide}
transparent
opacity={0.7}
visible={false}
/>
</mesh>
))}
</RigidBody>
)}
{/* Direction indicators */}
{showDirection && directionArrows?.map((arrow, i) => (
<primitive key={`arrow-${i}`} object={arrow} />
))}
{/* Hover highlight */}
{hoverState && (
<group>
{geometries.map((geometry, index) => (
<mesh
key={`highlight-${index}`}
geometry={geometry}
position={[0, 0.002, 0]} // Slightly above conveyor
// visible={false}
>
<meshBasicMaterial
color={forward ? "#00ff0044" : "#ff000044"}
transparent
opacity={0.3}
/>
</mesh>
))}
</group>
)}
</group>
);
}
export default CurvedConveyorCollider;

View File

@@ -0,0 +1,250 @@
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';
interface NormalConveyorColliderProps {
points: [number, number, number][][];
boundingBox: THREE.Box3 | null;
asset: Asset;
forward: boolean;
isPaused: boolean;
onDirectionChange?: (newDirection: boolean) => void;
}
function NormalConveyorCollider({ points, boundingBox, asset, forward, isPaused, onDirectionChange }: NormalConveyorColliderProps) {
const conveyorRefs = useRef<(any)[]>([]);
const [objectsOnGeometry, setObjectsOnGeometry] = useState<Map<number, Set<any>>>(new Map());
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
const [showDirection, setShowDirection] = useState(false);
const conveyorSpeed = 2;
const lastClickTime = useRef(0);
const [hoverState, setHoverState] = useState(false);
const [localForward, setLocalForward] = useState()
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (e.button === 2) {
const now = Date.now();
if (now - lastClickTime.current < 300) {
if (onDirectionChange) {
console.log('forwardnormal: ', forward);
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, forward]);
const handleMaterialEnter = (e: CollisionPayload, index: number) => {
if (e.other.rigidBody) {
setObjectsOnGeometry(prev => {
const newMap = new Map(prev);
if (!newMap.has(index)) newMap.set(index, new Set());
newMap.get(index)!.add(e.other.rigidBody);
return newMap;
});
}
};
const handleMaterialExit = (e: CollisionPayload, index: number) => {
if (e.other.rigidBody) {
setObjectsOnGeometry(prev => {
const newMap = new Map(prev);
if (newMap.has(index)) {
newMap.get(index)!.delete(e.other.rigidBody);
}
return newMap;
});
}
};
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);
const inverseQuat = assetQuat.clone().invert();
points.forEach((segment, index) => {
if (segment.length < 2) return;
const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
const curvePoints = curve.getPoints((segment.length - 1) * 30);
if (!forward) curvePoints.reverse();
const bodies = objectsOnGeometry.get(index);
if (!bodies) return;
bodies.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 < curvePoints.length; i++) {
const dist = curvePoints[i].distanceToSquared(localPos);
if (dist < minDist) {
minDist = dist;
closestIndex = i;
}
}
const point = curvePoints[closestIndex];
const prev = curvePoints[closestIndex - 1] || point;
const next = curvePoints[closestIndex + 1] || point;
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
const side = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
const relative = new THREE.Vector3().subVectors(localPos, point);
const sideOffset = relative.dot(side);
const centeringForce = side.clone().multiplyScalar(-sideOffset * 10);
const forwardForce = tangent.clone().multiplyScalar(conveyorSpeed);
const totalForce = forwardForce.add(centeringForce).applyQuaternion(assetQuat);
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
rigidBody.setLinvel(totalForce, true);
});
});
})
const geometries = useMemo(() => {
const width = 1;
const segments = 1;
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((geo): geo is THREE.BufferGeometry => geo !== null);
}, [points, asset.position, asset.rotation, forward]);
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);
for (let i = 0; i < curvePoints.length; i++) {
const point = curvePoints[i];
let direction: THREE.Vector3;
if (i < curvePoints.length - 1) {
direction = new THREE.Vector3().subVectors(curvePoints[i + 1], point).normalize();
} else {
direction = new THREE.Vector3().subVectors(point, curvePoints[i - 1]).normalize();
}
if (!forward) {
direction.multiplyScalar(-1);
}
const arrow = new THREE.Mesh(arrowGeometry, arrowMaterial);
arrow.position.copy(point);
arrow.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
arrows.push(arrow);
}
});
return arrows;
}, [points, showDirection, forward]);
return (
<group
onPointerEnter={() => { setShowDirection(true); setHoverState(true); }}
onPointerLeave={() => { setShowDirection(false); setHoverState(false); }}
>
{geometries.map((geometry, index) => (
<RigidBody
key={index}
ref={el => (conveyorRefs.current[index] = el)}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={e => handleMaterialEnter(e, index)}
onCollisionExit={e => handleMaterialExit(e, index)}
colliders="trimesh"
>
<mesh geometry={geometry}>
<meshStandardMaterial
color={forward ? "skyblue" : "pink"}
side={THREE.DoubleSide}
transparent
opacity={0.5}
visible={false}
/>
</mesh>
</RigidBody>
))}
{showDirection && directionArrows?.map((arrow, i) => (
<primitive key={`arrow-${i}`} object={arrow} />
))}
{hoverState && (
<group>
{geometries.map((geometry, index) => (
<mesh
key={`highlight-${index}`}
geometry={geometry}
position={[0, 0.002, 0]}
// visible={false}
>
<meshBasicMaterial
color={forward ? "#00ff0044" : "#ff000044"}
transparent
opacity={0.3}
/>
</mesh>
))}
</group>
)}
</group>
);
}
export default NormalConveyorCollider;

View File

@@ -0,0 +1,249 @@
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';
interface YsplitConveyorColliderProps {
points: [number, number, number][][];
boundingBox: THREE.Box3 | null;
asset: Asset;
forward: boolean;
isPaused: boolean;
onDirectionChange?: (newDirection: boolean) => void;
}
function YSplitConveyorCollider({ points, boundingBox, asset, forward, isPaused, onDirectionChange }: YsplitConveyorColliderProps) {
const conveyorRefs = useRef<(any)[]>([]);
const [objectsOnGeometry, setObjectsOnGeometry] = useState<Map<number, Set<any>>>(new Map());
const conveyorDirection = useRef<THREE.Vector3>(new THREE.Vector3());
const [showDirection, setShowDirection] = useState(false);
const conveyorSpeed = 2;
const lastClickTime = useRef(0);
const [hoverState, setHoverState] = useState(false);
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (e.button === 2) {
const now = Date.now();
if (now - lastClickTime.current < 300) {
if (onDirectionChange) {
console.log('forwardySplit: ', forward);
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, forward]);
const handleMaterialEnter = (e: CollisionPayload, index: number) => {
if (e.other.rigidBody) {
setObjectsOnGeometry(prev => {
const newMap = new Map(prev);
if (!newMap.has(index)) newMap.set(index, new Set());
newMap.get(index)!.add(e.other.rigidBody);
return newMap;
});
}
};
const handleMaterialExit = (e: CollisionPayload, index: number) => {
if (e.other.rigidBody) {
setObjectsOnGeometry(prev => {
const newMap = new Map(prev);
if (newMap.has(index)) {
newMap.get(index)!.delete(e.other.rigidBody);
}
return newMap;
});
}
};
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);
const inverseQuat = assetQuat.clone().invert();
points.forEach((segment, index) => {
if (segment.length < 2) return;
const curve = new THREE.CatmullRomCurve3(segment.map(p => new THREE.Vector3(...p)));
const curvePoints = curve.getPoints((segment.length - 1) * 30);
if (!forward) curvePoints.reverse();
const bodies = objectsOnGeometry.get(index);
if (!bodies) return;
bodies.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 < curvePoints.length; i++) {
const dist = curvePoints[i].distanceToSquared(localPos);
if (dist < minDist) {
minDist = dist;
closestIndex = i;
}
}
const point = curvePoints[closestIndex];
const prev = curvePoints[closestIndex - 1] || point;
const next = curvePoints[closestIndex + 1] || point;
const tangent = new THREE.Vector3().subVectors(next, prev).normalize();
const side = new THREE.Vector3().crossVectors(tangent, new THREE.Vector3(0, 1, 0)).normalize();
const relative = new THREE.Vector3().subVectors(localPos, point);
const sideOffset = relative.dot(side);
const centeringForce = side.clone().multiplyScalar(-sideOffset * 10);
const forwardForce = tangent.clone().multiplyScalar(conveyorSpeed);
const totalForce = forwardForce.add(centeringForce).applyQuaternion(assetQuat);
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
rigidBody.setLinvel(totalForce, true);
});
});
})
const geometries = useMemo(() => {
const width = 1;
const segments = 1;
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((geo): geo is THREE.BufferGeometry => geo !== null);
}, [points, asset.position, asset.rotation, forward]);
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);
for (let i = 0; i < curvePoints.length; i++) {
const point = curvePoints[i];
let direction: THREE.Vector3;
if (i < curvePoints.length - 1) {
direction = new THREE.Vector3().subVectors(curvePoints[i + 1], point).normalize();
} else {
direction = new THREE.Vector3().subVectors(point, curvePoints[i - 1]).normalize();
}
if (!forward) {
direction.multiplyScalar(-1);
}
const arrow = new THREE.Mesh(arrowGeometry, arrowMaterial);
arrow.position.copy(point);
arrow.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
arrows.push(arrow);
}
});
return arrows;
}, [points, showDirection, forward]);
return (
<group
onPointerEnter={() => { setShowDirection(true); setHoverState(true); }}
onPointerLeave={() => { setShowDirection(false); setHoverState(false); }}
>
{geometries.map((geometry, index) => (
<RigidBody
key={index}
ref={el => (conveyorRefs.current[index] = el)}
type="fixed"
position={[0, 0.001, 0]}
userData={{ isConveyor: true }}
onCollisionEnter={e => handleMaterialEnter(e, index)}
onCollisionExit={e => handleMaterialExit(e, index)}
colliders="trimesh"
>
<mesh geometry={geometry}>
<meshStandardMaterial
color={forward ? "skyblue" : "pink"}
side={THREE.DoubleSide}
transparent
opacity={0.5}
visible={false}
/>
</mesh>
</RigidBody>
))}
{showDirection && directionArrows?.map((arrow, i) => (
<primitive key={`arrow-${i}`} object={arrow} />
))}
{hoverState && (
<group>
{geometries.map((geometry, index) => (
<mesh
key={`highlight-${index}`}
geometry={geometry}
position={[0, 0.002, 0]}
// visible={false}
>
<meshBasicMaterial
color={forward ? "green" : "red"}
transparent
opacity={0.3}
/>
</mesh>
))}
</group>
)}
</group>
);
}
export default YSplitConveyorCollider;

View File

@@ -0,0 +1,287 @@
import { RigidBody, RapierRigidBody } from '@react-three/rapier';
import React, { useEffect, useRef, useState } from 'react';
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 { generateUniqueId } from '../../../functions/generateUniqueId';
type MaterialSpawnerProps = {
position: [number, number, number];
spawnInterval: number;
spawnCount: number;
};
function MaterialSpawner({ position: initialPos, spawnInterval, spawnCount }: MaterialSpawnerProps) {
const { loadingProgress } = useLoadingProgress();
const [spawned, setSpawned] = useState<{
id: string;
position: [number, number, number];
ref: React.RefObject<RapierRigidBody>;
materialType: string;
}[]>([]);
const [spawningPaused, setSpawningPaused] = useState(true);
const spawnedCount = useRef(0);
const { gl, camera, pointer, controls } = useThree();
const [draggedId, setDraggedId] = useState<string | null>(null);
const dragOffset = useRef<THREE.Vector3>(new THREE.Vector3());
const initialDepth = useRef<number>(0);
const materialTypes = ['Default material', 'Material 1', 'Material 2', 'Material 3'];
const spawnerRef = useRef<THREE.Mesh>(null!);
const [isDraggingSpawner, setIsDraggingSpawner] = useState(false);
const spawnerDragOffset = useRef<THREE.Vector3>(new THREE.Vector3());
const spawnerInitialDepth = useRef<number>(0);
const [boxPosition, setBoxPosition] = useState<[number, number, number]>(initialPos);
useEffect(() => {
if (loadingProgress !== 0) return;
let interval: NodeJS.Timeout | null = null;
const startSpawning = () => {
if (!interval) {
interval = setInterval(() => {
if (spawningPaused) return;
setSpawned(prev => {
if (spawnCount !== undefined && spawnedCount.current >= spawnCount) {
clearInterval(interval!);
interval = null;
return prev;
}
spawnedCount.current++;
const randomMaterialType = materialTypes[Math.floor(Math.random() * materialTypes.length)];
return [
...prev,
{
id: generateUniqueId(),
position: [...boxPosition] as [number, number, number], // use latest position state
ref: React.createRef<RapierRigidBody>(),
materialType: randomMaterialType,
}
];
});
}, spawnInterval);
}
};
const stopSpawning = () => {
if (interval) {
clearInterval(interval);
interval = null;
}
};
const handleVisibility = () => {
if (document.visibilityState === 'visible') {
startSpawning();
} else {
stopSpawning();
}
};
document.addEventListener('visibilitychange', handleVisibility);
handleVisibility();
return () => {
stopSpawning();
document.removeEventListener('visibilitychange', handleVisibility);
};
}, [loadingProgress, spawnInterval, spawnCount, spawningPaused, boxPosition]);
const handleSleep = (id: string) => {
setSpawned(prev => prev.filter(obj => obj.id !== id));
};
const handlePointerDown = (id: string) => {
if (controls) (controls as CameraControls).enabled = false;
setDraggedId(id);
const obj = spawned.find(o => o.id === id);
if (!obj || !obj.ref.current) return;
const currentPosition = obj.ref.current.translation();
const screenPosition = new THREE.Vector3(currentPosition.x, currentPosition.y, currentPosition.z).project(camera);
dragOffset.current = new THREE.Vector3(
screenPosition.x - pointer.x,
0,
screenPosition.y - pointer.y
);
initialDepth.current = new THREE.Vector3(currentPosition.x, currentPosition.y, currentPosition.z)
.sub(camera.position)
.length();
obj.ref.current.setGravityScale(0, true);
obj.ref.current.setLinearDamping(10);
obj.ref.current.setAngularDamping(10);
};
const handlePointerMove = () => {
if (!draggedId) return;
const obj = spawned.find(o => o.id === draggedId);
if (!obj || !obj.ref.current) return;
const targetScreenPos = new THREE.Vector3(
pointer.x + dragOffset.current.x,
0,
pointer.y + dragOffset.current.z
);
const targetWorldPos = new THREE.Vector3(
targetScreenPos.x,
targetScreenPos.z,
0.5
).unproject(camera);
const direction = targetWorldPos.sub(camera.position).normalize();
const finalPosition = camera.position.clone().add(direction.multiplyScalar(initialDepth.current));
const currentPosition = obj.ref.current.translation();
const moveDirection = new THREE.Vector3().subVectors(finalPosition, currentPosition);
obj.ref.current.setLinvel({
x: moveDirection.x * 20,
y: moveDirection.y * 20,
z: moveDirection.z * 20
}, true);
};
const handlePointerUp = () => {
if (controls) (controls as CameraControls).enabled = true;
if (!draggedId) return;
const obj = spawned.find(o => o.id === draggedId);
if (obj?.ref.current) {
obj.ref.current.setGravityScale(1, true);
obj.ref.current.setLinearDamping(0.5);
obj.ref.current.setAngularDamping(0.5);
}
setDraggedId(null);
};
useEffect(() => {
const canvasElement = gl.domElement;
canvasElement.addEventListener('pointermove', handlePointerMove);
canvasElement.addEventListener('pointerup', handlePointerUp);
return () => {
canvasElement.removeEventListener('pointermove', handlePointerMove);
canvasElement.removeEventListener('pointerup', handlePointerUp);
};
}, [draggedId, spawned, controls, camera, gl]);
const handleSpawnerPointerDown = (e: any) => {
if (e.button !== 2) return; // right click only
e.stopPropagation();
if (controls) (controls as CameraControls).enabled = false;
setIsDraggingSpawner(true);
const worldPos = spawnerRef.current.getWorldPosition(new THREE.Vector3());
const screenPos = worldPos.clone().project(camera);
spawnerDragOffset.current.set(
screenPos.x - pointer.x,
0,
screenPos.y - pointer.y
);
spawnerInitialDepth.current = worldPos.clone().sub(camera.position).length();
};
const handleSpawnerPointerMove = () => {
if (!isDraggingSpawner) return;
const targetScreenPos = new THREE.Vector3(
pointer.x + spawnerDragOffset.current.x,
0,
pointer.y + spawnerDragOffset.current.z
);
const targetWorldPos = new THREE.Vector3(
targetScreenPos.x,
targetScreenPos.z,
0.5
).unproject(camera);
const direction = targetWorldPos.sub(camera.position).normalize();
const finalPosition = camera.position.clone().add(direction.multiplyScalar(spawnerInitialDepth.current));
spawnerRef.current.position.copy(finalPosition);
};
const handleSpawnerPointerUp = () => {
if (controls) (controls as CameraControls).enabled = true;
setIsDraggingSpawner(false);
if (spawnerRef.current) {
const worldPos = spawnerRef.current.getWorldPosition(new THREE.Vector3());
setBoxPosition([worldPos.x, worldPos.y, worldPos.z] as [number, number, number]);
}
};
useEffect(() => {
const canvas = gl.domElement;
canvas.addEventListener("pointermove", handleSpawnerPointerMove);
canvas.addEventListener("pointerup", handleSpawnerPointerUp);
return () => {
canvas.removeEventListener("pointermove", handleSpawnerPointerMove);
canvas.removeEventListener("pointerup", handleSpawnerPointerUp);
};
}, [isDraggingSpawner, camera, gl, controls]);
const handleBoxClick = () => {
setSpawningPaused(prev => !prev);
};
return (
<>
<mesh
name='Spawner Box'
ref={spawnerRef}
position={boxPosition}
onClick={handleBoxClick}
onPointerDown={handleSpawnerPointerDown}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={spawningPaused ? "red" : "white"} transparent opacity={0.2} />
</mesh>
{spawned.map(({ id, position, materialType, ref }) => (
<RigidBody
key={id}
ref={ref}
position={position}
colliders="hull"
angularDamping={0.5}
linearDamping={0.5}
restitution={0.1}
userData={{ materialType, materialUuid: id }}
onSleep={() => handleSleep(id)}
>
<MaterialModel
materialId={id}
materialType={materialType}
onPointerDown={(e) => {
e.stopPropagation();
handlePointerDown(id);
}}
/>
</RigidBody>
))}
</>
);
}
export default MaterialSpawner;

View File

@@ -0,0 +1,33 @@
import MaterialSpawner from './materialSpawner'
import ColliderCreator from './colliders/colliderCreator'
import { SplineCreator } from './conveyor/splineCreator'
function PhysicsSimulator() {
return (
<>
<MaterialSpawner
position={[7.3, 2, 12.6]}
spawnInterval={1000}
spawnCount={50}
/>
{/* <MaterialSpawner
position={[3.8, 3, 3]}
spawnInterval={1000}
spawnCount={50}
/> */}
{/* <MaterialSpawner
position={[6, 3, -6]}
spawnInterval={1000}
spawnCount={5}
/> */}
<ColliderCreator />
{/* <SplineCreator /> */}
</>
)
}
export default PhysicsSimulator

View File

@@ -0,0 +1,507 @@
import React, { useState, useRef, useEffect } from "react";
import { useSceneContext } from "../../sceneContext";
import { get } from "http";
// Define TypeScript interfaces
interface Arrow {
id: string;
content: string;
}
interface ArrowItemProps {
arrow: Arrow;
index: number;
moveArrow: (fromIndex: number, toIndex: number) => void;
removeArrow: (index: number) => void;
}
interface DragState {
isDragging: boolean;
draggedIndex: number | null;
hoverIndex: number | null;
}
// ArrowItem component with manual drag-and-drop
const ArrowItem: React.FC<ArrowItemProps> = ({ arrow, index, moveArrow, removeArrow }) => {
const [isDragging, setIsDragging] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
const handleDragStart = (e: React.DragEvent) => {
e.dataTransfer.setData("text/plain", index.toString());
setIsDragging(true);
// Add a small delay to allow the drag operation to start
setTimeout(() => {
if (elementRef.current) {
elementRef.current.style.opacity = "0.5";
}
}, 0);
};
const handleDragEnd = () => {
setIsDragging(false);
if (elementRef.current) {
elementRef.current.style.opacity = "1";
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
if (elementRef.current) {
elementRef.current.style.backgroundColor = "#3f3f46";
}
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
if (elementRef.current) {
elementRef.current.style.backgroundColor = "#2d2d36";
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (elementRef.current) {
elementRef.current.style.backgroundColor = "#2d2d36";
}
const fromIndex = parseInt(e.dataTransfer.getData("text/plain"));
if (fromIndex !== index) {
moveArrow(fromIndex, index);
}
};
return (
<div
ref={elementRef}
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
padding: "4px 8px",
borderRadius: "6px",
fontSize: "13px",
backgroundColor: "#2d2d36",
color: index === 0 ? "#34d399" : "white",
opacity: isDragging ? 0.5 : 1,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
cursor: "move",
marginBottom: "4px",
transition: "background-color 0.2s, opacity 0.2s",
}}
>
{arrow.content}
<button
onClick={() => removeArrow(index)}
style={{
background: "transparent",
border: "none",
color: "#f87171",
cursor: "pointer",
fontSize: "12px",
}}
>
</button>
</div>
);
};
// Main ColliderProperties component
const ColliderProperties: React.FC = () => {
const { colliderStore } = useSceneContext();
const { colliders, setSelectedArrow, selectedCollider, selectedArrowId,
removeArrow, addArrow, getColliderArrows,
getColliderCondition, updateColliderCondition } = colliderStore();
const condition = selectedCollider ? getColliderCondition(selectedCollider.id) : null;
const addDirection = () => {
if (selectedCollider) {
addArrow(selectedCollider.id, { arrowId: Date.now().toString(), arrowName: `Arrow ${getColliderArrows(selectedCollider?.id || '').length + 1}`, position: [selectedCollider?.position[0] + 0.5, selectedCollider?.position[1], selectedCollider?.position[2] + 0.5] });
}
};
const removeDirection = (id: string) => {
removeArrow(selectedCollider?.id || "", id);
};
const updateConditions = (id: string, updates: Partial<Collider['colliderCondition']>) => {
console.log('updates: ', updates);
updateColliderCondition(id, updates);
};
useEffect(() => {
let collider = getColliderCondition(selectedCollider?.id || "")
}, [colliders, selectedCollider]);
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
maxHeight: "70vh",
overflowY: "auto",
backgroundColor: "#1a202c",
padding: "20px",
}}
>
<div
style={{
backgroundColor: "#2d2d36",
borderRadius: "12px",
boxShadow: "0 4px 10px rgba(0,0,0,0.5)",
width: "380px",
padding: "20px",
color: "#e5e7eb",
fontFamily: "sans-serif",
display: "flex",
flexDirection: "column",
gap: "24px",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
borderBottom: "1px solid #3f3f46",
paddingBottom: "12px",
}}
>
<h2 style={{ fontSize: "16px", fontWeight: 600 }}>
Collider Properties
</h2>
<button
style={{
backgroundColor: "#7c3aed",
color: "white",
padding: "4px 12px",
borderRadius: "6px",
fontSize: "13px",
border: "none",
cursor: "pointer",
}}
>
Reset
</button>
</div>
{/* Directions */}
<div
style={{
backgroundColor: "#374151",
borderRadius: "8px",
padding: "16px",
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span style={{ fontWeight: 600 }}>Directions</span>
<button
onClick={addDirection}
style={{
color: "#a78bfa",
fontSize: "13px",
background: "transparent",
border: "none",
cursor: "pointer",
}}
>
+add
</button>
</div>
{getColliderArrows(selectedCollider?.id || '').length === 0 && (
<p
style={{
color: "#6b7280",
fontSize: "13px",
fontStyle: "italic",
}}
>
No directions yet
</p>
)}
{getColliderArrows(selectedCollider?.id || '').map((arrow, i) => (
<div
key={i}
style={{
backgroundColor: "#2d2d36",
borderRadius: "6px",
padding: "4px 12px",
fontSize: "13px",
color: selectedArrowId === arrow.arrowId ? "#34d399" : "white",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
onClick={() => { setSelectedArrow(arrow.arrowId) }}
>
{arrow.arrowName}
<button
onClick={() => removeDirection(arrow.arrowId)}
style={{
background: "transparent",
border: "none",
color: "#f87171",
cursor: "pointer",
fontSize: "12px",
}}
>
</button>
</div>
))}
</div>
<div
style={{
backgroundColor: "#374151",
borderRadius: "8px",
padding: "16px",
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span style={{ fontWeight: 600 }}>Conditions</span>
</div>
{condition?.conditionType && (
<div
style={{
backgroundColor: "#2d2d36",
borderRadius: "8px",
padding: "12px",
display: "flex",
flexDirection: "column",
gap: "12px",
boxShadow: "inset 0 1px 3px rgba(0,0,0,0.5)",
}}
>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
fontSize: "13px",
gap: "8px",
alignItems: "center",
}}
>
<div style={{ color: "#9ca3af" }}>Type</div>
<select
value={condition.conditionType}
onChange={(e) => {
updateColliderCondition(selectedCollider!.id, {
conditionType: e.target.value as "material" | "count",
...(e.target.value === "material"
? { arrowCondition: [] }
: { count: 0, arrowsOrder: [] }),
});
}}
style={{
backgroundColor: "#374151",
color: "white",
border: "none",
padding: "4px",
borderRadius: "6px",
}}
>
<option value="material">Material</option>
<option value="count">Count</option>
</select>
</div>
{condition.conditionType === "count" && (
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
fontSize: "13px",
gap: "8px",
alignItems: "center",
}}
>
<div style={{ color: "#9ca3af" }}>Count</div>
<input
type="number"
value={condition.count || 0}
onChange={(e) =>
updateConditions(selectedCollider!.id, {
count: parseInt(e.target.value) || 0,
})
}
style={{
backgroundColor: "#374151",
color: "white",
border: "none",
padding: "4px",
borderRadius: "6px",
width: "100%",
}}
/>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
<span style={{ fontWeight: 600, fontSize: "13px" }}>Arrow Order</span>
{(() => {
const arrows = getColliderArrows(selectedCollider?.id || "");
const orderedArrows =
condition.arrowsOrder?.length > 0
? condition.arrowsOrder
.map((id: string) => arrows.find((a) => a.arrowId === id))
.filter(Boolean)
: arrows;
const moveArrow = (fromIndex: number, toIndex: number) => {
const updated = [...orderedArrows];
const [moved] = updated.splice(fromIndex, 1);
updated.splice(toIndex, 0, moved);
updateColliderCondition(selectedCollider!.id, {
arrowsOrder: updated.map((a) => a!.arrowId),
});
};
const removeArrowOrder = (index: number) => {
const updated = [...orderedArrows];
updated.splice(index, 1);
updateColliderCondition(selectedCollider!.id, {
arrowsOrder: updated.map((a) => a!.arrowId),
});
};
return (
<div>
{orderedArrows.length === 0 && (
<p style={{ color: "#6b7280", fontSize: "13px", fontStyle: "italic" }}>
No arrows to order
</p>
)}
{orderedArrows.map((arrow, i) => (
<ArrowItem
key={arrow!.arrowId}
arrow={{ id: arrow!.arrowId, content: arrow!.arrowName }}
index={i}
moveArrow={moveArrow}
removeArrow={removeArrowOrder}
/>
))}
</div>
);
})()}
</div>
</div>
)}
{condition.conditionType === "material" && (
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{getColliderArrows(selectedCollider?.id || "").map((arrow) => {
let existing =
condition.arrowCondition?.find(
(ac: any) => ac.arrowId === arrow.arrowId
);
if (!existing) {
existing = { arrowId: arrow.arrowId, materialType: "Any" };
updateColliderCondition(selectedCollider!.id, {
arrowCondition: [
...(condition.arrowCondition || []),
existing,
],
});
}
return (
<div
key={arrow.arrowId}
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
fontSize: "13px",
gap: "8px",
alignItems: "center",
}}
>
<div style={{ color: "#9ca3af" }}>{arrow.arrowName}</div>
<select
value={existing.materialType}
onChange={(e) => {
const newConditions = [
...(condition.arrowCondition || []),
];
const idx = newConditions.findIndex(
(ac) => ac.arrowId === arrow.arrowId
);
if (idx > -1) {
newConditions[idx] = {
...newConditions[idx],
materialType: e.target.value,
};
} else {
newConditions.push({
arrowId: arrow.arrowId,
materialType: e.target.value,
});
}
updateColliderCondition(selectedCollider!.id, {
arrowCondition: newConditions,
});
}}
style={{
backgroundColor: "#374151",
color: "white",
border: "none",
padding: "4px",
borderRadius: "6px",
}}
>
<option value="Material 1">Material 1</option>
<option value="Material 2">Material 2</option>
<option value="Material 3">Material 3</option>
<option value="Default material">Default Material</option>
<option value="Any">Any</option>
</select>
</div>
);
})}
</div>
)}
</div>
)}
</div>
</div>
</div >
);
};
export default ColliderProperties;

View File

@@ -1,7 +1,8 @@
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { Canvas } from "@react-three/fiber";
import { Physics } from "@react-three/rapier";
import { Color, SRGBColorSpace } from "three";
import { KeyboardControls } from "@react-three/drei";
import { useSceneContext } from "./sceneContext";
import Builder from "../builder/builder";
import Visualization from "../visualization/visualization";
@@ -12,11 +13,12 @@ import useModuleStore from "../../store/useModuleStore";
import { useParams } from "react-router-dom";
import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { getUserData } from "../../functions/getUserData";
import { useSceneContext } from "./sceneContext";
import { useLoadingProgress, useSocketStore } from "../../store/builder/store";
import { Color, SRGBColorSpace } from "three";
import { compressImage } from "../../utils/compressImage";
export default function Scene({ layout }: { readonly layout: "Main Layout" | "Comparison Layout"; }) {
const [visible, setVisible] = useState(false);
const map = useMemo(() => [
{ name: "forward", keys: ["ArrowUp", "w", "W"] },
{ name: "backward", keys: ["ArrowDown", "s", "S"] },
@@ -56,6 +58,18 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
// eslint-disable-next-line
}, [activeModule, assets, loadingProgress]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "F1") {
event.preventDefault();
setVisible(prev => !prev);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<KeyboardControls map={map}>
<Canvas
@@ -70,8 +84,10 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
>
<Setup />
<Collaboration />
<Builder />
<Simulation />
<Physics gravity={[0, -9.81, 0]} allowedLinearError={50} numSolverIterations={50} debug={visible} >
<Builder />
<Simulation />
</Physics>
<Visualization />
</Canvas>
</KeyboardControls>

View File

@@ -22,6 +22,8 @@ import { createStorageUnitStore, StorageUnitStoreType } from '../../store/simula
import { createHumanStore, HumanStoreType } from '../../store/simulation/useHumanStore';
import { createCraneStore, CraneStoreType } from '../../store/simulation/useCraneStore';
import { createColliderStore, ColliderStoreType } from '../../store/useColliderStore';
type SceneContextValue = {
assetStore: AssetStoreType,
@@ -46,6 +48,8 @@ type SceneContextValue = {
humanStore: HumanStoreType;
craneStore: CraneStoreType;
colliderStore: ColliderStoreType;
humanEventManagerRef: React.RefObject<HumanEventManagerState>;
craneEventManagerRef: React.RefObject<CraneEventManagerState>;
@@ -86,6 +90,8 @@ export function SceneProvider({
const humanStore = useMemo(() => createHumanStore(), []);
const craneStore = useMemo(() => createCraneStore(), []);
const colliderStore = useMemo(() => createColliderStore(), []);
const humanEventManagerRef = useRef<HumanEventManagerState>({ humanStates: [] });
const craneEventManagerRef = useRef<CraneEventManagerState>({ craneStates: [] });
@@ -110,7 +116,8 @@ export function SceneProvider({
craneStore.getState().clearCranes();
humanEventManagerRef.current.humanStates = [];
craneEventManagerRef.current.craneStates = [];
}, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, undoRedo3DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore]);
colliderStore.getState().clearColliders();
}, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, undoRedo3DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, colliderStore]);
const contextValue = useMemo(() => (
{
@@ -134,10 +141,11 @@ export function SceneProvider({
craneStore,
humanEventManagerRef,
craneEventManagerRef,
colliderStore,
clearStores,
layout
}
), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, undoRedo3DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, clearStores, layout]);
), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, undoRedo3DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, colliderStore, clearStores, layout]);
return (
<SceneContext.Provider value={contextValue}>

View File

@@ -22,6 +22,7 @@ function Setup() {
<Environment files={background} environmentIntensity={1.5} />
{/* <SecondaryCamera /> */}
<StatsHelper />
<AdaptiveEvents />

View File

@@ -0,0 +1,169 @@
import { useEffect, useRef } from 'react';
import { useThree, useFrame } from '@react-three/fiber';
import { CameraControls, Html, PerspectiveCamera, PivotControls } from '@react-three/drei';
import * as THREE from 'three';
import {
useSecondaryCameraData,
useSecondaryCameraEdit,
useSecondaryCameraState,
} from '../../store/builder/store';
type CameraData = {
id: any;
name: string;
position: [number, number, number];
target: [number, number, number];
};
function SecondaryCamera() {
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const helperRef = useRef<THREE.CameraHelper | null>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const dummyMeshRef = useRef<THREE.Mesh | null>(null);
const { scene, size, controls } = useThree();
const { secondaryCameraData, setSecondaryCameraData, updateSecondaryCameraData } = useSecondaryCameraData();
const { selectedSecondaryCamera, setSelectedSecondaryCamera } = useSecondaryCameraState();
const { secondaryCameraEdit } = useSecondaryCameraEdit();
// Sync main controls to selected camera
useEffect(() => {
if (selectedSecondaryCamera) {
(controls as CameraControls).setLookAt(
...selectedSecondaryCamera.position,
...selectedSecondaryCamera.target
);
}
}, [selectedSecondaryCamera?.id]);
// Setup WebGLRenderer
useEffect(() => {
const secondaryCanvas = document.getElementById('secondary-canvas');
if (!secondaryCanvas) return;
const customRenderer = new THREE.WebGLRenderer({
canvas: secondaryCanvas,
antialias: true,
alpha: true,
});
customRenderer.setSize(
secondaryCanvas.getBoundingClientRect().width,
secondaryCanvas.getBoundingClientRect().height
);
customRenderer.setPixelRatio(window.devicePixelRatio);
rendererRef.current = customRenderer;
return () => {
customRenderer.dispose();
};
}, [size]);
// Camera helper
useEffect(() => {
if (selectedSecondaryCamera && cameraRef.current) {
const helper = new THREE.CameraHelper(cameraRef.current);
helperRef.current = helper;
scene.add(helper);
return () => {
scene.remove(helper);
helper.geometry.dispose();
(helper.material as any).dispose?.();
};
}
}, [scene, selectedSecondaryCamera?.id]);
// Render to secondary canvas
useFrame(() => {
if (rendererRef.current && cameraRef.current && selectedSecondaryCamera) {
rendererRef.current.render(scene, cameraRef.current);
}
});
// Live update camera if editing enabled
useFrame(() => {
if (secondaryCameraEdit && selectedSecondaryCamera && cameraRef.current) {
const target = (controls as CameraControls).getTarget(new THREE.Vector3());
const position = (controls as CameraControls).getPosition(new THREE.Vector3());
updateSecondaryCameraData(selectedSecondaryCamera.id, {
position: position.toArray(),
target: target.toArray(),
});
setSelectedSecondaryCamera({
id: selectedSecondaryCamera.id,
position: position.toArray(),
target: target.toArray(),
});
}
});
function handleCamera() {
const target = (controls as CameraControls).getTarget(new THREE.Vector3());
const position = (controls as CameraControls).getPosition(new THREE.Vector3());
const newCameraData: CameraData = {
id: secondaryCameraData.length + 1,
name: `Camera ${secondaryCameraData.length + 1}`,
position: position.toArray(),
target: target.toArray(),
};
setSecondaryCameraData([...secondaryCameraData, newCameraData]);
}
return (
<>
<Html
className='secondary-camera-controls'
style={{
zIndex: 10,
position: 'absolute',
bottom: 350,
right: 0,
border: '1px solid black',
backgroundColor: 'black',
}}
>
<button onClick={handleCamera}>+Add Camera</button>
</Html>
{selectedSecondaryCamera && (
<PerspectiveCamera
ref={cameraRef}
position={selectedSecondaryCamera.position}
onUpdate={(self) => self.lookAt(...selectedSecondaryCamera.target)}
>
<PivotControls
anchor={[0, 0, 0]}
onDrag={() => {
if (dummyMeshRef.current) {
const worldPos = new THREE.Vector3();
dummyMeshRef.current.getWorldPosition(worldPos);
const updatedPos: [number, number, number] = worldPos.toArray() as [number, number, number];
updateSecondaryCameraData(selectedSecondaryCamera.id, {
position: updatedPos,
});
setSelectedSecondaryCamera({
...selectedSecondaryCamera,
position: updatedPos,
});
}
}}
>
<mesh ref={dummyMeshRef}>
<sphereGeometry args={[0.1, 32, 32]} />
<meshBasicMaterial color="yellow" />
</mesh>
</PivotControls>
</PerspectiveCamera>
)}
</>
);
}
export default SecondaryCamera;

View File

@@ -0,0 +1,46 @@
import React, { useEffect } from 'react'
import { useSecondaryCameraData, useSecondaryCameraEdit, useSecondaryCameraState } from '../../store/builder/store'
type CameraData = {
id: any
name: string
position: [number, number, number]
target: [number, number, number]
}
function SecondaryCanvas() {
const { secondaryCameraData } = useSecondaryCameraData()
const { selectedSecondaryCamera, setSelectedSecondaryCamera } = useSecondaryCameraState();
const { setSecondaryCameraEdit, secondaryCameraEdit } = useSecondaryCameraEdit()
function handleSelectCamera(camera: any) {
const selectedCamera = camera
if (!selectedCamera || (selectedSecondaryCamera && selectedCamera.id === selectedSecondaryCamera.id)) {
setSelectedSecondaryCamera(null);
} else {
setSelectedSecondaryCamera({
id: selectedCamera.id,
position: selectedCamera.position,
target: selectedCamera.target
});
}
}
return (
<>
{secondaryCameraData.map((camera: CameraData) => (
<div key={camera.id} className='secondary-camera-item' onClick={() => handleSelectCamera(camera)} style={{ zIndex: 10, position: 'absolute', bottom: 300 - (camera.id * 30), right: 0, border: '1px solid black', backgroundColor: 'black' }}>
<p>{camera.name}</p>
</div>
))}
<div onClick={() => { setSecondaryCameraEdit(!secondaryCameraEdit) }} style={{ zIndex: 10, position: 'absolute', bottom: 500, right: 0, border: '1px solid black', backgroundColor: secondaryCameraEdit ? 'black' : "white", color: !secondaryCameraEdit ? "black" : "white", padding: '10px' }}>
Edit
</div>
<div className='secondary-canvas-container' style={{ zIndex: 10, position: 'absolute', bottom: 0, right: 0, width: '300px', height: '200px' }}>
<canvas id='secondary-canvas' className='secondary-canvas' style={{ height: "100%", width: "100%", background: "gray" }}></canvas>
</div>
</>
)
}
export default SecondaryCanvas

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { RapierRigidBody } from '@react-three/rapier';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
@@ -32,6 +33,7 @@ function WorkerAnimator({ path, handleCallBack, human, reset, startUnloadingProc
const [objectRotation, setObjectRotation] = useState<[number, number, number] | null>((action as HumanAction)?.pickUpPoint?.rotation || [0, 0, 0]);
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const rigidBodyRef = useRef<RapierRigidBody>(null);
const { scene } = useThree();
useEffect(() => {

View File

@@ -19,7 +19,7 @@ type ModelType = keyof typeof modelPaths;
interface ModelProps extends React.ComponentProps<'group'> {
materialId: string;
materialType: ModelType;
matRef: React.Ref<THREE.Group<THREE.Object3DEventMap>>
matRef?: React.Ref<THREE.Group<THREE.Object3DEventMap>>
}
export function MaterialModel({ materialId, materialType, matRef, ...props }: Readonly<ModelProps>) {

View File

@@ -13,6 +13,7 @@ import Products from './products/products';
import Trigger from './triggers/trigger';
import useModuleStore from '../../store/useModuleStore';
import SimulationAnalysis from './analysis/simulationAnalysis';
import PhysicsSimulator from '../scene/physics/physicsSimulator';
import { useSceneContext } from '../scene/sceneContext';
function Simulation() {
@@ -62,6 +63,8 @@ function Simulation() {
<SimulationAnalysis />
<PhysicsSimulator />
</>
}

View File

@@ -6,7 +6,7 @@ import { Line } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useActiveTool, useSelectedPath } from '../../../../../store/builder/store';
import { RapierRigidBody } from '@react-three/rapier';
interface VehicleAnimatorProps {
path: [number, number, number][];
@@ -31,10 +31,16 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const [objectRotation, setObjectRotation] = useState<{ x: number; y: number; z: number } | undefined>(agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 })
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const rigidBodyRef = useRef<RapierRigidBody>(null);
const { scene, controls } = useThree();
const { selectedPath } = useSelectedPath();
const [isAnyDragging, setIsAnyDragging] = useState<string>("");
const object = scene.getObjectByProperty('uuid', agvUuid);
if (object?.userData.rigidBodyRef) {
(rigidBodyRef as React.MutableRefObject<RapierRigidBody | null>).current = object.userData.rigidBodyRef;
}
useEffect(() => {
if (currentPhase === 'stationed-pickup' && path.length > 0 && selectedPath === "auto") {
@@ -62,11 +68,17 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
progressRef.current = 0;
setReset(false);
setRestingRotation(true);
const object = scene.getObjectByProperty('uuid', agvUuid);
const vehicle = getVehicleById(agvDetail.modelUuid);
if (object && vehicle) {
object.position.set(vehicle.position[0], vehicle.position[1], vehicle.position[2]);
object.rotation.set(vehicle.rotation[0], vehicle.rotation[1], vehicle.rotation[2]);
if (rigidBodyRef.current && vehicle) {
rigidBodyRef.current.setTranslation(
{ x: vehicle.position[0], y: vehicle.position[1], z: vehicle.position[2] },
true
);
rigidBodyRef.current.setRotation(
{ x: vehicle.rotation[0], y: vehicle.rotation[1], z: vehicle.rotation[2], w: 1 },
true
);
}
}
}, [isReset, isPlaying])
@@ -79,8 +91,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const delta = (now - lastTimeRef.current) / 1000;
lastTimeRef.current = now;
const object = scene.getObjectByProperty('uuid', agvUuid);
if (!object || currentPath.length < 2) return;
if (!rigidBodyRef.current || currentPath.length < 2) return;
if (isPaused) return;
let totalDistance = 0;
@@ -107,21 +118,42 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
const end = new THREE.Vector3(...currentPath[index + 1]);
const segmentDistance = distances[index];
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0)));
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(
new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0))
);
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
targetQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
const currentRotation = rigidBodyRef.current.rotation();
const currentQuaternion = new THREE.Quaternion(
currentRotation.x,
currentRotation.y,
currentRotation.z,
currentRotation.w
);
const angle = currentQuaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
rigidBodyRef.current.setRotation(
{ x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
true
);
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
const angle = object.quaternion.angleTo(targetQuaternion);
const angle = currentQuaternion.angleTo(targetQuaternion);
if (angle < step) {
object.quaternion.copy(targetQuaternion);
rigidBodyRef.current.setRotation(
{ x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
true
);
} else {
object.quaternion.rotateTowards(targetQuaternion, step);
const newQuaternion = currentQuaternion.clone().rotateTowards(targetQuaternion, step);
rigidBodyRef.current.setRotation(
{ x: newQuaternion.x, y: newQuaternion.y, z: newQuaternion.z, w: newQuaternion.w },
true
);
}
}
@@ -131,21 +163,35 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
progressRef.current += delta * (speed * agvDetail.speed);
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
const position = start.clone().lerp(end, t);
object.position.copy(position);
rigidBodyRef.current.setTranslation(
{ x: position.x, y: position.y, z: position.z },
true
);
}
}
if (progressRef.current >= totalDistance) {
if (restRotation && objectRotation) {
const targetEuler = new THREE.Euler(0, objectRotation.y - agvDetail.point.action.steeringAngle, 0);
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const targetQuaternion = baseQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
const currentRotation = rigidBodyRef.current.rotation();
const currentQuaternion = new THREE.Quaternion(
currentRotation.x,
currentRotation.y,
currentRotation.z,
currentRotation.w
);
const angle = currentQuaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
rigidBodyRef.current.setRotation(
{ x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
true
);
setRestingRotation(false);
setTimeout(() => {
setRestingRotation(true);
@@ -159,12 +205,19 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
}, 0)
} else {
const step = rotationSpeed * delta * speed * agvDetail.speed;
const angle = object.quaternion.angleTo(targetQuaternion);
const angle = currentQuaternion.angleTo(targetQuaternion);
if (angle < step) {
object.quaternion.copy(targetQuaternion);
rigidBodyRef.current.setRotation(
{ x: targetQuaternion.x, y: targetQuaternion.y, z: targetQuaternion.z, w: targetQuaternion.w },
true
);
} else {
object.quaternion.rotateTowards(targetQuaternion, step);
const newQuaternion = currentQuaternion.clone().rotateTowards(targetQuaternion, step);
rigidBodyRef.current.setRotation(
{ x: newQuaternion.x, y: newQuaternion.y, z: newQuaternion.z, w: newQuaternion.w },
true
);
}
}
return;

View File

@@ -181,6 +181,7 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
// setSelectedChartId(null);
let response = await getSelect2dZoneData(zoneUuid, organization, projectId, selectedVersion?.versionId || '');
console.log('response: ', response);
//
let res = await getFloatingZoneData(zoneUuid, organization, projectId, selectedVersion?.versionId || '');

View File

@@ -23,6 +23,7 @@ import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/
import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore";
import { VersionProvider } from "../modules/builder/version/versionContext";
import { sharedWithMeProjects } from "../services/dashboard/sharedWithMeProject";
import SecondaryCanvas from "../modules/secondaryCamera/secondaryCanvas";
import { handleCanvasCursors } from "../utils/mouseUtils/handleCanvasCursors";
const Project: React.FC = () => {
@@ -134,6 +135,9 @@ const Project: React.FC = () => {
<ComparisonSceneProvider />
</VersionProvider>
</SceneProvider>
{/* <SecondaryCanvas /> */}
{selectedUser && <FollowPerson />}
{isLogListVisible && (
<RenderOverlay>

View File

@@ -0,0 +1,36 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
export const getAssetConveyorPoints = async (assetId: string) => {
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/v2/asset/${assetId}/conveyor-points`,
{
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
console.error("Failed to fetch assetIks");
}
const result = await response.json();
return result;
} catch (error) {
echo.error("Failed to get assetIks");
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
}
}
};

View File

@@ -660,6 +660,52 @@ export const useSelectedComment = create<any>((set: any) => ({
commentPositionState: null,
setCommentPositionState: (x: any) => set({ commentPositionState: x }),
}));
type CameraData = {
id: any;
name: string;
position: [number, number, number];
target: [number, number, number];
};
type SecondaryCameraStore = {
secondaryCameraData: CameraData[];
setSecondaryCameraData: (data: CameraData[]) => void;
updateSecondaryCameraData: (id: any, updatedData: Partial<CameraData>) => void;
};
export const useSecondaryCameraData = create<SecondaryCameraStore>((set) => ({
secondaryCameraData: [],
setSecondaryCameraData: (data) => set({ secondaryCameraData: data }),
updateSecondaryCameraData: (id, updatedData) =>
set((state) => ({
secondaryCameraData: state.secondaryCameraData.map((camera) =>
camera.id === id ? { ...camera, ...updatedData } : camera
),
})),
}));
type SecondaryCamera = {
id: number;
position: [number, number, number];
target: [number, number, number];
};
type SecondaryCameraState = {
selectedSecondaryCamera: SecondaryCamera | null;
setSelectedSecondaryCamera: (camera: SecondaryCamera | null) => void;
};
export const useSecondaryCameraState = create<SecondaryCameraState>((set) => ({
selectedSecondaryCamera: null,
setSelectedSecondaryCamera: (camera) => set({ selectedSecondaryCamera: camera }),
}));
export const useSecondaryCameraEdit = create<any>((set) => ({
secondaryCameraEdit: false,
setSecondaryCameraEdit: (x: boolean) => set({ secondaryCameraEdit: x }),
}));
export const useSelectedPath = create<any>((set: any) => ({
selectedPath: "auto",
setSelectedPath: (x: any) => set({ selectedPath: x }),

View File

@@ -0,0 +1,163 @@
import { create } from "zustand";
type ColliderStore = {
colliders: Collider[];
selectedCollider: Collider | null;
setColliders: (colliders: Collider[]) => void;
addCollider: (collider: Collider) => void;
updateCollider: (id: string, collider: Partial<Collider>) => void;
removeCollider: (id: string) => void;
setSelectedCollider: (collider: Collider | null) => void;
clearColliders: () => void;
clearSelectedCollider: () => void;
// 🔹 Arrow helpers
addArrow: (
colliderId: string,
arrow: { arrowId: string; arrowName: string; position: [number, number, number] }
) => void;
updateArrow: (
colliderId: string,
arrowId: string,
arrow: Partial<{ arrowId: string; arrowName: string; position: [number, number, number] }>
) => void;
removeArrow: (colliderId: string, arrowId: string) => void;
// 🔹 Condition helpers
updateColliderCondition: (
colliderId: string,
condition: Partial<Collider["colliderCondition"]>
) => void;
addColliderCondition: (colliderId: string, condition: Collider["colliderCondition"]) => void;
removeColliderCondition: (colliderId: string) => void;
// 🔹 Getters & selection
getColliderArrows: (colliderId: string) => Collider["arrows"] | [];
getColliderCondition: (colliderId: string) => Collider["colliderCondition"] | null;
setSelectedArrow: (arrowId: string) => void;
clearSelectedArrow: () => void;
getArrowByArrowId: (
arrowId: string
) => { arrowId: string; arrowName: string; position: [number, number, number] } | null;
selectedArrowId: string | null;
};
export const createColliderStore = () =>
create<ColliderStore>((set, get) => ({
colliders: [],
selectedCollider: null,
setColliders: (colliders) => set({ colliders }),
addCollider: (collider) =>
set((state) => ({ colliders: [...state.colliders, collider] })),
updateCollider: (id, collider) =>
set((state) => ({
colliders: state.colliders.map((c) =>
c.id === id ? { ...c, ...collider } : c
),
selectedCollider:
state.selectedCollider?.id === id
? { ...state.selectedCollider, ...collider }
: state.selectedCollider,
})),
removeCollider: (id) =>
set((state) => ({
colliders: state.colliders.filter((c) => c.id !== id),
selectedCollider: state.selectedCollider?.id === id ? null : state.selectedCollider,
})),
setSelectedCollider: (collider) => set({ selectedCollider: collider }),
clearColliders: () => set({ colliders: [] }),
clearSelectedCollider: () => set({ selectedCollider: null }),
// 🔹 Arrow helpers
addArrow: (colliderId, arrow) =>
set((state) => ({
colliders: state.colliders.map((c) =>
c.id === colliderId ? { ...c, arrows: [...c.arrows, arrow] } : c
),
})),
updateArrow: (colliderId, arrowId, arrow) =>
set((state) => ({
colliders: state.colliders.map((c) =>
c.id === colliderId
? {
...c,
arrows: c.arrows.map((a) =>
a.arrowId === arrowId ? { ...a, ...arrow } : a
),
}
: c
),
})),
removeArrow: (colliderId, arrowId) =>
set((state) => ({
colliders: state.colliders.map((c) =>
c.id === colliderId
? { ...c, arrows: c.arrows.filter((a) => a.arrowId !== arrowId) }
: c
),
})),
// 🔹 Condition helpers
updateColliderCondition: (colliderId, condition) =>
set((state) => ({
colliders: state.colliders.map((c) =>
c.id === colliderId
? {
...c,
colliderCondition: {
...(c.colliderCondition ?? {}),
...condition,
} as Collider["colliderCondition"],
}
: c
),
})),
addColliderCondition: (colliderId, condition) =>
set((state) => ({
colliders: state.colliders.map((c) =>
c.id === colliderId ? { ...c, colliderCondition: condition } : c
),
})),
removeColliderCondition: (colliderId) =>
set((state) => ({
colliders: state.colliders.map((c) =>
c.id === colliderId ? { ...c, colliderCondition: undefined as any } : c
),
})),
// 🔹 Getters & selection
getColliderArrows: (colliderId) => {
const collider = get().colliders.find((c) => c.id === colliderId);
return collider ? collider.arrows : [];
},
getColliderCondition: (colliderId) => {
const collider = get().colliders.find((c) => c.id === colliderId);
return collider ? collider.colliderCondition : null;
},
setSelectedArrow: (arrowId) => set({ selectedArrowId: arrowId }),
clearSelectedArrow: () => set({ selectedArrowId: null }),
getArrowByArrowId: (arrowId) => {
for (const collider of get().colliders) {
const arrow = collider.arrows.find((a) => a.arrowId === arrowId);
if (arrow) return arrow;
}
return null;
},
selectedArrowId: null,
}));
export type ColliderStoreType = ReturnType<typeof createColliderStore>;

View File

@@ -48,6 +48,22 @@ interface Asset {
type Assets = Asset[];
type NormalConveyor = {
type: 'normal';
points: [number, number, number][][];
}
type YJunctionConveyor = {
type: 'y-Split';
points: [number, number, number][][];
}
type CurvedConveyor = {
type: 'curved';
points: [number, number, number][][];
}
type ConveyorPoints = NormalConveyor | YJunctionConveyor | CurvedConveyor;
// Wall-Asset

View File

@@ -444,6 +444,34 @@ type IK = {
minheight?: number;
};
// Collider
type ColliderConditionMaterial = {
conditionType: "material",
arrowCondition: {
arrowId: string;
materialType: string;
}[]
}
type ColliderConditionCount = {
conditionType: "count",
count: number,
arrowsOrder: string[]
}
type Collider = {
id: string;
position: [number, number, number];
rotation: [number, number, number];
arrows: {
arrowId: string;
arrowName: string;
position: [number, number, number];
}[];
colliderCondition: ColliderConditionMaterial | ColliderConditionCount;
};
// Conveyor Spline Points
type NormalConveyor = {
@@ -476,4 +504,4 @@ type PillarJibCrane = {
hookMaxOffset: number;
}
type CraneConstraints = PillarJibCrane;
type CraneConstraints = PillarJibCrane;

View File

@@ -7,10 +7,10 @@ services:
- REACT_APP_SERVER_SOCKET_API_BASE_URL=185.100.212.76:9902
- REACT_APP_SERVER_REST_API_BASE_URL=185.100.212.76:9901
- REACT_APP_SERVER_MARKETPLACE_URL=185.100.212.76:50011
container_name: aalaiDemoTwo
container_name: aalaiDemoThreePhy
stdin_open: true
tty: true
ports:
- "8400:80"
- "8401:80"
volumes:
- ./app:/app