added crane mechanics ui and dblclick zoom effect chnage

This commit is contained in:
2025-08-07 18:11:11 +05:30
parent 2760e73ab8
commit a7a55bf137
6 changed files with 340 additions and 58 deletions

View File

@@ -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" && <MachineMechanics />}
{assetType === "storageUnit" && <StorageMechanics />}
{assetType === "human" && <HumanMechanics />}
{assetType === "crane" && <CraneMechanics />}
</>
)}
{!currentEventData && selectedEventSphere && (

View File

@@ -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<CranePointSchema | undefined>();
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 (
<>
<section>
<ActionsList
selectedPointData={selectedPointData}
multipleAction
handleAddAction={handleAddAction}
handleDeleteAction={handleDeleteAction}
/>
{selectedAction.actionId && currentAction && (
<div className="selected-actions-details">
<div className="selected-actions-header">
<RenameInput
value={selectedAction.actionName || ""}
canEdit={false}
/>
</div>
<div className="selected-actions-list">
<LabledDropdown
defaultOption={activeOption}
options={availableActions.options}
onSelect={() => { }}
disabled={true}
/>
</div>
<div className="tirgger">
<Trigger
selectedPointData={selectedPointData as any}
type={"RoboticArm"}
/>
</div>
</div>
)}
</section>
</>
);
}
export default CraneMechanics

View File

@@ -24,7 +24,7 @@ export function useModelEventHandlers({
boundingBox: THREE.Box3 | null,
groupRef: React.RefObject<THREE.Group>,
}) {
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);
}

View File

@@ -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<number>(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) => (
<Box
key={`original-${i}`}
position={point}
args={[0.15, 0.15, 0.15]}
>
<meshStandardMaterial color="yellow" />
</Box>
))}
{clampedPoints.map((point, i) => (
<Sphere
key={`clamped-${i}`}
position={point}
args={[0.1, 16, 16]}
>
<meshStandardMaterial color={isInside[i] ? 'lightgreen' : 'orange'} />
</Sphere>
))}
</>
);
}

View File

@@ -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 (
<mesh
geometry={geometry}
position={position}
rotation={[Math.PI / 2, 0, 0]}
>
<meshStandardMaterial
color={0x888888}
metalness={0.5}
roughness={0.4}
side={THREE.DoubleSide}
transparent={true}
opacity={0.3}
/>
</mesh>
<>
<mesh
geometry={geometry}
position={position}
rotation={[Math.PI / 2, 0, 0]}
>
<meshStandardMaterial
color={0x888888}
metalness={0.5}
roughness={0.4}
side={THREE.DoubleSide}
transparent={true}
opacity={0.3}
/>
</mesh>
{points.map((point, i) => (
<Box
key={`original-${i}`}
position={point}
args={[0.15, 0.15, 0.15]}
>
<meshStandardMaterial color="yellow" />
</Box>
))}
{clampedPoints && clampedPoints.map((point, i) => (
<Sphere
key={`clamped-${i}`}
position={point}
args={[0.1, 16, 16]}
>
<meshStandardMaterial color={isInside[i] ? 'lightgreen' : 'orange'} />
</Sphere>
))}
</>
);
}

View File

@@ -36,7 +36,13 @@ function PillarJibInstance({ crane }: { crane: CraneStatus }) {
onAnimationComplete={handleAnimationComplete}
/>
<PillarJibHelper crane={crane} />
{/* <PillarJibHelper
crane={crane}
points={[
new THREE.Vector3(...position1),
new THREE.Vector3(...position2)]
}
/> */}
</>
)