- Added ArmBot component to manage ArmBot instances in the simulation. - Created ArmBotInstances component to render individual ArmBot models. - Developed IKAnimationController for handling inverse kinematics during animations. - Introduced IkInstances component to load and manage IK-enabled arm models. - Defined simulation types for ArmBot events and connections in TypeScript. - Enhanced type definitions for better clarity and maintainability.
400 lines
17 KiB
TypeScript
400 lines
17 KiB
TypeScript
import React, { useRef, useMemo, useCallback, useState } from "react";
|
|
import { InfoIcon, AddIcon, RemoveIcon, ResizeHeightIcon } from "../../../icons/ExportCommonIcons";
|
|
import InputWithDropDown from "../../../ui/inputs/InputWithDropDown";
|
|
import { useSelectedActionSphere, useSimulationStates, useSocketStore } from "../../../../store/store";
|
|
import * as SimulationTypes from '../../../../types/simulation';
|
|
import LabledDropdown from "../../../ui/inputs/LabledDropdown";
|
|
import { handleResize } from "../../../../functions/handleResizePannel";
|
|
|
|
interface ConnectedModel {
|
|
modelUUID: string;
|
|
modelName: string;
|
|
points: {
|
|
uuid: string;
|
|
position: [number, number, number];
|
|
index?: number;
|
|
}[];
|
|
triggers?: {
|
|
uuid: string;
|
|
name: string;
|
|
type: string;
|
|
isUsed: boolean;
|
|
}[];
|
|
}
|
|
|
|
const ArmBotMechanics: React.FC = () => {
|
|
const { selectedActionSphere } = useSelectedActionSphere();
|
|
const { simulationStates, setSimulationStates } = useSimulationStates();
|
|
const { socket } = useSocketStore();
|
|
const [selectedProcessIndex, setSelectedProcessIndex] = useState<number | null>(null);
|
|
const actionsContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Get connected models and their triggers
|
|
const connectedModels = useMemo<ConnectedModel[]>(() => {
|
|
if (!selectedActionSphere?.points?.uuid) return [];
|
|
|
|
const armBotPaths = simulationStates.filter(
|
|
(path): path is SimulationTypes.ArmBotEventsSchema => path.type === "ArmBot"
|
|
);
|
|
|
|
const currentPoint = armBotPaths.find(
|
|
(path) => path.points.uuid === selectedActionSphere.points.uuid
|
|
)?.points;
|
|
|
|
if (!currentPoint?.connections?.targets) return [];
|
|
|
|
return currentPoint.connections.targets.reduce<ConnectedModel[]>((acc, target) => {
|
|
const connectedModel = simulationStates.find(
|
|
(model) => model.modeluuid === target.modelUUID
|
|
);
|
|
|
|
if (!connectedModel) return acc;
|
|
|
|
let triggers: { uuid: string; name: string; type: string; isUsed: boolean }[] = [];
|
|
let points: { uuid: string; position: [number, number, number] }[] = [];
|
|
|
|
if (connectedModel.type === "Conveyor") {
|
|
const conveyor = connectedModel as SimulationTypes.ConveyorEventsSchema;
|
|
|
|
const connectedPointUUIDs = currentPoint?.connections?.targets
|
|
.filter(t => t.modelUUID === connectedModel.modeluuid)
|
|
.map(t => t.pointUUID) || [];
|
|
|
|
points = conveyor.points
|
|
.map((point, idx) => ({
|
|
uuid: point.uuid,
|
|
position: point.position,
|
|
index: idx
|
|
}))
|
|
.filter(point => connectedPointUUIDs.includes(point.uuid));
|
|
|
|
|
|
triggers = conveyor.points.flatMap(p => p.triggers?.filter(t => t.isUsed) || []);
|
|
}
|
|
else if (connectedModel.type === "StaticMachine") {
|
|
const staticMachine = connectedModel as SimulationTypes.StaticMachineEventsSchema;
|
|
|
|
points = [{
|
|
uuid: staticMachine.points.uuid,
|
|
position: staticMachine.points.position
|
|
}];
|
|
|
|
triggers = staticMachine.points.triggers ?
|
|
[{
|
|
uuid: staticMachine.points.triggers.uuid,
|
|
name: staticMachine.points.triggers.name,
|
|
type: staticMachine.points.triggers.type,
|
|
isUsed: true // StaticMachine triggers are always considered used
|
|
}] : [];
|
|
}
|
|
|
|
if (!acc.some(m => m.modelUUID === connectedModel.modeluuid)) {
|
|
acc.push({
|
|
modelUUID: connectedModel.modeluuid,
|
|
modelName: connectedModel.modelName,
|
|
points,
|
|
triggers
|
|
});
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
}, [selectedActionSphere, simulationStates]);
|
|
|
|
// Get triggers from connected models
|
|
const connectedTriggers = useMemo(() => {
|
|
return connectedModels.flatMap(model =>
|
|
(model.triggers || []).map(trigger => ({
|
|
...trigger,
|
|
displayName: `${model.modelName} - ${trigger.name}`,
|
|
modelUUID: model.modelUUID
|
|
}))
|
|
);
|
|
}, [connectedModels]);
|
|
|
|
// Get all points from connected models
|
|
const connectedPoints = useMemo(() => {
|
|
return connectedModels.flatMap(model =>
|
|
model.points.map(point => ({
|
|
...point,
|
|
displayName: `${model.modelName} - Point${typeof point.index === 'number' ? ` ${point.index}` : ''}`,
|
|
modelUUID: model.modelUUID
|
|
}))
|
|
);
|
|
}, [connectedModels]);
|
|
|
|
|
|
const { selectedPoint } = useMemo(() => {
|
|
if (!selectedActionSphere?.points?.uuid) return { selectedPoint: null };
|
|
|
|
const armBotPaths = simulationStates.filter(
|
|
(path): path is SimulationTypes.ArmBotEventsSchema => path.type === "ArmBot"
|
|
);
|
|
|
|
const points = armBotPaths.find(
|
|
(path) => path.points.uuid === selectedActionSphere.points.uuid
|
|
)?.points;
|
|
|
|
return {
|
|
selectedPoint: points || null
|
|
};
|
|
}, [selectedActionSphere, simulationStates]);
|
|
|
|
const updateBackend = async (updatedPath: SimulationTypes.ArmBotEventsSchema | undefined) => {
|
|
if (!updatedPath) return;
|
|
const email = localStorage.getItem("email");
|
|
const organization = email ? email.split("@")[1].split(".")[0] : "";
|
|
|
|
const data = {
|
|
organization: organization,
|
|
modeluuid: updatedPath.modeluuid,
|
|
eventData: { type: "ArmBot", points: updatedPath.points }
|
|
}
|
|
|
|
socket.emit('v2:model-asset:updateEventData', data);
|
|
}
|
|
|
|
const handleActionUpdate = useCallback((updatedAction: Partial<SimulationTypes.ArmBotEventsSchema['points']['actions']>) => {
|
|
if (!selectedActionSphere?.points?.uuid || !selectedPoint) return;
|
|
|
|
const updatedPaths = simulationStates.map((path) => {
|
|
if (path.type === "ArmBot" && path.points.uuid === selectedActionSphere.points.uuid) {
|
|
return {
|
|
...path,
|
|
points: {
|
|
...path.points,
|
|
actions: {
|
|
...path.points.actions,
|
|
...updatedAction
|
|
}
|
|
}
|
|
};
|
|
}
|
|
return path;
|
|
});
|
|
|
|
const updatedPath = updatedPaths.find(
|
|
(path): path is SimulationTypes.ArmBotEventsSchema =>
|
|
path.type === "ArmBot" &&
|
|
path.points.uuid === selectedActionSphere.points.uuid
|
|
);
|
|
updateBackend(updatedPath);
|
|
|
|
setSimulationStates(updatedPaths);
|
|
}, [selectedActionSphere?.points?.uuid, selectedPoint, simulationStates, setSimulationStates]);
|
|
|
|
const handleSpeedChange = useCallback((speed: number) => {
|
|
handleActionUpdate({ speed });
|
|
}, [handleActionUpdate]);
|
|
|
|
const handleProcessChange = useCallback((processes: SimulationTypes.ArmBotEventsSchema['points']['actions']['processes']) => {
|
|
handleActionUpdate({ processes });
|
|
}, [handleActionUpdate]);
|
|
|
|
const handleAddProcess = useCallback(() => {
|
|
if (!selectedPoint) return;
|
|
|
|
const newProcess: any = {
|
|
triggerId: "",
|
|
startPoint: "",
|
|
endPoint: ""
|
|
};
|
|
|
|
const updatedProcesses = selectedPoint.actions.processes ? [...selectedPoint.actions.processes, newProcess] : [newProcess];
|
|
|
|
handleProcessChange(updatedProcesses);
|
|
setSelectedProcessIndex(updatedProcesses.length - 1);
|
|
}, [selectedPoint, handleProcessChange]);
|
|
|
|
const handleDeleteProcess = useCallback((index: number) => {
|
|
if (!selectedPoint?.actions.processes) return;
|
|
|
|
const updatedProcesses = [...selectedPoint.actions.processes];
|
|
updatedProcesses.splice(index, 1);
|
|
|
|
handleProcessChange(updatedProcesses);
|
|
|
|
// Reset selection if deleting the currently selected process
|
|
if (selectedProcessIndex === index) {
|
|
setSelectedProcessIndex(null);
|
|
} else if (selectedProcessIndex !== null && selectedProcessIndex > index) {
|
|
// Adjust selection index if needed
|
|
setSelectedProcessIndex(selectedProcessIndex - 1);
|
|
}
|
|
}, [selectedPoint, selectedProcessIndex, handleProcessChange]);
|
|
|
|
const handleTriggerSelect = useCallback((displayName: string, index: number) => {
|
|
const selected = connectedTriggers.find(t => t.displayName === displayName);
|
|
if (!selected || !selectedPoint?.actions.processes) return;
|
|
|
|
const oldProcess = selectedPoint.actions.processes[index];
|
|
|
|
const updatedProcesses = [...selectedPoint.actions.processes];
|
|
|
|
// Only reset start/end if new trigger invalidates them (your logic can expand this)
|
|
updatedProcesses[index] = {
|
|
...oldProcess,
|
|
triggerId: selected.uuid,
|
|
startPoint: oldProcess.startPoint || "", // preserve if exists
|
|
endPoint: oldProcess.endPoint || "" // preserve if exists
|
|
};
|
|
|
|
handleProcessChange(updatedProcesses);
|
|
}, [connectedTriggers, selectedPoint, handleProcessChange]);
|
|
|
|
const handleStartPointSelect = useCallback((displayName: string, index: number) => {
|
|
if (!selectedPoint?.actions.processes) return;
|
|
|
|
const point = connectedPoints.find(p => p.displayName === displayName);
|
|
if (!point) return;
|
|
|
|
const updatedProcesses = [...selectedPoint.actions.processes];
|
|
updatedProcesses[index] = {
|
|
...updatedProcesses[index],
|
|
startPoint: point.uuid
|
|
};
|
|
|
|
handleProcessChange(updatedProcesses);
|
|
}, [selectedPoint, connectedPoints, handleProcessChange]);
|
|
|
|
const handleEndPointSelect = useCallback((displayName: string, index: number) => {
|
|
if (!selectedPoint?.actions.processes) return;
|
|
|
|
const point = connectedPoints.find(p => p.displayName === displayName);
|
|
if (!point) return;
|
|
|
|
const updatedProcesses = [...selectedPoint.actions.processes];
|
|
updatedProcesses[index] = {
|
|
...updatedProcesses[index],
|
|
endPoint: point.uuid
|
|
};
|
|
|
|
handleProcessChange(updatedProcesses);
|
|
}, [selectedPoint, connectedPoints, handleProcessChange]);
|
|
|
|
const getProcessByIndex = useCallback((index: number) => {
|
|
if (!selectedPoint?.actions.processes || index >= selectedPoint.actions.processes.length) return null;
|
|
return selectedPoint.actions.processes[index];
|
|
}, [selectedPoint]);
|
|
|
|
const getFilteredTriggerOptions = (currentIndex: number) => {
|
|
const usedTriggerUUIDs = selectedPoint?.actions.processes?.filter((_, i) => i !== currentIndex).map(p => p.triggerId).filter(Boolean) ?? [];
|
|
|
|
return connectedTriggers.filter(trigger => !usedTriggerUUIDs.includes(trigger.uuid)).map(trigger => trigger.displayName);
|
|
};
|
|
|
|
return (
|
|
<div className="machine-mechanics-container" key={selectedPoint?.uuid}>
|
|
<div className="machine-mechanics-header">
|
|
{selectedActionSphere?.path?.modelName || "ArmBot point not found"}
|
|
</div>
|
|
|
|
<div className="machine-mechanics-content-container">
|
|
<div className="selected-properties-container">
|
|
<div className="properties-header">ArmBot Properties</div>
|
|
|
|
{selectedPoint && (
|
|
<>
|
|
<InputWithDropDown
|
|
key={`speed-${selectedPoint.uuid}`}
|
|
label="ArmBot Speed"
|
|
value={selectedPoint.actions.speed.toString()}
|
|
onChange={(value) => handleSpeedChange(parseInt(value))}
|
|
/>
|
|
|
|
<div className="actions">
|
|
<div className="header">
|
|
<div className="header-value">Processes</div>
|
|
<div className="add-button" onClick={handleAddProcess}>
|
|
<AddIcon /> Add
|
|
</div>
|
|
</div>
|
|
<div
|
|
className="lists-main-container"
|
|
ref={actionsContainerRef}
|
|
style={{ height: "120px" }}
|
|
>
|
|
<div className="list-container">
|
|
{selectedPoint.actions.processes?.map((process, index) => (
|
|
<div
|
|
key={`process-${index}`}
|
|
className={`list-item ${selectedProcessIndex === index ? "active" : ""}`}
|
|
>
|
|
<div
|
|
className="value"
|
|
onClick={() => setSelectedProcessIndex(index)}
|
|
>
|
|
Process {index + 1}
|
|
</div>
|
|
<div
|
|
className="remove-button"
|
|
onClick={() => handleDeleteProcess(index)}
|
|
>
|
|
<RemoveIcon />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div
|
|
className="resize-icon"
|
|
id="action-resize"
|
|
onMouseDown={(e) => handleResize(e, actionsContainerRef)}
|
|
>
|
|
<ResizeHeightIcon />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedProcessIndex !== null && (
|
|
<div className="process-configuration">
|
|
<LabledDropdown
|
|
key={`trigger-select-${selectedProcessIndex}`}
|
|
label="Select Trigger"
|
|
defaultOption={
|
|
connectedTriggers.find(t =>
|
|
t.uuid === getProcessByIndex(selectedProcessIndex)?.triggerId
|
|
)?.displayName || 'Select a trigger'
|
|
}
|
|
onSelect={(value) => handleTriggerSelect(value, selectedProcessIndex)}
|
|
options={getFilteredTriggerOptions(selectedProcessIndex)}
|
|
/>
|
|
|
|
<LabledDropdown
|
|
key={`start-point-${selectedProcessIndex}`}
|
|
label="Start Point"
|
|
defaultOption={
|
|
connectedPoints.find(p =>
|
|
p.uuid === getProcessByIndex(selectedProcessIndex)?.startPoint
|
|
)?.displayName || 'Select start point'
|
|
}
|
|
onSelect={(value) => handleStartPointSelect(value, selectedProcessIndex)}
|
|
options={connectedPoints.map(point => point.displayName)}
|
|
/>
|
|
|
|
<LabledDropdown
|
|
key={`end-point-${selectedProcessIndex}`}
|
|
label="End Point"
|
|
defaultOption={
|
|
connectedPoints.find(p =>
|
|
p.uuid === getProcessByIndex(selectedProcessIndex)?.endPoint
|
|
)?.displayName || 'Select end point'
|
|
}
|
|
onSelect={(value) => handleEndPointSelect(value, selectedProcessIndex)}
|
|
options={connectedPoints.map(point => point.displayName)}
|
|
/>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="footer">
|
|
<InfoIcon />
|
|
Configure ArmBot properties and trigger-based processes.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default React.memo(ArmBotMechanics); |