From d1c78495ea96be3e4d580bc1308d66cee3dc1103 Mon Sep 17 00:00:00 2001 From: Gomathi9520 Date: Thu, 21 Aug 2025 17:57:45 +0530 Subject: [PATCH] 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. --- .../layout/sidebarRight/SideBarRight.tsx | 17 +- .../builder/asset/models/model/model.tsx | 10 +- .../undoRedo2D/undoRedo2DControls.tsx | 2 +- .../physics/colliders/colliderCreator.tsx | 46 +- .../colliderInstance/colliderArrow.tsx | 115 ++++ .../colliderInstance/colliderInstance.tsx | 139 +++-- .../conveyor/types/ySplitConveyorCollider.tsx | 27 +- .../modules/scene/physics/materialSpawner.tsx | 1 + .../scene/physics/physicsSimulator.tsx | 5 +- .../scene/physics/ui/ColliderProperties.tsx | 513 +++++++++++++++++- app/src/modules/scene/scene.tsx | 4 +- app/src/modules/scene/sceneContext.tsx | 12 +- app/src/store/useColliderStore.ts | 163 ++++++ app/src/types/simulationTypes.d.ts | 29 + 14 files changed, 965 insertions(+), 118 deletions(-) create mode 100644 app/src/modules/scene/physics/colliders/colliderInstance/colliderArrow.tsx create mode 100644 app/src/store/useColliderStore.ts diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index e237cd3..ba55f6f 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -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("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 ; case "visualization": return ; + case "colliderProperties": + return ; default: return null; } diff --git a/app/src/modules/builder/asset/models/model/model.tsx b/app/src/modules/builder/asset/models/model/model.tsx index 5df0b7c..c7b2424 100644 --- a/app/src/modules/builder/asset/models/model/model.tsx +++ b/app/src/modules/builder/asset/models/model/model.tsx @@ -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); + } }; diff --git a/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx b/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx index d45518c..ecd70e1 100644 --- a/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx +++ b/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx @@ -18,7 +18,7 @@ function UndoRedo2DControls() { const { selectedVersion } = selectedVersionStore(); useEffect(() => { - console.log(undoStack, redoStack); + // console.log(undoStack, redoStack); }, [undoStack, redoStack]); useEffect(() => { diff --git a/app/src/modules/scene/physics/colliders/colliderCreator.tsx b/app/src/modules/scene/physics/colliders/colliderCreator.tsx index c0b55b0..e2ad4c5 100644 --- a/app/src/modules/scene/physics/colliders/colliderCreator.tsx +++ b/app/src/modules/scene/physics/colliders/colliderCreator.tsx @@ -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 }) => ( - + {colliders.map((collider) => ( + ))} ); diff --git a/app/src/modules/scene/physics/colliders/colliderInstance/colliderArrow.tsx b/app/src/modules/scene/physics/colliders/colliderInstance/colliderArrow.tsx new file mode 100644 index 0000000..4af478f --- /dev/null +++ b/app/src/modules/scene/physics/colliders/colliderInstance/colliderArrow.tsx @@ -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(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 ( + + + + + + + + + + + + ); +} diff --git a/app/src/modules/scene/physics/colliders/colliderInstance/colliderInstance.tsx b/app/src/modules/scene/physics/colliders/colliderInstance/colliderInstance.tsx index c7eb34e..d052c69 100644 --- a/app/src/modules/scene/physics/colliders/colliderInstance/colliderInstance.tsx +++ b/app/src/modules/scene/physics/colliders/colliderInstance/colliderInstance.tsx @@ -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>; - 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(null); const dragOffset = useRef(new THREE.Vector3()); const initialDepth = useRef(0); const ref = useRef(null); - const [color, setColor] = useState(new THREE.Color('white')); const [objectsOnCollider, setObjectsOnCollider] = useState>(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 ( { 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 - ) - ); }} - > - + + {isSelected && collider.arrows.map((arrow) => ( + + ))} + ); } diff --git a/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx b/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx index 2b52ca9..8c1cc96 100644 --- a/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx +++ b/app/src/modules/scene/physics/conveyor/types/ySplitConveyorCollider.tsx @@ -30,12 +30,11 @@ function YSplitConveyorCollider({ const lastClickTime = useRef(0); const arrowRefs = useRef([]); - // Toggle direction on double right click useEffect(() => { const handleClick = (e: MouseEvent) => { - if (e.button === 2 && hoverState) { // Right click and hovering over conveyor + 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({ > ))} - {/* Direction indicators */} {showDirection && directionArrows?.map((arrow, i) => ( {/* -
Collider UI
- - ) +// 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 = ({ arrow, index, moveArrow, removeArrow }) => { + const [isDragging, setIsDragging] = useState(false); + const elementRef = useRef(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 ( +
+ {arrow.content} + +
+ ); +}; + +// 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) => { + console.log('updates: ', updates); + updateColliderCondition(id, updates); + + }; + useEffect(() => { + let collider = getColliderCondition(selectedCollider?.id || "") + console.log('collider: ', collider); + }, [colliders, selectedCollider]); + + + + return ( +
+
+
+

+ Collider Properties +

+ +
+ + {/* Directions */} +
+
+ Directions + +
+ {getColliderArrows(selectedCollider?.id || '').length === 0 && ( +

+ No directions yet +

+ )} + {getColliderArrows(selectedCollider?.id || '').map((arrow, i) => ( +
{ setSelectedArrow(arrow.arrowId) }} + > + {arrow.arrowName} + +
+ ))} +
+ +
+
+ Conditions +
+ + {condition?.conditionType && ( +
+ +
+
Type
+ +
+ + {condition.conditionType === "count" && ( +
+
+
Count
+ + updateConditions(selectedCollider!.id, { + count: parseInt(e.target.value) || 0, + }) + } + style={{ + backgroundColor: "#374151", + color: "white", + border: "none", + padding: "4px", + borderRadius: "6px", + width: "100%", + }} + /> +
+ + +
+ Arrow Order + + {(() => { + 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 ( +
+ {orderedArrows.length === 0 && ( +

+ No arrows to order +

+ )} + + {orderedArrows.map((arrow, i) => ( + + ))} +
+ ); + })()} +
+
+ )} + {condition.conditionType === "material" && ( +
+ {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 ( +
+
{arrow.arrowName}
+ +
+ ); + })} +
+ )} +
+ )} +
+ + +
+
+ ); +}; + +export default ColliderProperties; \ No newline at end of file diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index 9d51f4a..7a10131 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -75,8 +75,8 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co > - - {/* */} + {/* */} + diff --git a/app/src/modules/scene/sceneContext.tsx b/app/src/modules/scene/sceneContext.tsx index bf65c36..f1ee21c 100644 --- a/app/src/modules/scene/sceneContext.tsx +++ b/app/src/modules/scene/sceneContext.tsx @@ -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; clearStores: () => void; @@ -79,6 +83,8 @@ export function SceneProvider({ const storageUnitStore = useMemo(() => createStorageUnitStore(), []); const humanStore = useMemo(() => createHumanStore(), []); + const colliderStore = useMemo(() => createColliderStore(), []); + const humanEventManagerRef = useRef({ 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 ( diff --git a/app/src/store/useColliderStore.ts b/app/src/store/useColliderStore.ts new file mode 100644 index 0000000..3b3ab76 --- /dev/null +++ b/app/src/store/useColliderStore.ts @@ -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) => 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 + ) => 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((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; diff --git a/app/src/types/simulationTypes.d.ts b/app/src/types/simulationTypes.d.ts index 01a1603..0ddb197 100644 --- a/app/src/types/simulationTypes.d.ts +++ b/app/src/types/simulationTypes.d.ts @@ -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; +}; \ No newline at end of file