add initial components and utility functions for simulation and builder modules

This commit is contained in:
2025-03-25 14:00:03 +05:30
parent 61b3c4ee2c
commit 2303682a15
164 changed files with 13967 additions and 52 deletions

View File

@@ -0,0 +1,9 @@
import React from 'react'
function ColliderCreator() {
return (
<></>
)
}
export default ColliderCreator

View File

@@ -0,0 +1,404 @@
import { useEffect, useState } from 'react';
import * as THREE from 'three';
import { useThree, useFrame } from '@react-three/fiber';
import { Line, TransformControls } from '@react-three/drei';
import { useDrawMaterialPath } from '../../../../store/store';
type PathPoint = {
position: THREE.Vector3;
rotation: THREE.Quaternion;
uuid: string;
};
type PathCreatorProps = {
simulationPaths: PathPoint[][];
setSimulationPaths: React.Dispatch<React.SetStateAction<PathPoint[][]>>;
connections: { start: PathPoint; end: PathPoint }[];
setConnections: React.Dispatch<React.SetStateAction<{ start: PathPoint; end: PathPoint }[]>>
};
const PathCreator = ({ simulationPaths, setSimulationPaths, connections, setConnections }: PathCreatorProps) => {
const { camera, scene, raycaster, pointer, gl } = useThree();
const { drawMaterialPath } = useDrawMaterialPath();
const [currentPath, setCurrentPath] = useState<{ position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string }[]>([]);
const [temporaryPoint, setTemporaryPoint] = useState<THREE.Vector3 | null>(null);
const [selectedPoint, setSelectedPoint] = useState<{ position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string } | null>(null);
const [selectedConnectionPoint, setSelectedConnectionPoint] = useState<{ point: PathPoint; pathIndex: number } | null>(null);
const [previewConnection, setPreviewConnection] = useState<{ start: PathPoint; end?: THREE.Vector3 } | null>(null);
const [transformMode, setTransformMode] = useState<'translate' | 'rotate'>('translate');
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (selectedPoint) {
if (event.key === 'g') {
setTransformMode('translate');
} else if (event.key === 'r') {
setTransformMode('rotate');
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedPoint]);
useEffect(() => {
const canvasElement = gl.domElement;
let drag = false;
let MouseDown = false;
const onMouseDown = () => {
MouseDown = true;
drag = false;
};
const onMouseUp = () => {
MouseDown = false;
};
const onMouseMove = () => {
if (MouseDown) {
drag = true;
}
};
const onContextMenu = (e: any) => {
e.preventDefault();
if (drag || e.button === 0) return;
if (currentPath.length > 1) {
setSimulationPaths((prevPaths) => [...prevPaths, currentPath]);
}
setCurrentPath([]);
setTemporaryPoint(null);
setPreviewConnection(null);
setSelectedConnectionPoint(null);
};
const onMouseClick = (evt: any) => {
if (drag || evt.button !== 0) return;
evt.preventDefault();
raycaster.setFromCamera(pointer, camera);
let intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.some((intersect) => intersect.object.name.includes("path-point"))) {
intersects = [];
} else {
intersects = intersects.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.userData.isPathObject &&
!(intersect.object.type === "GridHelper")
);
}
if (intersects.length > 0 && selectedPoint === null) {
let point = intersects[0].point;
if (point.y < 0.05) {
point = new THREE.Vector3(point.x, 0.05, point.z);
}
const newPoint = {
position: point,
rotation: new THREE.Quaternion(),
uuid: THREE.MathUtils.generateUUID(),
};
setCurrentPath((prevPath) => [...prevPath, newPoint]);
setTemporaryPoint(null);
} else {
setSelectedPoint(null);
}
};
if (drawMaterialPath) {
canvasElement.addEventListener("mousedown", onMouseDown);
canvasElement.addEventListener("mouseup", onMouseUp);
canvasElement.addEventListener("mousemove", onMouseMove);
canvasElement.addEventListener("click", onMouseClick);
canvasElement.addEventListener("contextmenu", onContextMenu);
} else {
if (currentPath.length > 1) {
setSimulationPaths((prevPaths) => [...prevPaths, currentPath]);
}
setCurrentPath([]);
setTemporaryPoint(null);
}
return () => {
canvasElement.removeEventListener("mousedown", onMouseDown);
canvasElement.removeEventListener("mouseup", onMouseUp);
canvasElement.removeEventListener("mousemove", onMouseMove);
canvasElement.removeEventListener("click", onMouseClick);
canvasElement.removeEventListener("contextmenu", onContextMenu);
};
}, [camera, scene, raycaster, currentPath, drawMaterialPath, selectedPoint]);
useFrame(() => {
if (drawMaterialPath && currentPath.length > 0) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true).filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.userData.isPathObject &&
!(intersect.object.type === "GridHelper")
);
if (intersects.length > 0) {
let point = intersects[0].point;
if (point.y < 0.05) {
point = new THREE.Vector3(point.x, 0.05, point.z);
}
setTemporaryPoint(point);
} else {
setTemporaryPoint(null);
}
} else {
setTemporaryPoint(null);
}
});
const handlePointClick = (point: { position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string }) => {
if (currentPath.length === 0 && drawMaterialPath) {
setSelectedPoint(point);
} else {
setSelectedPoint(null);
}
};
const handleTransform = (e: any) => {
if (selectedPoint) {
const updatedPosition = e.target.object.position.clone();
const updatedRotation = e.target.object.quaternion.clone();
const updatedPaths = simulationPaths.map((path) =>
path.map((p) =>
p.uuid === selectedPoint.uuid ? { ...p, position: updatedPosition, rotation: updatedRotation } : p
)
);
setSimulationPaths(updatedPaths);
}
};
const meshContext = (uuid: string) => {
const pathIndex = simulationPaths.findIndex(path => path.some(point => point.uuid === uuid));
if (pathIndex === -1) return;
const clickedPoint = simulationPaths[pathIndex].find(point => point.uuid === uuid);
if (!clickedPoint) return;
const isStart = simulationPaths[pathIndex][0].uuid === uuid;
const isEnd = simulationPaths[pathIndex][simulationPaths[pathIndex].length - 1].uuid === uuid;
if (pathIndex === 0 && isStart) {
console.log("The first-ever point is not connectable.");
setSelectedConnectionPoint(null);
setPreviewConnection(null);
return;
}
if (!isStart && !isEnd) {
console.log("Selected point is not a valid connection point (not start or end)");
setSelectedConnectionPoint(null);
setPreviewConnection(null);
return;
}
if (connections.some(conn => conn.start.uuid === uuid || conn.end.uuid === uuid)) {
console.log("The selected point is already connected.");
setSelectedConnectionPoint(null);
setPreviewConnection(null);
return;
}
if (!selectedConnectionPoint) {
setSelectedConnectionPoint({ point: clickedPoint, pathIndex });
setPreviewConnection({ start: clickedPoint });
console.log("First point selected for connection:", clickedPoint);
return;
}
if (selectedConnectionPoint.pathIndex === pathIndex) {
console.log("Cannot connect points within the same path.");
setSelectedConnectionPoint(null);
setPreviewConnection(null);
return;
}
if (connections.some(conn => conn.start.uuid === clickedPoint.uuid || conn.end.uuid === clickedPoint.uuid)) {
console.log("The target point is already connected.");
setSelectedConnectionPoint(null);
setPreviewConnection(null);
return;
}
setConnections(prevConnections => [
...prevConnections,
{ start: selectedConnectionPoint.point, end: clickedPoint },
]);
setSelectedConnectionPoint(null);
setPreviewConnection(null);
};
useEffect(() => {
if (!selectedConnectionPoint) {
setPreviewConnection(null);
}
}, [selectedConnectionPoint, connections]);
useFrame(() => {
if (selectedConnectionPoint) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true).filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.userData.isPathObject &&
!(intersect.object.type === "GridHelper")
);
if (intersects.length > 0) {
let point = intersects[0].point;
if (point.y < 0.05) {
point = new THREE.Vector3(point.x, 0.05, point.z);
}
setPreviewConnection({ start: selectedConnectionPoint.point, end: point });
} else {
setPreviewConnection(null);
}
}
});
return (
<>
<group name='pathObjects'>
{/* Render finalized simulationPaths */}
{simulationPaths.map((path, pathIndex) => (
<group key={`path-line-${pathIndex}`}>
<Line
name={`path-line-${pathIndex}`}
points={path.map((point) => point.position)}
color="yellow"
lineWidth={5}
userData={{ isPathObject: true }}
/>
</group>
))}
{/* Render finalized points */}
{simulationPaths.map((path) =>
path.map((point) => (
<mesh
key={`path-point-${point.uuid}`}
name={`path-point-${point.uuid}`}
uuid={`${point.uuid}`}
position={point.position}
userData={{ isPathObject: true }}
onClick={() => handlePointClick(point)}
onPointerMissed={() => { setSelectedPoint(null) }}
onContextMenu={() => { meshContext(point.uuid); }}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="blue" wireframe />
</mesh>
))
)}
{connections.map((conn, index) => (
<Line
key={`connection-${index}`}
points={[conn.start.position, conn.end.position]}
color="white"
dashed
lineWidth={4}
dashSize={1}
dashScale={15}
userData={{ isPathObject: true }}
/>
))}
</group>
{/* Render current path */}
{currentPath.length > 1 && (
<group>
<Line
points={currentPath.map((point) => point.position)}
color="red"
lineWidth={5}
userData={{ isPathObject: true }}
/>
</group>
)}
{/* Render current path points */}
{currentPath.map((point) => (
<mesh
key={`current-point-${point.uuid}`}
position={point.position}
userData={{ isPathObject: true }}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="red" />
</mesh>
))}
{/* Render temporary indicator line */}
{temporaryPoint && currentPath.length > 0 && (
<group>
<Line
points={[currentPath[currentPath.length - 1].position, temporaryPoint]}
color="white"
lineWidth={2}
userData={{ isPathObject: true }}
/>
</group>
)}
{/* Render dashed preview connection */}
{previewConnection && previewConnection.end && (
<Line
points={[previewConnection.start.position, previewConnection.end]}
color="white"
dashed
lineWidth={4}
dashSize={1}
dashScale={15}
userData={{ isPathObject: true }}
/>
)}
{/* Render temporary point */}
{temporaryPoint && (
<mesh
position={temporaryPoint}
userData={{ isPathObject: true }}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="white" />
</mesh>
)}
{/* Attach TransformControls to the selected point */}
{selectedPoint && (
<TransformControls
object={scene.getObjectByProperty('uuid', selectedPoint.uuid)}
mode={transformMode}
onObjectChange={handleTransform}
/>
)}
</>
);
};
export default PathCreator;

View File

@@ -0,0 +1,164 @@
import * as THREE from 'three';
import { useState, useEffect, useRef, useMemo } from "react";
import { useLoader, useFrame } from "@react-three/fiber";
import { GLTFLoader } from "three-stdlib";
import crate from "../../../../assets/models/gltf-glb/crate_box.glb";
import { useOrganization } from '../../../../store/store';
import { useControls } from 'leva';
type PathPoint = {
position: THREE.Vector3;
rotation: THREE.Quaternion;
uuid: string;
};
type PathFlowProps = {
path: PathPoint[];
connections: { start: PathPoint; end: PathPoint }[];
};
export default function PathFlow({ path, connections }: PathFlowProps) {
const { organization } = useOrganization();
const [isPaused, setIsPaused] = useState(false);
const [isStopped, setIsStopped] = useState(false);
const { spawnInterval, speed, pauseResume, startStop } = useControls({
spawnInterval: { value: 1000, min: 500, max: 5000, step: 100 },
speed: { value: 2, min: 1, max: 20, step: 0.5 },
pauseResume: { value: false, label: "Pause/Resume" },
startStop: { value: false, label: "Start/Stop" },
});
const [meshes, setMeshes] = useState<{ id: number }[]>([]);
const gltf = useLoader(GLTFLoader, crate);
const meshIdRef = useRef(0);
const lastSpawnTime = useRef(performance.now());
const totalPausedTime = useRef(0);
const pauseStartTime = useRef<number | null>(null);
useEffect(() => {
setIsPaused(pauseResume);
setIsStopped(startStop);
}, [pauseResume, startStop]);
const removeMesh = (id: number) => {
setMeshes((prev) => prev.filter((m) => m.id !== id));
};
useFrame(() => {
if (organization !== 'hexrfactory' || isStopped || !path) return;
const now = performance.now();
if (isPaused) {
if (pauseStartTime.current === null) {
pauseStartTime.current = now;
}
return;
}
if (pauseStartTime.current !== null) {
totalPausedTime.current += now - pauseStartTime.current;
pauseStartTime.current = null;
}
const adjustedTime = now - totalPausedTime.current;
if (adjustedTime - lastSpawnTime.current >= spawnInterval) {
setMeshes((prev) => [...prev, { id: meshIdRef.current++ }]);
lastSpawnTime.current = adjustedTime;
}
});
return (
<>
{meshes.map((mesh) => (
<MovingMesh
key={mesh.id}
meshId={mesh.id}
points={path}
speed={speed}
gltf={gltf}
removeMesh={removeMesh}
isPaused={isPaused}
/>
))}
</>
);
}
function MovingMesh({ meshId, points, speed, gltf, removeMesh, isPaused }: any) {
const meshRef = useRef<any>();
const startTime = useRef<number | null>(null); // Initialize as null
const pausedTime = useRef(0);
const pauseStartTime = useRef<number | null>(null);
const distances = useMemo(() => {
if (!points || points.length < 2) return [];
return points.slice(1).map((point: any, i: number) => points[i].position.distanceTo(point.position));
}, [points]);
useFrame(() => {
if (!points || points.length < 2) return;
if (startTime.current === null && points.length > 0) {
startTime.current = performance.now();
}
if (!meshRef.current) return;
if (isPaused) {
if (pauseStartTime.current === null) {
pauseStartTime.current = performance.now();
}
return;
}
if (pauseStartTime.current !== null) {
pausedTime.current += performance.now() - pauseStartTime.current;
pauseStartTime.current = null;
}
if (startTime.current === null) return;
const elapsed = performance.now() - startTime.current - pausedTime.current;
const distanceTraveled = elapsed / 1000 * speed;
let remainingDistance = distanceTraveled;
let currentSegmentIndex = 0;
while (currentSegmentIndex < distances.length && remainingDistance > distances[currentSegmentIndex]) {
remainingDistance -= distances[currentSegmentIndex];
currentSegmentIndex++;
}
if (currentSegmentIndex >= distances.length) {
removeMesh(meshId);
return;
}
const progress = remainingDistance / distances[currentSegmentIndex];
const start = points[currentSegmentIndex].position;
const end = points[currentSegmentIndex + 1].position;
meshRef.current.position.lerpVectors(start, end, Math.min(progress, 1));
const startRotation = points[currentSegmentIndex].rotation;
const endRotation = points[currentSegmentIndex + 1].rotation;
const interpolatedRotation = new THREE.Quaternion().slerpQuaternions(startRotation, endRotation, Math.min(progress, 1));
meshRef.current.quaternion.copy(interpolatedRotation);
});
return (
<>
{points && points.length > 0 &&
<mesh ref={meshRef}>
<primitive object={gltf.scene.clone()} />
</mesh>
}
</>
);
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
function ProcessCreator() {
return (
<></>
)
}
export default ProcessCreator

View File

@@ -0,0 +1,26 @@
import React, { useState } from 'react';
import * as THREE from 'three';
import PathCreator from './path/pathCreator';
import PathFlow from './path/pathFlow';
type PathPoint = {
position: THREE.Vector3;
rotation: THREE.Quaternion;
uuid: string;
};
function Simulation() {
const [simulationPaths, setSimulationPaths] = useState<{ position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string }[][]>([]);
const [connections, setConnections] = useState<{ start: PathPoint; end: PathPoint }[]>([]);
return (
<>
<PathCreator simulationPaths={simulationPaths} setSimulationPaths={setSimulationPaths} connections={connections} setConnections={setConnections} />
{simulationPaths.map((path, index) => (
<PathFlow key={index} path={path} connections={connections} />
))}
</>
);
}
export default Simulation;