diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx index 252e44d..a6332db 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx @@ -15,6 +15,7 @@ import { useProductContext } from "../../../../../modules/simulation/products/pr import { useParams } from "react-router-dom"; import { useVersionContext } from "../../../../../modules/builder/version/versionContext"; import { useSceneContext } from "../../../../../modules/scene/sceneContext"; +import CraneMechanics from "./mechanics/craneMechanics"; const EventProperties: React.FC = () => { const { selectedEventData } = useSelectedEventData(); @@ -63,6 +64,8 @@ const EventProperties: React.FC = () => { return "storageUnit"; case "human": return "human"; + case "crane": + return "crane"; default: return null; } @@ -83,6 +86,7 @@ const EventProperties: React.FC = () => { {assetType === "machine" && } {assetType === "storageUnit" && } {assetType === "human" && } + {assetType === "crane" && } > )} {!currentEventData && selectedEventSphere && ( diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx new file mode 100644 index 0000000..f5d1229 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx @@ -0,0 +1,210 @@ +import { useEffect, useState } from "react"; +import { MathUtils } from "three"; +import RenameInput from "../../../../../ui/inputs/RenameInput"; +import LabledDropdown from "../../../../../ui/inputs/LabledDropdown"; +import Trigger from "../trigger/Trigger"; +import { useSelectedEventData, useSelectedAction } from "../../../../../../store/simulation/useSimulationStore"; +import ActionsList from "../components/ActionsList"; +import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi"; +import { useProductContext } from "../../../../../../modules/simulation/products/productContext"; +import { useParams } from "react-router-dom"; +import { useVersionContext } from "../../../../../../modules/builder/version/versionContext"; +import { useSceneContext } from "../../../../../../modules/scene/sceneContext"; + +function CraneMechanics() { + const [activeOption, setActiveOption] = useState<"pickAndDrop">("pickAndDrop"); + const [selectedPointData, setSelectedPointData] = useState(); + + const { selectedEventData } = useSelectedEventData(); + const { productStore } = useSceneContext(); + const { getPointByUuid, updateAction, addAction, removeAction } = productStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); + const { selectedAction, setSelectedAction, clearSelectedAction } = useSelectedAction(); + const { selectedVersionStore } = useVersionContext(); + const { selectedVersion } = selectedVersionStore(); + const { projectId } = useParams(); + + useEffect(() => { + if (selectedEventData) { + const point = getPointByUuid( + selectedProduct.productUuid, + selectedEventData.data.modelUuid, + selectedEventData.selectedPoint + ) as CranePointSchema | undefined; + + if (point?.actions) { + setSelectedPointData(point); + + if (point.actions.length > 0) { + const firstAction = point.actions[0]; + setActiveOption(firstAction.actionType); + setSelectedAction(firstAction.actionUuid, firstAction.actionName); + } + } + } else { + clearSelectedAction(); + } + }, [selectedEventData, selectedProduct]); + + const updateBackend = ( + productName: string, + productUuid: string, + projectId: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName, + productUuid, + projectId, + eventDatas: eventData, + versionId: selectedVersion?.versionId || "", + }); + }; + + const handleRenameAction = (newName: string) => { + if (!selectedAction.actionId || !selectedPointData) return; + + const event = updateAction( + selectedProduct.productUuid, + selectedAction.actionId, + { actionName: newName } + ); + + const updatedActions = selectedPointData.actions.map(action => + action.actionUuid === selectedAction.actionId + ? { ...action, actionName: newName } + : action + ); + + setSelectedPointData({ + ...selectedPointData, + actions: updatedActions, + }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + } + }; + + const handleAddAction = () => { + if (!selectedEventData || !selectedPointData) return; + + const newAction = { + actionUuid: MathUtils.generateUUID(), + actionName: `Action ${selectedPointData.actions.length + 1}`, + actionType: "pickAndDrop" as const, + maxPickUpCount: 1, + triggers: [] as TriggerSchema[], + }; + + const event = addAction( + selectedProduct.productUuid, + selectedEventData.data.modelUuid, + selectedEventData.selectedPoint, + newAction + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + } + + setSelectedPointData({ + ...selectedPointData, + actions: [...selectedPointData.actions, newAction], + }); + setSelectedAction(newAction.actionUuid, newAction.actionName); + }; + + const handleDeleteAction = (actionUuid: string) => { + if (!selectedPointData) return; + + const event = removeAction( + selectedProduct.productUuid, + actionUuid + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + } + + const index = selectedPointData.actions.findIndex(a => a.actionUuid === actionUuid); + const newActions = selectedPointData.actions.filter(a => a.actionUuid !== actionUuid); + + setSelectedPointData({ + ...selectedPointData, + actions: newActions, + }); + + if (selectedAction.actionId === actionUuid) { + const nextAction = newActions[index] || newActions[index - 1]; + if (nextAction) { + setSelectedAction(nextAction.actionUuid, nextAction.actionName); + } else { + clearSelectedAction(); + } + } + }; + + const availableActions = { + defaultOption: "pickAndDrop", + options: ["pickAndDrop"], + }; + + const currentAction = selectedPointData?.actions.find(a => a.actionUuid === selectedAction.actionId); + + return ( + <> + + + + {selectedAction.actionId && currentAction && ( + + + + + + { }} + disabled={true} + /> + + + + + + )} + + > + ); +} + +export default CraneMechanics \ No newline at end of file diff --git a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts index c08144c..a0ea33a 100644 --- a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts +++ b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts @@ -24,7 +24,7 @@ export function useModelEventHandlers({ boundingBox: THREE.Box3 | null, groupRef: React.RefObject, }) { - const { controls, gl } = useThree(); + const { controls, gl, camera } = useThree(); const { activeTool } = useActiveTool(); const { activeModule } = useModuleStore(); const { toggleView } = useToggleView(); @@ -68,25 +68,47 @@ export function useModelEventHandlers({ const handleDblClick = (asset: Asset) => { if (asset && activeTool === "cursor" && boundingBox && groupRef.current && activeModule === 'builder') { - const size = boundingBox.getSize(new THREE.Vector3()); - const center = boundingBox.getCenter(new THREE.Vector3()); - const front = new THREE.Vector3(0, 0, 1); - groupRef.current.localToWorld(front); - front.sub(groupRef.current.position).normalize(); + const frontView = false; - const distance = Math.max(size.x, size.y, size.z) * 2; - const newPosition = center.clone().addScaledVector(front, distance); + if (frontView) { + const size = boundingBox.getSize(new THREE.Vector3()); + const center = boundingBox.getCenter(new THREE.Vector3()); - (controls as CameraControls).setPosition(newPosition.x, newPosition.y, newPosition.z, true); - (controls as CameraControls).setTarget(center.x, center.y, center.z, true); - (controls as CameraControls).fitToBox(groupRef.current, true, { - cover: true, - paddingTop: 5, - paddingLeft: 5, - paddingBottom: 5, - paddingRight: 5, - }); + const front = new THREE.Vector3(0, 0, 1); + groupRef.current.localToWorld(front); + front.sub(groupRef.current.position).normalize(); + + const distance = Math.max(size.x, size.y, size.z) * 2; + const newPosition = center.clone().addScaledVector(front, distance); + + (controls as CameraControls).setPosition(newPosition.x, newPosition.y, newPosition.z, true); + (controls as CameraControls).setTarget(center.x, center.y, center.z, true); + (controls as CameraControls).fitToBox(groupRef.current, true, { + cover: true, + paddingTop: 5, + paddingLeft: 5, + paddingBottom: 5, + paddingRight: 5, + }); + } else { + + const collisionPos = new THREE.Vector3(); + groupRef.current.getWorldPosition(collisionPos); + + const currentPos = new THREE.Vector3().copy(camera.position); + + const target = new THREE.Vector3(); + if (!controls) return; + (controls as CameraControls).getTarget(target); + const direction = new THREE.Vector3().subVectors(target, currentPos).normalize(); + + const offsetDistance = 5; + const newCameraPos = new THREE.Vector3().copy(collisionPos).sub(direction.multiplyScalar(offsetDistance)); + + camera.position.copy(newCameraPos); + (controls as CameraControls).setLookAt(newCameraPos.x, newCameraPos.y, newCameraPos.z, collisionPos.x, 0, collisionPos.z, true); + } setSelectedFloorItem(groupRef.current); } diff --git a/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx b/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx index d287d04..9c7f3ab 100644 --- a/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx +++ b/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; import * as THREE from 'three'; import { useFrame, useThree } from '@react-three/fiber'; -import { Sphere, Box } from '@react-three/drei'; import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; import { useSceneContext } from '../../../../scene/sceneContext'; @@ -26,7 +25,6 @@ function PillarJibAnimator({ const { speed } = useAnimationPlaySpeed(); const [clampedPoints, setClampedPoints] = useState<[THREE.Vector3, THREE.Vector3]>(); - const [isInside, setIsInside] = useState<[boolean, boolean]>([false, false]); const [currentTargetIndex, setCurrentTargetIndex] = useState(0); useEffect(() => { @@ -101,7 +99,6 @@ function PillarJibAnimator({ }); setClampedPoints(newClampedPoints); - setIsInside(newIsInside); }, [crane.modelUuid]); useFrame(() => { @@ -227,29 +224,8 @@ function PillarJibAnimator({ } }); - if (!clampedPoints) return null; - return ( <> - {points.map((point, i) => ( - - - - ))} - - {clampedPoints.map((point, i) => ( - - - - ))} > ); } diff --git a/app/src/modules/simulation/crane/instances/helper/pillarJibHelper.tsx b/app/src/modules/simulation/crane/instances/helper/pillarJibHelper.tsx index 945cf2f..84883fa 100644 --- a/app/src/modules/simulation/crane/instances/helper/pillarJibHelper.tsx +++ b/app/src/modules/simulation/crane/instances/helper/pillarJibHelper.tsx @@ -1,9 +1,18 @@ -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import * as THREE from "three"; import { useThree } from "@react-three/fiber"; +import { Box, Sphere } from "@react-three/drei"; -function PillarJibHelper({ crane }: { crane: CraneStatus }) { +function PillarJibHelper({ + crane, + points +}: { + crane: CraneStatus, + points: [THREE.Vector3, THREE.Vector3]; +}) { const { scene } = useThree(); + const [clampedPoints, setClampedPoints] = useState<[THREE.Vector3, THREE.Vector3]>(); + const [isInside, setIsInside] = useState<[boolean, boolean]>([false, false]); const { geometry, position } = useMemo(() => { const model = scene.getObjectByProperty('uuid', crane.modelUuid); @@ -51,26 +60,81 @@ function PillarJibHelper({ crane }: { crane: CraneStatus }) { const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); const position: [number, number, number] = [baseWorld.x, cylinderYPosition, baseWorld.z]; + const yMin = hookWorld.y + hookMaxOffset; + const yMax = hookWorld.y + hookMinOffset; + + function clampToCylinder(pos: THREE.Vector3) { + const xzDist = new THREE.Vector2(pos.x - baseWorld.x, pos.z - baseWorld.z); + const distance = xzDist.length(); + + let clampedDistance = THREE.MathUtils.clamp(distance, innerRadius, outerRadius); + if (distance > 0) { + clampedDistance = Math.max(innerRadius, Math.min(distance, outerRadius)); + } + + const clampedXZ = xzDist.normalize().multiplyScalar(clampedDistance); + const y = THREE.MathUtils.clamp(pos.y, yMin, yMax); + + return new THREE.Vector3(baseWorld.x + clampedXZ.x, y, baseWorld.z + clampedXZ.y); + } + + const newClampedPoints: [THREE.Vector3, THREE.Vector3] = [new THREE.Vector3(), new THREE.Vector3()]; + const newIsInside: [boolean, boolean] = [false, false]; + + points.forEach((point, i) => { + const xzDist = new THREE.Vector2(point.x - baseWorld.x, point.z - baseWorld.z).length(); + const insideXZ = xzDist >= innerRadius && xzDist <= outerRadius; + const insideY = point.y >= yMin && point.y <= yMax; + newIsInside[i] = insideXZ && insideY; + + newClampedPoints[i] = newIsInside[i] ? point.clone() : clampToCylinder(point); + }); + + setClampedPoints(newClampedPoints); + setIsInside(newIsInside); + return { geometry, position }; }, [scene, crane.modelUuid]); if (!geometry || !position) return null; return ( - - - + <> + + + + + {points.map((point, i) => ( + + + + ))} + + {clampedPoints && clampedPoints.map((point, i) => ( + + + + ))} + > ); } diff --git a/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx b/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx index d6b8fd8..bf0fa43 100644 --- a/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx +++ b/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx @@ -36,7 +36,13 @@ function PillarJibInstance({ crane }: { crane: CraneStatus }) { onAnimationComplete={handleAnimationComplete} /> - + {/* */} > )