feat: Implement collider functionality with drag-and-drop arrows and conditions
- Added Collider and ColliderCondition types to manage collider properties. - Created a new useColliderStore for managing colliders and their arrows. - Implemented ColliderArrow component for visual representation and interaction of arrows. - Enhanced ColliderProperties UI for managing directions and conditions. - Updated PhysicsSimulator and Scene components to integrate collider features. - Refactored existing code for better organization and clarity.
This commit is contained in:
@@ -31,6 +31,8 @@ import WallProperties from "./properties/WallProperties";
|
||||
import FloorProperties from "./properties/FloorProperties";
|
||||
import SelectedWallProperties from "./properties/SelectedWallProperties";
|
||||
import SelectedFloorProperties from "./properties/SelectedFloorProperties";
|
||||
import ColliderProperties from "../../../modules/scene/physics/ui/ColliderProperties";
|
||||
import { useSceneContext } from "../../../modules/scene/sceneContext";
|
||||
|
||||
type DisplayComponent =
|
||||
| "versionHistory"
|
||||
@@ -46,6 +48,7 @@ type DisplayComponent =
|
||||
| "mechanics"
|
||||
| "analysis"
|
||||
| "visualization"
|
||||
| "colliderProperties"
|
||||
| "none";
|
||||
|
||||
const SideBarRight: React.FC = () => {
|
||||
@@ -59,6 +62,9 @@ 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");
|
||||
|
||||
@@ -102,6 +108,13 @@ const SideBarRight: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isVersionSaved && activeModule === "builder") {
|
||||
if (selectedCollider) {
|
||||
setDisplayComponent("colliderProperties");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (subModule === "properties" && activeModule !== "visualization") {
|
||||
if (selectedFloorItem) {
|
||||
setDisplayComponent("assetProperties");
|
||||
@@ -143,7 +156,7 @@ const SideBarRight: React.FC = () => {
|
||||
}
|
||||
|
||||
setDisplayComponent("none");
|
||||
}, [viewVersionHistory, activeModule, subModule, isVersionSaved, selectedFloorItem, selectedWall, selectedFloor, selectedAisle, toolMode,]);
|
||||
}, [viewVersionHistory, activeModule, subModule, isVersionSaved, selectedFloorItem, selectedWall, selectedFloor, selectedAisle, toolMode, selectedCollider]);
|
||||
|
||||
const renderComponent = () => {
|
||||
switch (displayComponent) {
|
||||
@@ -173,6 +186,8 @@ const SideBarRight: React.FC = () => {
|
||||
return <Analysis />;
|
||||
case "visualization":
|
||||
return <Visualization />;
|
||||
case "colliderProperties":
|
||||
return <ColliderProperties />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
|
||||
useEffect(() => {
|
||||
if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
|
||||
getAssetConveyorPoints(asset.assetId).then((data) => {
|
||||
console.log('asset.assetI: ', asset.assetId);
|
||||
console.log('data: ', data);
|
||||
|
||||
|
||||
if (data && data.conveyorPoints && data.conveyorPoints.type !== '') {
|
||||
setRibbonData(data.conveyorPoints)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
|
||||
}, [asset.modelUuid, asset.eventData, ribbonData, boundingBox])
|
||||
// useEffect(() => {
|
||||
// if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
|
||||
// console.log('asset: ', asset);
|
||||
//
|
||||
// if (asset.assetId === "97e037828ce57fa7bd1cc615") {
|
||||
// setRibbonData({
|
||||
// type: 'normal',
|
||||
@@ -209,7 +209,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
|
||||
setGltfScene(gltf.scene.clone());
|
||||
calculateBoundingBox(gltf.scene);
|
||||
} catch (error) {
|
||||
console.error(`[Backend] Error storing/loading ${asset.modelName}:`, error);
|
||||
|
||||
}
|
||||
};
|
||||
loader.load(modelUrl,
|
||||
@@ -220,7 +220,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to load model:", asset.assetId, err);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ function UndoRedo2DControls() {
|
||||
const { selectedVersion } = selectedVersionStore();
|
||||
|
||||
useEffect(() => {
|
||||
console.log(undoStack, redoStack);
|
||||
// console.log(undoStack, redoStack);
|
||||
}, [undoStack, redoStack]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
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 [colliders, setColliders] = useState<
|
||||
{ id: string; position: [number, number, number]; rotation: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[]
|
||||
>([]);
|
||||
const { colliderStore } = useSceneContext();
|
||||
const { colliders, addCollider, clearSelectedCollider } = colliderStore();
|
||||
const drag = useRef(false);
|
||||
const isLeftMouseDown = useRef(false);
|
||||
const { toggleView } = useToggleView();
|
||||
@@ -24,18 +24,25 @@ function ColliderCreator() {
|
||||
|
||||
const spawnPosition: [number, number, number] = [point.x, point.y + 0.5, point.z];
|
||||
|
||||
setColliders(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
position: spawnPosition,
|
||||
rotation: [0, 0, 0],
|
||||
colliderType: 'Default material',
|
||||
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;
|
||||
|
||||
@@ -53,18 +60,20 @@ function ColliderCreator() {
|
||||
};
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) {
|
||||
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);
|
||||
@@ -73,15 +82,8 @@ function ColliderCreator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{colliders.map(({ id, position, rotation }) => (
|
||||
<ColliderInstance
|
||||
key={id}
|
||||
id={id}
|
||||
colliders={colliders}
|
||||
setColliders={setColliders}
|
||||
position={position}
|
||||
rotation={rotation}
|
||||
/>
|
||||
{colliders.map((collider) => (
|
||||
<ColliderInstance key={collider.id} collider={collider} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -3,28 +3,28 @@ 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({ id, colliders, setColliders, position, rotation }: {
|
||||
id: string;
|
||||
colliders: { id: string; position: [number, number, number]; rotation: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[];
|
||||
setColliders: React.Dispatch<React.SetStateAction<{ id: string; position: [number, number, number]; rotation: [number, number, number]; colliderType: 'Default material' | 'Material 1' | 'Material 2' | 'Material 3' }[]>>;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
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 [color, setColor] = useState(new THREE.Color('white'));
|
||||
const [objectsOnCollider, setObjectsOnCollider] = useState<Set<RapierRigidBody>>(new Set());
|
||||
const isSelected = selectedCollider?.id === collider.id;
|
||||
|
||||
const getColorByType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Material 1':
|
||||
return new THREE.Color('blue');
|
||||
case 'Material 2':
|
||||
return new THREE.Color('green');
|
||||
return new THREE.Color('purple');
|
||||
case 'Material 3':
|
||||
return new THREE.Color('yellow');
|
||||
default:
|
||||
@@ -32,14 +32,6 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const current = colliders.find(c => c.id === id);
|
||||
if (current) {
|
||||
setColor(getColorByType(current.colliderType));
|
||||
}
|
||||
}, [colliders, id]);
|
||||
|
||||
|
||||
const handlePointerDown = (id: string) => {
|
||||
if (controls) {
|
||||
(controls as CameraControls).enabled = false;
|
||||
@@ -60,11 +52,19 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
ref.current.setAngularDamping(10);
|
||||
};
|
||||
|
||||
const handlePointerMove = () => {
|
||||
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,
|
||||
@@ -73,6 +73,7 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
);
|
||||
|
||||
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));
|
||||
|
||||
@@ -80,7 +81,7 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
{ x: finalPos.x, y: finalPos.y, z: finalPos.z },
|
||||
true
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const handlePointerUp = () => {
|
||||
if (controls) {
|
||||
@@ -89,9 +90,20 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
if (!draggedId) return;
|
||||
|
||||
if (ref.current) {
|
||||
// restore physics
|
||||
ref.current.setGravityScale(1, true);
|
||||
ref.current.setLinearDamping(0.5);
|
||||
ref.current.setAngularDamping(0.5);
|
||||
|
||||
// get final transform
|
||||
const pos = ref.current.translation();
|
||||
const rot = ref.current.rotation();
|
||||
|
||||
// update Zustand store
|
||||
updateCollider(draggedId, {
|
||||
position: [pos.x, pos.y, pos.z],
|
||||
rotation: [rot.x, rot.y, rot.z], // quaternion
|
||||
});
|
||||
}
|
||||
|
||||
setDraggedId(null);
|
||||
@@ -109,24 +121,17 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
}, [colliders, draggedId, controls]);
|
||||
|
||||
const handleMaterialEnter = (e: CollisionPayload) => {
|
||||
setColor(new THREE.Color('pink'));
|
||||
const body = e.other.rigidBody;
|
||||
const current = colliders.find(c => c.id === id);
|
||||
if (current) {
|
||||
if (body && (body.userData as any)?.materialType === current.colliderType) {
|
||||
setObjectsOnCollider(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(body);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
if (body && (body.userData as any)?.materialType) {
|
||||
setObjectsOnCollider(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(body);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMaterialExit = (e: CollisionPayload) => {
|
||||
const current = colliders.find(c => c.id === id);
|
||||
if (current)
|
||||
setColor(getColorByType(current.colliderType));
|
||||
const body = e.other.rigidBody;
|
||||
if (body) {
|
||||
setObjectsOnCollider(prev => {
|
||||
@@ -140,20 +145,48 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
useFrame(() => {
|
||||
objectsOnCollider.forEach(rigidBody => {
|
||||
if (!rigidBody) return;
|
||||
// if (collider.colliderCondition.conditionType === 'material') {
|
||||
// if (collider.colliderCondition.materialType === (rigidBody as any).userData.materialType) {
|
||||
// console.log('(rigidBody as any).userData.materialType: ', (rigidBody as any).userData.materialType);
|
||||
// collider.colliderCondition.arrowsOrder.forEach((arrowId) => {
|
||||
// console.log('arrow: ', arrowId);
|
||||
// let arrowDetails = getArrowByArrowId(arrowId);
|
||||
// if (arrowDetails?.position) {
|
||||
// const arrowPos = new THREE.Vector3(...arrowDetails?.position); // arrow position
|
||||
// const colliderPos = new THREE.Vector3(...(selectedCollider?.position || [0, 0, 0])); // collider position
|
||||
// const direction = new THREE.Vector3();
|
||||
// direction.subVectors(arrowPos, colliderPos).normalize();
|
||||
// console.log('Direction vector:', direction);
|
||||
// rigidBody.setLinvel({ x: direction.x, y: direction.y, z: direction.z }, true);
|
||||
// }
|
||||
|
||||
rigidBody.setLinvel({ x: 0, y: 0, z: 2 }, true);
|
||||
rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
||||
// // rigidBody.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
||||
|
||||
|
||||
// // Direction vector from collider to arrow
|
||||
|
||||
|
||||
// });
|
||||
|
||||
// } else {
|
||||
|
||||
|
||||
// }
|
||||
// } else if (collider.colliderCondition.conditionType === 'count') {
|
||||
|
||||
// }
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<RigidBody
|
||||
name='Sensor-Collider'
|
||||
key={id}
|
||||
key={collider.id}
|
||||
ref={ref}
|
||||
type="fixed"
|
||||
sensor
|
||||
position={position}
|
||||
position={collider.position}
|
||||
rotation={collider.rotation}
|
||||
colliders="cuboid"
|
||||
includeInvisible
|
||||
gravityScale={0}
|
||||
@@ -163,34 +196,28 @@ function ColliderInstance({ id, colliders, setColliders, position, rotation }: {
|
||||
<mesh
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePointerDown(id);
|
||||
handlePointerDown(collider.id);
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedCollider(collider);
|
||||
|
||||
setColliders(prev =>
|
||||
prev.map(c =>
|
||||
c.id === id
|
||||
? {
|
||||
...c,
|
||||
colliderType:
|
||||
c.colliderType === 'Default material'
|
||||
? 'Material 1'
|
||||
: c.colliderType === 'Material 1'
|
||||
? 'Material 2'
|
||||
: c.colliderType === 'Material 2'
|
||||
? 'Material 3'
|
||||
: 'Default material',
|
||||
}
|
||||
: c
|
||||
)
|
||||
);
|
||||
}}
|
||||
|
||||
>
|
||||
<boxGeometry args={[0.1, 1, 1]} />
|
||||
<meshStandardMaterial color={color} transparent opacity={0.3} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,12 +30,11 @@ function YSplitConveyorCollider({
|
||||
const lastClickTime = useRef(0);
|
||||
const arrowRefs = useRef<THREE.Group[]>([]);
|
||||
|
||||
// Toggle direction on double right click
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (e.button === 2 && hoverState) { // Right click and hovering over conveyor
|
||||
if (e.button === 2 && hoverState) {
|
||||
const now = Date.now();
|
||||
if (now - lastClickTime.current < 300) { // Double click within 300ms
|
||||
if (now - lastClickTime.current < 300) {
|
||||
const newDirection = !forward;
|
||||
setForward(newDirection);
|
||||
if (onDirectionChange) {
|
||||
@@ -81,7 +80,6 @@ function YSplitConveyorCollider({
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (isPaused) return;
|
||||
// Update physics
|
||||
const assetPos = new THREE.Vector3(...(asset.position || [0, 0, 0]));
|
||||
const assetRot = new THREE.Euler(...(asset.rotation || [0, 0, 0]));
|
||||
const assetQuat = new THREE.Quaternion().setFromEuler(assetRot);
|
||||
@@ -124,15 +122,13 @@ function YSplitConveyorCollider({
|
||||
rigidBody.setLinvel(totalForce, true);
|
||||
});
|
||||
|
||||
// Animate direction arrows
|
||||
if (showDirection && arrowRefs.current.length > 0) {
|
||||
const elapsedTime = clock.getElapsedTime();
|
||||
arrowRefs.current.forEach((arrowGroup, index) => {
|
||||
// Pulse animation
|
||||
|
||||
const pulseScale = 0.9 + 0.1 * Math.sin(elapsedTime * 5 + index * 0.5);
|
||||
arrowGroup.scale.setScalar(pulseScale);
|
||||
|
||||
// Flow animation (color intensity)
|
||||
const intensity = 0.7 + 0.3 * Math.sin(elapsedTime * 3 + index * 0.3);
|
||||
arrowGroup.children.forEach(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
@@ -201,11 +197,7 @@ function YSplitConveyorCollider({
|
||||
const point = curvePoints[i];
|
||||
const next = curvePoints[i + 1] || point;
|
||||
const direction = new THREE.Vector3().subVectors(next, point).normalize();
|
||||
|
||||
// Create arrow group
|
||||
const arrowGroup = new THREE.Group();
|
||||
|
||||
// Arrow shaft (cylinder)
|
||||
const shaftLength = arrowHeight * 0.7;
|
||||
const shaftGeometry = new THREE.CylinderGeometry(arrowRadius * 0.3, arrowRadius * 0.3, shaftLength, 8);
|
||||
const shaftMaterial = new THREE.MeshBasicMaterial({
|
||||
@@ -214,25 +206,20 @@ function YSplitConveyorCollider({
|
||||
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.position.y += 0.1;
|
||||
arrowGroup.quaternion.setFromUnitVectors(
|
||||
new THREE.Vector3(0, 1, 0),
|
||||
new THREE.Vector3(direction.x, 0.1, direction.z)
|
||||
);
|
||||
|
||||
arrows.push(arrowGroup);
|
||||
}
|
||||
});
|
||||
@@ -266,16 +253,18 @@ function YSplitConveyorCollider({
|
||||
>
|
||||
<mesh geometry={geometry}>
|
||||
<meshStandardMaterial
|
||||
// visible={false}
|
||||
color={forward ? "#64b5f6" : "#f48fb1"} // More subtle colors
|
||||
side={THREE.DoubleSide}
|
||||
transparent
|
||||
opacity={0.7}
|
||||
depthWrite={false}
|
||||
|
||||
/>
|
||||
</mesh>
|
||||
</RigidBody>
|
||||
))}
|
||||
|
||||
{/* Direction indicators */}
|
||||
{showDirection && directionArrows?.map((arrow, i) => (
|
||||
<primitive
|
||||
key={`arrow-${i}`}
|
||||
@@ -290,7 +279,7 @@ function YSplitConveyorCollider({
|
||||
<mesh
|
||||
key={`highlight-${index}`}
|
||||
geometry={geometry}
|
||||
position={[0, 0.002, 0]} // Slightly above conveyor
|
||||
position={[0, 0.002, 0]}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
color={forward ? "#00ff0044" : "#ff000044"}
|
||||
|
||||
@@ -53,6 +53,7 @@ function MaterialSpawner({ position: initialPos, spawnInterval, spawnCount }: Ma
|
||||
|
||||
const randomMaterialType = materialTypes[Math.floor(Math.random() * materialTypes.length)];
|
||||
|
||||
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
|
||||
@@ -3,13 +3,14 @@ import ColliderCreator from './colliders/colliderCreator'
|
||||
import { SplineCreator } from './conveyor/splineCreator'
|
||||
|
||||
function PhysicsSimulator() {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<MaterialSpawner
|
||||
position={[1, 2, 4]}
|
||||
position={[7.1, 2, 12.6]}
|
||||
spawnInterval={1000}
|
||||
spawnCount={50}
|
||||
spawnCount={10}
|
||||
/>
|
||||
{/* <MaterialSpawner
|
||||
position={[3.8, 3, 3]}
|
||||
|
||||
@@ -1,11 +1,508 @@
|
||||
import React from 'react'
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { useSceneContext } from "../../sceneContext";
|
||||
import { get } from "http";
|
||||
|
||||
function ColliderProperties() {
|
||||
return (
|
||||
<div>
|
||||
<div className="btn btn-primary">Collider UI</div>
|
||||
</div>
|
||||
)
|
||||
// Define TypeScript interfaces
|
||||
interface Arrow {
|
||||
id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default ColliderProperties
|
||||
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 || "")
|
||||
console.log('collider: ', collider);
|
||||
}, [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;
|
||||
@@ -75,8 +75,8 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
|
||||
>
|
||||
<Setup />
|
||||
<Collaboration />
|
||||
<Physics gravity={[0, -9.81, 0]} allowedLinearError={50} numSolverIterations={50} debug >
|
||||
{/* <Physics gravity={[0, -9.81, 0]} allowedLinearError={50} numSolverIterations={50} > */}
|
||||
{/* <Physics gravity={[0, -9.81, 0]} allowedLinearError={50} numSolverIterations={50} debug > */}
|
||||
<Physics gravity={[0, -9.81, 0]} allowedLinearError={50} numSolverIterations={50} >
|
||||
<Builder />
|
||||
|
||||
<Simulation />
|
||||
|
||||
@@ -20,6 +20,8 @@ import { createVehicleStore, VehicleStoreType } from '../../store/simulation/use
|
||||
import { createStorageUnitStore, StorageUnitStoreType } from '../../store/simulation/useStorageUnitStore';
|
||||
import { createHumanStore, HumanStoreType } from '../../store/simulation/useHumanStore';
|
||||
|
||||
import { createColliderStore, ColliderStoreType } from '../../store/useColliderStore';
|
||||
|
||||
type SceneContextValue = {
|
||||
|
||||
assetStore: AssetStoreType,
|
||||
@@ -42,6 +44,8 @@ type SceneContextValue = {
|
||||
storageUnitStore: StorageUnitStoreType;
|
||||
humanStore: HumanStoreType;
|
||||
|
||||
colliderStore: ColliderStoreType;
|
||||
|
||||
humanEventManagerRef: React.RefObject<HumanEventManagerState>;
|
||||
|
||||
clearStores: () => void;
|
||||
@@ -79,6 +83,8 @@ export function SceneProvider({
|
||||
const storageUnitStore = useMemo(() => createStorageUnitStore(), []);
|
||||
const humanStore = useMemo(() => createHumanStore(), []);
|
||||
|
||||
const colliderStore = useMemo(() => createColliderStore(), []);
|
||||
|
||||
const humanEventManagerRef = useRef<HumanEventManagerState>({ humanStates: [] });
|
||||
|
||||
const clearStores = useMemo(() => () => {
|
||||
@@ -98,8 +104,9 @@ export function SceneProvider({
|
||||
vehicleStore.getState().clearVehicles();
|
||||
storageUnitStore.getState().clearStorageUnits();
|
||||
humanStore.getState().clearHumans();
|
||||
colliderStore.getState().clearColliders();
|
||||
humanEventManagerRef.current.humanStates = [];
|
||||
}, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore]);
|
||||
}, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, colliderStore]);
|
||||
|
||||
const contextValue = useMemo(() => (
|
||||
{
|
||||
@@ -119,11 +126,12 @@ export function SceneProvider({
|
||||
vehicleStore,
|
||||
storageUnitStore,
|
||||
humanStore,
|
||||
colliderStore,
|
||||
humanEventManagerRef,
|
||||
clearStores,
|
||||
layout
|
||||
}
|
||||
), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, clearStores, layout]);
|
||||
), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, colliderStore, clearStores, layout]);
|
||||
|
||||
return (
|
||||
<SceneContext.Provider value={contextValue}>
|
||||
|
||||
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>;
|
||||
29
app/src/types/simulationTypes.d.ts
vendored
29
app/src/types/simulationTypes.d.ts
vendored
@@ -350,3 +350,32 @@ type IK = {
|
||||
maxheight?: number;
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user