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:
2025-08-21 17:57:45 +05:30
parent 57dffa0961
commit d1c78495ea
14 changed files with 965 additions and 118 deletions

View File

@@ -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;
}

View File

@@ -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);
}
};

View File

@@ -18,7 +18,7 @@ function UndoRedo2DControls() {
const { selectedVersion } = selectedVersionStore();
useEffect(() => {
console.log(undoStack, redoStack);
// console.log(undoStack, redoStack);
}, [undoStack, redoStack]);
useEffect(() => {

View File

@@ -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} />
))}
</>
);

View File

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

View File

@@ -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>
);
}

View File

@@ -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"}

View File

@@ -53,6 +53,7 @@ function MaterialSpawner({ position: initialPos, spawnInterval, spawnCount }: Ma
const randomMaterialType = materialTypes[Math.floor(Math.random() * materialTypes.length)];
return [
...prev,
{

View File

@@ -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]}

View File

@@ -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;

View File

@@ -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 />

View File

@@ -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}>

View File

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

View File

@@ -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;
};