Merge branch 'main-demo' into dev-resourceManagement

This commit is contained in:
2025-08-25 17:05:53 +05:30
6 changed files with 876 additions and 681 deletions

View File

@@ -125,7 +125,10 @@ const AssetManagement = () => {
<div className="header-wrapper"> <div className="header-wrapper">
{expandedAssetId === asset.id ? {expandedAssetId === asset.id ?
<img className='asset-image' src={asset.image} alt="" /> <>
<div className="drop-icon" onClick={() => setExpandedAssetId(null)}></div>
<img className='asset-image' src={asset.image} alt="" />
</>
: :
<div className="icon"><ForkLiftIcon /></div> <div className="icon"><ForkLiftIcon /></div>
} }
@@ -167,7 +170,7 @@ const AssetManagement = () => {
</div> </div>
</div> </div>
<div className="asset-estimate__view-button" onClick={() => setExpandedAssetId(null)}> <div className="asset-estimate__view-button">
<EyeIcon isClosed={false} /> <EyeIcon isClosed={false} />
<div className="asset-estimate__view-text" onClick={() => handleAssetClick(asset.id)}>View in Scene</div> <div className="asset-estimate__view-text" onClick={() => handleAssetClick(asset.id)}>View in Scene</div>
</div> </div>

View File

@@ -36,6 +36,7 @@ import {
} from "../../store/visualization/useDroppedObjectsStore"; } from "../../store/visualization/useDroppedObjectsStore";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useVersionContext } from "../../modules/builder/version/versionContext"; import { useVersionContext } from "../../modules/builder/version/versionContext";
import { MoveIcon, RotateIcon } from "../icons/ShortcutIcons";
// Utility component // Utility component
const ToolButton = ({ const ToolButton = ({
@@ -65,12 +66,8 @@ const Tools: React.FC = () => {
const { isPlaying, setIsPlaying } = usePlayButtonStore(); const { isPlaying, setIsPlaying } = usePlayButtonStore();
const { showShortcuts } = useShortcutStore(); const { showShortcuts } = useShortcutStore();
const { const { activeTool, setActiveTool, setToolMode, setAddAction } =
activeTool, useStoreHooks();
setActiveTool,
setToolMode,
setAddAction,
} = useStoreHooks();
const { setActiveSubTool, activeSubTool } = useActiveSubTool(); const { setActiveSubTool, activeSubTool } = useActiveSubTool();
const { setSelectedWallItem } = useSelectedWallItem(); const { setSelectedWallItem } = useSelectedWallItem();
@@ -81,14 +78,15 @@ const Tools: React.FC = () => {
const { selectedZone } = useSelectedZoneStore(); const { selectedZone } = useSelectedZoneStore();
const { floatingWidget } = useFloatingWidget(); const { floatingWidget } = useFloatingWidget();
const { widgets3D } = use3DWidget(); const { widgets3D } = use3DWidget();
const { visualizationSocket } = useSocketStore();
const dropdownRef = useRef<HTMLButtonElement>(null); const { visualizationSocket } = useSocketStore();
const [openDrop, setOpenDrop] = useState(false);
const { selectedVersionStore } = useVersionContext(); const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore(); const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams(); const { projectId } = useParams();
const dropdownRef = useRef<HTMLButtonElement>(null);
const [openDrop, setOpenDrop] = useState(false);
// 1. Set UI toggles on initial render // 1. Set UI toggles on initial render
useEffect(() => { useEffect(() => {
setToggleUI( setToggleUI(
@@ -155,7 +153,7 @@ const Tools: React.FC = () => {
if (!is2D) setAddAction("Pillar"); if (!is2D) setAddAction("Pillar");
break; break;
case "delete": case "delete":
is2D ? setToolMode('2D-Delete') : setToolMode('3D-Delete'); is2D ? setToolMode("2D-Delete") : setToolMode("3D-Delete");
break; break;
} }
}; };
@@ -251,7 +249,7 @@ const Tools: React.FC = () => {
templates, templates,
visualizationSocket, visualizationSocket,
projectId, projectId,
versionId: selectedVersion?.versionId || '' versionId: selectedVersion?.versionId || "",
}) })
} }
/> />
@@ -278,6 +276,10 @@ const Tools: React.FC = () => {
return FreeMoveIcon; return FreeMoveIcon;
case "delete": case "delete":
return DeleteIcon; return DeleteIcon;
case "move":
return MoveIcon;
case "rotate":
return RotateIcon;
default: default:
return CursorIcon; return CursorIcon;
} }
@@ -304,6 +306,10 @@ const Tools: React.FC = () => {
return <FreeMoveIcon isActive={false} />; return <FreeMoveIcon isActive={false} />;
case "delete": case "delete":
return <DeleteIcon isActive={false} />; return <DeleteIcon isActive={false} />;
case "move":
return <MoveIcon />;
case "rotate":
return <RotateIcon />;
default: default:
return null; return null;
} }
@@ -362,6 +368,24 @@ const Tools: React.FC = () => {
)} )}
</div> </div>
{activeModule !== "visualization" && (
<>
<div className="split"></div>
<div className="transform-tools">
{["move", "rotate"].map((tool) => (
<ToolButton
key={tool}
toolId={tool}
icon={getIconByTool(tool)}
tooltip={`${tool}`}
active={activeTool === tool}
onClick={() => setActiveTool(tool)}
/>
))}
</div>
</>
)}
<div className="split"></div> <div className="split"></div>
{activeModule === "builder" && renderBuilderTools()} {activeModule === "builder" && renderBuilderTools()}
{activeModule === "simulation" && renderSimulationTools()} {activeModule === "simulation" && renderSimulationTools()}

View File

@@ -2,240 +2,188 @@ import * as THREE from "three";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useThree, useFrame } from "@react-three/fiber"; import { useThree, useFrame } from "@react-three/fiber";
import { useToolMode } from "../../../store/builder/store"; import { useToolMode } from "../../../store/builder/store";
import { Html } from "@react-three/drei"; import { Html, Line } from "@react-three/drei";
const MeasurementTool = () => { const MeasurementTool = () => {
const { gl, raycaster, pointer, camera, scene } = useThree(); const { gl, raycaster, pointer, camera, scene } = useThree();
const { toolMode } = useToolMode(); const { toolMode } = useToolMode();
const [points, setPoints] = useState<THREE.Vector3[]>([]); const [points, setPoints] = useState<THREE.Vector3[]>([]);
const [tubeGeometry, setTubeGeometry] = useState<THREE.TubeGeometry | null>( const [linePoints, setLinePoints] = useState<THREE.Vector3[] | null>(null);
null const groupRef = useRef<THREE.Group>(null);
);
const groupRef = useRef<THREE.Group>(null);
const [startConePosition, setStartConePosition] =
useState<THREE.Vector3 | null>(null);
const [endConePosition, setEndConePosition] = useState<THREE.Vector3 | null>(
null
);
const [startConeQuaternion, setStartConeQuaternion] = useState(
new THREE.Quaternion()
);
const [endConeQuaternion, setEndConeQuaternion] = useState(
new THREE.Quaternion()
);
const [coneSize, setConeSize] = useState({ radius: 0.2, height: 0.5 });
const MIN_RADIUS = 0.001, MAX_RADIUS = 0.1; useEffect(() => {
const MIN_CONE_RADIUS = 0.01, MAX_CONE_RADIUS = 0.4; const canvasElement = gl.domElement;
const MIN_CONE_HEIGHT = 0.035, MAX_CONE_HEIGHT = 2.0; let drag = false;
let isLeftMouseDown = false;
useEffect(() => { const onMouseDown = () => {
const canvasElement = gl.domElement; isLeftMouseDown = true;
let drag = false; drag = false;
let isLeftMouseDown = false;
const onMouseDown = () => {
isLeftMouseDown = true;
drag = false;
};
const onMouseUp = (evt: any) => {
isLeftMouseDown = false;
if (evt.button === 0 && !drag) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("agv-collider") &&
!intersect.object.name.includes("zonePlane") &&
!intersect.object.name.includes("SelectionGroup") &&
!intersect.object.name.includes("selectionAssetGroup") &&
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper"
);
if (intersects.length > 0) {
const intersectionPoint = intersects[0].point.clone();
if (points.length < 2) {
setPoints([...points, intersectionPoint]);
} else {
setPoints([intersectionPoint]);
}
}
}
};
const onMouseMove = () => {
if (isLeftMouseDown) drag = true;
};
const onContextMenu = (evt: any) => {
evt.preventDefault();
if (!drag) {
evt.preventDefault();
setPoints([]);
setTubeGeometry(null);
}
};
if (toolMode === "MeasurementScale") {
canvasElement.addEventListener("pointerdown", onMouseDown);
canvasElement.addEventListener("pointermove", onMouseMove);
canvasElement.addEventListener("pointerup", onMouseUp);
canvasElement.addEventListener("contextmenu", onContextMenu);
} else {
resetMeasurement();
setPoints([]);
}
return () => {
canvasElement.removeEventListener("pointerdown", onMouseDown);
canvasElement.removeEventListener("pointermove", onMouseMove);
canvasElement.removeEventListener("pointerup", onMouseUp);
canvasElement.removeEventListener("contextmenu", onContextMenu);
};
}, [toolMode, camera, raycaster, pointer, scene, points]);
useFrame(() => {
if (points.length === 1) {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("agv-collider") &&
!intersect.object.name.includes("zonePlane") &&
!intersect.object.name.includes("SelectionGroup") &&
!intersect.object.name.includes("selectionAssetGroup") &&
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper"
);
if (intersects.length > 0) {
updateMeasurement(points[0], intersects[0].point);
}
} else if (points.length === 2) {
updateMeasurement(points[0], points[1]);
} else {
resetMeasurement();
}
});
const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => {
const distance = start.distanceTo(end);
const radius = THREE.MathUtils.clamp(distance * 0.02, MIN_RADIUS, MAX_RADIUS);
const coneRadius = THREE.MathUtils.clamp(distance * 0.05, MIN_CONE_RADIUS, MAX_CONE_RADIUS);
const coneHeight = THREE.MathUtils.clamp(distance * 0.2, MIN_CONE_HEIGHT, MAX_CONE_HEIGHT);
setConeSize({ radius: coneRadius, height: coneHeight });
const direction = new THREE.Vector3().subVectors(end, start).normalize();
const offset = direction.clone().multiplyScalar(coneHeight * 0.5);
let tubeStart = start.clone().add(offset);
let tubeEnd = end.clone().sub(offset);
tubeStart.y = Math.max(tubeStart.y, 0);
tubeEnd.y = Math.max(tubeEnd.y, 0);
const curve = new THREE.CatmullRomCurve3([tubeStart, tubeEnd]);
setTubeGeometry(new THREE.TubeGeometry(curve, 20, radius, 8, false));
setStartConePosition(tubeStart);
setEndConePosition(tubeEnd);
setStartConeQuaternion(getArrowOrientation(start, end));
setEndConeQuaternion(getArrowOrientation(end, start));
}; };
const resetMeasurement = () => { const onMouseUp = (evt: any) => {
setTubeGeometry(null); isLeftMouseDown = false;
setStartConePosition(null); if (evt.button === 0 && !drag) {
setEndConePosition(null); raycaster.setFromCamera(pointer, camera);
}; const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("agv-collider") &&
!intersect.object.name.includes("zonePlane") &&
!intersect.object.name.includes("SelectionGroup") &&
!intersect.object.name.includes("selectionAssetGroup") &&
!intersect.object.name.includes(
"SelectionGroupBoundingBoxLine"
) &&
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper"
);
const getArrowOrientation = (start: THREE.Vector3, end: THREE.Vector3) => { if (intersects.length > 0) {
const direction = new THREE.Vector3() const intersectionPoint = intersects[0].point.clone();
.subVectors(end, start) if (points.length < 2) {
.normalize() setPoints([...points, intersectionPoint]);
.negate(); } else {
const quaternion = new THREE.Quaternion(); setPoints([intersectionPoint]);
quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); }
return quaternion;
};
useEffect(() => {
if (points.length === 2) {
// console.log(points[0].distanceTo(points[1]));
} }
}, [points]); }
};
return ( const onMouseMove = () => {
<group ref={groupRef} name="MeasurementGroup"> if (isLeftMouseDown) drag = true;
{startConePosition && ( };
<mesh
name="MeasurementReference"
position={startConePosition}
quaternion={startConeQuaternion}
>
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} />
<meshBasicMaterial color="yellow" />
</mesh>
)}
{endConePosition && (
<mesh
name="MeasurementReference"
position={endConePosition}
quaternion={endConeQuaternion}
>
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} />
<meshBasicMaterial color="yellow" />
</mesh>
)}
{tubeGeometry && (
<mesh name="MeasurementReference" geometry={tubeGeometry}>
<meshBasicMaterial color="yellow" />
</mesh>
)}
{startConePosition && endConePosition && ( const onContextMenu = (evt: any) => {
<Html evt.preventDefault();
scale={THREE.MathUtils.clamp( if (!drag) {
startConePosition.distanceTo(endConePosition) * 0.25, setPoints([]);
0, setLinePoints(null);
10 }
)} };
position={[
(startConePosition.x + endConePosition.x) / 2, if (toolMode === "MeasurementScale") {
(startConePosition.y + endConePosition.y) / 2, canvasElement.addEventListener("pointerdown", onMouseDown);
(startConePosition.z + endConePosition.z) / 2, canvasElement.addEventListener("pointermove", onMouseMove);
]} canvasElement.addEventListener("pointerup", onMouseUp);
// class canvasElement.addEventListener("contextmenu", onContextMenu);
wrapperClass="distance-text-wrapper" } else {
className="distance-text" setPoints([]);
// other setLinePoints(null);
zIndexRange={[1, 0]} }
prepend
sprite return () => {
> canvasElement.removeEventListener("pointerdown", onMouseDown);
<div> canvasElement.removeEventListener("pointermove", onMouseMove);
{(startConePosition.distanceTo(endConePosition) + (coneSize.height)).toFixed(2)} m canvasElement.removeEventListener("pointerup", onMouseUp);
</div> canvasElement.removeEventListener("contextmenu", onContextMenu);
</Html> };
)} // eslint-disable-next-line react-hooks/exhaustive-deps
</group> }, [toolMode, camera, raycaster, pointer, scene, points]);
);
useFrame(() => {
if (points.length === 1) {
// live preview for second point
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("agv-collider") &&
!intersect.object.name.includes("zonePlane") &&
!intersect.object.name.includes("SelectionGroup") &&
!intersect.object.name.includes("selectionAssetGroup") &&
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper"
);
if (intersects.length > 0) {
const tempEnd = intersects[0].point.clone();
updateMeasurement(points[0], tempEnd);
}
} else if (points.length === 2) {
// second point already fixed
updateMeasurement(points[0], points[1]);
} else {
setLinePoints(null);
}
});
const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => {
setLinePoints([start.clone(), end.clone()]);
};
return (
<group ref={groupRef} name="MeasurementGroup">
{linePoints && (
<>
{/* Outline line */}
<Line
points={linePoints}
color="black"
lineWidth={6} // thicker than main line
depthTest={false}
depthWrite={false}
renderOrder={998} // render behind main line
/>
{/* Main line */}
<Line
points={linePoints}
color="#b18ef1"
lineWidth={2} // actual line width
depthTest={false}
depthWrite={false}
transparent={false}
opacity={1}
renderOrder={999} // render on top
/>
</>
)}
{points.map((point, index) => (
<Html
key={index}
position={point}
scale={0.5}
wrapperClass="measurement-label-wrapper"
className="measurement-label"
zIndexRange={[1, 0]}
prepend
sprite
>
<div className="measurement-point"></div>
</Html>
))}
{linePoints && linePoints.length === 2 && (
<Html
position={[
(linePoints[0].x + linePoints[1].x) / 2,
(linePoints[0].y + linePoints[1].y) / 2,
(linePoints[0].z + linePoints[1].z) / 2,
]}
scale={0.5}
wrapperClass="distance-text-wrapper"
className="distance-text"
zIndexRange={[2, 1]}
prepend
sprite
>
<div>{linePoints[0].distanceTo(linePoints[1]).toFixed(2)} m</div>
</Html>
)}
</group>
);
}; };
export default MeasurementTool; export default MeasurementTool;

View File

@@ -31,6 +31,7 @@
} }
.draw-tools, .draw-tools,
.transform-tools,
.general-options, .general-options,
.activeDropicon { .activeDropicon {
@include flex-center; @include flex-center;

File diff suppressed because it is too large Load Diff

View File

@@ -130,15 +130,25 @@
svg { svg {
display: none; display: none;
} }
.c-jiwtRJ{ .c-jiwtRJ {
align-items: center; align-items: center;
} }
} }
.stats{ .stats {
top: auto !important; top: auto !important;
bottom: 36px !important; bottom: 36px !important;
left: 12px !important; left: 12px !important;
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
} }
.measurement-point {
height: 12px;
width: 12px;
border-radius: 50%;
background: #b18ef1;
outline: 2px solid black;
outline-offset: -1px;
transform: translate(-50%, -50%);
}