519 lines
21 KiB
TypeScript
519 lines
21 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
import * as THREE from "three";
|
|
import { useSubModuleStore } from "../../../../store/useModuleStore";
|
|
import { useSelectedAction, useSelectedAsset } from "../../../../store/simulation/useSimulationStore";
|
|
import { useProductStore } from "../../../../store/simulation/useProductStore";
|
|
import { useEventsStore } from "../../../../store/simulation/useEventsStore";
|
|
import { useSelectedProduct } from "../../../../store/simulation/useSimulationStore";
|
|
import { handleAddEventToProduct } from "../points/functions/handleAddEventToProduct";
|
|
import { QuadraticBezierLine } from "@react-three/drei";
|
|
import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertProductOrEventApi";
|
|
import { useDeleteTool } from "../../../../store/builder/store";
|
|
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
|
|
import { ArrowOnQuadraticBezier, Arrows } from "../arrows/arrows";
|
|
|
|
interface ConnectionLine {
|
|
id: string;
|
|
startPointUuid: string;
|
|
endPointUuid: string;
|
|
trigger: TriggerSchema;
|
|
}
|
|
|
|
function TriggerConnector() {
|
|
const { gl, raycaster, scene, pointer, camera } = useThree();
|
|
const { subModule } = useSubModuleStore();
|
|
const { products, getPointByUuid, getIsEventInProduct, getActionByUuid, addTrigger, removeTrigger, addEvent, getEventByModelUuid, getPointUuidByActionUuid, getProductById } = useProductStore();
|
|
const { selectedAsset, clearSelectedAsset } = useSelectedAsset();
|
|
const { selectedProduct } = useSelectedProduct();
|
|
const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null);
|
|
const groupRefs = useRef<Record<string, any>>({});
|
|
const [helperlineColor, setHelperLineColor] = useState<string>("red");
|
|
const [currentLine, setCurrentLine] = useState<{ start: THREE.Vector3; end: THREE.Vector3; mid: THREE.Vector3; } | null>(null);
|
|
const { deleteTool } = useDeleteTool();
|
|
const { isPlaying } = usePlayButtonStore();
|
|
const { selectedAction } = useSelectedAction();
|
|
|
|
const [firstSelectedPoint, setFirstSelectedPoint] = useState<{
|
|
productId: string;
|
|
modelUuid: string;
|
|
pointUuid: string;
|
|
actionUuid?: string;
|
|
} | null>(null);
|
|
|
|
const [connections, setConnections] = useState<ConnectionLine[]>([]);
|
|
|
|
const email = localStorage.getItem('email')
|
|
const organization = (email!.split("@")[1]).split(".")[0];
|
|
|
|
const updateBackend = (
|
|
productName: string,
|
|
productId: string,
|
|
organization: string,
|
|
eventData: EventsSchema
|
|
) => {
|
|
upsertProductOrEventApi({
|
|
productName: productName,
|
|
productId: productId,
|
|
organization: organization,
|
|
eventDatas: eventData
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
const newConnections: ConnectionLine[] = [];
|
|
|
|
const product = getProductById(selectedProduct.productId);
|
|
if (!product || products.length === 0) return;
|
|
|
|
product.eventDatas.forEach(event => {
|
|
// Handle Conveyor points
|
|
if (event.type === "transfer" && 'points' in event) {
|
|
event.points.forEach(point => {
|
|
if (point.action?.triggers) {
|
|
point.action.triggers.forEach(trigger => {
|
|
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
|
newConnections.push({
|
|
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
|
startPointUuid: point.uuid,
|
|
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
|
trigger
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
// Handle Vehicle point
|
|
else if (event.type === "vehicle" && 'point' in event) {
|
|
const point = event.point;
|
|
if (point.action?.triggers) {
|
|
point.action.triggers.forEach(trigger => {
|
|
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
|
newConnections.push({
|
|
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
|
startPointUuid: point.uuid,
|
|
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
|
trigger
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// Handle Robotic Arm points
|
|
else if (event.type === "roboticArm" && 'point' in event) {
|
|
const point = event.point;
|
|
point.actions?.forEach(action => {
|
|
action.triggers?.forEach(trigger => {
|
|
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
|
newConnections.push({
|
|
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
|
startPointUuid: point.uuid,
|
|
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
|
trigger
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
// Handle Machine point
|
|
else if (event.type === "machine" && 'point' in event) {
|
|
const point = event.point;
|
|
if (point.action?.triggers) {
|
|
point.action.triggers.forEach(trigger => {
|
|
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
|
newConnections.push({
|
|
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
|
startPointUuid: point.uuid,
|
|
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
|
trigger
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// Handle StorageUnit point
|
|
else if (event.type === "storageUnit" && 'point' in event) {
|
|
const point = event.point;
|
|
if (point.action?.triggers) {
|
|
point.action.triggers.forEach(trigger => {
|
|
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
|
|
newConnections.push({
|
|
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
|
|
startPointUuid: point.uuid,
|
|
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
|
|
trigger
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
setConnections(newConnections);
|
|
}, [products, selectedProduct.productId]);
|
|
|
|
useEffect(() => {
|
|
const canvasElement = gl.domElement;
|
|
|
|
let drag = false;
|
|
let isRightMouseDown = false;
|
|
|
|
const onMouseDown = (evt: MouseEvent) => {
|
|
if (selectedAsset) {
|
|
clearSelectedAsset();
|
|
}
|
|
if (evt.button === 2) {
|
|
isRightMouseDown = true;
|
|
drag = false;
|
|
}
|
|
};
|
|
|
|
const onMouseUp = (evt: MouseEvent) => {
|
|
if (evt.button === 2) {
|
|
isRightMouseDown = false;
|
|
}
|
|
}
|
|
|
|
const onMouseMove = () => {
|
|
if (isRightMouseDown) {
|
|
drag = true;
|
|
}
|
|
};
|
|
|
|
const handleRightClick = (evt: MouseEvent) => {
|
|
if (drag) return;
|
|
evt.preventDefault();
|
|
|
|
const intersects = raycaster
|
|
.intersectObjects(scene.children, true)
|
|
.filter(
|
|
(intersect) =>
|
|
intersect.object.name === ('Event-Sphere')
|
|
);
|
|
if (intersects.length === 0) {
|
|
setFirstSelectedPoint(null);
|
|
return;
|
|
};
|
|
|
|
const currentObject = intersects[0].object;
|
|
if (!currentObject || currentObject.name !== 'Event-Sphere') {
|
|
setFirstSelectedPoint(null);
|
|
return;
|
|
};
|
|
|
|
const modelUuid = currentObject.userData.modelUuid;
|
|
const pointUuid = currentObject.userData.pointUuid;
|
|
|
|
if (firstSelectedPoint && firstSelectedPoint.pointUuid === pointUuid) {
|
|
setFirstSelectedPoint(null);
|
|
return;
|
|
}
|
|
|
|
if (selectedProduct && getIsEventInProduct(selectedProduct.productId, modelUuid)) {
|
|
|
|
const point = getPointByUuid(
|
|
selectedProduct.productId,
|
|
modelUuid,
|
|
pointUuid
|
|
);
|
|
|
|
const event = getEventByModelUuid(selectedProduct.productId, modelUuid);
|
|
const clickedPointUuid = getPointUuidByActionUuid(selectedProduct.productId, selectedAction.actionId || '');
|
|
|
|
if (!point || !event) {
|
|
setFirstSelectedPoint(null);
|
|
return;
|
|
};
|
|
|
|
let actionUuid: string | undefined;
|
|
if ('action' in point && point.action) {
|
|
actionUuid = point.action.actionUuid;
|
|
} else if ('actions' in point && point.actions.length === 1) {
|
|
actionUuid = point.actions[0].actionUuid;
|
|
}
|
|
|
|
if (!firstSelectedPoint) {
|
|
if (point.uuid !== clickedPointUuid) return;
|
|
setFirstSelectedPoint({
|
|
productId: selectedProduct.productId,
|
|
modelUuid,
|
|
pointUuid,
|
|
actionUuid: selectedAction.actionId || ''
|
|
});
|
|
} else {
|
|
const trigger: TriggerSchema = {
|
|
triggerUuid: THREE.MathUtils.generateUUID(),
|
|
triggerName: `Trigger ${firstSelectedPoint.pointUuid.slice(0, 4)} → ${pointUuid.slice(0, 4)}`,
|
|
triggerType: "onComplete",
|
|
delay: 0,
|
|
triggeredAsset: {
|
|
triggeredModel: {
|
|
modelName: event.modelName || 'Unknown',
|
|
modelUuid: modelUuid
|
|
},
|
|
triggeredPoint: {
|
|
pointName: 'Point',
|
|
pointUuid: point.uuid
|
|
},
|
|
triggeredAction: actionUuid ? {
|
|
actionName: getActionByUuid(selectedProduct.productId, actionUuid)?.actionName || 'Action',
|
|
actionUuid: actionUuid
|
|
} : null
|
|
}
|
|
};
|
|
|
|
if (firstSelectedPoint.actionUuid) {
|
|
const event = addTrigger(selectedProduct.productId, firstSelectedPoint.actionUuid, trigger);
|
|
|
|
if (event) {
|
|
updateBackend(
|
|
selectedProduct.productName,
|
|
selectedProduct.productId,
|
|
organization,
|
|
event
|
|
);
|
|
}
|
|
}
|
|
setFirstSelectedPoint(null);
|
|
}
|
|
} else if (!getIsEventInProduct(selectedProduct.productId, modelUuid) && firstSelectedPoint) {
|
|
handleAddEventToProduct({
|
|
event: useEventsStore.getState().getEventByModelUuid(modelUuid),
|
|
addEvent,
|
|
selectedProduct,
|
|
})
|
|
|
|
const point = getPointByUuid(
|
|
selectedProduct.productId,
|
|
modelUuid,
|
|
pointUuid
|
|
);
|
|
|
|
const event = getEventByModelUuid(selectedProduct.productId, modelUuid);
|
|
|
|
if (!point || !event) {
|
|
setFirstSelectedPoint(null);
|
|
return;
|
|
};
|
|
|
|
let actionUuid: string | undefined;
|
|
if ('action' in point && point.action) {
|
|
actionUuid = point.action.actionUuid;
|
|
} else if ('actions' in point && point.actions.length === 1) {
|
|
actionUuid = point.actions[0].actionUuid;
|
|
}
|
|
|
|
const trigger: TriggerSchema = {
|
|
triggerUuid: THREE.MathUtils.generateUUID(),
|
|
triggerName: `Trigger ${firstSelectedPoint.pointUuid.slice(0, 4)} → ${pointUuid.slice(0, 4)}`,
|
|
triggerType: "onComplete",
|
|
delay: 0,
|
|
triggeredAsset: {
|
|
triggeredModel: {
|
|
modelName: event.modelName || 'Unknown',
|
|
modelUuid: modelUuid
|
|
},
|
|
triggeredPoint: {
|
|
pointName: 'Point',
|
|
pointUuid: point.uuid
|
|
},
|
|
triggeredAction: actionUuid ? {
|
|
actionName: getActionByUuid(selectedProduct.productId, actionUuid)?.actionName || 'Action',
|
|
actionUuid: actionUuid
|
|
} : null
|
|
}
|
|
};
|
|
|
|
if (firstSelectedPoint.actionUuid) {
|
|
const event = addTrigger(selectedProduct.productId, firstSelectedPoint.actionUuid, trigger);
|
|
|
|
if (event) {
|
|
updateBackend(
|
|
selectedProduct.productName,
|
|
selectedProduct.productId,
|
|
organization,
|
|
event
|
|
);
|
|
}
|
|
}
|
|
setFirstSelectedPoint(null);
|
|
} else if (firstSelectedPoint) {
|
|
setFirstSelectedPoint(null);
|
|
}
|
|
};
|
|
|
|
if (subModule === 'mechanics' && !deleteTool && selectedAction.actionId && selectedAction.actionName) {
|
|
canvasElement.addEventListener("mousedown", onMouseDown);
|
|
canvasElement.addEventListener("mouseup", onMouseUp);
|
|
canvasElement.addEventListener("mousemove", onMouseMove);
|
|
canvasElement.addEventListener('contextmenu', handleRightClick);
|
|
} else {
|
|
setFirstSelectedPoint(null);
|
|
}
|
|
|
|
return () => {
|
|
canvasElement.removeEventListener("mousedown", onMouseDown);
|
|
canvasElement.removeEventListener("mouseup", onMouseUp);
|
|
canvasElement.removeEventListener("mousemove", onMouseMove);
|
|
canvasElement.removeEventListener('contextmenu', handleRightClick);
|
|
};
|
|
|
|
}, [gl, subModule, selectedProduct, firstSelectedPoint, deleteTool, selectedAction]);
|
|
|
|
useFrame(() => {
|
|
if (firstSelectedPoint) {
|
|
raycaster.setFromCamera(pointer, camera);
|
|
const intersects = raycaster.intersectObjects(scene.children, true).filter(
|
|
(intersect) =>
|
|
!intersect.object.name.includes("Roof") &&
|
|
!intersect.object.name.includes("agv-collider") &&
|
|
!intersect.object.name.includes("MeasurementReference") &&
|
|
!intersect.object.name.includes("ArrowWithTube") &&
|
|
!intersect.object.parent?.name.includes("Zone") &&
|
|
!(intersect.object.type === "GridHelper") &&
|
|
!(intersect.object.type === "Line2")
|
|
);
|
|
|
|
let point: THREE.Vector3 | null = null;
|
|
|
|
if (intersects.length > 0) {
|
|
point = intersects[0].point;
|
|
if (point.y < 0.05) {
|
|
point = new THREE.Vector3(point.x, 0.05, point.z);
|
|
}
|
|
}
|
|
|
|
const sphereIntersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.name === ('Event-Sphere'));
|
|
|
|
if (sphereIntersects.length > 0 && sphereIntersects[0].object.uuid === firstSelectedPoint.pointUuid) {
|
|
setHelperLineColor('red');
|
|
setCurrentLine(null);
|
|
return;
|
|
}
|
|
|
|
const startPoint = getWorldPositionFromScene(firstSelectedPoint.pointUuid);
|
|
|
|
if (point && startPoint) {
|
|
if (sphereIntersects.length > 0) {
|
|
point = sphereIntersects[0].object.getWorldPosition(new THREE.Vector3());
|
|
}
|
|
const distance = startPoint.distanceTo(point);
|
|
const heightFactor = Math.max(0.5, distance * 0.2);
|
|
const midPoint = new THREE.Vector3(
|
|
(startPoint.x + point.x) / 2,
|
|
Math.max(startPoint.y, point.y) + heightFactor,
|
|
(startPoint.z + point.z) / 2
|
|
);
|
|
|
|
const endPoint = point;
|
|
|
|
setCurrentLine({
|
|
start: startPoint,
|
|
mid: midPoint,
|
|
end: endPoint,
|
|
});
|
|
|
|
setHelperLineColor(sphereIntersects.length > 0 ? "#6cf542" : "red");
|
|
} else {
|
|
setCurrentLine(null);
|
|
}
|
|
} else {
|
|
setCurrentLine(null);
|
|
}
|
|
|
|
})
|
|
|
|
const getWorldPositionFromScene = (pointUuid: string): THREE.Vector3 | null => {
|
|
const pointObj = scene.getObjectByProperty("uuid", pointUuid);
|
|
if (!pointObj) return null;
|
|
|
|
const worldPosition = new THREE.Vector3();
|
|
pointObj.getWorldPosition(worldPosition);
|
|
return worldPosition;
|
|
};
|
|
|
|
const removeConnection = (connection: ConnectionLine) => {
|
|
if (connection.trigger.triggerUuid) {
|
|
const event = removeTrigger(selectedProduct.productId, connection.trigger.triggerUuid);
|
|
if (event) {
|
|
updateBackend(
|
|
selectedProduct.productName,
|
|
selectedProduct.productId,
|
|
organization,
|
|
event
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<group name="simulationConnectionGroup" visible={!isPlaying}>
|
|
{connections.map((connection) => {
|
|
const startPoint = getWorldPositionFromScene(connection.startPointUuid);
|
|
const endPoint = getWorldPositionFromScene(connection.endPointUuid);
|
|
|
|
if (!startPoint || !endPoint) return null;
|
|
|
|
const distance = startPoint.distanceTo(endPoint);
|
|
const heightFactor = Math.max(0.5, distance * 0.2);
|
|
const midPoint = new THREE.Vector3(
|
|
(startPoint.x + endPoint.x) / 2,
|
|
Math.max(startPoint.y, endPoint.y) + heightFactor,
|
|
(startPoint.z + endPoint.z) / 2
|
|
);
|
|
|
|
return (
|
|
<QuadraticBezierLine
|
|
key={connection.id}
|
|
ref={(el) => (groupRefs.current[connection.id] = el!)}
|
|
start={startPoint.toArray()}
|
|
end={endPoint.toArray()}
|
|
mid={midPoint.toArray()}
|
|
color={deleteTool && hoveredLineKey === connection.id ? "red" : "#42a5f5"}
|
|
lineWidth={4}
|
|
dashed={deleteTool && hoveredLineKey === connection.id ? false : true}
|
|
dashSize={0.75}
|
|
dashScale={20}
|
|
onPointerOver={() => setHoveredLineKey(connection.id)}
|
|
onPointerOut={() => setHoveredLineKey(null)}
|
|
onClick={() => {
|
|
if (deleteTool) {
|
|
setHoveredLineKey(null);
|
|
setCurrentLine(null);
|
|
removeConnection(connection);
|
|
}
|
|
}}
|
|
userData={connection.trigger}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
<Arrows connections={connections} />
|
|
|
|
{currentLine && (
|
|
<>
|
|
<QuadraticBezierLine
|
|
start={currentLine.start.toArray()}
|
|
end={currentLine.end.toArray()}
|
|
mid={currentLine.mid.toArray()}
|
|
color={helperlineColor}
|
|
lineWidth={4}
|
|
dashed
|
|
dashSize={1}
|
|
dashScale={20}
|
|
/>
|
|
<ArrowOnQuadraticBezier
|
|
start={currentLine.start.toArray()}
|
|
mid={currentLine.mid.toArray()}
|
|
end={currentLine.end.toArray()}
|
|
color={helperlineColor}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export default TriggerConnector; |