Compare commits
30 Commits
feature/pr
...
dev-physic
| Author | SHA1 | Date | |
|---|---|---|---|
| a88c0e594b | |||
| f3ea48fcfd | |||
| 26a80ba177 | |||
| 6d557e8fde | |||
| 987bc88dfa | |||
| 4d124411d0 | |||
| 2bc0921902 | |||
| e725610674 | |||
| a830f03be9 | |||
| fcc8806ef4 | |||
| d1c78495ea | |||
| 57dffa0961 | |||
| 324b7aa40c | |||
| 7794e51d1f | |||
| 361480578a | |||
| 6de1cacf8d | |||
| 33d960cedd | |||
| 298399b3e5 | |||
| be1a24a136 | |||
| 3394c44ed1 | |||
| 1eac23607f | |||
| 04573b86dd | |||
| e001bfaa0a | |||
| d3697bb690 | |||
| ec4a4247b2 | |||
| d4d66d9d32 | |||
| 8191e955ca | |||
| bbe93a91d5 | |||
| 1e314bc290 | |||
| 660e21edcc |
29
app/package-lock.json
generated
29
app/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
92
app/src/modules/scene/physics/colliders/colliderCreator.tsx
Normal file
92
app/src/modules/scene/physics/colliders/colliderCreator.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
139
app/src/modules/scene/physics/conveyor/conveyorCollider.tsx
Normal file
139
app/src/modules/scene/physics/conveyor/conveyorCollider.tsx
Normal 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;
|
||||
58
app/src/modules/scene/physics/conveyor/ribbonCollider.tsx
Normal file
58
app/src/modules/scene/physics/conveyor/ribbonCollider.tsx
Normal 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;
|
||||
155
app/src/modules/scene/physics/conveyor/splineCreator.tsx
Normal file
155
app/src/modules/scene/physics/conveyor/splineCreator.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
287
app/src/modules/scene/physics/materialSpawner.tsx
Normal file
287
app/src/modules/scene/physics/materialSpawner.tsx
Normal 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;
|
||||
33
app/src/modules/scene/physics/physicsSimulator.tsx
Normal file
33
app/src/modules/scene/physics/physicsSimulator.tsx
Normal 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
|
||||
507
app/src/modules/scene/physics/ui/ColliderProperties.tsx
Normal file
507
app/src/modules/scene/physics/ui/ColliderProperties.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -22,6 +22,7 @@ function Setup() {
|
||||
|
||||
<Environment files={background} environmentIntensity={1.5} />
|
||||
|
||||
{/* <SecondaryCamera /> */}
|
||||
<StatsHelper />
|
||||
|
||||
<AdaptiveEvents />
|
||||
|
||||
169
app/src/modules/secondaryCamera/secondaryCamera.tsx
Normal file
169
app/src/modules/secondaryCamera/secondaryCamera.tsx
Normal 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;
|
||||
46
app/src/modules/secondaryCamera/secondaryCanvas.tsx
Normal file
46
app/src/modules/secondaryCamera/secondaryCanvas.tsx
Normal 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
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || '');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 }),
|
||||
|
||||
163
app/src/store/useColliderStore.ts
Normal file
163
app/src/store/useColliderStore.ts
Normal 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>;
|
||||
16
app/src/types/builderTypes.d.ts
vendored
16
app/src/types/builderTypes.d.ts
vendored
@@ -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
|
||||
|
||||
|
||||
30
app/src/types/simulationTypes.d.ts
vendored
30
app/src/types/simulationTypes.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user