Refactor Simulations, RenameTooltip, EditWidgetOption, and RoboticArmAnimator components: streamline imports, enhance UI elements, and improve event handling logic.
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
AddIcon,
|
AddIcon,
|
||||||
ArrowIcon,
|
ArrowIcon,
|
||||||
RemoveIcon,
|
RemoveIcon,
|
||||||
ResizeHeightIcon,
|
ResizeHeightIcon,
|
||||||
} from "../../../icons/ExportCommonIcons";
|
} from "../../../icons/ExportCommonIcons";
|
||||||
import RenameInput from "../../../ui/inputs/RenameInput";
|
import RenameInput from "../../../ui/inputs/RenameInput";
|
||||||
import { handleResize } from "../../../../functions/handleResizePannel";
|
import { handleResize } from "../../../../functions/handleResizePannel";
|
||||||
import {
|
import {
|
||||||
useSelectedAsset,
|
useSelectedAsset,
|
||||||
useSelectedProduct,
|
useSelectedProduct,
|
||||||
} from "../../../../store/simulation/useSimulationStore";
|
} from "../../../../store/simulation/useSimulationStore";
|
||||||
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
||||||
import { generateUUID } from "three/src/math/MathUtils";
|
import { generateUUID } from "three/src/math/MathUtils";
|
||||||
@@ -22,206 +22,222 @@ import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertP
|
|||||||
import { deleteProductApi } from "../../../../services/simulation/deleteProductApi";
|
import { deleteProductApi } from "../../../../services/simulation/deleteProductApi";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
pathName: string;
|
pathName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListProps {
|
interface ListProps {
|
||||||
val: Event;
|
val: Event;
|
||||||
}
|
}
|
||||||
|
|
||||||
const List: React.FC<ListProps> = ({ val }) => {
|
const List: React.FC<ListProps> = ({ val }) => {
|
||||||
return (
|
return (
|
||||||
<div className="process-container">
|
<div className="process-container">
|
||||||
<div className="value">{val.pathName}</div>
|
<div className="value">{val.pathName}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Simulations: React.FC = () => {
|
const Simulations: React.FC = () => {
|
||||||
const productsContainerRef = useRef<HTMLDivElement>(null);
|
const productsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const { products, addProduct, removeProduct, renameProduct, addEvent, removeEvent, } = useProductStore();
|
const {
|
||||||
const { selectedProduct, setSelectedProduct } = useSelectedProduct();
|
products,
|
||||||
const { getEventByModelUuid } = useEventsStore();
|
addProduct,
|
||||||
const { selectedAsset, clearSelectedAsset } = useSelectedAsset();
|
removeProduct,
|
||||||
const email = localStorage.getItem('email')
|
renameProduct,
|
||||||
const organization = (email!.split("@")[1]).split(".")[0];
|
addEvent,
|
||||||
const [openObjects, setOpenObjects] = useState(true);
|
removeEvent,
|
||||||
|
} = useProductStore();
|
||||||
|
const { selectedProduct, setSelectedProduct } = useSelectedProduct();
|
||||||
|
const { getEventByModelUuid } = useEventsStore();
|
||||||
|
const { selectedAsset, clearSelectedAsset } = useSelectedAsset();
|
||||||
|
const email = localStorage.getItem("email");
|
||||||
|
const organization = email!.split("@")[1].split(".")[0];
|
||||||
|
const [openObjects, setOpenObjects] = useState(true);
|
||||||
|
|
||||||
const handleAddProduct = () => {
|
const handleAddProduct = () => {
|
||||||
const id = generateUUID();
|
const id = generateUUID();
|
||||||
const name = `Product ${products.length + 1}`;
|
const name = `Product ${products.length + 1}`;
|
||||||
addProduct(name, id);
|
addProduct(name, id);
|
||||||
upsertProductOrEventApi({ productName: name, productId: id, organization: organization });
|
upsertProductOrEventApi({
|
||||||
};
|
productName: name,
|
||||||
|
productId: id,
|
||||||
|
organization: organization,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleRemoveProduct = (productId: string) => {
|
const handleRemoveProduct = (productId: string) => {
|
||||||
const currentIndex = products.findIndex((p) => p.productId === productId);
|
const currentIndex = products.findIndex((p) => p.productId === productId);
|
||||||
const isSelected = selectedProduct.productId === productId;
|
const isSelected = selectedProduct.productId === productId;
|
||||||
|
|
||||||
const updatedProducts = products.filter((p) => p.productId !== productId);
|
const updatedProducts = products.filter((p) => p.productId !== productId);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
if (updatedProducts.length > 0) {
|
if (updatedProducts.length > 0) {
|
||||||
let newSelectedIndex = currentIndex;
|
let newSelectedIndex = currentIndex;
|
||||||
if (currentIndex >= updatedProducts.length) {
|
if (currentIndex >= updatedProducts.length) {
|
||||||
newSelectedIndex = updatedProducts.length - 1;
|
newSelectedIndex = updatedProducts.length - 1;
|
||||||
}
|
|
||||||
setSelectedProduct(
|
|
||||||
updatedProducts[newSelectedIndex].productId,
|
|
||||||
updatedProducts[newSelectedIndex].productName
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedProduct("", "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setSelectedProduct(
|
||||||
|
updatedProducts[newSelectedIndex].productId,
|
||||||
|
updatedProducts[newSelectedIndex].productName
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedProduct("", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
removeProduct(productId);
|
removeProduct(productId);
|
||||||
deleteProductApi(productId, organization);
|
deleteProductApi(productId, organization);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRenameProduct = (productId: string, newName: string) => {
|
const handleRenameProduct = (productId: string, newName: string) => {
|
||||||
renameProduct(productId, newName);
|
renameProduct(productId, newName);
|
||||||
if (selectedProduct.productId === productId) {
|
if (selectedProduct.productId === productId) {
|
||||||
setSelectedProduct(productId, newName);
|
setSelectedProduct(productId, newName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveEventFromProduct = () => {
|
const handleRemoveEventFromProduct = () => {
|
||||||
if (selectedAsset) {
|
if (selectedAsset) {
|
||||||
const email = localStorage.getItem('email')
|
const email = localStorage.getItem("email");
|
||||||
const organization = (email!.split("@")[1]).split(".")[0];
|
const organization = email!.split("@")[1].split(".")[0];
|
||||||
deleteEventDataApi({
|
deleteEventDataApi({
|
||||||
productId: selectedProduct.productId,
|
productId: selectedProduct.productId,
|
||||||
modelUuid: selectedAsset.modelUuid,
|
modelUuid: selectedAsset.modelUuid,
|
||||||
organization: organization
|
organization: organization,
|
||||||
});
|
});
|
||||||
removeEvent(selectedProduct.productId, selectedAsset.modelUuid);
|
removeEvent(selectedProduct.productId, selectedAsset.modelUuid);
|
||||||
clearSelectedAsset();
|
clearSelectedAsset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedProductData = products.find(
|
const selectedProductData = products.find(
|
||||||
(product) => product.productId === selectedProduct.productId
|
(product) => product.productId === selectedProduct.productId
|
||||||
);
|
);
|
||||||
|
|
||||||
const events: Event[] = selectedProductData?.eventDatas.map((event) => ({
|
const events: Event[] =
|
||||||
pathName: event.modelName,
|
selectedProductData?.eventDatas.map((event) => ({
|
||||||
|
pathName: event.modelName,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="simulations-container">
|
<div className="simulations-container">
|
||||||
<div className="header">Simulations</div>
|
<div className="header">Simulations</div>
|
||||||
<div className="add-product-container">
|
<div className="add-product-container">
|
||||||
<div className="actions section">
|
<div className="actions section">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-value">Products</div>
|
<div className="header-value">Products</div>
|
||||||
<div className="add-button" onClick={handleAddProduct}>
|
<button className="add-button" onClick={handleAddProduct}>
|
||||||
<AddIcon /> Add
|
<AddIcon /> Add
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="lists-main-container"
|
className="lists-main-container"
|
||||||
ref={productsContainerRef}
|
ref={productsContainerRef}
|
||||||
style={{ height: "120px" }}
|
style={{ height: "120px" }}
|
||||||
>
|
>
|
||||||
<div className="list-container">
|
<div className="list-container">
|
||||||
{products.map((product, index) => (
|
{products.map((product, index) => (
|
||||||
<div
|
<div
|
||||||
key={product.productId}
|
key={product.productId}
|
||||||
className={`list-item ${selectedProduct.productId === product.productId
|
className={`list-item ${
|
||||||
? "active"
|
selectedProduct.productId === product.productId
|
||||||
: ""
|
? "active"
|
||||||
}`}
|
: ""
|
||||||
>
|
}`}
|
||||||
<div
|
>
|
||||||
className="value"
|
{/* eslint-disable-next-line */}
|
||||||
onClick={() =>
|
<div
|
||||||
setSelectedProduct(product.productId, product.productName)
|
className="value"
|
||||||
}
|
onClick={() =>
|
||||||
>
|
setSelectedProduct(product.productId, product.productName)
|
||||||
<input
|
}
|
||||||
type="radio"
|
>
|
||||||
name="products"
|
<input
|
||||||
checked={selectedProduct.productId === product.productId}
|
type="radio"
|
||||||
readOnly
|
name="products"
|
||||||
/>
|
checked={selectedProduct.productId === product.productId}
|
||||||
<RenameInput
|
readOnly
|
||||||
value={product.productName}
|
|
||||||
onRename={(newName) =>
|
|
||||||
handleRenameProduct(product.productId, newName)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{products.length > 1 && (
|
|
||||||
<div
|
|
||||||
className="remove-button"
|
|
||||||
onClick={() => handleRemoveProduct(product.productId)}
|
|
||||||
>
|
|
||||||
<RemoveIcon />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="resize-icon"
|
|
||||||
id="action-resize"
|
|
||||||
onMouseDown={(e) => handleResize(e, productsContainerRef)}
|
|
||||||
>
|
|
||||||
<ResizeHeightIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="simulation-process section">
|
|
||||||
<button
|
|
||||||
className="collapse-header-container"
|
|
||||||
onClick={() => setOpenObjects(!openObjects)}
|
|
||||||
>
|
|
||||||
<div className="header">Events</div>
|
|
||||||
<div className="arrow-container">
|
|
||||||
<ArrowIcon />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{openObjects &&
|
|
||||||
events.map((event, index) => <List key={index} val={event} />)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="compare-simulations-container">
|
|
||||||
<div className="compare-simulations-header">
|
|
||||||
Need to Compare Layout?
|
|
||||||
</div>
|
|
||||||
<div className="content">
|
|
||||||
Click <span>'Compare'</span> to review and analyze the layout
|
|
||||||
differences between them.
|
|
||||||
</div>
|
|
||||||
<div className="input">
|
|
||||||
<input type="button" value={"Compare"} className="submit" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedAsset && (
|
|
||||||
<RenderOverlay>
|
|
||||||
<EditWidgetOption
|
|
||||||
options={["Add to Product", "Remove from Product"]}
|
|
||||||
onClick={(option) => {
|
|
||||||
if (option === "Add to Product") {
|
|
||||||
handleAddEventToProduct({
|
|
||||||
event: getEventByModelUuid(selectedAsset.modelUuid),
|
|
||||||
addEvent,
|
|
||||||
selectedProduct,
|
|
||||||
clearSelectedAsset
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handleRemoveEventFromProduct();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</RenderOverlay>
|
<RenameInput
|
||||||
)}
|
value={product.productName}
|
||||||
|
onRename={(newName) =>
|
||||||
|
handleRenameProduct(product.productId, newName)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{products.length > 1 && (
|
||||||
|
<button
|
||||||
|
className="remove-button"
|
||||||
|
onClick={() => handleRemoveProduct(product.productId)}
|
||||||
|
>
|
||||||
|
<RemoveIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="resize-icon"
|
||||||
|
id="action-resize"
|
||||||
|
onMouseDown={(e: any) => handleResize(e, productsContainerRef)}
|
||||||
|
>
|
||||||
|
<ResizeHeightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
<div className="simulation-process section">
|
||||||
|
<button
|
||||||
|
className="collapse-header-container"
|
||||||
|
onClick={() => setOpenObjects(!openObjects)}
|
||||||
|
>
|
||||||
|
<div className="header">Events</div>
|
||||||
|
<div className="arrow-container">
|
||||||
|
<ArrowIcon />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{openObjects &&
|
||||||
|
events.map((event, index) => (
|
||||||
|
<List key={`${index}-${event.pathName}`} val={event} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="compare-simulations-container">
|
||||||
|
<div className="compare-simulations-header">
|
||||||
|
Need to Compare Layout?
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
Click '<span>Compare</span>' to review and analyze the layout
|
||||||
|
differences between them.
|
||||||
|
</div>
|
||||||
|
<div className="input">
|
||||||
|
<input type="button" value={"Compare"} className="submit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedAsset && (
|
||||||
|
<RenderOverlay>
|
||||||
|
<EditWidgetOption
|
||||||
|
options={["Add to Product", "Remove from Product"]}
|
||||||
|
onClick={(option) => {
|
||||||
|
if (option === "Add to Product") {
|
||||||
|
handleAddEventToProduct({
|
||||||
|
event: getEventByModelUuid(selectedAsset.modelUuid),
|
||||||
|
addEvent,
|
||||||
|
selectedProduct,
|
||||||
|
clearSelectedAsset,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleRemoveEventFromProduct();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RenderOverlay>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Simulations;
|
export default Simulations;
|
||||||
|
|||||||
@@ -25,10 +25,8 @@ const RenameTooltip: React.FC<RenameTooltipProps> = ({ name, onSubmit }) => {
|
|||||||
<div
|
<div
|
||||||
className="rename-tool-tip"
|
className="rename-tool-tip"
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
|
||||||
top: `${top}px`,
|
top: `${top}px`,
|
||||||
left: `${left}px`,
|
left: `${left}px`,
|
||||||
zIndex: 100,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
useLeftData,
|
useLeftData,
|
||||||
useTopData,
|
useTopData,
|
||||||
@@ -28,13 +28,13 @@ const EditWidgetOption: React.FC<EditWidgetOptionProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="context-menu-options">
|
<div className="context-menu-options">
|
||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<div
|
<button
|
||||||
className="option"
|
className="option"
|
||||||
key={index}
|
key={`${index}-${option}`}
|
||||||
onClick={() => onClick(option)}
|
onClick={() => onClick(option)}
|
||||||
>
|
>
|
||||||
{option}
|
{option}
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ const SimulationPlayer: React.FC = () => {
|
|||||||
|
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = () => {
|
||||||
isDragging.current = true;
|
isDragging.current = true;
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
@@ -80,11 +78,11 @@ const SimulationPlayer: React.FC = () => {
|
|||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
isDragging.current = false;
|
isDragging.current = false;
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
@@ -109,24 +107,6 @@ const SimulationPlayer: React.FC = () => {
|
|||||||
{ name: "process 9", completed: 90 }, // 90% completed
|
{ name: "process 9", completed: 90 }, // 90% completed
|
||||||
{ name: "process 10", completed: 30 }, // 30% completed
|
{ name: "process 10", completed: 30 }, // 30% completed
|
||||||
];
|
];
|
||||||
// Move getRandomColor out of render
|
|
||||||
const getRandomColor = () => {
|
|
||||||
const letters = "0123456789ABCDEF";
|
|
||||||
let color = "#";
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
color += letters[Math.floor(Math.random() * 16)];
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store colors for each process item
|
|
||||||
const [_, setProcessColors] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Generate colors on mount or when process changes
|
|
||||||
useEffect(() => {
|
|
||||||
const generatedColors = process.map(() => getRandomColor());
|
|
||||||
setProcessColors(generatedColors);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const intervals = [10, 20, 30, 40, 50, 60]; // in minutes
|
const intervals = [10, 20, 30, 40, 50, 60]; // in minutes
|
||||||
const totalSegments = intervals.length;
|
const totalSegments = intervals.length;
|
||||||
@@ -218,7 +198,7 @@ const SimulationPlayer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{subModule === "simulations" && (
|
{subModule !== "analysis" && (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
{playSimulation
|
{playSimulation
|
||||||
@@ -281,7 +261,7 @@ const SimulationPlayer: React.FC = () => {
|
|||||||
const segmentProgress = (index / totalSegments) * 100;
|
const segmentProgress = (index / totalSegments) * 100;
|
||||||
const isFilled = progress >= segmentProgress;
|
const isFilled = progress >= segmentProgress;
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={`${index}-${label}`}>
|
||||||
<div className="label-dot-wrapper">
|
<div className="label-dot-wrapper">
|
||||||
<div className="label">{label} mins</div>
|
<div className="label">{label} mins</div>
|
||||||
<div
|
<div
|
||||||
@@ -360,6 +340,7 @@ const SimulationPlayer: React.FC = () => {
|
|||||||
className="process-wrapper"
|
className="process-wrapper"
|
||||||
style={{ padding: expand ? "0px" : "5px 35px" }}
|
style={{ padding: expand ? "0px" : "5px 35px" }}
|
||||||
>
|
>
|
||||||
|
{/* eslint-disable-next-line */}
|
||||||
<div
|
<div
|
||||||
className="process-container"
|
className="process-container"
|
||||||
ref={processWrapperRef}
|
ref={processWrapperRef}
|
||||||
@@ -367,7 +348,7 @@ const SimulationPlayer: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{process.map((item, index) => (
|
{process.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={`${index}-${item.name}`}
|
||||||
className="process"
|
className="process"
|
||||||
style={{
|
style={{
|
||||||
width: `${item.completed}%`,
|
width: `${item.completed}%`,
|
||||||
|
|||||||
@@ -1,220 +1,261 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from "@react-three/fiber";
|
||||||
import * as THREE from 'three';
|
import * as THREE from "three";
|
||||||
import { Line } from '@react-three/drei';
|
import { Line } from "@react-three/drei";
|
||||||
import {
|
import {
|
||||||
useAnimationPlaySpeed,
|
useAnimationPlaySpeed,
|
||||||
usePauseButtonStore,
|
usePauseButtonStore,
|
||||||
usePlayButtonStore,
|
usePlayButtonStore,
|
||||||
useResetButtonStore
|
useResetButtonStore,
|
||||||
} from '../../../../../store/usePlayButtonStore';
|
} from "../../../../../store/usePlayButtonStore";
|
||||||
|
|
||||||
function RoboticArmAnimator({
|
function RoboticArmAnimator({
|
||||||
HandleCallback,
|
HandleCallback,
|
||||||
restPosition,
|
restPosition,
|
||||||
ikSolver,
|
ikSolver,
|
||||||
targetBone,
|
targetBone,
|
||||||
armBot,
|
armBot,
|
||||||
logStatus,
|
logStatus,
|
||||||
path
|
path,
|
||||||
}: any) {
|
}: any) {
|
||||||
const progressRef = useRef(0);
|
const progressRef = useRef(0);
|
||||||
const curveRef = useRef<THREE.Vector3[] | null>(null);
|
const curveRef = useRef<THREE.Vector3[] | null>(null);
|
||||||
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
|
const [currentPath, setCurrentPath] = useState<[number, number, number][]>(
|
||||||
const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]);
|
[]
|
||||||
const [customCurvePoints, setCustomCurvePoints] = useState<THREE.Vector3[] | null>(null);
|
);
|
||||||
|
const [circlePoints, setCirclePoints] = useState<[number, number, number][]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [customCurvePoints, setCustomCurvePoints] = useState<
|
||||||
|
THREE.Vector3[] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
// Zustand stores
|
// Zustand stores
|
||||||
const { isPlaying } = usePlayButtonStore();
|
const { isPlaying } = usePlayButtonStore();
|
||||||
const { isPaused } = usePauseButtonStore();
|
const { isPaused } = usePauseButtonStore();
|
||||||
const { isReset } = useResetButtonStore();
|
const { isReset } = useResetButtonStore();
|
||||||
const { speed } = useAnimationPlaySpeed();
|
const { speed } = useAnimationPlaySpeed();
|
||||||
|
|
||||||
// Update path state whenever `path` prop changes
|
// Update path state whenever `path` prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPath(path);
|
setCurrentPath(path);
|
||||||
}, [path]);
|
}, [path]);
|
||||||
|
|
||||||
// Reset logic when `isPlaying` changes
|
// Reset logic when `isPlaying` changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
setCurrentPath([]);
|
setCurrentPath([]);
|
||||||
curveRef.current = null;
|
curveRef.current = null;
|
||||||
}
|
|
||||||
}, [isPlaying]);
|
|
||||||
|
|
||||||
// Handle circle points based on armBot position
|
|
||||||
useEffect(() => {
|
|
||||||
const points = generateRingPoints(1.6, 64)
|
|
||||||
setCirclePoints(points);
|
|
||||||
}, [armBot.position]);
|
|
||||||
|
|
||||||
|
|
||||||
function generateRingPoints(radius: any, segments: any) {
|
|
||||||
const points: [number, number, number][] = [];
|
|
||||||
for (let i = 0; i < segments; i++) {
|
|
||||||
// Calculate angle for current segment
|
|
||||||
const angle = (i / segments) * Math.PI * 2;
|
|
||||||
// Calculate x and z coordinates (y remains the same for a flat ring)
|
|
||||||
const x = Math.cos(angle) * radius;
|
|
||||||
const z = Math.sin(angle) * radius;
|
|
||||||
points.push([x, 1.5, z]);
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
}
|
}
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
const findNearestIndex = (nearestPoint: [number, number, number], points: [number, number, number][], epsilon = 1e-6) => {
|
// Handle circle points based on armBot position
|
||||||
for (let i = 0; i < points.length; i++) {
|
useEffect(() => {
|
||||||
const [x, y, z] = points[i];
|
const points = generateRingPoints(1.6, 64);
|
||||||
if (
|
setCirclePoints(points);
|
||||||
Math.abs(x - nearestPoint[0]) < epsilon &&
|
}, [armBot.position]);
|
||||||
Math.abs(y - nearestPoint[1]) < epsilon &&
|
|
||||||
Math.abs(z - nearestPoint[2]) < epsilon
|
function generateRingPoints(radius: any, segments: any) {
|
||||||
) {
|
const points: [number, number, number][] = [];
|
||||||
return i; // Found the matching index
|
for (let i = 0; i < segments; i++) {
|
||||||
}
|
// Calculate angle for current segment
|
||||||
|
const angle = (i / segments) * Math.PI * 2;
|
||||||
|
// Calculate x and z coordinates (y remains the same for a flat ring)
|
||||||
|
const x = Math.cos(angle) * radius;
|
||||||
|
const z = Math.sin(angle) * radius;
|
||||||
|
points.push([x, 1.5, z]);
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findNearestIndex = (
|
||||||
|
nearestPoint: [number, number, number],
|
||||||
|
points: [number, number, number][],
|
||||||
|
epsilon = 1e-6
|
||||||
|
) => {
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const [x, y, z] = points[i];
|
||||||
|
if (
|
||||||
|
Math.abs(x - nearestPoint[0]) < epsilon &&
|
||||||
|
Math.abs(y - nearestPoint[1]) < epsilon &&
|
||||||
|
Math.abs(z - nearestPoint[2]) < epsilon
|
||||||
|
) {
|
||||||
|
return i; // Found the matching index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1; // Not found
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle nearest points and final path (including arc points)
|
||||||
|
useEffect(() => {
|
||||||
|
if (circlePoints.length > 0 && currentPath.length > 0) {
|
||||||
|
const start = currentPath[0];
|
||||||
|
const end = currentPath[currentPath.length - 1];
|
||||||
|
|
||||||
|
const raisedStart = [start[0], start[1] + 0.5, start[2]] as [
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
const raisedEnd = [end[0], end[1] + 0.5, end[2]] as [
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number
|
||||||
|
];
|
||||||
|
|
||||||
|
const findNearest = (target: [number, number, number]) => {
|
||||||
|
return circlePoints.reduce((nearest, point) => {
|
||||||
|
const distance = Math.hypot(
|
||||||
|
target[0] - point[0],
|
||||||
|
target[1] - point[1],
|
||||||
|
target[2] - point[2]
|
||||||
|
);
|
||||||
|
const nearestDistance = Math.hypot(
|
||||||
|
target[0] - nearest[0],
|
||||||
|
target[1] - nearest[1],
|
||||||
|
target[2] - nearest[2]
|
||||||
|
);
|
||||||
|
return distance < nearestDistance ? point : nearest;
|
||||||
|
}, circlePoints[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nearestToStart = findNearest(raisedStart);
|
||||||
|
|
||||||
|
const nearestToEnd = findNearest(raisedEnd);
|
||||||
|
|
||||||
|
const indexOfNearestStart = findNearestIndex(
|
||||||
|
nearestToStart,
|
||||||
|
circlePoints
|
||||||
|
);
|
||||||
|
|
||||||
|
const indexOfNearestEnd = findNearestIndex(nearestToEnd, circlePoints);
|
||||||
|
|
||||||
|
// Find clockwise and counter-clockwise distances
|
||||||
|
const clockwiseDistance =
|
||||||
|
(indexOfNearestEnd - indexOfNearestStart + 64) % 64;
|
||||||
|
|
||||||
|
const counterClockwiseDistance =
|
||||||
|
(indexOfNearestStart - indexOfNearestEnd + 64) % 64;
|
||||||
|
|
||||||
|
const clockwiseIsShorter = clockwiseDistance <= counterClockwiseDistance;
|
||||||
|
|
||||||
|
// Collect arc points between start and end
|
||||||
|
let arcPoints: [number, number, number][] = [];
|
||||||
|
|
||||||
|
if (clockwiseIsShorter) {
|
||||||
|
if (indexOfNearestStart <= indexOfNearestEnd) {
|
||||||
|
arcPoints = circlePoints.slice(
|
||||||
|
indexOfNearestStart,
|
||||||
|
indexOfNearestEnd + 1
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Wrap around
|
||||||
|
arcPoints = [
|
||||||
|
...circlePoints.slice(indexOfNearestStart, 64),
|
||||||
|
...circlePoints.slice(0, indexOfNearestEnd + 1),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return -1; // Not found
|
} else if (indexOfNearestStart >= indexOfNearestEnd) {
|
||||||
};
|
for (
|
||||||
|
let i = indexOfNearestStart;
|
||||||
|
i !== (indexOfNearestEnd - 1 + 64) % 64;
|
||||||
// Handle nearest points and final path (including arc points)
|
i = (i - 1 + 64) % 64
|
||||||
useEffect(() => {
|
) {
|
||||||
if (circlePoints.length > 0 && currentPath.length > 0) {
|
arcPoints.push(circlePoints[i]);
|
||||||
const start = currentPath[0];
|
|
||||||
const end = currentPath[currentPath.length - 1];
|
|
||||||
|
|
||||||
const raisedStart = [start[0], start[1] + 0.5, start[2]] as [number, number, number];
|
|
||||||
const raisedEnd = [end[0], end[1] + 0.5, end[2]] as [number, number, number];
|
|
||||||
|
|
||||||
const findNearest = (target: [number, number, number]) => {
|
|
||||||
return circlePoints.reduce((nearest, point) => {
|
|
||||||
const distance = Math.hypot(
|
|
||||||
target[0] - point[0],
|
|
||||||
target[1] - point[1],
|
|
||||||
target[2] - point[2]
|
|
||||||
);
|
|
||||||
const nearestDistance = Math.hypot(
|
|
||||||
target[0] - nearest[0],
|
|
||||||
target[1] - nearest[1],
|
|
||||||
target[2] - nearest[2]
|
|
||||||
);
|
|
||||||
return distance < nearestDistance ? point : nearest;
|
|
||||||
}, circlePoints[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const nearestToStart = findNearest(raisedStart);
|
|
||||||
|
|
||||||
const nearestToEnd = findNearest(raisedEnd);
|
|
||||||
|
|
||||||
const indexOfNearestStart = findNearestIndex(nearestToStart, circlePoints);
|
|
||||||
|
|
||||||
const indexOfNearestEnd = findNearestIndex(nearestToEnd, circlePoints);
|
|
||||||
|
|
||||||
// Find clockwise and counter-clockwise distances
|
|
||||||
const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + 64) % 64;
|
|
||||||
|
|
||||||
const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + 64) % 64;
|
|
||||||
|
|
||||||
const clockwiseIsShorter = clockwiseDistance <= counterClockwiseDistance;
|
|
||||||
|
|
||||||
// Collect arc points between start and end
|
|
||||||
let arcPoints: [number, number, number][] = [];
|
|
||||||
|
|
||||||
if (clockwiseIsShorter) {
|
|
||||||
if (indexOfNearestStart <= indexOfNearestEnd) {
|
|
||||||
arcPoints = circlePoints.slice(indexOfNearestStart, indexOfNearestEnd + 1);
|
|
||||||
} else {
|
|
||||||
// Wrap around
|
|
||||||
arcPoints = [
|
|
||||||
...circlePoints.slice(indexOfNearestStart, 64),
|
|
||||||
...circlePoints.slice(0, indexOfNearestEnd + 1)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (indexOfNearestStart >= indexOfNearestEnd) {
|
|
||||||
for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) {
|
|
||||||
arcPoints.push(circlePoints[i]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) {
|
|
||||||
arcPoints.push(circlePoints[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue your custom path logic
|
|
||||||
const pathVectors = [
|
|
||||||
new THREE.Vector3(start[0], start[1], start[2]), // start
|
|
||||||
new THREE.Vector3(raisedStart[0], raisedStart[1], raisedStart[2]), // lift up
|
|
||||||
new THREE.Vector3(nearestToStart[0], raisedStart[1], nearestToStart[2]), // move to arc start
|
|
||||||
...arcPoints.map(point => new THREE.Vector3(point[0], raisedStart[1], point[2])),
|
|
||||||
new THREE.Vector3(nearestToEnd[0], raisedEnd[1], nearestToEnd[2]), // move from arc end
|
|
||||||
new THREE.Vector3(raisedEnd[0], raisedEnd[1], raisedEnd[2]), // lowered end
|
|
||||||
new THREE.Vector3(end[0], end[1], end[2]) // end
|
|
||||||
];
|
|
||||||
|
|
||||||
const customCurve = new THREE.CatmullRomCurve3(pathVectors, false, 'centripetal', 1);
|
|
||||||
const generatedPoints = customCurve.getPoints(100);
|
|
||||||
setCustomCurvePoints(generatedPoints);
|
|
||||||
}
|
}
|
||||||
}, [circlePoints, currentPath]);
|
}
|
||||||
|
|
||||||
// Frame update for animation
|
// Continue your custom path logic
|
||||||
useFrame((_, delta) => {
|
const pathVectors = [
|
||||||
if (!ikSolver) return;
|
new THREE.Vector3(start[0], start[1], start[2]), // start
|
||||||
|
new THREE.Vector3(raisedStart[0], raisedStart[1], raisedStart[2]), // lift up
|
||||||
|
new THREE.Vector3(nearestToStart[0], raisedStart[1], nearestToStart[2]), // move to arc start
|
||||||
|
...arcPoints.map(
|
||||||
|
(point) => new THREE.Vector3(point[0], raisedStart[1], point[2])
|
||||||
|
),
|
||||||
|
new THREE.Vector3(nearestToEnd[0], raisedEnd[1], nearestToEnd[2]), // move from arc end
|
||||||
|
new THREE.Vector3(raisedEnd[0], raisedEnd[1], raisedEnd[2]), // lowered end
|
||||||
|
new THREE.Vector3(end[0], end[1], end[2]), // end
|
||||||
|
];
|
||||||
|
|
||||||
const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone);
|
const customCurve = new THREE.CatmullRomCurve3(
|
||||||
if (!bone) return;
|
pathVectors,
|
||||||
|
false,
|
||||||
|
"centripetal",
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const generatedPoints = customCurve.getPoints(100);
|
||||||
|
setCustomCurvePoints(generatedPoints);
|
||||||
|
}
|
||||||
|
}, [circlePoints, currentPath]);
|
||||||
|
|
||||||
if (isPlaying) {
|
// Frame update for animation
|
||||||
if (!isPaused && customCurvePoints && currentPath.length > 0) {
|
useFrame((_, delta) => {
|
||||||
const curvePoints = customCurvePoints;
|
if (!ikSolver) return;
|
||||||
const speedAdjustedProgress = progressRef.current + (speed * armBot.speed);
|
|
||||||
const index = Math.floor(speedAdjustedProgress);
|
|
||||||
|
|
||||||
if (index >= curvePoints.length) {
|
const bone = ikSolver.mesh.skeleton.bones.find(
|
||||||
// Reached the end of the curve
|
(b: any) => b.name === targetBone
|
||||||
HandleCallback();
|
|
||||||
setCurrentPath([]);
|
|
||||||
curveRef.current = null;
|
|
||||||
progressRef.current = 0;
|
|
||||||
} else {
|
|
||||||
const point = curvePoints[index];
|
|
||||||
bone.position.copy(point);
|
|
||||||
progressRef.current = speedAdjustedProgress;
|
|
||||||
}
|
|
||||||
} else if (isPaused) {
|
|
||||||
logStatus(armBot.modelUuid, 'Simulation Paused');
|
|
||||||
}
|
|
||||||
|
|
||||||
ikSolver.update();
|
|
||||||
} else if (!isPlaying && currentPath.length === 0) {
|
|
||||||
// Not playing anymore, reset to rest
|
|
||||||
bone.position.copy(restPosition);
|
|
||||||
ikSolver.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{customCurvePoints && currentPath && isPlaying && (
|
|
||||||
<mesh rotation={armBot.rotation} position={armBot.position}>
|
|
||||||
<Line
|
|
||||||
points={customCurvePoints.map((p) => [p.x, p.y, p.z] as [number, number, number])}
|
|
||||||
color="green"
|
|
||||||
lineWidth={5}
|
|
||||||
dashed={false}
|
|
||||||
/>
|
|
||||||
</mesh>
|
|
||||||
)}
|
|
||||||
<mesh position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
||||||
<ringGeometry args={[1.59, 1.61, 64]} />
|
|
||||||
<meshBasicMaterial color="green" side={THREE.DoubleSide} />
|
|
||||||
</mesh>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
if (!bone) return;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
if (!isPaused && customCurvePoints && currentPath.length > 0) {
|
||||||
|
const curvePoints = customCurvePoints;
|
||||||
|
const speedAdjustedProgress =
|
||||||
|
progressRef.current + speed * armBot.speed;
|
||||||
|
const index = Math.floor(speedAdjustedProgress);
|
||||||
|
|
||||||
|
if (index >= curvePoints.length) {
|
||||||
|
// Reached the end of the curve
|
||||||
|
HandleCallback();
|
||||||
|
setCurrentPath([]);
|
||||||
|
curveRef.current = null;
|
||||||
|
progressRef.current = 0;
|
||||||
|
} else {
|
||||||
|
const point = curvePoints[index];
|
||||||
|
bone.position.copy(point);
|
||||||
|
progressRef.current = speedAdjustedProgress;
|
||||||
|
}
|
||||||
|
} else if (isPaused) {
|
||||||
|
logStatus(armBot.modelUuid, "Simulation Paused");
|
||||||
|
}
|
||||||
|
|
||||||
|
ikSolver.update();
|
||||||
|
} else if (!isPlaying && currentPath.length === 0) {
|
||||||
|
// Not playing anymore, reset to rest
|
||||||
|
bone.position.copy(restPosition);
|
||||||
|
ikSolver.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{customCurvePoints && currentPath && isPlaying && (
|
||||||
|
<mesh rotation={armBot.rotation} position={armBot.position}>
|
||||||
|
<Line
|
||||||
|
points={customCurvePoints.map(
|
||||||
|
(p) => [p.x, p.y, p.z] as [number, number, number]
|
||||||
|
)}
|
||||||
|
color="green"
|
||||||
|
lineWidth={5}
|
||||||
|
dashed={false}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
<mesh
|
||||||
|
position={[
|
||||||
|
armBot.position[0],
|
||||||
|
armBot.position[1] + 1.5,
|
||||||
|
armBot.position[2],
|
||||||
|
]}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
visible={false}
|
||||||
|
>
|
||||||
|
<ringGeometry args={[1.59, 1.61, 64]} />
|
||||||
|
<meshBasicMaterial color="green" side={THREE.DoubleSide} />
|
||||||
|
</mesh>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RoboticArmAnimator;
|
export default RoboticArmAnimator;
|
||||||
|
|||||||
Reference in New Issue
Block a user