Merge pull request 'simulation-animation' (#55) from simulation-animation into main

Reviewed-on: http://185.100.212.76:7776/Dwinzo-Beta/Dwinzo_dev/pulls/55
This commit is contained in:
Vishnu 2025-04-10 12:24:47 +00:00
commit 8dda2cc93e
23 changed files with 2330 additions and 1622 deletions

50
app/package-lock.json generated
View File

@ -29,6 +29,7 @@
"chartjs-plugin-annotation": "^3.1.0",
"glob": "^11.0.0",
"gsap": "^3.12.5",
"html2canvas": "^1.4.1",
"leva": "^0.10.0",
"mqtt": "^5.10.4",
"postprocessing": "^6.36.4",
@ -8027,6 +8028,15 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -9082,6 +9092,15 @@
"postcss": "^8.4"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-loader": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
@ -12469,6 +12488,19 @@
}
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@ -20414,6 +20446,15 @@
"node": "*"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -21200,6 +21241,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",

View File

@ -24,6 +24,7 @@
"chartjs-plugin-annotation": "^3.1.0",
"glob": "^11.0.0",
"gsap": "^3.12.5",
"html2canvas": "^1.4.1",
"leva": "^0.10.0",
"mqtt": "^5.10.4",
"postprocessing": "^6.36.4",

View File

@ -1,68 +1,111 @@
import { useEffect, useState } from "react";
import { Line } from "@react-three/drei";
import { useNavMesh, useSimulationStates } from "../../../store/store";
import {
useNavMesh,
usePlayAgv,
useSimulationStates,
} from "../../../store/store";
import PathNavigator from "./pathNavigator";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
type PathPoints = {
modelUuid: string;
modelSpeed: number;
bufferTime: number;
points: { x: number; y: number; z: number }[];
hitCount: number;
modelUuid: string;
modelSpeed: number;
bufferTime: number;
points: { x: number; y: number; z: number }[];
hitCount: number;
};
interface ProcessContainerProps {
processes: any[];
agvRef: any;
MaterialRef: any;
}
const Agv = () => {
const [pathPoints, setPathPoints] = useState<PathPoints[]>([]);
const { simulationStates } = useSimulationStates();
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
const Agv: React.FC<ProcessContainerProps> = ({
processes,
agvRef,
MaterialRef,
}) => {
const [pathPoints, setPathPoints] = useState<PathPoints[]>([]);
const { simulationStates } = useSimulationStates();
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
const { PlayAgv, setPlayAgv } = usePlayAgv();
useEffect(() => {
if (simulationStates.length > 0) {
const agvModels = simulationStates.filter((val) => val.modelName === "agv" && val.type === "Vehicle");
const newPathPoints = agvModels.filter((model: any) => model.points && model.points.actions && typeof model.points.actions.start === "object" && typeof model.points.actions.end === "object" && "x" in model.points.actions.start && "y" in model.points.actions.start && "x" in model.points.actions.end && "y" in model.points.actions.end)
.map((model: any) => ({
modelUuid: model.modeluuid,
modelSpeed: model.points.speed,
bufferTime: model.points.actions.buffer,
hitCount: model.points.actions.hitCount,
points: [
{ x: model.position[0], y: model.position[1], z: model.position[2] },
{ x: model.points.actions.start.x, y: 0, z: model.points.actions.start.y },
{ x: model.points.actions.end.x, y: 0, z: model.points.actions.end.y },
],
}));
useEffect(() => {
if (simulationStates.length > 0) {
const agvModels = simulationStates.filter(
(val) => val.modelName === "agv" && val.type === "Vehicle"
);
setPathPoints(newPathPoints);
}
}, [simulationStates]);
const newPathPoints = agvModels
.filter(
(model: any) =>
model.points &&
model.points.actions &&
typeof model.points.actions.start === "object" &&
typeof model.points.actions.end === "object" &&
"x" in model.points.actions.start &&
"y" in model.points.actions.start &&
"x" in model.points.actions.end &&
"y" in model.points.actions.end
)
.map((model: any) => ({
modelUuid: model.modeluuid,
modelSpeed: model.points.speed,
bufferTime: model.points.actions.buffer,
hitCount: model.points.actions.hitCount,
points: [
{
x: model.position[0],
y: model.position[1],
z: model.position[2],
},
{
x: model.points.actions.start.x,
y: 0,
z: model.points.actions.start.y,
},
{
x: model.points.actions.end.x,
y: 0,
z: model.points.actions.end.y,
},
],
}));
return (
<>
{pathPoints.map((pair, i) => (
<group key={i} visible={!isPlaying}>
<PathNavigator
navMesh={navMesh}
pathPoints={pair.points}
id={pair.modelUuid}
speed={pair.modelSpeed}
bufferTime={pair.bufferTime}
hitCount={pair.hitCount}
/>
setPathPoints(newPathPoints);
}
}, [simulationStates]);
{pair.points.slice(1).map((point, idx) => (
<mesh position={[point.x, point.y, point.z]} key={idx}>
<sphereGeometry args={[0.3, 15, 15]} />
<meshStandardMaterial color="red" />
</mesh>
))}
</group>
))}
</>
);
return (
<>
{pathPoints.map((pair, i) => (
<group key={i} visible={!isPlaying}>
<PathNavigator
navMesh={navMesh}
pathPoints={pair.points}
id={pair.modelUuid}
speed={pair.modelSpeed}
bufferTime={pair.bufferTime}
hitCount={pair.hitCount}
processes={processes}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
{pair.points.slice(1).map((point, idx) => (
<mesh position={[point.x, point.y, point.z]} key={idx}>
<sphereGeometry args={[0.3, 15, 15]} />
<meshStandardMaterial color="red" />
</mesh>
))}
</group>
))}
</>
);
};
export default Agv;

View File

@ -1,216 +1,485 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState, useRef, useMemo } from "react";
import * as THREE from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { NavMeshQuery } from "@recast-navigation/core";
import { Line } from "@react-three/drei";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import { usePlayAgv } from "../../../store/store";
import crate from "../../../assets/gltf-glb/crate_box.glb";
interface PathNavigatorProps {
navMesh: any;
pathPoints: any;
id: string;
speed: number;
bufferTime: number;
hitCount: number;
navMesh: any;
pathPoints: any;
id: string;
speed: number;
bufferTime: number;
hitCount: number;
processes: any[];
agvRef: any;
MaterialRef: any;
}
interface AGVData {
processId: string;
vehicleId: string;
hitCount: number;
totalHits: number;
}
type Phase = "initial" | "toDrop" | "toPickup";
type MaterialType = "Box" | "Crate";
export default function PathNavigator({
navMesh,
pathPoints,
id,
speed,
bufferTime,
hitCount
navMesh,
pathPoints,
id,
speed,
bufferTime,
hitCount,
processes,
agvRef,
MaterialRef,
}: PathNavigatorProps) {
const [path, setPath] = useState<[number, number, number][]>([]);
const [currentPhase, setCurrentPhase] = useState<'initial' | 'loop'>('initial');
const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>([]);
const [pickupDropPath, setPickupDropPath] = useState<[number, number, number][]>([]);
const [dropPickupPath, setDropPickupPath] = useState<[number, number, number][]>([]);
const [initialPosition, setInitialPosition] = useState<THREE.Vector3 | null>(null);
const [initialRotation, setInitialRotation] = useState<THREE.Euler | null>(null);
const [targetPosition] = useState(new THREE.Vector3());
const [smoothPosition] = useState(new THREE.Vector3());
const [targetQuaternion] = useState(new THREE.Quaternion());
const distancesRef = useRef<number[]>([]);
const totalDistanceRef = useRef(0);
const progressRef = useRef(0);
const isWaiting = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const pathTransitionProgress = useRef(0);
const [currentPhase, setCurrentPhase] = useState<Phase>("initial");
//
const { scene } = useThree();
const { isPlaying } = usePlayButtonStore();
const [path, setPath] = useState<[number, number, number][]>([]);
const PickUpDrop = useRef([]);
useEffect(() => {
const object = scene.getObjectByProperty("uuid", id);
if (object) {
setInitialPosition(object.position.clone());
setInitialRotation(object.rotation.clone());
smoothPosition.copy(object.position.clone());
targetPosition.copy(object.position.clone());
targetQuaternion.setFromEuler(object.rotation.clone());
}
}, [scene, id]);
// const [currentPhase, setCurrentPhase] = useState<"initial" | "loop">(
// "initial"
// );
const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>(
[]
);
const computePath = (start: any, end: any) => {
try {
const navMeshQuery = new NavMeshQuery(navMesh);
const { path: segmentPath } = navMeshQuery.computePath(start, end);
return segmentPath?.map(({ x, y, z }) => [x, y + 0.1, z] as [number, number, number]) || [];
} catch {
return [];
}
};
const [pickupDropPath, setPickupDropPath] = useState<
[number, number, number][]
>([]);
const [dropPickupPath, setDropPickupPath] = useState<
[number, number, number][]
>([]);
const [initialPosition, setInitialPosition] = useState<THREE.Vector3 | null>(
null
);
const [initialRotation, setInitialRotation] = useState<THREE.Euler | null>(
null
);
const [boxVisible, setBoxVisible] = useState(false);
const resetState = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
const distancesRef = useRef<number[]>([]);
const totalDistanceRef = useRef(0);
const progressRef = useRef(0);
const isWaiting = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const hasStarted = useRef(false);
setPath([]);
setCurrentPhase('initial');
distancesRef.current = [];
totalDistanceRef.current = 0;
progressRef.current = 0;
isWaiting.current = false;
pathTransitionProgress.current = 0;
const { scene } = useThree();
const { isPlaying } = usePlayButtonStore();
const { PlayAgv, setPlayAgv } = usePlayAgv();
const object = scene.getObjectByProperty("uuid", id);
if (object && initialPosition && initialRotation) {
object.position.copy(initialPosition);
object.rotation.copy(initialRotation);
smoothPosition.copy(initialPosition);
targetPosition.copy(initialPosition);
targetQuaternion.setFromEuler(initialRotation);
}
};
useEffect(() => {
const object = scene.getObjectByProperty("uuid", id);
if (object) {
setInitialPosition(object.position.clone());
setInitialRotation(object.rotation.clone());
}
}, [scene, id]);
const computePath = (start: any, end: any) => {
try {
const navMeshQuery = new NavMeshQuery(navMesh);
const { path: segmentPath } = navMeshQuery.computePath(start, end);
return (
segmentPath?.map(
({ x, y, z }) => [x, y + 0.1, z] as [number, number, number]
) || []
);
} catch {
return [];
}
};
useEffect(() => {
if (!isPlaying) {
resetState();
}
const resetState = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (!navMesh || pathPoints.length < 2) return;
setPath([]);
setCurrentPhase("initial");
setPickupDropPath([]);
setDropPickupPath([]);
distancesRef.current = [];
totalDistanceRef.current = 0;
progressRef.current = 0;
isWaiting.current = false;
const [pickup, drop] = pathPoints.slice(-2);
const object = scene.getObjectByProperty("uuid", id);
if (!object) return;
if (initialPosition && initialRotation) {
const object = scene.getObjectByProperty("uuid", id);
if (object) {
object.position.copy(initialPosition);
object.rotation.copy(initialRotation);
}
}
};
const currentPosition = { x: object.position.x, y: object.position.y, z: object.position.z };
useEffect(() => {
if (!isPlaying) {
resetState();
}
const toPickupPath = computePath(currentPosition, pickup);
const pickupToDropPath = computePath(pickup, drop);
const dropToPickupPath = computePath(drop, pickup);
if (!navMesh || pathPoints.length < 2) return;
if (toPickupPath.length && pickupToDropPath.length && dropToPickupPath.length) {
setPickupDropPath(pickupToDropPath);
setDropPickupPath(dropToPickupPath);
setToPickupPath(toPickupPath);
setPath(toPickupPath);
setCurrentPhase('initial');
}
}, [navMesh, pathPoints, hitCount, isPlaying]);
const [pickup, drop] = pathPoints.slice(-2);
useEffect(() => {
if (path.length < 2) return;
PickUpDrop.current = pathPoints.slice(-2);
let total = 0;
const segmentDistances = path.slice(0, -1).map((point, i) => {
const dist = new THREE.Vector3(...point).distanceTo(new THREE.Vector3(...path[i + 1]));
total += dist;
return dist;
});
const object = scene.getObjectByProperty("uuid", id);
distancesRef.current = segmentDistances;
totalDistanceRef.current = total;
progressRef.current = 0;
isWaiting.current = false;
}, [path]);
if (!object) return;
useFrame((_, delta) => {
if (!isPlaying || path.length < 2 || !scene || !id) return;
const currentPosition = {
x: object.position.x,
y: object.position.y,
z: object.position.z,
};
const object = scene.getObjectByProperty("uuid", id);
if (!object) return;
const toPickupPath = computePath(currentPosition, pickup);
const pickupToDropPath = computePath(pickup, drop);
const dropToPickupPath = computePath(drop, pickup);
const speedFactor = speed;
progressRef.current += delta * speedFactor;
if (
toPickupPath.length &&
pickupToDropPath.length &&
dropToPickupPath.length
) {
setPickupDropPath(pickupToDropPath);
setDropPickupPath(dropToPickupPath);
setToPickupPath(toPickupPath);
setPath(toPickupPath);
setCurrentPhase("initial");
}
}, [navMesh, pathPoints, hitCount, isPlaying, PlayAgv]);
let covered = progressRef.current;
let accumulated = 0;
let index = 0;
useEffect(() => {
if (path.length < 2) return;
while (
index < distancesRef.current.length &&
covered > accumulated + distancesRef.current[index]
) {
accumulated += distancesRef.current[index];
index++;
}
let total = 0;
const segmentDistances = path.slice(0, -1).map((point, i) => {
const dist = new THREE.Vector3(...point).distanceTo(
new THREE.Vector3(...path[i + 1])
);
total += dist;
return dist;
});
if (index >= distancesRef.current.length) {
progressRef.current = totalDistanceRef.current;
distancesRef.current = segmentDistances;
totalDistanceRef.current = total;
progressRef.current = 0;
isWaiting.current = false;
}, [path]);
if (!isWaiting.current) {
isWaiting.current = true;
// Add these refs outside the useFrame if not already present:
const startPointReached = useRef(false);
timeoutRef.current = setTimeout(() => {
if (currentPhase === 'initial') {
setPath(pickupDropPath);
setCurrentPhase('loop');
} else {
setPath(prevPath =>
prevPath === pickupDropPath ? dropPickupPath : pickupDropPath
);
}
const baseMaterials = useMemo(
() => ({
Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }),
Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
// Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
Default: new THREE.MeshStandardMaterial(),
}),
[]
);
progressRef.current = 0;
isWaiting.current = false;
}, bufferTime * 1000);
}
return;
}
// Example usage:
const targetModelUUID = id; // Replace with your target ID
const matchedProcess = findProcessByTargetModelUUID(
processes,
targetModelUUID
);
const start = new THREE.Vector3(...path[index]);
const end = new THREE.Vector3(...path[index + 1]);
const dist = distancesRef.current[index];
const t = THREE.MathUtils.clamp((covered - accumulated) / dist, 0, 1);
function findProcessByTargetModelUUID(processes: any, targetModelUUID: any) {
for (const process of processes) {
for (const path of process.paths) {
for (const point of path.points) {
if (
point.connections?.targets?.some(
(target: any) => target.modelUUID === targetModelUUID
)
) {
//
return process.id; // Return the process if a match is found
}
}
}
}
return null; // Return null if no match is found
}
targetPosition.copy(start).lerp(end, t);
useFrame((_, delta) => {});
const boxRef = useRef<THREE.Mesh | null>(null);
smoothPosition.lerp(targetPosition, 0.1);
object.position.copy(smoothPosition);
useEffect(() => {
if (!scene || !boxRef || !processes) return;
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const targetRotationY = Math.atan2(direction.x, direction.z);
targetQuaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), targetRotationY);
object.quaternion.slerp(targetQuaternion, 0.1);
});
// 1. Find the existing object by UUID
const existingObject = scene.getObjectByProperty("uuid", id);
if (!existingObject) return;
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
// 2. Find the process that targets this object's modelUUID
if (boxVisible) {
const matchedProcess = findProcessByTargetModelUUID(processes, id);
return (
<group name="path-navigator-lines" visible={!isPlaying} >
{toPickupPath.length > 0 && (
<Line points={toPickupPath} color="blue" lineWidth={3} dashed dashSize={0.75} dashScale={2} />
)}
// 3. Determine the material (from materialref) if a process is matched
let materialType = "Default";
{pickupDropPath.length > 0 && (
<Line points={pickupDropPath} color="red" lineWidth={3} />
)}
if (matchedProcess) {
const materialEntry = MaterialRef.current.find((item: any) =>
item.objects.some((obj: any) => obj.processId === matchedProcess)
);
console.log("materialEntry: ", materialEntry);
if (materialEntry) {
materialType = materialEntry.material; // "Box" or "Crate"
}
}
{dropPickupPath.length > 0 && (
<Line points={dropPickupPath} color="red" lineWidth={3} />
)}
</group>
);
}
// 4. Create the box mesh with the assigned material
const boxGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const boxMaterial = baseMaterials[materialType as MaterialType];
const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
boxMesh.position.y = 1;
boxMesh.name = `box-${id}`;
// 5. Add to scene and cleanup
existingObject.add(boxMesh);
boxRef.current = boxMesh;
}
if (!startPointReached.current && boxVisible) {
setBoxVisible(false);
}
return () => {
if (boxRef.current?.parent) {
boxRef.current.parent.remove(boxRef.current);
}
boxRef.current = null;
};
}, [
processes,
MaterialRef,
boxVisible,
findProcessByTargetModelUUID,
startPointReached,
]);
useFrame((_, delta) => {
const currentAgv = (agvRef.current || []).find(
(agv: AGVData) => agv.vehicleId === id
);
if (!scene || !id || !isPlaying) return;
const object = scene.getObjectByProperty("uuid", id);
if (!object) return;
if (isPlaying && !hasStarted.current) {
hasStarted.current = false;
startPointReached.current = false;
progressRef.current = 0;
isWaiting.current = false;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}
const isAgvReady = () => {
if (!agvRef.current || agvRef.current.length === 0) return false;
if (!currentAgv) return false;
return hitCount === currentAgv.expectedHitCount;
};
// Step 1: Snap to start point on first play
if (isPlaying && !hasStarted.current && toPickupPath.length > 0) {
setBoxVisible(false);
const startPoint = new THREE.Vector3(...toPickupPath[0]);
object.position.copy(startPoint);
if (toPickupPath.length > 1) {
const nextPoint = new THREE.Vector3(...toPickupPath[1]);
const direction = nextPoint.clone().sub(startPoint).normalize();
object.rotation.y = Math.atan2(direction.x, direction.z);
}
hasStarted.current = true;
startPointReached.current = true;
progressRef.current = 0;
setToPickupPath(toPickupPath.slice(-1));
return;
}
// Step 2: Wait at start point for AGV readiness
if (isPlaying && startPointReached.current && path.length === 0) {
if (!isAgvReady()) {
return; // Prevent transitioning to the next phase if AGV is not ready
}
setBoxVisible(true);
setPath([...toPickupPath]); // Start path transition once the AGV is ready
setCurrentPhase("toDrop");
progressRef.current = 0;
console.log("startPointReached: ", startPointReached.current);
startPointReached.current = false;
return;
}
if (path.length < 2) return;
progressRef.current += delta * speed;
let covered = progressRef.current;
let accumulated = 0;
let index = 0;
if (distancesRef.current.length !== path.length - 1) {
distancesRef.current = [];
totalDistanceRef.current = 0;
for (let i = 0; i < path.length - 1; i++) {
const start = new THREE.Vector3(...path[i]);
const end = new THREE.Vector3(...path[i + 1]);
const distance = start.distanceTo(end);
distancesRef.current.push(distance);
totalDistanceRef.current += distance;
}
}
while (
index < distancesRef.current.length &&
covered > accumulated + distancesRef.current[index]
) {
accumulated += distancesRef.current[index];
index++;
}
// AGV has completed its path
if (index >= distancesRef.current.length) {
progressRef.current = totalDistanceRef.current;
timeoutRef.current = setTimeout(() => {
if (!isAgvReady()) {
isWaiting.current = false;
return;
}
let nextPath = [];
let nextPhase = currentPhase;
if (currentPhase === "toDrop") {
nextPath = dropPickupPath;
nextPhase = "toPickup";
setBoxVisible(false);
} else if (currentPhase === "toPickup") {
// When returning to start point (toPickup phase completed)
// Set position to toPickupPath[1] instead of [0]
if (toPickupPath.length > 1) {
object.position.copy(new THREE.Vector3(...toPickupPath[1]));
// Also set rotation towards the next point if available
if (toPickupPath.length > 2) {
const nextPoint = new THREE.Vector3(...toPickupPath[2]);
const direction = nextPoint
.clone()
.sub(object.position)
.normalize();
object.rotation.y = Math.atan2(direction.x, direction.z);
}
}
// Reset the path and mark start point as reached
setPath([]);
startPointReached.current = true;
progressRef.current = 0;
distancesRef.current = [];
// Stop the AGV by setting isplaying to false
if (currentAgv) {
currentAgv.isplaying = false;
}
// Clear timeout and return to prevent further movement
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
return;
} else {
nextPath = pickupDropPath;
nextPhase = "toDrop";
setBoxVisible(true);
}
setPath([...nextPath]);
setCurrentPhase(nextPhase);
progressRef.current = 0;
isWaiting.current = false;
distancesRef.current = [];
// Reset hit count for the next cycle
if (agvRef.current) {
agvRef.current = agvRef.current.map((agv: AGVData) =>
agv.vehicleId === id ? { ...agv, hitCount: 0 } : agv
);
}
}, bufferTime * 1000);
return;
}
// Step 4: Interpolate position and rotation
const start = new THREE.Vector3(...path[index]);
const end = new THREE.Vector3(...path[index + 1]);
const dist = distancesRef.current[index];
if (!dist || dist === 0) return;
const t = THREE.MathUtils.clamp((covered - accumulated) / dist, 0, 1);
object.position.copy(start.clone().lerp(end, t));
const direction = new THREE.Vector3().subVectors(end, start).normalize();
object.rotation.y = Math.atan2(direction.x, direction.z);
});
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return (
<group name="path-navigator-lines" visible={!isPlaying}>
{toPickupPath.length > 0 && (
<Line
points={toPickupPath}
color="blue"
lineWidth={3}
dashed
dashSize={0.75}
dashScale={2}
/>
)}
{pickupDropPath.length > 0 && (
<Line points={pickupDropPath} color="red" lineWidth={3} />
)}
{dropPickupPath.length > 0 && (
<Line points={dropPickupPath} color="red" lineWidth={3} />
)}
</group>
);
}

View File

@ -150,6 +150,7 @@ async function handleModelLoad(
const organization = email ? email.split("@")[1].split(".")[0] : "";
getAssetEventType(selectedItem.id, organization).then(async (res) => {
console.log('res: ', res);
if (res.type === "Conveyor") {
const pointUUIDs = res.points.map(() => THREE.MathUtils.generateUUID());
@ -224,6 +225,7 @@ async function handleModelLoad(
eventData as Types.ConveyorEventsSchema
]);
console.log('data: ', data);
socket.emit("v2:model-asset:add", data);
} else if (res.type === "Vehicle") {
@ -453,6 +455,7 @@ async function handleModelLoad(
return updatedItems;
});
console.log('data: ', data);
socket.emit("v2:model-asset:add", data);
}

View File

@ -1,143 +1,143 @@
import { useEffect, useState } from "react";
import { getLines } from "../../../../../services/factoryBuilder/lines/getLinesApi";
import * as THREE from "three";
import {
useActiveLayer,
useDeletedLines,
useNewLines,
useToggleView,
} from "../../../../../store/store";
import objectLinesToArray from "../lineConvertions/objectLinesToArray";
import { Html } from "@react-three/drei";
import * as Types from "../../../../../types/world/worldTypes";
const DistanceText = () => {
const [lines, setLines] = useState<
{
distance: string;
position: THREE.Vector3;
userData: Types.Line;
layer: string;
}[]
>([]);
const { activeLayer } = useActiveLayer();
const { toggleView } = useToggleView();
const { newLines, setNewLines } = useNewLines();
const { deletedLines, setDeletedLines } = useDeletedLines();
useEffect(() => {
const email = localStorage.getItem("email");
if (!email) return;
const organization = email.split("@")[1].split(".")[0];
getLines(organization).then((data) => {
data = objectLinesToArray(data);
const lines = data
.filter((line: Types.Line) => line[0][2] === activeLayer)
.map((line: Types.Line) => {
const point1 = new THREE.Vector3(
line[0][0].x,
line[0][0].y,
line[0][0].z
);
const point2 = new THREE.Vector3(
line[1][0].x,
line[1][0].y,
line[1][0].z
);
const distance = point1.distanceTo(point2);
const midpoint = new THREE.Vector3()
.addVectors(point1, point2)
.divideScalar(2);
return {
distance: distance.toFixed(1),
position: midpoint,
userData: line,
layer: activeLayer,
};
});
setLines(lines);
});
}, [activeLayer]);
useEffect(() => {
if (newLines.length > 0) {
if (newLines[0][0][2] !== activeLayer) return;
const newLinesData = newLines.map((line: Types.Line) => {
const point1 = new THREE.Vector3(
line[0][0].x,
line[0][0].y,
line[0][0].z
);
const point2 = new THREE.Vector3(
line[1][0].x,
line[1][0].y,
line[1][0].z
);
const distance = point1.distanceTo(point2);
const midpoint = new THREE.Vector3()
.addVectors(point1, point2)
.divideScalar(2);
return {
distance: distance.toFixed(1),
position: midpoint,
userData: line,
layer: activeLayer,
};
});
setLines((prevLines) => [...prevLines, ...newLinesData]);
setNewLines([]);
}
}, [newLines, activeLayer]);
useEffect(() => {
if ((deletedLines as Types.Lines).length > 0) {
setLines((prevLines) =>
prevLines.filter(
(line) =>
!deletedLines.some(
(deletedLine: any) =>
deletedLine[0][1] === line.userData[0][1] &&
deletedLine[1][1] === line.userData[1][1]
)
)
);
setDeletedLines([]);
}
}, [deletedLines]);
return (
<>
{toggleView && (
<group name="Distance_Text">
{lines.map((text) => (
<Html
// data
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
className={`distance line-${text.userData[0][1]}_${text.userData[1][1]}_${text.layer}`}
>
{text.distance} m
</div>
</Html>
))}
</group>
)}
</>
);
};
export default DistanceText;
import { useEffect, useState } from "react";
import { getLines } from "../../../../../services/factoryBuilder/lines/getLinesApi";
import * as THREE from "three";
import {
useActiveLayer,
useDeletedLines,
useNewLines,
useToggleView,
} from "../../../../../store/store";
import objectLinesToArray from "../lineConvertions/objectLinesToArray";
import { Html } from "@react-three/drei";
import * as Types from "../../../../../types/world/worldTypes";
const DistanceText = () => {
const [lines, setLines] = useState<
{
distance: string;
position: THREE.Vector3;
userData: Types.Line;
layer: string;
}[]
>([]);
const { activeLayer } = useActiveLayer();
const { toggleView } = useToggleView();
const { newLines, setNewLines } = useNewLines();
const { deletedLines, setDeletedLines } = useDeletedLines();
useEffect(() => {
const email = localStorage.getItem("email");
if (!email) return;
const organization = email.split("@")[1].split(".")[0];
getLines(organization).then((data) => {
data = objectLinesToArray(data);
const lines = data
.filter((line: Types.Line) => line[0][2] === activeLayer)
.map((line: Types.Line) => {
const point1 = new THREE.Vector3(
line[0][0].x,
line[0][0].y,
line[0][0].z
);
const point2 = new THREE.Vector3(
line[1][0].x,
line[1][0].y,
line[1][0].z
);
const distance = point1.distanceTo(point2);
const midpoint = new THREE.Vector3()
.addVectors(point1, point2)
.divideScalar(2);
return {
distance: distance.toFixed(1),
position: midpoint,
userData: line,
layer: activeLayer,
};
});
setLines(lines);
});
}, [activeLayer]);
useEffect(() => {
if (newLines.length > 0) {
if (newLines[0][0][2] !== activeLayer) return;
const newLinesData = newLines.map((line: Types.Line) => {
const point1 = new THREE.Vector3(
line[0][0].x,
line[0][0].y,
line[0][0].z
);
const point2 = new THREE.Vector3(
line[1][0].x,
line[1][0].y,
line[1][0].z
);
const distance = point1.distanceTo(point2);
const midpoint = new THREE.Vector3()
.addVectors(point1, point2)
.divideScalar(2);
return {
distance: distance.toFixed(1),
position: midpoint,
userData: line,
layer: activeLayer,
};
});
setLines((prevLines) => [...prevLines, ...newLinesData]);
setNewLines([]);
}
}, [newLines, activeLayer]);
useEffect(() => {
if ((deletedLines as Types.Lines).length > 0) {
setLines((prevLines) =>
prevLines.filter(
(line) =>
!deletedLines.some(
(deletedLine: any) =>
deletedLine[0][1] === line.userData[0][1] &&
deletedLine[1][1] === line.userData[1][1]
)
)
);
setDeletedLines([]);
}
}, [deletedLines]);
return (
<>
{toggleView && (
<group name="Distance_Text">
{lines.map((text) => (
<Html
// data
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
className={`distance line-${text.userData[0][1]}_${text.userData[1][1]}_${text.layer}`}
>
{text.distance} m
</div>
</Html>
))}
</group>
)}
</>
);
};
export default DistanceText;

View File

@ -1,71 +1,71 @@
import * as THREE from "three";
import { Html } from "@react-three/drei";
import { useState, useEffect } from "react";
import { useActiveLayer } from "../../../../../store/store";
const ReferenceDistanceText = ({ line }: { line: any }) => {
interface TextState {
distance: string;
position: THREE.Vector3;
userData: any;
layer: any;
}
const [text, setTexts] = useState<TextState | null>(null);
const { activeLayer } = useActiveLayer();
useEffect(() => {
if (line) {
if (line.parent === null) {
setTexts(null);
return;
}
const distance = line.userData.linePoints.cursorPosition.distanceTo(
line.userData.linePoints.startPoint
);
const midpoint = new THREE.Vector3()
.addVectors(
line.userData.linePoints.cursorPosition,
line.userData.linePoints.startPoint
)
.divideScalar(2);
const newTexts = {
distance: distance.toFixed(1),
position: midpoint,
userData: line,
layer: activeLayer,
};
setTexts(newTexts);
}
});
return (
<group name="Reference_Distance_Text">
<mesh>
{text !== null && (
<Html
// data
key={text.distance}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
className={`Reference_Distance line-${text.userData.userData}`}
>
{text.distance} m
</div>
</Html>
)}
</mesh>
</group>
);
};
export default ReferenceDistanceText;
import * as THREE from "three";
import { Html } from "@react-three/drei";
import { useState, useEffect } from "react";
import { useActiveLayer } from "../../../../../store/store";
const ReferenceDistanceText = ({ line }: { line: any }) => {
interface TextState {
distance: string;
position: THREE.Vector3;
userData: any;
layer: any;
}
const [text, setTexts] = useState<TextState | null>(null);
const { activeLayer } = useActiveLayer();
useEffect(() => {
if (line) {
if (line.parent === null) {
setTexts(null);
return;
}
const distance = line.userData.linePoints.cursorPosition.distanceTo(
line.userData.linePoints.startPoint
);
const midpoint = new THREE.Vector3()
.addVectors(
line.userData.linePoints.cursorPosition,
line.userData.linePoints.startPoint
)
.divideScalar(2);
const newTexts = {
distance: distance.toFixed(1),
position: midpoint,
userData: line,
layer: activeLayer,
};
setTexts(newTexts);
}
});
return (
<group name="Reference_Distance_Text">
<mesh>
{text !== null && (
<Html
// data
key={text.distance}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
className={`Reference_Distance line-${text.userData.userData}`}
>
{text.distance} m
</div>
</Html>
)}
</mesh>
</group>
);
};
export default ReferenceDistanceText;

View File

@ -20,6 +20,7 @@ async function loadInitialFloorItems(
const organization = (email!.split("@")[1]).split(".")[0];
const items = await getFloorAssets(organization);
console.log('items: ', items);
localStorage.setItem("FloorItems", JSON.stringify(items));
await initializeDB();

View File

@ -266,7 +266,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas
return {
uuid: pointUUID,
position: vehiclePoint?.position,
rotation: vehiclePoint?.rotation,
// rotation: vehiclePoint?.rotation,
actions: hasActions
? {
...vehiclePoint.actions,
@ -344,7 +344,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas
return {
uuid: pointUUID,
position: vehiclePoint?.position,
rotation: vehiclePoint?.rotation,
// rotation: vehiclePoint?.rotation,
actions: hasActions
? {
...vehiclePoint.actions,
@ -422,7 +422,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas
return {
uuid: pointUUID,
position: vehiclePoint?.position,
rotation: vehiclePoint?.rotation,
// rotation: vehiclePoint?.rotation,
actions: hasActions
? {
...vehiclePoint.actions,

View File

@ -245,7 +245,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb
return {
uuid: pointUUID,
position: vehiclePoint?.position,
rotation: vehiclePoint?.rotation,
// rotation: vehiclePoint?.rotation,
actions: hasActions
? {
...vehiclePoint.actions,
@ -323,7 +323,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb
return {
uuid: pointUUID,
position: vehiclePoint?.position,
rotation: vehiclePoint?.rotation,
// rotation: vehiclePoint?.rotation,
actions: hasActions
? {
...vehiclePoint.actions,
@ -401,7 +401,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb
return {
uuid: pointUUID,
position: vehiclePoint?.position,
rotation: vehiclePoint?.rotation,
// rotation: vehiclePoint?.rotation,
actions: hasActions
? {
...vehiclePoint.actions,

View File

@ -335,7 +335,6 @@ function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObjec
key={path.modeluuid}
ref={(el) => (groupRefs.current[path.modeluuid] = el!)}
position={path.position}
rotation={path.rotation}
onClick={(e) => {
if (isConnecting || eyeDropMode) return;
e.stopPropagation();
@ -389,7 +388,6 @@ function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObjec
key={path.modeluuid}
ref={(el) => (groupRefs.current[path.modeluuid] = el!)}
position={path.position}
rotation={path.rotation}
onClick={(e) => {
if (isConnecting || eyeDropMode) return;
e.stopPropagation();

View File

@ -1,7 +0,0 @@
import React from "react";
const Mesh: React.FC = () => {
return <mesh></mesh>;
};
export default Mesh;

View File

@ -1,502 +1,119 @@
import React, { useRef, useState, useEffect, useMemo } from "react";
import {
useAnimationPlaySpeed,
usePauseButtonStore,
usePlayButtonStore,
useResetButtonStore,
} from "../../../store/usePlayButtonStore";
import { GLTFLoader } from "three-stdlib";
import React, { useRef, useEffect, useMemo } from "react";
import { useLoader, useFrame } from "@react-three/fiber";
import { GLTFLoader } from "three-stdlib";
import * as THREE from "three";
import { GLTF } from "three-stdlib";
import crate from "../../../assets/gltf-glb/crate_box.glb";
import box from "../../../assets/gltf-glb/box.glb";
interface PointAction {
uuid: string;
name: string;
type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap";
objectType: string;
material: string;
delay: string | number;
spawnInterval: string | number;
isUsed: boolean;
import { useProcessAnimation } from "./useProcessAnimations";
import ProcessObject from "./processObject";
import { ProcessData } from "./types";
import { useSimulationStates } from "../../../store/store";
import { retrieveGLTF } from "../../../utils/indexDB/idbUtils";
interface ProcessContainerProps {
processes: ProcessData[];
setProcesses: React.Dispatch<React.SetStateAction<any[]>>;
agvRef: any;
MaterialRef: any;
}
interface ProcessPoint {
uuid: string;
position: number[];
rotation: number[];
actions: PointAction[];
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
}
interface ProcessPath {
modeluuid: string;
modelName: string;
points: ProcessPoint[];
pathPosition: number[];
pathRotation: number[];
speed: number;
}
interface ProcessData {
id: string;
paths: ProcessPath[];
animationPath: { x: number; y: number; z: number }[];
pointActions: PointAction[][];
speed: number;
customMaterials?: Record<string, THREE.Material>;
renderAs?: "box" | "custom";
}
interface AnimationState {
currentIndex: number;
progress: number;
isAnimating: boolean;
speed: number;
isDelaying: boolean;
delayStartTime: number;
currentDelayDuration: number;
delayComplete: boolean;
currentPathIndex: number;
}
interface SpawnedObject {
ref: React.RefObject<THREE.Group | THREE.Mesh>;
state: AnimationState;
visible: boolean;
material: THREE.Material;
spawnTime: number;
currentMaterialType: string;
position: THREE.Vector3;
}
interface ProcessAnimationState {
spawnedObjects: { [objectId: string]: SpawnedObject };
nextSpawnTime: number;
objectIdCounter: number;
isProcessDelaying: boolean;
processDelayStartTime: number;
processDelayDuration: number;
}
const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
const ProcessAnimator: React.FC<ProcessContainerProps> = ({
processes,
setProcesses,
agvRef,
MaterialRef,
}) => {
const gltf = useLoader(GLTFLoader, crate) as GLTF;
const { isPlaying, setIsPlaying } = usePlayButtonStore();
const { isPaused, setIsPaused } = usePauseButtonStore();
const { isReset, setReset } = useResetButtonStore();
const groupRef = useRef<THREE.Group>(null);
const debugRef = useRef<boolean>(false);
const clockRef = useRef<THREE.Clock>(new THREE.Clock());
const pauseTimeRef = useRef<number>(0);
const elapsedBeforePauseRef = useRef<number>(0);
const animationStatesRef = useRef<Record<string, ProcessAnimationState>>({});
const { speed, setSpeed } = useAnimationPlaySpeed();
const prevIsPlaying = useRef<boolean | null>(null);
const [internalResetFlag, setInternalResetFlag] = useState(false);
const [animationStates, setAnimationStates] = useState<
Record<string, ProcessAnimationState>
>({});
// Store the speed in a ref to access the latest value in animation frames
const speedRef = useRef<number>(speed);
const {
animationStates,
setAnimationStates,
clockRef,
elapsedBeforePauseRef,
speedRef,
debugRef,
findSpawnPoint,
createSpawnedObject,
handlePointActions,
hasNonInheritActions,
getPointDataForAnimationIndex,
processes: processedProcesses,
checkAndCountTriggers,
} = useProcessAnimation(processes, setProcesses, agvRef);
// Update the ref when speed changes
useEffect(() => {
speedRef.current = speed;
}, [speed]);
useEffect(() => {
if (prevIsPlaying.current !== null) {
setAnimationStates({});
}
// Update ref to current isPlaying after effect
prevIsPlaying.current = isPlaying;
// setAnimationStates({});
}, [isPlaying]);
// Sync ref with state
useEffect(() => {
animationStatesRef.current = animationStates;
}, [animationStates]);
// Base materials
const baseMaterials = useMemo(
() => ({
Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }),
Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
// Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }),
Default: new THREE.MeshStandardMaterial(),
}),
[]
);
// Replace your reset effect with this:
useEffect(() => {
if (isReset) {
// 1. Mark that we're doing an internal reset
setInternalResetFlag(true);
// 2. Pause the animation first
setIsPlaying(false);
setIsPaused(false);
// 3. Reset all animation states
setAnimationStates({});
animationStatesRef.current = {};
// 4. Reset timing references
clockRef.current = new THREE.Clock();
elapsedBeforePauseRef.current = 0;
pauseTimeRef.current = 0;
// 5. Clear the external reset flag
setReset(false);
// 6. After state updates are complete, restart
setTimeout(() => {
setInternalResetFlag(false);
setIsPlaying(true);
}, 0);
}
}, [isReset, setReset, setIsPlaying, setIsPaused]);
// Handle pause state changes
useEffect(() => {
if (isPaused) {
pauseTimeRef.current = clockRef.current.getElapsedTime();
} else if (pauseTimeRef.current > 0) {
const pausedDuration =
clockRef.current.getElapsedTime() - pauseTimeRef.current;
elapsedBeforePauseRef.current += pausedDuration;
}
}, [isPaused]);
// Initialize animation states when processes or play state changes
useEffect(() => {
if (isPlaying && !internalResetFlag) {
const newStates: Record<string, ProcessAnimationState> = {};
processes.forEach((process) => {
newStates[process.id] = {
spawnedObjects: {},
nextSpawnTime: 0,
objectIdCounter: 0,
isProcessDelaying: false,
processDelayStartTime: 0,
processDelayDuration: 0,
// Update material references for all spawned objects
Object.entries(animationStates).forEach(([processId, processState]) => {
Object.keys(processState.spawnedObjects).forEach((objectId) => {
const entry = {
processId,
objectId,
};
});
setAnimationStates(newStates);
animationStatesRef.current = newStates;
clockRef.current.start();
}
}, [isPlaying, processes, internalResetFlag]);
const findSpawnPoint = (process: ProcessData): ProcessPoint | null => {
for (const path of process.paths || []) {
for (const point of path.points || []) {
const spawnAction = point.actions?.find(
(a) => a.isUsed && a.type === "Spawn"
const materialType =
processState.spawnedObjects[objectId]?.currentMaterialType;
if (!materialType) {
return;
}
const matRefArray = MaterialRef.current;
// Find existing material group
const existing = matRefArray.find(
(entryGroup: { material: string; objects: any[] }) =>
entryGroup.material === materialType
);
if (spawnAction) {
return point;
}
}
}
return null;
};
const findAnimationPathPoint = (
process: ProcessData,
spawnPoint: ProcessPoint
): THREE.Vector3 => {
if (process.animationPath && process.animationPath.length > 0) {
let pointIndex = 0;
for (const path of process.paths || []) {
for (let i = 0; i < (path.points?.length || 0); i++) {
const point = path.points?.[i];
if (point && point.uuid === spawnPoint.uuid) {
if (process.animationPath[pointIndex]) {
const p = process.animationPath[pointIndex];
return new THREE.Vector3(p.x, p.y, p.z);
}
if (existing) {
// Check if this processId + objectId already exists
const alreadyExists = existing.objects.some(
(o: any) =>
o.processId === entry.processId && o.objectId === entry.objectId
);
if (!alreadyExists) {
existing.objects.push(entry);
}
pointIndex++;
} else {
// Create new group for this material type
matRefArray.push({
material: materialType,
objects: [entry],
});
}
}
}
return new THREE.Vector3(
spawnPoint.position[0],
spawnPoint.position[1],
spawnPoint.position[2]
);
};
const createSpawnedObject = (
process: ProcessData,
currentTime: number,
materialType: string,
spawnPoint: ProcessPoint
): SpawnedObject => {
const processMaterials = {
...baseMaterials,
...(process.customMaterials || {}),
};
const spawnPosition = findAnimationPathPoint(process, spawnPoint);
const material =
processMaterials[materialType as keyof typeof processMaterials] ||
baseMaterials.Default;
if (debugRef.current) {
console.log(`Creating object with material: ${materialType}`, material);
}
return {
ref: React.createRef(),
state: {
currentIndex: 0,
progress: 0,
isAnimating: true,
speed: process.speed || 1, // Process base speed (will be multiplied by global speed)
isDelaying: false,
delayStartTime: 0,
currentDelayDuration: 0,
delayComplete: false,
currentPathIndex: 0,
},
visible: true,
material: material,
currentMaterialType: materialType,
spawnTime: currentTime,
position: spawnPosition,
};
};
const handleMaterialSwap = (
processId: string,
objectId: string,
materialType: string
) => {
if (debugRef.current) {
console.log(`Attempting material swap to: ${materialType}`);
}
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState || !processState.spawnedObjects[objectId]) {
if (debugRef.current) console.log("Object not found for swap");
return prev;
}
const process = processes.find((p) => p.id === processId);
if (!process) {
if (debugRef.current) console.log("Process not found");
return prev;
}
const processMaterials = {
...baseMaterials,
...(process.customMaterials || {}),
};
const newMaterial =
processMaterials[materialType as keyof typeof processMaterials];
if (!newMaterial) {
if (debugRef.current) console.log(`Material ${materialType} not found`);
return prev;
}
if (debugRef.current) {
console.log(`Swapping material for ${objectId} to ${materialType}`);
}
return {
...prev,
[processId]: {
...processState,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: {
...processState.spawnedObjects[objectId],
material: newMaterial,
currentMaterialType: materialType,
},
},
},
};
});
});
};
}, [animationStates, MaterialRef, agvRef]);
const handlePointActions = (
processId: string,
objectId: string,
actions: PointAction[] = [],
currentTime: number
): boolean => {
let shouldStopAnimation = false;
actions.forEach((action) => {
if (!action.isUsed) return;
if (debugRef.current) {
console.log(`Processing action: ${action.type} for ${objectId}`);
}
switch (action.type) {
case "Delay":
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState || processState.isProcessDelaying) {
return prev;
}
const delayDuration =
typeof action.delay === "number"
? action.delay
: parseFloat(action.delay as string) || 0;
if (delayDuration > 0) {
return {
...prev,
[processId]: {
...processState,
isProcessDelaying: true,
processDelayStartTime: currentTime,
processDelayDuration: delayDuration,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: {
...processState.spawnedObjects[objectId],
state: {
...processState.spawnedObjects[objectId].state,
isAnimating: false,
isDelaying: true,
delayStartTime: currentTime,
currentDelayDuration: delayDuration,
delayComplete: false,
},
},
},
},
};
}
return prev;
});
shouldStopAnimation = true;
break;
case "Despawn":
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState) return prev;
const newSpawnedObjects = { ...processState.spawnedObjects };
delete newSpawnedObjects[objectId];
return {
...prev,
[processId]: {
...processState,
spawnedObjects: newSpawnedObjects,
},
};
});
shouldStopAnimation = true;
break;
case "Swap":
if (action.material) {
handleMaterialSwap(processId, objectId, action.material);
}
break;
default:
break;
}
});
return shouldStopAnimation;
};
const hasNonInheritActions = (actions: PointAction[] = []): boolean => {
return actions.some((action) => action.isUsed && action.type !== "Inherit");
};
const getPointDataForAnimationIndex = (
process: ProcessData,
index: number
): ProcessPoint | null => {
if (!process.paths) return null;
let cumulativePoints = 0;
for (const path of process.paths) {
const pointCount = path.points?.length || 0;
if (index < cumulativePoints + pointCount) {
const pointIndex = index - cumulativePoints;
return path.points?.[pointIndex] || null;
}
cumulativePoints += pointCount;
}
return null;
};
// In processAnimator.tsx - only the relevant spawn logic part that needs fixes
useFrame(() => {
if (!isPlaying || isPaused) return;
// Spawn logic frame
const currentTime =
clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current;
setAnimationStates((prev) => {
const newStates = { ...prev };
processes.forEach((process) => {
processedProcesses.forEach((process) => {
const processState = newStates[process.id];
if (!processState) return;
if (processState.isProcessDelaying) {
// Apply global speed to delays (faster speed = shorter delays)
const effectiveDelayTime =
processState.processDelayDuration / speedRef.current;
if (
currentTime - processState.processDelayStartTime >=
effectiveDelayTime
) {
newStates[process.id] = {
...processState,
isProcessDelaying: false,
spawnedObjects: Object.entries(
processState.spawnedObjects
).reduce(
(acc, [id, obj]) => ({
...acc,
[id]: {
...obj,
state: {
...obj.state,
isDelaying: false,
delayComplete: true,
isAnimating: true,
progress:
obj.state.progress === 0 ? 0.001 : obj.state.progress,
},
},
}),
{}
),
};
}
// Existing delay handling logic...
return;
}
@ -513,7 +130,14 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
? spawnAction.spawnInterval
: parseFloat(spawnAction.spawnInterval as string) || 0;
// Apply global speed to spawn intervals (faster speed = more frequent spawns)
// Check if this is a zero interval spawn and we already spawned an object
if (
spawnInterval === 0 &&
processState.hasSpawnedZeroIntervalObject === true
) {
return; // Don't spawn more objects for zero interval
}
const effectiveSpawnInterval = spawnInterval / speedRef.current;
if (currentTime >= processState.nextSpawnTime) {
@ -522,9 +146,11 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
process,
currentTime,
spawnAction.material || "Default",
spawnPoint
spawnPoint,
baseMaterials
);
// Update state with the new object and flag for zero interval
newStates[process.id] = {
...processState,
spawnedObjects: {
@ -533,6 +159,11 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
},
objectIdCounter: processState.objectIdCounter + 1,
nextSpawnTime: currentTime + effectiveSpawnInterval,
// Mark that we've spawned an object for zero interval case
hasSpawnedZeroIntervalObject:
spawnInterval === 0
? true
: processState.hasSpawnedZeroIntervalObject,
};
}
});
@ -542,20 +173,18 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
});
useFrame((_, delta) => {
if (!isPlaying || isPaused) return;
// Animation logic frame
const currentTime =
clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current;
setAnimationStates((prev) => {
const newStates = { ...prev };
processes.forEach((process) => {
processedProcesses.forEach((process) => {
const processState = newStates[process.id];
if (!processState) return;
if (processState.isProcessDelaying) {
// Apply global speed to delays (faster speed = shorter delays)
const effectiveDelayTime =
processState.processDelayDuration / speedRef.current;
@ -617,7 +246,6 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
const stateRef = obj.state;
if (stateRef.isDelaying) {
// Apply global speed to delays (faster speed = shorter delays)
const effectiveDelayTime =
stateRef.currentDelayDuration / speedRef.current;
@ -654,18 +282,15 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
stateRef.currentIndex
);
// Handle point actions when first arriving at point
if (stateRef.progress === 0 && currentPointData?.actions) {
if (debugRef.current) {
console.log(
`At point ${stateRef.currentIndex} with actions:`,
currentPointData.actions
);
}
const shouldStop = handlePointActions(
process.id,
objectId,
currentPointData.actions,
currentTime
currentTime,
processedProcesses,
baseMaterials
);
if (shouldStop) {
updatedObjects[objectId] = { ...obj, state: { ...stateRef } };
@ -676,14 +301,39 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
const nextPointIdx = stateRef.currentIndex + 1;
const isLastPoint = nextPointIdx >= path.length;
// if (isLastPoint) {
// if (currentPointData?.actions) {
// const shouldStop = !hasNonInheritActions(
// currentPointData.actions
// );
// if (shouldStop) {
// return;
// }
// }
// }
if (isLastPoint) {
if (currentPointData?.actions) {
const shouldStop = !hasNonInheritActions(
const hasNonInherit = hasNonInheritActions(
currentPointData.actions
);
if (shouldStop) {
if (!hasNonInherit) {
// Remove the object if all actions are inherit
updatedObjects[objectId] = {
...obj,
visible: false,
state: { ...stateRef, isAnimating: false },
};
return;
}
} else {
// No actions at last point - remove the object
updatedObjects[objectId] = {
...obj,
visible: false,
state: { ...stateRef, isAnimating: false },
};
return;
}
}
@ -691,8 +341,6 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
const nextPoint = path[nextPointIdx];
const distance =
path[stateRef.currentIndex].distanceTo(nextPoint);
// Apply both process-specific speed and global speed multiplier
const effectiveSpeed = stateRef.speed * speedRef.current;
const movement = effectiveSpeed * delta;
@ -708,17 +356,19 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
stateRef.progress = 0;
currentRef.position.copy(nextPoint);
// TRIGGER CHECK - When object arrives at new point
checkAndCountTriggers(
process.id,
objectId,
stateRef.currentIndex, // The new point index
processedProcesses,
currentTime
);
const newPointData = getPointDataForAnimationIndex(
process,
stateRef.currentIndex
);
if (newPointData?.actions && debugRef.current) {
console.log(
`Reached new point with actions:`,
newPointData.actions
);
}
} else {
currentRef.position.lerpVectors(
path[stateRef.currentIndex],
@ -742,56 +392,32 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({
});
});
if (!processes || processes.length === 0) {
if (!processedProcesses || processedProcesses.length === 0) {
return null;
}
return (
<>
<group ref={groupRef}>
{Object.entries(animationStates).flatMap(([processId, processState]) =>
Object.entries(processState.spawnedObjects)
.filter(([_, obj]) => obj.visible)
.map(([objectId, obj]) => {
const process = processes.find((p) => p.id === processId);
const process = processedProcesses.find((p) => p.id === processId);
const renderAs = process?.renderAs || "custom";
if (renderAs === "box") {
return (
<mesh
key={objectId}
ref={obj.ref as React.RefObject<THREE.Mesh>}
material={obj.material}
position={obj.position}
>
<boxGeometry args={[1, 1, 1]} />
</mesh>
);
}
if (gltf?.scene) {
// Clone the scene and apply the material to all meshes
const clonedScene = gltf.scene.clone();
clonedScene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = obj.material;
}
});
return (
<group
key={objectId}
ref={obj.ref as React.RefObject<THREE.Group>}
position={obj.position}
>
<primitive object={clonedScene} />
</group>
);
}
return null;
return (
<ProcessObject
key={objectId}
objectId={objectId}
obj={obj}
renderAs={renderAs}
gltf={gltf}
/>
);
})
)}
</>
</group>
);
};

View File

@ -2,15 +2,28 @@ import React, { useState } from "react";
import ProcessCreator from "./processCreator";
import ProcessAnimator from "./processAnimator";
const ProcessContainer: React.FC = () => {
const [processes, setProcesses] = useState<any[]>([]);
interface ProcessContainerProps {
processes: any[];
setProcesses: React.Dispatch<React.SetStateAction<any[]>>;
agvRef: any;
MaterialRef: any;
}
const ProcessContainer: React.FC<ProcessContainerProps> = ({
processes,
setProcesses,
agvRef,
MaterialRef,
}) => {
return (
<>
<ProcessCreator onProcessesCreated={setProcesses} />
{processes.length > 0 && <ProcessAnimator processes={processes} />}
<ProcessAnimator
processes={processes}
setProcesses={setProcesses}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
</>
);
};

View File

@ -24,16 +24,26 @@
// isUsed: boolean;
// }
// export interface PointTrigger {
// uuid: string;
// bufferTime: number;
// name: string;
// type: string;
// isUsed: boolean;
// }
// export interface PathPoint {
// uuid: string;
// position: [number, number, number];
// actions: PointAction[];
// triggers: PointTrigger[];
// connections: {
// targets: Array<{ modelUUID: string }>;
// };
// }
// export interface SimulationPath {
// type: string;
// modeluuid: string;
// points: PathPoint[];
// pathPosition: [number, number, number];
@ -45,7 +55,9 @@
// paths: SimulationPath[];
// animationPath: THREE.Vector3[];
// pointActions: PointAction[][];
// pointTriggers: PointTrigger[][];
// speed: number;
// isplaying: boolean;
// }
// interface ProcessCreatorProps {
@ -58,18 +70,33 @@
// ): SimulationPath {
// const { modeluuid } = path;
// // Simplified normalizeAction function that preserves exact original properties
// // Normalized action handler
// const normalizeAction = (action: any): PointAction => {
// return { ...action }; // Return exact copy with no modifications
// };
// // Normalized trigger handler
// const normalizeTrigger = (trigger: any): PointTrigger => {
// return { ...trigger }; // Return exact copy with no modifications
// };
// if (path.type === "Conveyor") {
// return {
// type: path.type,
// modeluuid,
// points: path.points.map((point) => ({
// uuid: point.uuid,
// position: point.position,
// actions: point.actions.map(normalizeAction), // Preserve exact actions
// actions: Array.isArray(point.actions)
// ? point.actions.map(normalizeAction)
// : point.actions
// ? [normalizeAction(point.actions)]
// : [],
// triggers: Array.isArray(point.triggers)
// ? point.triggers.map(normalizeTrigger)
// : point.triggers
// ? [normalizeTrigger(point.triggers)]
// : [],
// connections: {
// targets: point.connections.targets.map((target) => ({
// modelUUID: target.modelUUID,
@ -83,44 +110,44 @@
// : path.speed || 1,
// };
// } else {
// // For vehicle paths, handle the case where triggers might not exist
// return {
// type: path.type,
// modeluuid,
// points: [
// {
// uuid: path.point.uuid,
// position: path.point.position,
// actions: Array.isArray(path.point.actions)
// ? path.point.actions.map(normalizeAction)
// : [normalizeAction(path.point.actions)],
// uuid: path.points.uuid,
// position: path.points.position,
// actions: Array.isArray(path.points.actions)
// ? path.points.actions.map(normalizeAction)
// : path.points.actions
// ? [normalizeAction(path.points.actions)]
// : [],
// // For vehicle paths, since triggers might not exist in the schema,
// // we always define default to an empty array
// triggers: [],
// connections: {
// targets: path.point.connections.targets.map((target) => ({
// targets: path.points.connections.targets.map((target) => ({
// modelUUID: target.modelUUID,
// })),
// },
// },
// ],
// pathPosition: path.position,
// speed: path.point.speed || 1,
// speed: path.points.speed || 1,
// };
// }
// }
// // Custom shallow comparison for arrays
// const areArraysEqual = (a: any[], b: any[]) => {
// if (a.length !== b.length) return false;
// for (let i = 0; i < a.length; i++) {
// if (a[i] !== b[i]) return false;
// }
// return true;
// };
// // Helper function to create an empty process
// const createEmptyProcess = (): Process => ({
// id: `process-${Math.random().toString(36).substring(2, 11)}`,
// paths: [],
// animationPath: [],
// pointActions: [],
// pointTriggers: [], // Added point triggers array
// speed: 1,
// isplaying: false,
// });
// // Enhanced connection checking function
@ -156,12 +183,38 @@
// return connectsToLast && !connectsToFirst;
// }
// // Check if a point has a spawn action
// function hasSpawnAction(point: PathPoint): boolean {
// return point.actions.some((action) => action.type.toLowerCase() === "spawn");
// }
// // Ensure spawn point is always at the beginning of the path
// function ensureSpawnPointIsFirst(path: SimulationPath): SimulationPath {
// if (path.points.length !== 3) return path;
// // If the third point has spawn action and first doesn't, reverse the array
// if (hasSpawnAction(path.points[2]) && !hasSpawnAction(path.points[0])) {
// return {
// ...path,
// points: [...path.points].reverse(),
// };
// }
// return path;
// }
// // Updated path adjustment function
// function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] {
// if (paths.length < 2) return paths;
// if (paths.length < 1) return paths;
// const adjustedPaths = [...paths];
// // First ensure all paths have spawn points at the beginning
// for (let i = 0; i < adjustedPaths.length; i++) {
// adjustedPaths[i] = ensureSpawnPointIsFirst(adjustedPaths[i]);
// }
// // Then handle connections between paths
// for (let i = 0; i < adjustedPaths.length - 1; i++) {
// const currentPath = adjustedPaths[i];
// const nextPath = adjustedPaths[i + 1];
@ -189,6 +242,7 @@
// const [processes, setProcesses] = useState<Process[]>([]);
// const hasSpawnAction = useCallback((path: SimulationPath): boolean => {
// if (path.type !== "Conveyor") return false;
// return path.points.some((point) =>
// point.actions.some((action) => action.type.toLowerCase() === "spawn")
// );
@ -202,19 +256,23 @@
// const animationPath: THREE.Vector3[] = [];
// const pointActions: PointAction[][] = [];
// const pointTriggers: PointTrigger[][] = []; // Added point triggers collection
// const processSpeed = paths[0]?.speed || 1;
// for (const path of paths) {
// for (const point of path.points) {
// const obj = scene.getObjectByProperty("uuid", point.uuid);
// if (!obj) {
// console.warn(`Object with UUID ${point.uuid} not found in scene`);
// continue;
// }
// if (path.type === "Conveyor") {
// const obj = scene.getObjectByProperty("uuid", point.uuid);
// if (!obj) {
// console.warn(`Object with UUID ${point.uuid} not found in scene`);
// continue;
// }
// const position = obj.getWorldPosition(new THREE.Vector3());
// animationPath.push(position.clone());
// pointActions.push(point.actions);
// const position = obj.getWorldPosition(new THREE.Vector3());
// animationPath.push(position.clone());
// pointActions.push(point.actions);
// pointTriggers.push(point.triggers); // Collect triggers for each point
// }
// }
// }
@ -223,7 +281,9 @@
// paths,
// animationPath,
// pointActions,
// pointTriggers,
// speed: processSpeed,
// isplaying: false,
// };
// },
// [scene]
@ -326,21 +386,35 @@
// );
// }, [simulationStates]);
// // Enhanced dependency tracking that includes action and trigger types
// const pathsDependency = useMemo(() => {
// if (!convertedPaths) return null;
// return convertedPaths.map((path) => ({
// id: path.modeluuid,
// hasSpawn: path.points.some((p: PathPoint) =>
// p.actions.some((a: PointAction) => a.type.toLowerCase() === "spawn")
// ),
// // Track all action types for each point
// actionSignature: path.points
// .map((point, index) =>
// point.actions.map((action) => `${index}-${action.type}`).join("|")
// )
// .join(","),
// // Track all trigger types for each point
// triggerSignature: path.points
// .map((point, index) =>
// point.triggers
// .map((trigger) => `${index}-${trigger.type}`)
// .join("|")
// )
// .join(","),
// connections: path.points
// .flatMap((p: PathPoint) =>
// p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID)
// )
// .join(","),
// isplaying: false,
// }));
// }, [convertedPaths]);
// // Force process recreation when paths change
// useEffect(() => {
// if (!convertedPaths || convertedPaths.length === 0) {
// if (prevProcessesRef.current.length > 0) {
@ -350,42 +424,16 @@
// return;
// }
// if (areArraysEqual(prevPathsRef.current, convertedPaths)) {
// return;
// }
// prevPathsRef.current = convertedPaths;
// // Always regenerate processes if the pathsDependency has changed
// // This ensures action and trigger type changes will be detected
// const newProcesses = createProcessesFromPaths(convertedPaths);
// prevPathsRef.current = convertedPaths;
// // console.log("--- Action Types in Paths ---");
// // convertedPaths.forEach((path) => {
// // path.points.forEach((point) => {
// // point.actions.forEach((action) => {
// // console.log(
// // `Path ${path.modeluuid}, Point ${point.uuid}: ${action.type}`
// // );
// // });
// // });
// // });
// // console.log("New processes:", newProcesses);
// if (
// newProcesses.length !== prevProcessesRef.current.length ||
// !newProcesses.every(
// (proc, i) =>
// proc.paths.length === prevProcessesRef.current[i]?.paths.length &&
// proc.paths.every(
// (path, j) =>
// path.modeluuid ===
// prevProcessesRef.current[i]?.paths[j]?.modeluuid
// )
// )
// ) {
// onProcessesCreated(newProcesses);
// // prevProcessesRef.current = newProcesses;
// }
// // Always update processes when action or trigger types change
// onProcessesCreated(newProcesses);
// prevProcessesRef.current = newProcesses;
// }, [
// pathsDependency,
// pathsDependency, // This now includes action and trigger types
// onProcessesCreated,
// convertedPaths,
// createProcessesFromPaths,
@ -423,10 +471,19 @@ export interface PointAction {
isUsed: boolean;
}
export interface PointTrigger {
uuid: string;
bufferTime: number;
name: string;
type: string;
isUsed: boolean;
}
export interface PathPoint {
uuid: string;
position: [number, number, number];
actions: PointAction[];
triggers: PointTrigger[];
connections: {
targets: Array<{ modelUUID: string }>;
};
@ -438,6 +495,7 @@ export interface SimulationPath {
points: PathPoint[];
pathPosition: [number, number, number];
speed?: number;
isplaying: boolean;
}
export interface Process {
@ -445,7 +503,9 @@ export interface Process {
paths: SimulationPath[];
animationPath: THREE.Vector3[];
pointActions: PointAction[][];
pointTriggers: PointTrigger[][];
speed: number;
isplaying: boolean;
}
interface ProcessCreatorProps {
@ -458,11 +518,16 @@ function convertToSimulationPath(
): SimulationPath {
const { modeluuid } = path;
// Simplified normalizeAction function that preserves exact original properties
// Normalized action handler
const normalizeAction = (action: any): PointAction => {
return { ...action }; // Return exact copy with no modifications
};
// Normalized trigger handler
const normalizeTrigger = (trigger: any): PointTrigger => {
return { ...trigger }; // Return exact copy with no modifications
};
if (path.type === "Conveyor") {
return {
type: path.type,
@ -470,7 +535,16 @@ function convertToSimulationPath(
points: path.points.map((point) => ({
uuid: point.uuid,
position: point.position,
actions: point.actions.map(normalizeAction), // Preserve exact actions
actions: Array.isArray(point.actions)
? point.actions.map(normalizeAction)
: point.actions
? [normalizeAction(point.actions)]
: [],
triggers: Array.isArray(point.triggers)
? point.triggers.map(normalizeTrigger)
: point.triggers
? [normalizeTrigger(point.triggers)]
: [],
connections: {
targets: point.connections.targets.map((target) => ({
modelUUID: target.modelUUID,
@ -482,8 +556,10 @@ function convertToSimulationPath(
typeof path.speed === "string"
? parseFloat(path.speed) || 1
: path.speed || 1,
isplaying: false, // Added missing property
};
} else {
// For vehicle paths, handle the case where triggers might not exist
return {
type: path.type,
modeluuid,
@ -493,7 +569,10 @@ function convertToSimulationPath(
position: path.points.position,
actions: Array.isArray(path.points.actions)
? path.points.actions.map(normalizeAction)
: [normalizeAction(path.points.actions)],
: path.points.actions
? [normalizeAction(path.points.actions)]
: [],
triggers: [],
connections: {
targets: path.points.connections.targets.map((target) => ({
modelUUID: target.modelUUID,
@ -503,26 +582,20 @@ function convertToSimulationPath(
],
pathPosition: path.position,
speed: path.points.speed || 1,
isplaying: false,
};
}
}
// Custom shallow comparison for arrays
const areArraysEqual = (a: any[], b: any[]) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
};
// Helper function to create an empty process
const createEmptyProcess = (): Process => ({
id: `process-${Math.random().toString(36).substring(2, 11)}`,
paths: [],
animationPath: [],
pointActions: [],
pointTriggers: [], // Added point triggers array
speed: 1,
isplaying: false,
});
// Enhanced connection checking function
@ -631,19 +704,23 @@ export function useProcessCreation() {
const animationPath: THREE.Vector3[] = [];
const pointActions: PointAction[][] = [];
const pointTriggers: PointTrigger[][] = []; // Added point triggers collection
const processSpeed = paths[0]?.speed || 1;
for (const path of paths) {
for (const point of path.points) {
const obj = scene.getObjectByProperty("uuid", point.uuid);
if (!obj) {
console.warn(`Object with UUID ${point.uuid} not found in scene`);
continue;
}
if (path.type === "Conveyor") {
const obj = scene.getObjectByProperty("uuid", point.uuid);
if (!obj) {
console.warn(`Object with UUID ${point.uuid} not found in scene`);
continue;
}
const position = obj.getWorldPosition(new THREE.Vector3());
animationPath.push(position.clone());
pointActions.push(point.actions);
const position = obj.getWorldPosition(new THREE.Vector3());
animationPath.push(position.clone());
pointActions.push(point.actions);
pointTriggers.push(point.triggers); // Collect triggers for each point
}
}
}
@ -652,7 +729,9 @@ export function useProcessCreation() {
paths,
animationPath,
pointActions,
pointTriggers,
speed: processSpeed,
isplaying: false,
};
},
[scene]
@ -755,7 +834,7 @@ const ProcessCreator: React.FC<ProcessCreatorProps> = React.memo(
);
}, [simulationStates]);
// Enhanced dependency tracking that includes action types
// Enhanced dependency tracking that includes action and trigger types
const pathsDependency = useMemo(() => {
if (!convertedPaths) return null;
return convertedPaths.map((path) => ({
@ -766,11 +845,20 @@ const ProcessCreator: React.FC<ProcessCreatorProps> = React.memo(
point.actions.map((action) => `${index}-${action.type}`).join("|")
)
.join(","),
// Track all trigger types for each point
triggerSignature: path.points
.map((point, index) =>
point.triggers
.map((trigger) => `${index}-${trigger.type}`)
.join("|")
)
.join(","),
connections: path.points
.flatMap((p: PathPoint) =>
p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID)
)
.join(","),
isplaying: false,
}));
}, [convertedPaths]);
@ -785,15 +873,15 @@ const ProcessCreator: React.FC<ProcessCreatorProps> = React.memo(
}
// Always regenerate processes if the pathsDependency has changed
// This ensures action type changes will be detected
// This ensures action and trigger type changes will be detected
const newProcesses = createProcessesFromPaths(convertedPaths);
prevPathsRef.current = convertedPaths;
// Always update processes when action types change
// Always update processes when action or trigger types change
onProcessesCreated(newProcesses);
prevProcessesRef.current = newProcesses;
}, [
pathsDependency, // This now includes action types
pathsDependency, // This now includes action and trigger types
onProcessesCreated,
convertedPaths,
createProcessesFromPaths,

View File

@ -0,0 +1,58 @@
import React, { useMemo } from "react";
import * as THREE from "three";
import { GLTF } from "three-stdlib";
import { SpawnedObject } from "./types";
interface ProcessObjectProps {
objectId: string;
obj: SpawnedObject;
renderAs?: "box" | "custom";
gltf?: GLTF;
}
const ProcessObject: React.FC<ProcessObjectProps> = ({
objectId,
obj,
renderAs = "custom",
gltf,
}) => {
const renderedObject = useMemo(() => {
if (renderAs === "box") {
return (
<mesh
key={objectId}
ref={obj.ref as React.RefObject<THREE.Mesh>}
material={obj.material}
position={obj.position}
>
<boxGeometry args={[1, 1, 1]} />
</mesh>
);
}
if (gltf?.scene) {
const clonedScene = gltf.scene.clone();
clonedScene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = obj.material;
}
});
return (
<group
key={objectId}
ref={obj.ref as React.RefObject<THREE.Group>}
position={obj.position}
>
<primitive object={clonedScene} />
</group>
);
}
return null;
}, [objectId, obj, renderAs, gltf]);
return renderedObject;
};
export default ProcessObject;

View File

@ -1,114 +0,0 @@
import React, { useRef, useEffect } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
import { GLTF } from "three-stdlib";
import { Box3Helper } from "three";
import { SpawnedObject, ProcessData } from "./types";
interface ProcessObjectRendererProps {
objectId: string;
object: SpawnedObject;
process: ProcessData;
gltf: GLTF;
showBoundingBox?: boolean;
}
export const ProcessObjectRenderer: React.FC<ProcessObjectRendererProps> = ({
objectId,
object,
process,
gltf,
showBoundingBox = false,
}) => {
const meshRef = useRef<THREE.Mesh>(null);
const boxHelperRef = useRef<THREE.Box3Helper | null>(null);
const boundingBoxRef = useRef<THREE.Box3>(new THREE.Box3());
// Issue 1: Can't assign to ref.current as it's read-only
useEffect(() => {
if (object.ref && meshRef.current) {
// Instead of direct assignment, we need to store the mesh reference another way
// Option 1: If you can modify the SpawnedObject interface, add a setMesh method
if (typeof (object as any).setMesh === 'function') {
(object as any).setMesh(meshRef.current);
}
// Option 2: Store the mesh in a property that isn't ref.current
// This requires modifying your SpawnedObject interface to include this property
(object as any).meshInstance = meshRef.current;
// Option 3: If you need to maintain compatibility, you could use Object.defineProperty
// But this is a hack and not recommended
// Object.defineProperty(object.ref, 'current', { value: meshRef.current, writable: true });
}
}, [object.ref]);
// Create a bounding box helper for visualization
useFrame(() => {
if (meshRef.current && showBoundingBox) {
// Update the bounding box to match the mesh position
if (!boxHelperRef.current) {
// Get the size of the mesh
const size = new THREE.Vector3(1, 1, 1);
// If the mesh has geometry, use its dimensions
if (meshRef.current.geometry) {
const box = new THREE.Box3().setFromObject(meshRef.current);
box.getSize(size);
}
// Create a new bounding box centered on the mesh
boundingBoxRef.current = new THREE.Box3().setFromCenterAndSize(
meshRef.current.position,
size
);
// Create a helper to visualize the box
boxHelperRef.current = new Box3Helper(
boundingBoxRef.current,
new THREE.Color(0xff0000)
);
// Add the helper to the scene
meshRef.current.parent?.add(boxHelperRef.current);
} else {
// Update the box position to match the mesh
boundingBoxRef.current.setFromCenterAndSize(
meshRef.current.position,
boundingBoxRef.current.getSize(new THREE.Vector3())
);
// Force the helper to update
boxHelperRef.current.updateMatrixWorld(true);
}
}
});
if (gltf?.scene) {
return (
<primitive
ref={meshRef}
object={gltf.scene.clone()}
position={[0, 0, 0]}
material={object.material}
/>
);
}
// Issue 2: Material color type problem
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial
// Fix the color property access
color={
object.material && 'color' in object.material
? (object.material as THREE.MeshStandardMaterial).color
: "#00ff00"
}
metalness={0.5}
roughness={0.3}
/>
</mesh>
);
};

View File

@ -1,79 +1,86 @@
import * as THREE from "three";
import React from "react";
export interface ProcessPoint {
export interface Trigger {
uuid: string;
position: number[];
actions?: PointAction[];
name: string;
type: string;
bufferTime: number;
isUsed: boolean;
}
export interface PointAction {
type: string;
uuid: string;
name: string;
type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap";
objectType: string;
material: string;
delay: string | number;
spawnInterval: string | number;
isUsed: boolean;
spawnInterval?: number | string;
material?: string;
delay?: number | string;
hitCount?: number;
}
export interface ProcessPoint {
uuid: string;
position: number[];
rotation: number[];
actions: PointAction[];
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
triggers?: Trigger[];
}
export interface ProcessPath {
modeluuid: string;
modelName: string;
points: ProcessPoint[];
pathPosition: number[];
pathRotation: number[];
speed: number;
type: "Conveyor" | "Vehicle";
isplaying: boolean
}
export interface ProcessData {
id: string;
name: string;
paths?: {
points?: ProcessPoint[];
}[];
animationPath?: { x: number; y: number; z: number }[];
speed?: number;
paths: ProcessPath[];
animationPath: { x: number; y: number; z: number }[];
pointActions: PointAction[][];
speed: number;
customMaterials?: Record<string, THREE.Material>;
renderAs?: "box" | "custom";
pointTriggers: [];
}
export interface AnimationState {
currentIndex: number;
progress: number;
isAnimating: boolean;
speed: number;
isDelaying: boolean;
delayStartTime: number;
currentDelayDuration: number;
delayComplete: boolean;
currentPathIndex: number;
}
export interface SpawnedObject {
ref: React.RefObject<THREE.Group | THREE.Mesh>;
state: AnimationState;
visible: boolean;
material: THREE.Material;
spawnTime: number;
currentMaterialType: string;
position: THREE.Vector3;
}
export interface ProcessAnimationState {
spawnedObjects: Record<string, SpawnedObject | SpawnedObjectWithCollision>;
spawnedObjects: { [objectId: string]: SpawnedObject };
nextSpawnTime: number;
objectIdCounter: number;
isProcessDelaying: boolean;
processDelayStartTime: number;
processDelayDuration: number;
isCollisionPaused?: boolean;
hasSpawnedZeroIntervalObject?: boolean;
}
export interface SpawnedObject {
ref: React.RefObject<THREE.Object3D>;
state: {
currentIndex: number;
progress: number;
isAnimating: boolean;
speed: number;
isDelaying: boolean;
delayStartTime: number;
currentDelayDuration: number;
delayComplete: boolean;
currentPathIndex: number;
};
visible: boolean;
material: THREE.Material;
currentMaterialType: string;
spawnTime: number;
position: THREE.Vector3;
collision?: {
boundingBox: THREE.Box3;
isColliding: boolean;
colliding: boolean; // Added this property
};
}
// For use in your processAnimator.tsx
// Update the CollisionState interface to include all required properties
interface CollisionState {
boundingBox: THREE.Box3;
isColliding: boolean;
colliding: boolean; // This was missing
collidingWith: string[];
}
export interface SpawnedObjectWithCollision extends SpawnedObject {
collision: {
boundingBox: THREE.Box3;
isColliding: boolean;
colliding: boolean;
collidingWith: string[];
};
}

View File

@ -0,0 +1,540 @@
import { useCallback, useEffect, useRef, useState } from "react";
import * as THREE from "three";
import {
ProcessData,
ProcessAnimationState,
SpawnedObject,
AnimationState,
ProcessPoint,
PointAction,
Trigger,
} from "./types";
import {
useAnimationPlaySpeed,
usePauseButtonStore,
usePlayButtonStore,
useResetButtonStore,
} from "../../../store/usePlayButtonStore";
import { usePlayAgv } from "../../../store/store";
// Enhanced ProcessAnimationState with trigger tracking
interface EnhancedProcessAnimationState extends ProcessAnimationState {
triggerCounts: Record<string, number>;
triggerLogs: Array<{
timestamp: number;
pointId: string;
objectId: string;
triggerId: string;
}>;
}
interface ProcessContainerProps {
processes: ProcessData[];
setProcesses: React.Dispatch<React.SetStateAction<any[]>>;
agvRef: any;
}
interface PlayAgvState {
playAgv: Record<string, any>;
setPlayAgv: (data: any) => void;
}
export const useProcessAnimation = (
processes: ProcessData[],
setProcesses: React.Dispatch<React.SetStateAction<any[]>>,
agvRef: any
) => {
// State and refs initialization
const { isPlaying, setIsPlaying } = usePlayButtonStore();
const { isPaused, setIsPaused } = usePauseButtonStore();
const { isReset, setReset } = useResetButtonStore();
const debugRef = useRef<boolean>(false);
const clockRef = useRef<THREE.Clock>(new THREE.Clock());
const pauseTimeRef = useRef<number>(0);
const elapsedBeforePauseRef = useRef<number>(0);
const animationStatesRef = useRef<
Record<string, EnhancedProcessAnimationState>
>({});
const { speed } = useAnimationPlaySpeed();
const prevIsPlaying = useRef<boolean | null>(null);
const [internalResetFlag, setInternalResetFlag] = useState(false);
const [animationStates, setAnimationStates] = useState<
Record<string, EnhancedProcessAnimationState>
>({});
const speedRef = useRef<number>(speed);
const { PlayAgv, setPlayAgv } = usePlayAgv();
// Effect hooks
useEffect(() => {
speedRef.current = speed;
}, [speed]);
useEffect(() => {
if (prevIsPlaying.current !== null) {
setAnimationStates({});
}
prevIsPlaying.current = isPlaying;
}, [isPlaying]);
useEffect(() => {
animationStatesRef.current = animationStates;
}, [animationStates]);
// Reset handler
useEffect(() => {
if (isReset) {
setInternalResetFlag(true);
setIsPlaying(false);
setIsPaused(false);
setAnimationStates({});
animationStatesRef.current = {};
clockRef.current = new THREE.Clock();
elapsedBeforePauseRef.current = 0;
pauseTimeRef.current = 0;
setReset(false);
setTimeout(() => {
setInternalResetFlag(false);
setIsPlaying(true);
}, 0);
}
}, [isReset, setReset, setIsPlaying, setIsPaused]);
// Pause handler
useEffect(() => {
if (isPaused) {
pauseTimeRef.current = clockRef.current.getElapsedTime();
} else if (pauseTimeRef.current > 0) {
const pausedDuration =
clockRef.current.getElapsedTime() - pauseTimeRef.current;
elapsedBeforePauseRef.current += pausedDuration;
}
}, [isPaused]);
// Initialize animation states with trigger tracking
useEffect(() => {
if (isPlaying && !internalResetFlag) {
const newStates: Record<string, EnhancedProcessAnimationState> = {};
processes.forEach((process) => {
const triggerCounts: Record<string, number> = {};
// Initialize trigger counts for all On-Hit triggers
process.paths?.forEach((path) => {
path.points?.forEach((point) => {
point.triggers?.forEach((trigger: Trigger) => {
if (trigger.type === "On-Hit" && trigger.isUsed) {
triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0;
}
});
});
});
newStates[process.id] = {
spawnedObjects: {},
nextSpawnTime: 0,
objectIdCounter: 0,
isProcessDelaying: false,
processDelayStartTime: 0,
processDelayDuration: 0,
triggerCounts,
triggerLogs: [],
};
});
setAnimationStates(newStates);
animationStatesRef.current = newStates;
clockRef.current.start();
}
}, [isPlaying, processes, internalResetFlag]);
// Helper functions
const findSpawnPoint = (process: ProcessData): ProcessPoint | null => {
for (const path of process.paths || []) {
for (const point of path.points || []) {
const spawnAction = point.actions?.find(
(a) => a.isUsed && a.type === "Spawn"
);
if (spawnAction) {
return point;
}
}
}
return null;
};
const findAnimationPathPoint = (
process: ProcessData,
spawnPoint: ProcessPoint
): THREE.Vector3 => {
if (process.animationPath && process.animationPath.length > 0) {
let pointIndex = 0;
for (const path of process.paths || []) {
for (let i = 0; i < (path.points?.length || 0); i++) {
const point = path.points?.[i];
if (point && point.uuid === spawnPoint.uuid) {
if (process.animationPath[pointIndex]) {
const p = process.animationPath[pointIndex];
return new THREE.Vector3(p.x, p.y, p.z);
}
}
pointIndex++;
}
}
}
return new THREE.Vector3(
spawnPoint.position[0],
spawnPoint.position[1],
spawnPoint.position[2]
);
};
// Optimized object creation
const createSpawnedObject = useCallback(
(
process: ProcessData,
currentTime: number,
materialType: string,
spawnPoint: ProcessPoint,
baseMaterials: Record<string, THREE.Material>
): SpawnedObject => {
const processMaterials = {
...baseMaterials,
...(process.customMaterials || {}),
};
const spawnPosition = findAnimationPathPoint(process, spawnPoint);
const material =
processMaterials[materialType as keyof typeof processMaterials] ||
baseMaterials.Default;
return {
ref: { current: null },
state: {
currentIndex: 0,
progress: 0,
isAnimating: true,
speed: process.speed || 1,
isDelaying: false,
delayStartTime: 0,
currentDelayDuration: 0,
delayComplete: false,
currentPathIndex: 0,
},
visible: true,
material: material,
currentMaterialType: materialType,
spawnTime: currentTime,
position: spawnPosition,
};
},
[]
);
// Material handling
const handleMaterialSwap = useCallback(
(
processId: string,
objectId: string,
materialType: string,
processes: ProcessData[],
baseMaterials: Record<string, THREE.Material>
) => {
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState || !processState.spawnedObjects[objectId])
return prev;
const process = processes.find((p) => p.id === processId);
if (!process) return prev;
const processMaterials = {
...baseMaterials,
...(process.customMaterials || {}),
};
const newMaterial =
processMaterials[materialType as keyof typeof processMaterials];
if (!newMaterial) return prev;
return {
...prev,
[processId]: {
...processState,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: {
...processState.spawnedObjects[objectId],
material: newMaterial,
currentMaterialType: materialType,
},
},
},
};
});
},
[]
);
// Point action handler with trigger counting
const handlePointActions = useCallback(
(
processId: string,
objectId: string,
actions: PointAction[] = [],
currentTime: number,
processes: ProcessData[],
baseMaterials: Record<string, THREE.Material>
): boolean => {
let shouldStopAnimation = false;
actions.forEach((action) => {
if (!action.isUsed) return;
switch (action.type) {
case "Delay":
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState || processState.isProcessDelaying) return prev;
const delayDuration =
typeof action.delay === "number"
? action.delay
: parseFloat(action.delay as string) || 0;
if (delayDuration > 0) {
return {
...prev,
[processId]: {
...processState,
isProcessDelaying: true,
processDelayStartTime: currentTime,
processDelayDuration: delayDuration,
spawnedObjects: {
...processState.spawnedObjects,
[objectId]: {
...processState.spawnedObjects[objectId],
state: {
...processState.spawnedObjects[objectId].state,
isAnimating: false,
isDelaying: true,
delayStartTime: currentTime,
currentDelayDuration: delayDuration,
delayComplete: false,
},
},
},
},
};
}
return prev;
});
shouldStopAnimation = true;
break;
case "Despawn":
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState) return prev;
const newSpawnedObjects = { ...processState.spawnedObjects };
delete newSpawnedObjects[objectId];
return {
...prev,
[processId]: {
...processState,
spawnedObjects: newSpawnedObjects,
},
};
});
shouldStopAnimation = true;
break;
case "Swap":
if (action.material) {
handleMaterialSwap(
processId,
objectId,
action.material,
processes,
baseMaterials
);
}
break;
default:
break;
}
});
return shouldStopAnimation;
},
[handleMaterialSwap]
);
// Trigger counting system
const checkAndCountTriggers = useCallback(
(
processId: string,
objectId: string,
currentPointIndex: number,
processes: ProcessData[],
currentTime: number
) => {
setAnimationStates((prev) => {
const processState = prev[processId];
if (!processState) return prev;
const process = processes.find((p) => p.id === processId);
if (!process) return prev;
const point = getPointDataForAnimationIndex(process, currentPointIndex);
if (!point?.triggers) return prev;
const onHitTriggers = point.triggers.filter(
(t: Trigger) => t.type === "On-Hit" && t.isUsed
);
if (onHitTriggers.length === 0) return prev;
const newTriggerCounts = { ...processState.triggerCounts };
const newTriggerLogs = [...processState.triggerLogs];
let shouldLog = false;
// Update counts for all valid triggers
onHitTriggers.forEach((trigger: Trigger) => {
const triggerKey = `${point.uuid}-${trigger.uuid}`;
newTriggerCounts[triggerKey] =
(newTriggerCounts[triggerKey] || 0) + 1;
shouldLog = true;
newTriggerLogs.push({
timestamp: currentTime,
pointId: point.uuid,
objectId,
triggerId: trigger.uuid,
});
});
const processTotalHits = Object.values(newTriggerCounts).reduce(
(a, b) => a + b,
0
);
if (shouldLog) {
const vehiclePaths = process.paths.filter(
(path) => path.type === "Vehicle"
);
vehiclePaths.forEach((vehiclePath) => {
if (vehiclePath.points?.length > 0) {
const vehiclePoint = vehiclePath.points[0];
const action = vehiclePoint.actions?.[0];
let expectedHitCount = action?.hitCount;
if (expectedHitCount !== undefined) {
const vehicleId = vehiclePath.modeluuid;
let vehicleEntry = agvRef.current.find(
(v: any) =>
v.vehicleId === vehicleId && v.processId === processId
);
if (!vehicleEntry) {
vehicleEntry = {
processId,
vehicleId,
expectedHitCount,
isplaying: false,
hitCount: 0, // Initialize hitCount
};
agvRef.current.push(vehicleEntry);
}
// Increment expectedHitCount if not playing
if (!vehicleEntry.isplaying) {
vehicleEntry.expectedHitCount = processTotalHits;
}
// Set vehicle's hitCount to the processTotalHits
vehicleEntry.hitCount = processTotalHits;
vehicleEntry.lastUpdated = currentTime;
// If hitCount matches expectedHitCount, set isplaying to true
if (vehicleEntry.hitCount >= vehicleEntry.expectedHitCount) {
vehicleEntry.isplaying = true;
}
}
}
});
}
return {
...prev,
[processId]: {
...processState,
triggerCounts: newTriggerCounts,
triggerLogs: newTriggerLogs,
totalHits: processTotalHits,
},
};
});
},
[]
);
// Utility functions
const hasNonInheritActions = useCallback(
(actions: PointAction[] = []): boolean => {
return actions.some(
(action) => action.isUsed && action.type !== "Inherit"
);
},
[]
);
const getPointDataForAnimationIndex = useCallback(
(process: ProcessData, index: number): ProcessPoint | null => {
if (!process.paths) return null;
let cumulativePoints = 0;
for (const path of process.paths) {
const pointCount = path.points?.length || 0;
if (index < cumulativePoints + pointCount) {
const pointIndex = index - cumulativePoints;
return path.points?.[pointIndex] || null;
}
cumulativePoints += pointCount;
}
return null;
},
[]
);
const getTriggerCounts = useCallback((processId: string) => {
return animationStatesRef.current[processId]?.triggerCounts || {};
}, []);
const getTriggerLogs = useCallback((processId: string) => {
return animationStatesRef.current[processId]?.triggerLogs || [];
}, []);
return {
animationStates,
setAnimationStates,
clockRef,
elapsedBeforePauseRef,
speedRef,
debugRef,
findSpawnPoint,
createSpawnedObject,
handlePointActions,
hasNonInheritActions,
getPointDataForAnimationIndex,
checkAndCountTriggers,
getTriggerCounts,
getTriggerLogs,
processes,
};
};

View File

@ -16,23 +16,9 @@ function Simulation() {
const { activeModule } = useModuleStore();
const pathsGroupRef = useRef() as React.MutableRefObject<THREE.Group>;
const { simulationStates, setSimulationStates } = useSimulationStates();
const [processes, setProcesses] = useState([]);
useEffect(() => {
// console.log('simulationStates: ', simulationStates);
}, [simulationStates]);
// useEffect(() => {
// if (selectedActionSphere) {
// console.log('selectedActionSphere: ', selectedActionSphere);
// }
// }, [selectedActionSphere]);
// useEffect(() => {
// if (selectedPath) {
// console.log('selectedPath: ', selectedPath);
// }
// }, [selectedPath]);
const [processes, setProcesses] = useState<any[]>([]);
const agvRef = useRef([]);
const MaterialRef = useRef([]);
return (
<>
@ -41,8 +27,17 @@ function Simulation() {
<>
<PathCreation pathsGroupRef={pathsGroupRef} />
<PathConnector pathsGroupRef={pathsGroupRef} />
<ProcessContainer />
<Agv />
<ProcessContainer
processes={processes}
setProcesses={setProcesses}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
<Agv
processes={processes}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
</>
)}
</>

View File

@ -70,9 +70,8 @@ export const useZones = create<any>((set: any) => ({
zones: [],
setZones: (callback: any) =>
set((state: any) => ({
zones:
typeof callback === 'function' ? callback(state.zones) : callback
}))
zones: typeof callback === "function" ? callback(state.zones) : callback,
})),
}));
interface ZonePointsState {
@ -314,9 +313,7 @@ export const useActiveUsers = create<any>((set: any) => ({
setActiveUsers: (callback: (prev: any[]) => any[] | any[]) =>
set((state: { activeUsers: any[] }) => ({
activeUsers:
typeof callback === "function"
? callback(state.activeUsers)
: callback,
typeof callback === "function" ? callback(state.activeUsers) : callback,
})),
}));
@ -356,12 +353,33 @@ export const useSelectedPath = create<any>((set: any) => ({
}));
interface SimulationPathsStore {
simulationStates: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[];
simulationStates: (
| Types.ConveyorEventsSchema
| Types.VehicleEventsSchema
| Types.StaticMachineEventsSchema
| Types.ArmBotEventsSchema
)[];
setSimulationStates: (
paths:
| (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[]
| ((prev: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[]
) => (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[])
| (
| Types.ConveyorEventsSchema
| Types.VehicleEventsSchema
| Types.StaticMachineEventsSchema
| Types.ArmBotEventsSchema
)[]
| ((
prev: (
| Types.ConveyorEventsSchema
| Types.VehicleEventsSchema
| Types.StaticMachineEventsSchema
| Types.ArmBotEventsSchema
)[]
) => (
| Types.ConveyorEventsSchema
| Types.VehicleEventsSchema
| Types.StaticMachineEventsSchema
| Types.ArmBotEventsSchema
)[])
) => void;
}
@ -372,7 +390,7 @@ export const useSimulationStates = create<SimulationPathsStore>((set) => ({
simulationStates:
typeof paths === "function" ? paths(state.simulationStates) : paths,
})),
}))
}));
export const useNavMesh = create<any>((set: any) => ({
navMesh: null,
@ -456,6 +474,11 @@ export const useTileDistance = create<any>((set: any) => ({
})),
}));
export const usePlayAgv = create<any>((set, get) => ({
PlayAgv: [],
setPlayAgv: (updateFn: (prev: any[]) => any[]) =>
set({ PlayAgv: updateFn(get().PlayAgv) }),
}));
// Define the Asset type
type Asset = {
id: string;
@ -474,4 +497,4 @@ type ZoneAssetState = {
export const useZoneAssetId = create<ZoneAssetState>((set) => ({
zoneAssetId: null,
setZoneAssetId: (asset) => set({ zoneAssetId: asset }),
}));
}));

View File

@ -1,13 +1,13 @@
// Importing core classes and types from THREE.js and @react-three/fiber
import * as THREE from "three";
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { DragControls } from 'three/examples/jsm/controls/DragControls';
import { IntersectionEvent } from '@react-three/fiber/dist/declarations/src/core/events';
import { TransformControls } from "three/examples/jsm/controls/TransformControls";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { DragControls } from "three/examples/jsm/controls/DragControls";
import { IntersectionEvent } from "@react-three/fiber/dist/declarations/src/core/events";
import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events";
import { RootState } from "@react-three/fiber";
import { CSM } from "three/examples/jsm/csm/CSM";
import { CSMHelper } from 'three/examples/jsm/csm/CSMHelper';
import { CSMHelper } from "three/examples/jsm/csm/CSMHelper";
import { CameraControls } from "@react-three/drei";
/** Core THREE.js and React-Fiber Event Types **/
@ -15,7 +15,6 @@ import { CameraControls } from "@react-three/drei";
// Event type specific to pointer events in @react-three/fiber
export type ThreeEvent = ThreeEvent<PointerEvent>;
/** Vector and Reference Types **/
// 2D Vector type from THREE.js
@ -33,7 +32,6 @@ export type RefVector3 = React.MutableRefObject<THREE.Vector3 | null>;
// Quaternion type for rotations, using the base structure from THREE.js
export type QuaternionType = THREE.QuaternionLike;
/** Basic Object Types for Scene Management **/
// THREE.js mesh object
@ -49,7 +47,9 @@ export type Shape = THREE.Shape;
export type IntersectionEvent = THREE.Intersection;
// Array type for intersections with objects in the scene
export type IntersectsType = THREE.Intersection<THREE.Object3D<THREE.Object3DEventMap>>[];
export type IntersectsType = THREE.Intersection<
THREE.Object3D<THREE.Object3DEventMap>
>[];
// Event type for mesh interactions
export type MeshEvent = IntersectionEvent<MouseEvent>;
@ -60,7 +60,6 @@ export type DragEvent = DragEvent;
// Generic type for user data attached to objects
export type UserData = any;
/** React Mutable References for Scene Objects **/
// Mutable reference to the scene, used in React-based projects
@ -92,7 +91,6 @@ export type Vector3Array = THREE.Vector3[];
export type DragControl = DragControls | null;
export type RefDragControl = React.MutableRefObject<DragControls | null>;
/** Primitive Types with Mutable References **/
export type String = string;
@ -109,15 +107,15 @@ export type NumberArray = number[];
export type RefRaycaster = React.MutableRefObject<THREE.Raycaster>;
// Camera reference, supporting both perspective and basic cameras
export type RefCamera = React.MutableRefObject<THREE.Camera | THREE.PerspectiveCamera>;
export type RefCamera = React.MutableRefObject<
THREE.Camera | THREE.PerspectiveCamera
>;
/** Three.js Root State Management **/
// Root state of the @react-three/fiber instance, providing context of the scene
export type ThreeState = RootState;
/** Point and Line Types for Spatial Geometry **/
// Defines a point in 3D space with metadata for unique identification
@ -132,11 +130,16 @@ export type RefLine = React.MutableRefObject<Line | [Point] | []>;
// Collection of lines for structured geometry
export type Lines = Array<Line>;
/** Wall and Room Types for 3D Space Management **/
// Defines a wall with its geometry, position, rotation, material, and layer information
export type Wall = [THREE.ExtrudeGeometry, [number, number, number], [number, number, number], string, number];
export type Wall = [
THREE.ExtrudeGeometry,
[number, number, number],
[number, number, number],
string,
number
];
// Collection of walls, useful in scene construction
export type Walls = Array<Wall>;
@ -145,15 +148,22 @@ export type Walls = Array<Wall>;
export type RefWalls = React.MutableRefObject<Walls>;
// Room type, containing coordinates and layer metadata for spatial management
export type Rooms = Array<{ coordinates: Array<{ position: THREE.Vector3, uuid: string }>, layer: number }>;
export type Rooms = Array<{
coordinates: Array<{ position: THREE.Vector3; uuid: string }>;
layer: number;
}>;
// Reference for room objects, enabling updates within React components
export type RefRooms = React.MutableRefObject<Array<{ coordinates: Array<{ position: THREE.Vector3, uuid: string }>, layer: number }>>;
export type RefRooms = React.MutableRefObject<
Array<{
coordinates: Array<{ position: THREE.Vector3; uuid: string }>;
layer: number;
}>
>;
// Reference for lines, supporting React-based state changes
export type RefLines = React.MutableRefObject<Lines>;
/** Floor Line Types for Layered Structures **/
// Floor line type for single lines on the floor level
@ -168,18 +178,16 @@ export type OnlyFloorLines = Array<Lines>;
// Reference for multi-level floor lines, allowing structured updates
export type RefOnlyFloorLines = React.MutableRefObject<OnlyFloorLines>;
/** GeoJSON Line Integration **/
// Structure for representing GeoJSON lines, integrating external data sources
export type GeoJsonLine = {
line: any;
uuids: [string, string];
layer: number;
type: string;
line: any;
uuids: [string, string];
layer: number;
type: string;
};
/** State Management Types for React Components **/
// Dispatch types for number and boolean states, commonly used in React hooks
@ -189,68 +197,71 @@ export type BooleanState = React.Dispatch<React.SetStateAction<boolean>>;
// Mutable reference for TubeGeometry, allowing dynamic geometry updates
export type RefTubeGeometry = React.MutableRefObject<THREE.TubeGeometry | null>;
/** Floor Item Configuration **/
// Type for individual items placed on the floor, with positioning and rotation metadata
export type FloorItemType = {
modeluuid: string;
modelname: string;
position: [number, number, number];
rotation: { x: number; y: number; z: number };
modelfileID: string;
isLocked: boolean;
isVisible: boolean;
modeluuid: string;
modelname: string;
position: [number, number, number];
rotation: { x: number; y: number; z: number };
modelfileID: string;
isLocked: boolean;
isVisible: boolean;
};
// Array of floor items for managing multiple objects on the floor
export type FloorItems = Array<FloorItemType>;
// Dispatch type for setting floor item state in React
export type setFloorItemSetState = React.Dispatch<React.SetStateAction<FloorItems | null | undefined>>;
export type setFloorItemSetState = React.Dispatch<
React.SetStateAction<FloorItems | null | undefined>
>;
/** Asset Configuration for Loading and Positioning **/
// Configuration for assets, allowing model URLs, scaling, positioning, and types
interface AssetConfiguration {
modelUrl: string;
scale?: [number, number, number];
csgscale?: [number, number, number];
csgposition?: [number, number, number];
positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number;
type?: "Fixed-Move" | "Free-Move";
modelUrl: string;
scale?: [number, number, number];
csgscale?: [number, number, number];
csgposition?: [number, number, number];
positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number;
type?: "Fixed-Move" | "Free-Move";
}
// Collection of asset configurations, keyed by unique identifiers
export type AssetConfigurations = {
[key: string]: AssetConfiguration;
[key: string]: AssetConfiguration;
};
/** Wall Item Configuration **/
// Configuration for wall items, including model, scale, position, and rotation
interface WallItem {
type: "Fixed-Move" | "Free-Move" | undefined;
model?: THREE.Group;
modeluuid?: string
modelname?: string;
scale?: [number, number, number];
csgscale?: [number, number, number];
csgposition?: [number, number, number];
position?: [number, number, number];
quaternion?: Types.QuaternionType;
type: "Fixed-Move" | "Free-Move" | undefined;
model?: THREE.Group;
modeluuid?: string;
modelname?: string;
scale?: [number, number, number];
csgscale?: [number, number, number];
csgposition?: [number, number, number];
position?: [number, number, number];
quaternion?: Types.QuaternionType;
}
// Collection of wall items, allowing for multiple items in a scene
export type wallItems = Array<WallItem>;
// Dispatch for setting wall item state in React
export type setWallItemSetState = React.Dispatch<React.SetStateAction<WallItem[]>>;
export type setWallItemSetState = React.Dispatch<
React.SetStateAction<WallItem[]>
>;
// Dispatch for setting vector3 state in React
export type setVector3State = React.Dispatch<React.SetStateAction<THREE.Vector3>>;
export type setVector3State = React.Dispatch<
React.SetStateAction<THREE.Vector3>
>;
// Dispatch for setting euler state in React
export type setEulerState = React.Dispatch<React.SetStateAction<THREE.Euler>>;
@ -258,147 +269,250 @@ export type setEulerState = React.Dispatch<React.SetStateAction<THREE.Euler>>;
// Reference type for wall items, allowing direct access to the mutable array
export type RefWallItems = React.MutableRefObject<WallItem[]>;
/** Wall and Item Selection State Management **/
// State management for selecting, removing, and indexing wall items
export type setRemoveLayerSetState = (layer: number | null) => void;
export type setSelectedWallItemSetState = (item: THREE.Object3D | null) => void;
export type setSelectedFloorItemSetState = (item: THREE.Object3D | null) => void;
export type setSelectedFloorItemSetState = (
item: THREE.Object3D | null
) => void;
export type setSelectedItemsIndexSetState = (index: number | null) => void;
export type RefCSM = React.MutableRefObject<CSM>;
export type RefCSMHelper = React.MutableRefObject<CSMHelper>;
interface PathConnection {
fromModelUUID: string;
fromUUID: string;
toConnections: {
toModelUUID: string;
toUUID: string;
}[];
fromModelUUID: string;
fromUUID: string;
toConnections: {
toModelUUID: string;
toUUID: string;
}[];
}
interface ConnectionStore {
connections: PathConnection[];
setConnections: (connections: PathConnection[]) => void;
addConnection: (newConnection: PathConnection) => void;
removeConnection: (fromUUID: string, toUUID: string) => void;
connections: PathConnection[];
setConnections: (connections: PathConnection[]) => void;
addConnection: (newConnection: PathConnection) => void;
removeConnection: (fromUUID: string, toUUID: string) => void;
}
interface ConveyorEventsSchema {
modeluuid: string;
modelName: string;
type: 'Conveyor';
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | [];
triggers: { uuid: string; name: string; type: string; isUsed: boolean; bufferTime: number }[] | [];
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
}[];
modeluuid: string;
modelName: string;
type: "Conveyor";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
speed: number | string;
actions:
| {
uuid: string;
name: string;
type: string;
material: string;
delay: number | string;
spawnInterval: number | string;
isUsed: boolean;
}[]
| [];
triggers:
| {
uuid: string;
name: string;
type: string;
isUsed: boolean;
bufferTime: number;
}[]
| [];
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
}[];
position: [number, number, number];
rotation: [number, number, number];
speed: number | string;
}
interface VehicleEventsSchema {
modeluuid: string;
modelName: string;
type: 'Vehicle';
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; type: string; start: { x: number, y: number } | {}, hitCount: number, end: { x: number, y: number } | {}, buffer: number };
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
speed: number;
};
modeluuid: string;
modelName: string;
type: "Vehicle";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: {
uuid: string;
name: string;
type: string;
start: { x: number; y: number } | {};
hitCount: number;
end: { x: number; y: number } | {};
buffer: number;
};
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
speed: number;
isPlaying: boolean;
};
position: [number, number, number];
}
interface StaticMachineEventsSchema {
modeluuid: string;
modelName: string;
type: 'StaticMachine';
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; buffer: number; material: string; };
triggers: { uuid: string; name: string; type: string };
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
};
modeluuid: string;
modelName: string;
type: "StaticMachine";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: {
uuid: string;
name: string;
buffer: number | string;
material: string;
isUsed: boolean;
};
triggers: { uuid: string; name: string; type: string };
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
};
position: [number, number, number];
}
interface ArmBotEventsSchema {
modeluuid: string;
modelName: string;
type: 'ArmBot';
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[] };
triggers: { uuid: string; name: string; type: string };
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
};
modeluuid: string;
modelName: string;
type: "ArmBot";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: {
uuid: string;
name: string;
speed: number;
processes: { triggerId: string; startPoint: string; endPoint: string }[];
};
triggers: { uuid: string; name: string; type: string };
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
};
position: [number, number, number];
}
export type EventData = {
modeluuid: string;
modelname: string;
position: [number, number, number];
rotation: { x: number; y: number; z: number };
modelfileID: string;
isLocked: boolean;
isVisible: boolean;
eventData?: {
type: 'Conveyor';
modeluuid: string;
modelname: string;
position: [number, number, number];
rotation: { x: number; y: number; z: number };
modelfileID: string;
isLocked: boolean;
isVisible: boolean;
eventData?:
| {
type: "Conveyor";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | [];
triggers: { uuid: string; name: string; type: string; isUsed: boolean; bufferTime: number }[] | [];
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions:
| {
uuid: string;
name: string;
type: string;
material: string;
delay: number | string;
spawnInterval: number | string;
isUsed: boolean;
}[]
| [];
triggers:
| {
uuid: string;
name: string;
type: string;
isUsed: boolean;
bufferTime: number;
}[]
| [];
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
}[];
speed: number | string;
} | {
type: 'Vehicle';
}
| {
type: "Vehicle";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; type: string; start: { x: number, y: number } | {}, hitCount: number, end: { x: number, y: number } | {}, buffer: number };
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
name: string;
type: string;
start: { x: number; y: number } | {};
hitCount: number;
end: { x: number; y: number } | {};
buffer: number;
};
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
speed: number;
};
}
| {
type: "StaticMachine";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: {
uuid: string;
name: string;
buffer: number;
material: string;
};
triggers: { uuid: string; name: string; type: string };
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
};
}
| {
type: "ArmBot";
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: {
uuid: string;
name: string;
speed: number;
processes: {
triggerId: string;
startPoint: string;
endPoint: string;
}[];
};
triggers: { uuid: string; name: string; type: string };
connections: {
source: { modelUUID: string; pointUUID: string };
targets: { modelUUID: string; pointUUID: string }[];
};
};
} | {
type: 'StaticMachine';
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; buffer: number; material: string; };
triggers: { uuid: string; name: string; type: string };
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
};
} | {
type: 'ArmBot';
points: {
uuid: string;
position: [number, number, number];
rotation: [number, number, number];
actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[] };
triggers: { uuid: string; name: string; type: string };
connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] };
};
};
}
};
};

View File

@ -1,198 +1,198 @@
// Importing React and useEffect from React library
import React, { useEffect } from "react";
// Importing the necessary hooks and types from the application's state management stores
import useModuleStore, { useThreeDStore } from "../../store/useModuleStore";
import useToggleStore from "../../store/useUIToggleStore";
import {
useActiveSubTool,
useActiveTool,
useAddAction,
useDeleteTool,
useSelectedWallItem,
useToggleView,
useToolMode,
} from "../../store/store";
import { usePlayButtonStore } from "../../store/usePlayButtonStore";
// Utility function to detect modifier keys (Ctrl, Alt, Shift) and key combinations
import { detectModifierKeys } from "./detectModifierKeys";
// KeyPressListener component to handle global keyboard shortcuts
const KeyPressListener: React.FC = () => {
// Accessing state and actions from different stores
const { activeModule, setActiveModule } = useModuleStore(); // Module management (e.g., builder, simulation, visualization)
const { setActiveSubTool } = useActiveSubTool(); // Sub-tool management
const { toggleUI, setToggleUI } = useToggleStore(); // UI visibility toggle
const { setToggleThreeD } = useThreeDStore(); // 3D view toggle
const { setToolMode } = useToolMode(); // Tool mode management
const { setIsPlaying } = usePlayButtonStore(); // Play button state management
// Wall and tool-related actions
const { toggleView, setToggleView } = useToggleView(); // 2D/3D toggle state
const { setDeleteTool } = useDeleteTool(); // Delete tool action
const { setAddAction } = useAddAction(); // Add action management
const { setSelectedWallItem } = useSelectedWallItem(); // Selected wall item management
const { setActiveTool } = useActiveTool(); // Active tool management
// useEffect to manage global keyboard shortcuts
useEffect(() => {
// Function to handle keydown events
const handleKeyPress = (event: KeyboardEvent) => {
// Identify the currently focused element
const activeElement = document.activeElement;
// Check if the user is typing in an input field, textarea, or contenteditable element
const isTyping =
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
(activeElement && activeElement.getAttribute("contenteditable") === "true");
if (isTyping) {
return; // Skip shortcut handling while typing
}
// Detect the combination of keys pressed
const keyCombination = detectModifierKeys(event);
// Allow browser default behavior for specific keys (e.g., F5 for refresh)
if (["F5", "F11", "F12"].includes(event.key) || keyCombination === "Ctrl+R") {
return;
}
// Prevent the default browser action for other handled key presses
event.preventDefault();
if (keyCombination) {
// Switch between different modules (e.g., builder, simulation)
if (keyCombination === "1" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setActiveModule("builder");
}
if (keyCombination === "2" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setActiveModule("simulation");
}
if (keyCombination === "3" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setActiveModule("visualization");
}
if (keyCombination === "4" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setToggleUI(false);
setActiveModule("market");
}
// Toggle UI visibility
if (keyCombination === "Ctrl+." && activeModule !== "market") {
setToggleUI(!toggleUI);
}
// Tool selection shortcuts
if (keyCombination === "V") {
setActiveTool("cursor");
setActiveSubTool("cursor");
}
if (keyCombination === "X") {
setActiveTool("delete");
setActiveSubTool("delete");
}
if (keyCombination === "H") {
setActiveTool("free-hand");
setActiveSubTool("free-hand");
}
// Toggle play mode
if (keyCombination === "Ctrl+P" && !toggleView) {
setIsPlaying(true);
}
// Builder-specific shortcuts
if (activeModule === "builder") {
if (keyCombination === "TAB") {
// Switch between 2D and 3D views
if (toggleView) {
setToggleView(false);
setToggleThreeD(true);
setActiveTool("cursor");
} else {
setSelectedWallItem(null);
setDeleteTool(false);
setAddAction(null);
setToggleView(true);
setToggleThreeD(false);
setActiveTool("cursor");
}
}
// Wall-related tools
if (toggleView) {
if (keyCombination === "Q" || keyCombination === "6") {
setActiveTool("draw-wall");
setToolMode("Wall");
}
if (keyCombination === "R" || keyCombination === "7") {
setActiveTool("draw-aisle");
setToolMode("Aisle");
}
if (keyCombination === "E" || keyCombination === "8") {
setActiveTool("draw-zone");
setToolMode("Zone");
}
if (keyCombination === "T" || keyCombination === "9") {
setActiveTool("draw-floor");
setToolMode("Floor");
}
}
// Measurement tool
if (keyCombination === "M") {
setActiveTool("measure");
setToolMode("MeasurementScale");
}
}
// Undo and redo actions
if (keyCombination === "Ctrl+Z") {
// Handle undo action
}
if (keyCombination === "Ctrl+Y" || keyCombination === "Ctrl+Shift+Z") {
// Handle redo action
}
// Helper actions
if (keyCombination === "Ctrl+H") {
// Open help
}
if (keyCombination === "Ctrl+F") {
// Open find functionality
}
if (keyCombination === "Ctrl+?") {
// Show shortcuts info
}
// Reset to cursor tool and stop play mode
if (keyCombination === "ESCAPE") {
setActiveTool("cursor");
setIsPlaying(false);
}
}
};
// Add keydown event listener
window.addEventListener("keydown", handleKeyPress);
// Cleanup function to remove the event listener
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, [activeModule, toggleUI, toggleView]); // Dependencies to reapply effect if these values change
return null; // This component does not render any UI
};
export default KeyPressListener;
// Importing React and useEffect from React library
import React, { useEffect } from "react";
// Importing the necessary hooks and types from the application's state management stores
import useModuleStore, { useThreeDStore } from "../../store/useModuleStore";
import useToggleStore from "../../store/useUIToggleStore";
import {
useActiveSubTool,
useActiveTool,
useAddAction,
useDeleteTool,
useSelectedWallItem,
useToggleView,
useToolMode,
} from "../../store/store";
import { usePlayButtonStore } from "../../store/usePlayButtonStore";
// Utility function to detect modifier keys (Ctrl, Alt, Shift) and key combinations
import { detectModifierKeys } from "./detectModifierKeys";
// KeyPressListener component to handle global keyboard shortcuts
const KeyPressListener: React.FC = () => {
// Accessing state and actions from different stores
const { activeModule, setActiveModule } = useModuleStore(); // Module management (e.g., builder, simulation, visualization)
const { setActiveSubTool } = useActiveSubTool(); // Sub-tool management
const { toggleUI, setToggleUI } = useToggleStore(); // UI visibility toggle
const { setToggleThreeD } = useThreeDStore(); // 3D view toggle
const { setToolMode } = useToolMode(); // Tool mode management
const { setIsPlaying } = usePlayButtonStore(); // Play button state management
// Wall and tool-related actions
const { toggleView, setToggleView } = useToggleView(); // 2D/3D toggle state
const { setDeleteTool } = useDeleteTool(); // Delete tool action
const { setAddAction } = useAddAction(); // Add action management
const { setSelectedWallItem } = useSelectedWallItem(); // Selected wall item management
const { setActiveTool } = useActiveTool(); // Active tool management
// useEffect to manage global keyboard shortcuts
useEffect(() => {
// Function to handle keydown events
const handleKeyPress = (event: KeyboardEvent) => {
// Identify the currently focused element
const activeElement = document.activeElement;
// Check if the user is typing in an input field, textarea, or contenteditable element
const isTyping =
activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
(activeElement && activeElement.getAttribute("contenteditable") === "true");
if (isTyping) {
return; // Skip shortcut handling while typing
}
// Detect the combination of keys pressed
const keyCombination = detectModifierKeys(event);
// Allow browser default behavior for specific keys (e.g., F5 for refresh)
if (["F5", "F11", "F12"].includes(event.key) || keyCombination === "Ctrl+R") {
return;
}
// Prevent the default browser action for other handled key presses
event.preventDefault();
if (keyCombination) {
// Switch between different modules (e.g., builder, simulation)
if (keyCombination === "1" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setActiveModule("builder");
}
if (keyCombination === "2" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setActiveModule("simulation");
}
if (keyCombination === "3" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setActiveModule("visualization");
}
if (keyCombination === "4" && !toggleView) {
setActiveTool("cursor");
setActiveSubTool("cursor");
setToggleUI(false);
setActiveModule("market");
}
// Toggle UI visibility
if (keyCombination === "Ctrl+." && activeModule !== "market") {
setToggleUI(!toggleUI);
}
// Tool selection shortcuts
if (keyCombination === "V") {
setActiveTool("cursor");
setActiveSubTool("cursor");
}
if (keyCombination === "X") {
setActiveTool("delete");
setActiveSubTool("delete");
}
if (keyCombination === "H") {
setActiveTool("free-hand");
setActiveSubTool("free-hand");
}
// Toggle play mode
if (keyCombination === "Ctrl+P" && !toggleView) {
setIsPlaying(true);
}
// Builder-specific shortcuts
if (activeModule === "builder") {
if (keyCombination === "TAB") {
// Switch between 2D and 3D views
if (toggleView) {
setToggleView(false);
setToggleThreeD(true);
setActiveTool("cursor");
} else {
setSelectedWallItem(null);
setDeleteTool(false);
setAddAction(null);
setToggleView(true);
setToggleThreeD(false);
setActiveTool("cursor");
}
}
// Wall-related tools
if (toggleView) {
if (keyCombination === "Q" || keyCombination === "6") {
setActiveTool("draw-wall");
setToolMode("Wall");
}
if (keyCombination === "R" || keyCombination === "7") {
setActiveTool("draw-aisle");
setToolMode("Aisle");
}
if (keyCombination === "E" || keyCombination === "8") {
setActiveTool("draw-zone");
setToolMode("Zone");
}
if (keyCombination === "T" || keyCombination === "9") {
setActiveTool("draw-floor");
setToolMode("Floor");
}
}
// Measurement tool
if (keyCombination === "M") {
setActiveTool("measure");
setToolMode("MeasurementScale");
}
}
// Undo and redo actions
if (keyCombination === "Ctrl+Z") {
// Handle undo action
}
if (keyCombination === "Ctrl+Y" || keyCombination === "Ctrl+Shift+Z") {
// Handle redo action
}
// Helper actions
if (keyCombination === "Ctrl+H") {
// Open help
}
if (keyCombination === "Ctrl+F") {
// Open find functionality
}
if (keyCombination === "Ctrl+?") {
// Show shortcuts info
}
// Reset to cursor tool and stop play mode
if (keyCombination === "ESCAPE") {
setActiveTool("cursor");
setIsPlaying(false);
}
}
};
// Add keydown event listener
window.addEventListener("keydown", handleKeyPress);
// Cleanup function to remove the event listener
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, [activeModule, toggleUI, toggleView]); // Dependencies to reapply effect if these values change
return null; // This component does not render any UI
};
export default KeyPressListener;