Merge remote-tracking branch 'origin/main-demo' into ui

This commit is contained in:
Nalvazhuthi
2025-08-25 12:41:10 +05:30
9 changed files with 1156 additions and 838 deletions

48
app/package-lock.json generated
View File

@@ -2027,7 +2027,7 @@
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
@@ -2039,7 +2039,7 @@
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -4190,6 +4190,26 @@
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@@ -4301,25 +4321,25 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true
"devOptional": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
"devOptional": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
"devOptional": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true
"devOptional": true
},
"node_modules/@turf/along": {
"version": "7.2.0",
@@ -9090,7 +9110,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
"devOptional": true
},
"node_modules/cross-env": {
"version": "7.0.3",
@@ -9967,7 +9987,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=0.3.1"
}
@@ -15351,7 +15371,7 @@
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
"devOptional": true
},
"node_modules/makeerror": {
"version": "1.0.12",
@@ -20886,7 +20906,7 @@
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -20929,7 +20949,7 @@
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"devOptional": true,
"dependencies": {
"acorn": "^8.11.0"
},
@@ -20941,7 +20961,7 @@
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
"devOptional": true
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
@@ -21437,7 +21457,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
"devOptional": true
},
"node_modules/v8-to-istanbul": {
"version": "8.1.1",
@@ -22496,7 +22516,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=6"
}

View File

@@ -30,14 +30,6 @@ const ResourceManagement = () => {
<div className="search-container">
<Search onChange={() => { }} />
<div className="select-catagory">
<RegularDropDown
header={"floor"}
options={["floor"]} // Pass layout names as options
onSelect={() => { }}
search={false}
/>
</div>
</div>
{selectType === "assetManagement" ? <AssetManagement /> : <Hrm />}

View File

@@ -1,96 +1,156 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { ClockThreeIcon, LocationPinIcon, TargetIcon } from '../../../../icons/ExportCommonIcons'
import { useSceneContext } from '../../../../../modules/scene/sceneContext';
import { useProductContext } from '../../../../../modules/simulation/products/productContext';
import RenameInput from '../../../../ui/inputs/RenameInput';
// import NavigateCatagory from '../NavigateCatagory'
const Hrm = () => {
const [selectedCard, setSelectedCard] = useState(0);
const [workers, setWorkers] = useState<any[]>([]);
const employee_details = [
{
"employee": {
image: "",
"name": "John Doe",
"employee_id": "HR-204",
"status": "Active",
const { productStore } = useSceneContext();
const { products, getProductById } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
},
"task": {
"status": "Ongoing",
"title": "Inspecting Machine X",
"location": {
"floor": 4,
"zone": "B"
},
"planned_time_hours": 6,
"time_spent_hours": 2,
"total_tasks": 12,
"completed_tasks": 3
},
"actions": [
"Assign Task",
"Reassign Task",
"Pause",
"Emergency Stop"
],
"location": "Floor 4 . Zone B"
},
{
"employee": {
image: "",
"name": "Alice Smith",
"employee_id": "HR-205",
"status": "Active",
useEffect(() => {
if (selectedProduct) {
const productDetails = getProductById(selectedProduct.productUuid);
const workerDetails = productDetails?.eventDatas || [];
},
"task": {
"status": "Ongoing",
"title": "Calibrating Sensor Y",
"location": {
"floor": 2,
"zone": "A"
},
"planned_time_hours": 4,
"time_spent_hours": 1.5,
"total_tasks": 10,
"completed_tasks": 2
},
"actions": [
"Assign Task",
"Reassign Task",
"Pause",
"Emergency Stop"
],
"location": "Floor 4 . Zone B"
},
{
"employee": {
image: "",
"name": "Michael Lee",
"employee_id": "HR-206",
"status": "Active",
const formattedWorkers = workerDetails
.filter((worker: any) => worker.type === "human")
.map((worker: any, index: number) => ({
employee: {
image: "",
name: worker.modelName,
employee_id: `HR-${204 + index}`,
status: "Active",
},
task: {
status: "Ongoing",
title: worker.taskTitle || "No Task Assigned",
location: {
floor: worker.floor || 0,
zone: worker.zone || "N/A"
},
planned_time_hours: worker.plannedTime || 0,
time_spent_hours: worker.timeSpent || 0,
total_tasks: worker.totalTasks || 0,
completed_tasks: worker.completedTasks || 0
},
actions: [
"Assign Task",
"Reassign Task",
"Pause",
"Emergency Stop"
],
location: `Floor ${worker.floor || "-"} . Zone ${worker.zone || "-"}`
}));
setWorkers(formattedWorkers);
}
}, [selectedProduct, getProductById]);
useEffect(() => {
// console.log("Workers data updated:", workers);
}, [workers]);
// const employee_details = [
// {
// "employee": {
// image: "",
// "name": "John Doe",
// "employee_id": "HR-204",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Inspecting Machine X",
// "location": {
// "floor": 4,
// "zone": "B"
// },
// "planned_time_hours": 6,
// "time_spent_hours": 2,
// "total_tasks": 12,
// "completed_tasks": 3
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// {
// "employee": {
// image: "",
// "name": "Alice Smith",
// "employee_id": "HR-205",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Calibrating Sensor Y",
// "location": {
// "floor": 2,
// "zone": "A"
// },
// "planned_time_hours": 4,
// "time_spent_hours": 1.5,
// "total_tasks": 10,
// "completed_tasks": 2
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// {
// "employee": {
// image: "",
// "name": "Michael Lee",
// "employee_id": "HR-206",
// "status": "Active",
// },
// "task": {
// "status": "Ongoing",
// "title": "Testing Conveyor Belt Z",
// "location": {
// "floor": 5,
// "zone": "C"
// },
// "planned_time_hours": 5,
// "time_spent_hours": 3,
// "total_tasks": 8,
// "completed_tasks": 5
// },
// "actions": [
// "Assign Task",
// "Reassign Task",
// "Pause",
// "Emergency Stop"
// ],
// "location": "Floor 4 . Zone B"
// },
// ]
function handleRenameWorker(newName: string) {
// console.log('newName: ', newName);
}
},
"task": {
"status": "Ongoing",
"title": "Testing Conveyor Belt Z",
"location": {
"floor": 5,
"zone": "C"
},
"planned_time_hours": 5,
"time_spent_hours": 3,
"total_tasks": 8,
"completed_tasks": 5
},
"actions": [
"Assign Task",
"Reassign Task",
"Pause",
"Emergency Stop"
],
"location": "Floor 4 . Zone B"
},
]
return (
<>
@@ -101,7 +161,7 @@ const Hrm = () => {
/> */}
<div className='hrm-container assetManagement-wrapper'>
{employee_details.map((employee, index) => (
{workers.map((employee, index) => (
<div
className={`analysis-wrapper ${selectedCard === index ? "active" : ""}`}
onClick={() => setSelectedCard(index)}
@@ -114,7 +174,8 @@ const Hrm = () => {
<div className={`status ${employee.employee.status}`}></div>
</div>
<div className="details">
<div className="employee-name">{employee.employee.name}</div>
{/* <div className="employee-name">{employee.employee.name}</div> */}
<RenameInput value={employee.employee.name} onRename={handleRenameWorker} />
<div className="employee-id">{employee.employee.employee_id}</div>
</div>
</div>

View File

@@ -1,49 +1,105 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
// import NavigateCatagory from '../../NavigateCatagory'
import { EyeIcon, ForkLiftIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons';
import assetImage from "../../../../../../assets/image/asset-image.png"
import { useSceneContext } from '../../../../../../modules/scene/sceneContext';
import { useProductContext } from '../../../../../../modules/simulation/products/productContext';
import RenameInput from '../../../../../ui/inputs/RenameInput';
const AssetManagement = () => {
// const [selectedCategory, setSelectedCategory] = useState("All Assets");
const [expandedAssetId, setExpandedAssetId] = useState<string | null>(null);
const [assets, setAssets] = useState<any[]>([]);
const dummyAssets = [
{
id: '1',
name: 'Forklift Model X200',
model: 'FLK-0025',
status: 'Online',
usageRate: 15,
level: 'Level 1',
image: assetImage,
description: 'Electric forklift used for moving goods and materials in warehouse operations.',
cost: 122000,
count: 5,
},
{
id: '2',
name: 'Warehouse Robot WR-300',
model: 'WRB-3001',
status: 'Online',
usageRate: 50,
level: 'Level 2',
image: assetImage,
description: 'Automated robot for handling packages and inventory in the warehouse.',
cost: 85000,
count: 3,
},
{
id: '3',
name: 'Conveyor Belt System CB-150',
model: 'CBS-150X',
status: 'Online',
usageRate: 95,
level: 'Level 3',
image: assetImage,
description: 'High-speed conveyor belt system for efficient material handling.',
cost: 45000,
count: 2,
},
];
const { productStore } = useSceneContext();
const { products, getProductById } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
useEffect(() => {
if (selectedProduct) {
const productDetails = getProductById(selectedProduct.productUuid);
const productAssets = productDetails?.eventDatas || [];
const grouped: Record<string, any> = {};
productAssets.forEach((asset: any) => {
if (asset.type === "storageUnit" || asset.type === "human") return;
if (!grouped[asset.modelName]) {
grouped[asset.modelName] = {
id: asset.assetId,
name: asset.modelName,
model: asset.modelCode || "N/A",
status: asset.status || "Online",
usageRate: asset.usageRate || 15,
level: asset.level || "Level 1",
image: assetImage,
description: asset.description || "No description",
cost: asset.cost || 0,
count: 1,
};
} else {
grouped[asset.modelName].count += 1;
}
});
setAssets(Object.values(grouped));
}
}, [selectedProduct]);
function handleRenameAsset(newName: string) {
// console.log('newName: ', newName);
// if (expandedAssetId) {
// setAssets(prevAssets =>
// prevAssets.map(asset =>
// asset.id === expandedAssetId ? { ...asset, name: newName } : asset
// )
// );
// }
}
useEffect(() => {
}, [assets]);
// const dummyAssets = [
// {
// id: '1',
// name: 'Forklift Model X200',
// model: 'FLK-0025',
// status: 'Online',
// usageRate: 15,
// level: 'Level 1',
// image: assetImage,
// description: 'Electric forklift used for moving goods and materials in warehouse operations.',
// cost: 122000,
// count: 5,
// },
// {
// id: '2',
// name: 'Warehouse Robot WR-300',
// model: 'WRB-3001',
// status: 'Online',
// usageRate: 50,
// level: 'Level 2',
// image: assetImage,
// description: 'Automated robot for handling packages and inventory in the warehouse.',
// cost: 85000,
// count: 3,
// },
// {
// id: '3',
// name: 'Conveyor Belt System CB-150',
// model: 'CBS-150X',
// status: 'Online',
// usageRate: 95,
// level: 'Level 3',
// image: assetImage,
// description: 'High-speed conveyor belt system for efficient material handling.',
// cost: 45000,
// count: 2,
// },
// ];
return (
@@ -55,7 +111,7 @@ const AssetManagement = () => {
/> */}
<div className='assetManagement-container assetManagement-wrapper'>
{dummyAssets.map((asset, index) => (
{assets.map((asset, index) => (
<div className={`assetManagement-card-wrapper ${expandedAssetId === asset.id ? "openViewMore" : ""}`} key={index}>
<header>
<div className="header-wrapper">
@@ -70,7 +126,14 @@ const AssetManagement = () => {
}
<div className="asset-details-container">
<div className="asset-details">
<div className="asset-name">{asset.name}</div>
{/* <div className="asset-name">{asset.name}</div> */}
<RenameInput value={asset.name} onRename={handleRenameAsset} />
{asset.count !== 1 && <div>
<span className="asset-id-label">x</span>
<span className="asset-id">{asset.count}</span>
</div>}
<div className="asset-model">{asset.model}</div>
</div>
<div className="asset-status-wrapper">

View File

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

View File

@@ -2,240 +2,188 @@ import * as THREE from "three";
import { useEffect, useRef, useState } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import { useToolMode } from "../../../store/builder/store";
import { Html } from "@react-three/drei";
import { Html, Line } from "@react-three/drei";
const MeasurementTool = () => {
const { gl, raycaster, pointer, camera, scene } = useThree();
const { toolMode } = useToolMode();
const { gl, raycaster, pointer, camera, scene } = useThree();
const { toolMode } = useToolMode();
const [points, setPoints] = useState<THREE.Vector3[]>([]);
const [tubeGeometry, setTubeGeometry] = useState<THREE.TubeGeometry | null>(
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 [points, setPoints] = useState<THREE.Vector3[]>([]);
const [linePoints, setLinePoints] = useState<THREE.Vector3[] | null>(null);
const groupRef = useRef<THREE.Group>(null);
const MIN_RADIUS = 0.001, MAX_RADIUS = 0.1;
const MIN_CONE_RADIUS = 0.01, MAX_CONE_RADIUS = 0.4;
const MIN_CONE_HEIGHT = 0.035, MAX_CONE_HEIGHT = 2.0;
useEffect(() => {
const canvasElement = gl.domElement;
let drag = false;
let isLeftMouseDown = false;
useEffect(() => {
const canvasElement = gl.domElement;
let 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 onMouseDown = () => {
isLeftMouseDown = true;
drag = false;
};
const resetMeasurement = () => {
setTubeGeometry(null);
setStartConePosition(null);
setEndConePosition(null);
};
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"
);
const getArrowOrientation = (start: THREE.Vector3, end: THREE.Vector3) => {
const direction = new THREE.Vector3()
.subVectors(end, start)
.normalize()
.negate();
const quaternion = new THREE.Quaternion();
quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
return quaternion;
};
useEffect(() => {
if (points.length === 2) {
// console.log(points[0].distanceTo(points[1]));
if (intersects.length > 0) {
const intersectionPoint = intersects[0].point.clone();
if (points.length < 2) {
setPoints([...points, intersectionPoint]);
} else {
setPoints([intersectionPoint]);
}
}
}, [points]);
}
};
return (
<group ref={groupRef} name="MeasurementGroup">
{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>
)}
const onMouseMove = () => {
if (isLeftMouseDown) drag = true;
};
{startConePosition && endConePosition && (
<Html
scale={THREE.MathUtils.clamp(
startConePosition.distanceTo(endConePosition) * 0.25,
0,
10
)}
position={[
(startConePosition.x + endConePosition.x) / 2,
(startConePosition.y + endConePosition.y) / 2,
(startConePosition.z + endConePosition.z) / 2,
]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div>
{(startConePosition.distanceTo(endConePosition) + (coneSize.height)).toFixed(2)} m
</div>
</Html>
)}
</group>
);
const onContextMenu = (evt: any) => {
evt.preventDefault();
if (!drag) {
setPoints([]);
setLinePoints(null);
}
};
if (toolMode === "MeasurementScale") {
canvasElement.addEventListener("pointerdown", onMouseDown);
canvasElement.addEventListener("pointermove", onMouseMove);
canvasElement.addEventListener("pointerup", onMouseUp);
canvasElement.addEventListener("contextmenu", onContextMenu);
} else {
setPoints([]);
setLinePoints(null);
}
return () => {
canvasElement.removeEventListener("pointerdown", onMouseDown);
canvasElement.removeEventListener("pointermove", onMouseMove);
canvasElement.removeEventListener("pointerup", onMouseUp);
canvasElement.removeEventListener("contextmenu", onContextMenu);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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;

View File

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

File diff suppressed because it is too large Load Diff

View File

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