Merge remote-tracking branch 'origin/main-demo' into main-dev
This commit is contained in:
@@ -55,6 +55,7 @@ interface ShortcutHelperProps {
|
|||||||
const ShortcutHelper: React.FC<ShortcutHelperProps> = ({
|
const ShortcutHelper: React.FC<ShortcutHelperProps> = ({
|
||||||
setShowShortcuts,
|
setShowShortcuts,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const shortcuts: ShortcutGroup[] = [
|
const shortcuts: ShortcutGroup[] = [
|
||||||
// Essential
|
// Essential
|
||||||
{
|
{
|
||||||
@@ -310,6 +311,7 @@ const ShortcutHelper: React.FC<ShortcutHelperProps> = ({
|
|||||||
>
|
>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="header-wrapper">
|
<div className="header-wrapper">
|
||||||
{shortcuts.map((group) => (
|
{shortcuts.map((group) => (
|
||||||
@@ -326,9 +328,8 @@ const ShortcutHelper: React.FC<ShortcutHelperProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`shortcut-wrapper ${
|
className={`shortcut-wrapper ${activeShortcuts.length === 1 ? "single-item" : ""
|
||||||
activeShortcuts.length === 1 ? "single-item" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{activeShortcuts.map((item) => (
|
{activeShortcuts.map((item) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -212,7 +212,11 @@ function MainScene() {
|
|||||||
{activeModule !== "market" && !selectedUser && <Footer />}
|
{activeModule !== "market" && !selectedUser && <Footer />}
|
||||||
|
|
||||||
<VersionSaved />
|
<VersionSaved />
|
||||||
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat />}
|
|
||||||
|
{
|
||||||
|
(commentPositionState !== null || selectedComment !== null) &&
|
||||||
|
<ThreadChat />
|
||||||
|
}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ const AssetProperties: React.FC = () => {
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div className="header">User Data</div>
|
<div className="header">User Data</div>
|
||||||
{userData.map((data) => (
|
{userData.map((data, i) => (
|
||||||
<div className="input-container">
|
<div className="input-container" key={i}>
|
||||||
<InputWithDropDown
|
<InputWithDropDown
|
||||||
key={data.id}
|
key={data.id}
|
||||||
label={data.label}
|
label={data.label}
|
||||||
@@ -103,9 +103,16 @@ const AssetProperties: React.FC = () => {
|
|||||||
</section>
|
</section>
|
||||||
<div className="header">Animations</div>
|
<div className="header">Animations</div>
|
||||||
<section className="animations-lists">
|
<section className="animations-lists">
|
||||||
{assets.map((asset) => {
|
{assets.map((asset, i) => {
|
||||||
if (asset.modelUuid !== selectedFloorItem.uuid || !asset.animations)
|
if (asset.modelUuid !== selectedFloorItem.uuid || !asset.animations)
|
||||||
return null;
|
return (
|
||||||
|
i === 0 && (
|
||||||
|
<div className="no-animation">
|
||||||
|
Looks like there are no preset animations yet. Stay tuned for
|
||||||
|
future additions!
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return asset.animations.map((animation, index) => (
|
return asset.animations.map((animation, index) => (
|
||||||
<div key={index} className="animations-list-wrapper">
|
<div key={index} className="animations-list-wrapper">
|
||||||
|
|||||||
@@ -30,14 +30,6 @@ const ResourceManagement = () => {
|
|||||||
|
|
||||||
<div className="search-container">
|
<div className="search-container">
|
||||||
<Search onChange={() => { }} />
|
<Search onChange={() => { }} />
|
||||||
<div className="select-catagory">
|
|
||||||
<RegularDropDown
|
|
||||||
header={"floor"}
|
|
||||||
options={["floor"]} // Pass layout names as options
|
|
||||||
onSelect={() => { }}
|
|
||||||
search={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectType === "assetManagement" ? <AssetManagement /> : <Hrm />}
|
{selectType === "assetManagement" ? <AssetManagement /> : <Hrm />}
|
||||||
|
|||||||
@@ -1,96 +1,165 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ClockThreeIcon, LocationPinIcon, TargetIcon } from '../../../../icons/ExportCommonIcons'
|
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 { useResourceManagementId } from '../../../../../store/builder/store';
|
||||||
|
import { set } from 'immer/dist/internal';
|
||||||
// import NavigateCatagory from '../NavigateCatagory'
|
// import NavigateCatagory from '../NavigateCatagory'
|
||||||
|
|
||||||
const Hrm = () => {
|
const Hrm = () => {
|
||||||
const [selectedCard, setSelectedCard] = useState(0);
|
const [selectedCard, setSelectedCard] = useState(0);
|
||||||
|
const [workers, setWorkers] = useState<any[]>([]);
|
||||||
|
|
||||||
const employee_details = [
|
const { productStore } = useSceneContext();
|
||||||
{
|
const { products, getProductById } = productStore();
|
||||||
"employee": {
|
const { selectedProductStore } = useProductContext();
|
||||||
image: "",
|
const { selectedProduct } = selectedProductStore();
|
||||||
"name": "John Doe",
|
const { setResourceManagementId } = useResourceManagementId();
|
||||||
"employee_id": "HR-204",
|
|
||||||
"status": "Active",
|
|
||||||
|
|
||||||
},
|
useEffect(() => {
|
||||||
"task": {
|
if (selectedProduct) {
|
||||||
"status": "Ongoing",
|
const productDetails = getProductById(selectedProduct.productUuid);
|
||||||
"title": "Inspecting Machine X",
|
const workerDetails = productDetails?.eventDatas || [];
|
||||||
"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",
|
|
||||||
|
|
||||||
},
|
const formattedWorkers = workerDetails
|
||||||
"task": {
|
.filter((worker: any) => worker.type === "human")
|
||||||
"status": "Ongoing",
|
.map((worker: any, index: number) => ({
|
||||||
"title": "Calibrating Sensor Y",
|
employee: {
|
||||||
"location": {
|
image: "",
|
||||||
"floor": 2,
|
name: worker.modelName,
|
||||||
"zone": "A"
|
modelId: worker.modelUuid,
|
||||||
},
|
employee_id: `HR-${204 + index}`,
|
||||||
"planned_time_hours": 4,
|
status: "Active",
|
||||||
"time_spent_hours": 1.5,
|
},
|
||||||
"total_tasks": 10,
|
task: {
|
||||||
"completed_tasks": 2
|
status: "Ongoing",
|
||||||
},
|
title: worker.taskTitle || "No Task Assigned",
|
||||||
"actions": [
|
location: {
|
||||||
"Assign Task",
|
floor: worker.floor || 0,
|
||||||
"Reassign Task",
|
zone: worker.zone || "N/A"
|
||||||
"Pause",
|
},
|
||||||
"Emergency Stop"
|
planned_time_hours: worker.plannedTime || 0,
|
||||||
],
|
time_spent_hours: worker.timeSpent || 0,
|
||||||
"location": "Floor 4 . Zone B"
|
total_tasks: worker.totalTasks || 0,
|
||||||
},
|
completed_tasks: worker.completedTasks || 0
|
||||||
{
|
},
|
||||||
"employee": {
|
actions: [
|
||||||
image: "",
|
"Assign Task",
|
||||||
"name": "Michael Lee",
|
"Reassign Task",
|
||||||
"employee_id": "HR-206",
|
"Pause",
|
||||||
"status": "Active",
|
"Emergency Stop"
|
||||||
|
],
|
||||||
|
location: `Floor ${worker.floor || "-"} . Zone ${worker.zone || "-"}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
setWorkers(formattedWorkers);
|
||||||
|
}
|
||||||
|
}, [selectedProduct, getProductById]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//
|
||||||
|
}, [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) {
|
||||||
|
//
|
||||||
|
|
||||||
|
}
|
||||||
|
function handleHumanClick(employee: any) {
|
||||||
|
if (employee.modelId) {
|
||||||
|
setResourceManagementId(employee.modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
},
|
|
||||||
"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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -101,7 +170,7 @@ const Hrm = () => {
|
|||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
<div className='hrm-container assetManagement-wrapper'>
|
<div className='hrm-container assetManagement-wrapper'>
|
||||||
{employee_details.map((employee, index) => (
|
{workers.map((employee, index) => (
|
||||||
<div
|
<div
|
||||||
className={`analysis-wrapper ${selectedCard === index ? "active" : ""}`}
|
className={`analysis-wrapper ${selectedCard === index ? "active" : ""}`}
|
||||||
onClick={() => setSelectedCard(index)}
|
onClick={() => setSelectedCard(index)}
|
||||||
@@ -113,13 +182,14 @@ const Hrm = () => {
|
|||||||
<img className='user-image' src={employee.employee.image} alt="" />
|
<img className='user-image' src={employee.employee.image} alt="" />
|
||||||
<div className={`status ${employee.employee.status}`}></div>
|
<div className={`status ${employee.employee.status}`}></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="details">
|
<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 className="employee-id">{employee.employee.employee_id}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="see-more">View more</div>
|
<div className="see-more" onClick={() => { handleHumanClick(employee.employee) }}>View in Scene</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="content">
|
<div className="content">
|
||||||
|
|||||||
@@ -1,73 +1,145 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
// import NavigateCatagory from '../../NavigateCatagory'
|
// import NavigateCatagory from '../../NavigateCatagory'
|
||||||
import { EyeIcon, ForkLiftIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons';
|
import { EyeIcon, ForkLiftIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons';
|
||||||
import assetImage from "../../../../../../assets/image/asset-image.png"
|
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';
|
||||||
|
import { useResourceManagementId } from '../../../../../../store/builder/store';
|
||||||
const AssetManagement = () => {
|
const AssetManagement = () => {
|
||||||
// const [selectedCategory, setSelectedCategory] = useState("All Assets");
|
// const [selectedCategory, setSelectedCategory] = useState("All Assets");
|
||||||
const [expandedAssetId, setExpandedAssetId] = useState<string | null>(null);
|
const [expandedAssetId, setExpandedAssetId] = useState<string | null>(null);
|
||||||
|
const [assets, setAssets] = useState<any[]>([]);
|
||||||
|
|
||||||
const dummyAssets = [
|
const { productStore } = useSceneContext();
|
||||||
{
|
const { products, getProductById } = productStore();
|
||||||
id: '1',
|
const { selectedProductStore } = useProductContext();
|
||||||
name: 'Forklift Model X200',
|
const { selectedProduct } = selectedProductStore();
|
||||||
model: 'FLK-0025',
|
const { setResourceManagementId } = useResourceManagementId();
|
||||||
status: 'Online',
|
|
||||||
usageRate: 15,
|
|
||||||
level: 'Level 1',
|
|
||||||
image: assetImage,
|
useEffect(() => {
|
||||||
description: 'Electric forklift used for moving goods and materials in warehouse operations.',
|
if (selectedProduct) {
|
||||||
cost: 122000,
|
const productDetails = getProductById(selectedProduct.productUuid);
|
||||||
count: 5,
|
const productAssets = productDetails?.eventDatas || [];
|
||||||
},
|
const grouped: Record<string, any> = {};
|
||||||
{
|
productAssets.forEach((asset: any) => {
|
||||||
id: '2',
|
if (asset.type === "storageUnit" || asset.type === "human") return;
|
||||||
name: 'Warehouse Robot WR-300',
|
if (!grouped[asset.modelName]) {
|
||||||
model: 'WRB-3001',
|
grouped[asset.modelName] = {
|
||||||
status: 'Online',
|
id: asset.modelUuid,
|
||||||
usageRate: 50,
|
name: asset.modelName,
|
||||||
level: 'Level 2',
|
model: asset.modelCode || "N/A",
|
||||||
image: assetImage,
|
status: asset.status || "Online",
|
||||||
description: 'Automated robot for handling packages and inventory in the warehouse.',
|
usageRate: asset.usageRate || 15,
|
||||||
cost: 85000,
|
level: asset.level || "Level 1",
|
||||||
count: 3,
|
image: assetImage,
|
||||||
},
|
description: asset.description || "No description",
|
||||||
{
|
cost: asset.cost || 0,
|
||||||
id: '3',
|
count: 1,
|
||||||
name: 'Conveyor Belt System CB-150',
|
};
|
||||||
model: 'CBS-150X',
|
} else {
|
||||||
status: 'Online',
|
grouped[asset.modelName].count += 1;
|
||||||
usageRate: 95,
|
}
|
||||||
level: 'Level 3',
|
});
|
||||||
image: assetImage,
|
|
||||||
description: 'High-speed conveyor belt system for efficient material handling.',
|
setAssets(Object.values(grouped));
|
||||||
cost: 45000,
|
}
|
||||||
count: 2,
|
}, [selectedProduct]);
|
||||||
},
|
|
||||||
];
|
function handleRenameAsset(newName: string) {
|
||||||
|
//
|
||||||
|
// if (expandedAssetId) {
|
||||||
|
// setAssets(prevAssets =>
|
||||||
|
// prevAssets.map(asset =>
|
||||||
|
// asset.id === expandedAssetId ? { ...asset, name: newName } : asset
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
|
||||||
|
}, [assets]);
|
||||||
|
|
||||||
|
function handleAssetClick(id: string) {
|
||||||
|
|
||||||
|
|
||||||
|
setResourceManagementId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <NavigateCatagory
|
{/* <NavigateCatagory
|
||||||
category={["All Assets", "Machines", "Workstation", "Vehicles"]}
|
category={["All Assets", "Machines", "Workstation", "Vehicles"]}
|
||||||
selectedCategory={selectedCategory}
|
selectedCategory={selectedCategory}
|
||||||
setSelectedCategory={setSelectedCategory}
|
setSelectedCategory={setSelectedCategory}
|
||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
<div className='assetManagement-container assetManagement-wrapper'>
|
<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}>
|
<div className={`assetManagement-card-wrapper ${expandedAssetId === asset.id ? "openViewMore" : ""}`} key={index}>
|
||||||
<header>
|
<header>
|
||||||
<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>
|
||||||
}
|
}
|
||||||
<div className="asset-details-container">
|
<div className="asset-details-container">
|
||||||
<div className="asset-details">
|
<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 className="asset-model">{asset.model}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="asset-status-wrapper">
|
<div className="asset-status-wrapper">
|
||||||
@@ -98,9 +170,9 @@ 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">View in Scene</div>
|
<div className="asset-estimate__view-text" onClick={() => handleAssetClick(asset.id)}>View in Scene</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -231,7 +231,10 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
|
|||||||
<div className="time">{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}</div>
|
<div className="time">{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}</div>
|
||||||
</div>
|
</div>
|
||||||
{(val as Reply).creatorId === userId && (
|
{(val as Reply).creatorId === userId && (
|
||||||
<div className="more-options">
|
<div
|
||||||
|
className="more-options"
|
||||||
|
onMouseLeave={() => setOpenOptions(false)}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="more-options-button"
|
className="more-options-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -240,35 +243,41 @@ const Messages: React.FC<MessageProps> = ({ val, i, setMessages, mode, setIsEdit
|
|||||||
>
|
>
|
||||||
<KebabIcon />
|
<KebabIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{openOptions && (
|
{openOptions && (
|
||||||
<div className="options-list">
|
<div className="options-list">
|
||||||
<button
|
<button
|
||||||
className="option"
|
className="option"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMode && setMode("edit")
|
setMode && setMode("edit");
|
||||||
setOpenOptions(false);
|
setOpenOptions(false);
|
||||||
setEditedThread && setEditedThread(true);
|
setEditedThread && setEditedThread(true);
|
||||||
setIsEditComment(true)
|
setIsEditComment(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
{!(isEditableThread) && <button
|
|
||||||
className="option"
|
{!isEditableThread && (
|
||||||
onClick={() => {
|
<button
|
||||||
handleDeleteAction((val as Reply).replyId);
|
className="option"
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
handleDeleteAction((val as Reply).replyId);
|
||||||
Delete
|
}}
|
||||||
</button>}
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="message">
|
<div className="message">
|
||||||
{"comment" in val ? val.comment : val.threadTitle}
|
{"comment" in val ? val.comment : val.threadTitle}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div >
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -131,9 +131,10 @@ const ThreadChat: React.FC = () => {
|
|||||||
if (dragging) updatePosition(e, true);
|
if (dragging) updatePosition(e, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Commented this useEffect to prevent offset after user saved the comment
|
||||||
updatePosition({ clientX: position.x, clientY: position.y }, true);
|
// useEffect(() => {
|
||||||
}, [selectedComment]);
|
// updatePosition({ clientX: position.x, clientY: position.y }, true);
|
||||||
|
// }, [selectedComment]);
|
||||||
|
|
||||||
|
|
||||||
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
|
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||||
@@ -144,6 +145,10 @@ const ThreadChat: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateComments = async (e: any) => {
|
const handleCreateComments = async (e: any) => {
|
||||||
|
// Continue send or create message only there is only value avalibale
|
||||||
|
// To prevent empty value
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
// const createComments = await addCommentsApi(projectId, value, selectedComment?.threadId, selectedVersion?.versionId || "")/
|
// const createComments = await addCommentsApi(projectId, value, selectedComment?.threadId, selectedVersion?.versionId || "")/
|
||||||
@@ -163,6 +168,7 @@ const ThreadChat: React.FC = () => {
|
|||||||
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
if (threadSocket && mode === "create") {
|
if (threadSocket && mode === "create") {
|
||||||
const addComment = {
|
const addComment = {
|
||||||
versionId: selectedVersion?.versionId || "",
|
versionId: selectedVersion?.versionId || "",
|
||||||
@@ -190,7 +196,7 @@ const ThreadChat: React.FC = () => {
|
|||||||
// removeComment(selectedComment?.threadId)
|
// removeComment(selectedComment?.threadId)
|
||||||
// setSelectedComment([])
|
// setSelectedComment([])
|
||||||
// }
|
// }
|
||||||
console.log('threadSocket:threadChat ', threadSocket);
|
|
||||||
if (threadSocket) {
|
if (threadSocket) {
|
||||||
// projectId, userId, organization, threadId
|
// projectId, userId, organization, threadId
|
||||||
const deleteThread = {
|
const deleteThread = {
|
||||||
@@ -258,7 +264,7 @@ const ThreadChat: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (threadSocket) {
|
if (threadSocket) {
|
||||||
console.log('createThread: ', createThread);
|
|
||||||
|
|
||||||
setInputActive(false);
|
setInputActive(false);
|
||||||
threadSocket.emit("v1:thread:create", createThread);
|
threadSocket.emit("v1:thread:create", createThread);
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => {
|
|||||||
const { zoneStore } = useSceneContext();
|
const { zoneStore } = useSceneContext();
|
||||||
const { zones, setZoneName } = zoneStore();
|
const { zones, setZoneName } = zoneStore();
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
useSelectedZoneStore.getState().setSelectedZone({
|
useSelectedZoneStore.getState().setSelectedZone({
|
||||||
zoneName: "",
|
zoneName: "",
|
||||||
@@ -121,6 +122,7 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleAssetClick(asset: Asset) {
|
function handleAssetClick(asset: Asset) {
|
||||||
|
|
||||||
setZoneAssetId(asset);
|
setZoneAssetId(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +159,9 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => {
|
|||||||
modelUuid: zoneAssetId.id,
|
modelUuid: zoneAssetId.id,
|
||||||
modelName: newName,
|
modelName: newName,
|
||||||
projectId,
|
projectId,
|
||||||
|
versionId: selectedVersion?.versionId || ''
|
||||||
|
|
||||||
});
|
});
|
||||||
// console.log("response: ", response);
|
|
||||||
|
|
||||||
setName(zoneAssetId.id, response.modelName);
|
setName(zoneAssetId.id, response.modelName);
|
||||||
}
|
}
|
||||||
@@ -255,9 +258,8 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`list-item ${
|
className={`list-item ${selectedZone.zoneUuid === item.id ? "active" : ""
|
||||||
selectedZone.zoneUuid === item.id ? "active" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="zone-header">
|
<div className="zone-header">
|
||||||
<button className="value" id="zone-name">
|
<button className="value" id="zone-name">
|
||||||
@@ -301,9 +303,8 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => {
|
|||||||
{item.assets.map((asset) => (
|
{item.assets.map((asset) => (
|
||||||
<li
|
<li
|
||||||
key={`asset-${asset.id}`}
|
key={`asset-${asset.id}`}
|
||||||
className={`list-container asset-item ${
|
className={`list-container asset-item ${zoneAssetId?.id === asset.id ? "active" : ""
|
||||||
zoneAssetId?.id === asset.id ? "active" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => handleAssetClick(asset)}
|
onClick={() => handleAssetClick(asset)}
|
||||||
>
|
>
|
||||||
<div className="list-item">
|
<div className="list-item">
|
||||||
|
|||||||
47
app/src/hooks/useCameraShortcuts.ts
Normal file
47
app/src/hooks/useCameraShortcuts.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import type { CameraControls } from "@react-three/drei";
|
||||||
|
|
||||||
|
export const useCameraShortcuts = (controlsRef: React.RefObject<CameraControls>) => {
|
||||||
|
const { camera } = useThree();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!controlsRef.current) return;
|
||||||
|
|
||||||
|
// get current distance from camera to target
|
||||||
|
const target = new THREE.Vector3();
|
||||||
|
controlsRef.current.getTarget(target);
|
||||||
|
|
||||||
|
const distance = camera.position.distanceTo(target);
|
||||||
|
let pos: THREE.Vector3 | null = null;
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case "1": // Front
|
||||||
|
pos = new THREE.Vector3(0, 0, distance).add(target);
|
||||||
|
break;
|
||||||
|
case "3": // Right
|
||||||
|
pos = new THREE.Vector3(distance, 0, 0).add(target);
|
||||||
|
break;
|
||||||
|
case "7": // Top
|
||||||
|
pos = new THREE.Vector3(0, distance, 0).add(target);
|
||||||
|
break;
|
||||||
|
case "9": // Back
|
||||||
|
pos = new THREE.Vector3(0, 0, -distance).add(target);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos) {
|
||||||
|
controlsRef.current.setLookAt(
|
||||||
|
pos.x, pos.y, pos.z, // camera position
|
||||||
|
target.x, target.y, target.z, // keep same target
|
||||||
|
true // smooth transition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [controlsRef, camera]);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { CameraControls } from '@react-three/drei';
|
|||||||
import { ThreeEvent, useThree } from '@react-three/fiber';
|
import { ThreeEvent, useThree } from '@react-three/fiber';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { useActiveTool, useDeletableFloorItem, useSelectedFloorItem, useToggleView, useZoneAssetId } from '../../../../../../store/builder/store';
|
import { useActiveTool, useDeletableFloorItem, useResourceManagementId, useSelectedFloorItem, useToggleView, useZoneAssetId } from '../../../../../../store/builder/store';
|
||||||
import useModuleStore, { useSubModuleStore } from '../../../../../../store/useModuleStore';
|
import useModuleStore, { useSubModuleStore } from '../../../../../../store/useModuleStore';
|
||||||
import { useSocketStore } from '../../../../../../store/builder/store';
|
import { useSocketStore } from '../../../../../../store/builder/store';
|
||||||
import { useSceneContext } from '../../../../../scene/sceneContext';
|
import { useSceneContext } from '../../../../../scene/sceneContext';
|
||||||
@@ -36,6 +36,7 @@ export function useModelEventHandlers({
|
|||||||
const { push3D } = undoRedo3DStore();
|
const { push3D } = undoRedo3DStore();
|
||||||
const { getAssetById, removeAsset } = assetStore();
|
const { getAssetById, removeAsset } = assetStore();
|
||||||
const { zoneAssetId, setZoneAssetId } = useZoneAssetId();
|
const { zoneAssetId, setZoneAssetId } = useZoneAssetId();
|
||||||
|
const { resourceManagementId, setResourceManagementId } = useResourceManagementId();
|
||||||
const { removeEvent, getEventByModelUuid } = eventStore();
|
const { removeEvent, getEventByModelUuid } = eventStore();
|
||||||
const { getIsEventInProduct, addPoint, deleteEvent } = productStore();
|
const { getIsEventInProduct, addPoint, deleteEvent } = productStore();
|
||||||
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
|
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
|
||||||
@@ -74,7 +75,17 @@ export function useModelEventHandlers({
|
|||||||
if (zoneAssetId.id === asset.modelUuid) {
|
if (zoneAssetId.id === asset.modelUuid) {
|
||||||
handleDblClick(asset);
|
handleDblClick(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [zoneAssetId])
|
}, [zoneAssetId])
|
||||||
|
useEffect(() => {
|
||||||
|
if (!resourceManagementId) return
|
||||||
|
if (resourceManagementId === asset.modelUuid) {
|
||||||
|
|
||||||
|
|
||||||
|
handleDblClick(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [resourceManagementId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedFloorItem) {
|
if (!selectedFloorItem) {
|
||||||
@@ -83,10 +94,8 @@ export function useModelEventHandlers({
|
|||||||
}, [selectedFloorItem])
|
}, [selectedFloorItem])
|
||||||
|
|
||||||
const handleDblClick = (asset: Asset) => {
|
const handleDblClick = (asset: Asset) => {
|
||||||
if (asset && activeTool === "cursor" && boundingBox && groupRef.current && activeModule === 'builder') {
|
if (asset && activeTool === "cursor" && boundingBox && groupRef.current && (activeModule === 'builder' || (activeModule === 'simulation' && resourceManagementId))) {
|
||||||
|
|
||||||
const frontView = false;
|
const frontView = false;
|
||||||
|
|
||||||
if (frontView) {
|
if (frontView) {
|
||||||
const size = boundingBox.getSize(new THREE.Vector3());
|
const size = boundingBox.getSize(new THREE.Vector3());
|
||||||
const center = boundingBox.getCenter(new THREE.Vector3());
|
const center = boundingBox.getCenter(new THREE.Vector3());
|
||||||
@@ -107,6 +116,7 @@ export function useModelEventHandlers({
|
|||||||
paddingBottom: 5,
|
paddingBottom: 5,
|
||||||
paddingRight: 5,
|
paddingRight: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
const collisionPos = new THREE.Vector3();
|
const collisionPos = new THREE.Vector3();
|
||||||
@@ -127,6 +137,7 @@ export function useModelEventHandlers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelectedFloorItem(groupRef.current);
|
setSelectedFloorItem(groupRef.current);
|
||||||
|
setResourceManagementId("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default function Builder() {
|
|||||||
const { setHoveredPoint, setHoveredLine } = useBuilderStore();
|
const { setHoveredPoint, setHoveredLine } = useBuilderStore();
|
||||||
const { userId, organization } = getUserData();
|
const { userId, organization } = getUserData();
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!toggleView) {
|
if (!toggleView) {
|
||||||
setHoveredLine(null);
|
setHoveredLine(null);
|
||||||
|
|||||||
@@ -1,222 +1,243 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from "@react-three/fiber";
|
||||||
import { CameraControls, Html, ScreenSpace } from '@react-three/drei';
|
import { CameraControls, Html, ScreenSpace } from "@react-three/drei";
|
||||||
import { useContextActionStore, useRenameModeStore, useSelectedAssets } from '../../../../store/builder/store';
|
import {
|
||||||
import ContextMenu from '../../../../components/ui/menu/contextMenu';
|
useContextActionStore,
|
||||||
|
useRenameModeStore,
|
||||||
|
useSelectedAssets,
|
||||||
|
} from "../../../../store/builder/store";
|
||||||
|
import ContextMenu from "../../../../components/ui/menu/contextMenu";
|
||||||
|
|
||||||
function ContextControls() {
|
function ContextControls() {
|
||||||
const { gl, controls } = useThree();
|
const { gl, controls } = useThree();
|
||||||
const [canRender, setCanRender] = useState(false);
|
const [canRender, setCanRender] = useState(false);
|
||||||
const [visibility, setVisibility] = useState({ rename: true, focus: true, flipX: true, flipZ: true, move: true, rotate: true, duplicate: true, copy: true, paste: true, modifier: false, group: false, array: false, delete: true, });
|
const [visibility, setVisibility] = useState({
|
||||||
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
rename: true,
|
||||||
const { selectedAssets } = useSelectedAssets();
|
focus: true,
|
||||||
const { setContextAction } = useContextActionStore();
|
flipX: true,
|
||||||
const { setIsRenameMode } = useRenameModeStore();
|
flipZ: true,
|
||||||
const rightDrag = useRef(false);
|
move: true,
|
||||||
const isRightMouseDown = useRef(false);
|
rotate: true,
|
||||||
|
duplicate: true,
|
||||||
|
copy: true,
|
||||||
|
paste: true,
|
||||||
|
modifier: false,
|
||||||
|
group: false,
|
||||||
|
array: false,
|
||||||
|
delete: true,
|
||||||
|
});
|
||||||
|
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const { selectedAssets } = useSelectedAssets();
|
||||||
|
const { setContextAction } = useContextActionStore();
|
||||||
|
const { setIsRenameMode } = useRenameModeStore();
|
||||||
|
const rightDrag = useRef(false);
|
||||||
|
const isRightMouseDown = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedAssets.length === 1) {
|
if (selectedAssets.length === 1) {
|
||||||
setVisibility({
|
setVisibility({
|
||||||
rename: true,
|
rename: true,
|
||||||
focus: true,
|
focus: true,
|
||||||
flipX: true,
|
flipX: true,
|
||||||
flipZ: true,
|
flipZ: true,
|
||||||
move: true,
|
move: true,
|
||||||
rotate: true,
|
rotate: true,
|
||||||
duplicate: true,
|
duplicate: true,
|
||||||
copy: true,
|
copy: true,
|
||||||
paste: true,
|
paste: true,
|
||||||
modifier: false,
|
modifier: false,
|
||||||
group: false,
|
group: false,
|
||||||
array: false,
|
array: false,
|
||||||
delete: true,
|
delete: true,
|
||||||
});
|
});
|
||||||
} else if (selectedAssets.length > 1) {
|
} else if (selectedAssets.length > 1) {
|
||||||
setVisibility({
|
setVisibility({
|
||||||
rename: false,
|
rename: false,
|
||||||
focus: true,
|
focus: true,
|
||||||
flipX: true,
|
flipX: true,
|
||||||
flipZ: true,
|
flipZ: true,
|
||||||
move: true,
|
move: true,
|
||||||
rotate: true,
|
rotate: true,
|
||||||
duplicate: true,
|
duplicate: true,
|
||||||
copy: true,
|
copy: true,
|
||||||
paste: true,
|
paste: true,
|
||||||
modifier: false,
|
modifier: false,
|
||||||
group: true,
|
group: true,
|
||||||
array: false,
|
array: false,
|
||||||
delete: true,
|
delete: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setVisibility({
|
setVisibility({
|
||||||
rename: false,
|
rename: false,
|
||||||
focus: false,
|
focus: false,
|
||||||
flipX: false,
|
flipX: false,
|
||||||
flipZ: false,
|
flipZ: false,
|
||||||
move: false,
|
move: false,
|
||||||
rotate: false,
|
rotate: false,
|
||||||
duplicate: false,
|
duplicate: false,
|
||||||
copy: false,
|
copy: false,
|
||||||
paste: false,
|
paste: false,
|
||||||
modifier: false,
|
modifier: false,
|
||||||
group: false,
|
group: false,
|
||||||
array: false,
|
array: false,
|
||||||
delete: false,
|
delete: false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}, [selectedAssets]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvasElement = gl.domElement;
|
||||||
|
|
||||||
|
const onPointerDown = (evt: any) => {
|
||||||
|
if (evt.button === 2) {
|
||||||
|
isRightMouseDown.current = true;
|
||||||
|
rightDrag.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerMove = () => {
|
||||||
|
if (isRightMouseDown.current) {
|
||||||
|
rightDrag.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerUp = (evt: any) => {
|
||||||
|
if (evt.button === 2) {
|
||||||
|
isRightMouseDown.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextClick = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (rightDrag.current) return;
|
||||||
|
if (selectedAssets.length > 0) {
|
||||||
|
setMenuPosition({
|
||||||
|
x: event.clientX - gl.domElement.width / 2,
|
||||||
|
y: event.clientY - gl.domElement.height / 2,
|
||||||
|
});
|
||||||
|
setCanRender(true);
|
||||||
|
if (controls) {
|
||||||
|
(controls as CameraControls).enabled = false;
|
||||||
}
|
}
|
||||||
}, [selectedAssets]);
|
} else {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvasElement = gl.domElement;
|
|
||||||
|
|
||||||
const onPointerDown = (evt: any) => {
|
|
||||||
if (evt.button === 2) {
|
|
||||||
isRightMouseDown.current = true;
|
|
||||||
rightDrag.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerMove = () => {
|
|
||||||
if (isRightMouseDown.current) {
|
|
||||||
rightDrag.current = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerUp = (evt: any) => {
|
|
||||||
if (evt.button === 2) {
|
|
||||||
isRightMouseDown.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleContextClick = (event: MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (rightDrag.current) return;
|
|
||||||
if (selectedAssets.length > 0) {
|
|
||||||
setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2 });
|
|
||||||
setCanRender(true);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setCanRender(false);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (selectedAssets.length > 0) {
|
|
||||||
canvasElement.addEventListener('pointerdown', onPointerDown);
|
|
||||||
canvasElement.addEventListener('pointermove', onPointerMove);
|
|
||||||
canvasElement.addEventListener('pointerup', onPointerUp);
|
|
||||||
canvasElement.addEventListener('contextmenu', handleContextClick)
|
|
||||||
} else {
|
|
||||||
setCanRender(false);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = true;
|
|
||||||
}
|
|
||||||
setMenuPosition({ x: 0, y: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
canvasElement.removeEventListener('pointerdown', onPointerDown);
|
|
||||||
canvasElement.removeEventListener('pointermove', onPointerMove);
|
|
||||||
canvasElement.removeEventListener('pointerup', onPointerUp);
|
|
||||||
canvasElement.removeEventListener('contextmenu', handleContextClick);
|
|
||||||
};
|
|
||||||
}, [gl, selectedAssets]);
|
|
||||||
|
|
||||||
const handleAssetRename = () => {
|
|
||||||
setCanRender(false);
|
setCanRender(false);
|
||||||
if (controls) {
|
if (controls) {
|
||||||
(controls as CameraControls).enabled = true;
|
(controls as CameraControls).enabled = true;
|
||||||
}
|
}
|
||||||
setContextAction("renameAsset");
|
}
|
||||||
setIsRenameMode(true);
|
};
|
||||||
}
|
|
||||||
const handleAssetFocus = () => {
|
if (selectedAssets.length > 0) {
|
||||||
setCanRender(false);
|
canvasElement.addEventListener("pointerdown", onPointerDown);
|
||||||
if (controls) {
|
canvasElement.addEventListener("pointermove", onPointerMove);
|
||||||
(controls as CameraControls).enabled = true;
|
canvasElement.addEventListener("pointerup", onPointerUp);
|
||||||
}
|
canvasElement.addEventListener("contextmenu", handleContextClick);
|
||||||
setContextAction("focusAsset");
|
} else {
|
||||||
}
|
setCanRender(false);
|
||||||
const handleAssetMove = () => {
|
if (controls) {
|
||||||
setCanRender(false);
|
(controls as CameraControls).enabled = true;
|
||||||
if (controls) {
|
}
|
||||||
(controls as CameraControls).enabled = true;
|
setMenuPosition({ x: 0, y: 0 });
|
||||||
}
|
|
||||||
setContextAction("moveAsset")
|
|
||||||
}
|
|
||||||
const handleAssetRotate = () => {
|
|
||||||
setCanRender(false);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = true;
|
|
||||||
}
|
|
||||||
setContextAction("rotateAsset")
|
|
||||||
}
|
|
||||||
const handleAssetCopy = () => {
|
|
||||||
setCanRender(false);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = true;
|
|
||||||
}
|
|
||||||
setContextAction("copyAsset")
|
|
||||||
}
|
|
||||||
const handleAssetPaste = () => {
|
|
||||||
setCanRender(false);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = true;
|
|
||||||
}
|
|
||||||
setContextAction("pasteAsset")
|
|
||||||
}
|
|
||||||
const handleAssetDelete = () => {
|
|
||||||
setCanRender(false);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = true;
|
|
||||||
}
|
|
||||||
setContextAction("deleteAsset")
|
|
||||||
}
|
|
||||||
const handleAssetDuplicate = () => {
|
|
||||||
setCanRender(false);
|
|
||||||
if (controls) {
|
|
||||||
(controls as CameraControls).enabled = true;
|
|
||||||
}
|
|
||||||
setContextAction("duplicateAsset")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return () => {
|
||||||
<>
|
canvasElement.removeEventListener("pointerdown", onPointerDown);
|
||||||
{canRender && (
|
canvasElement.removeEventListener("pointermove", onPointerMove);
|
||||||
<ScreenSpace depth={1} >
|
canvasElement.removeEventListener("pointerup", onPointerUp);
|
||||||
<Html
|
canvasElement.removeEventListener("contextmenu", handleContextClick);
|
||||||
style={{
|
};
|
||||||
position: 'fixed',
|
}, [controls, gl, selectedAssets]);
|
||||||
top: menuPosition.y,
|
|
||||||
left: menuPosition.x,
|
const handleAssetRename = () => {
|
||||||
zIndex: 1000
|
setCanRender(false);
|
||||||
}}
|
if (controls) {
|
||||||
>
|
(controls as CameraControls).enabled = true;
|
||||||
<ContextMenu
|
}
|
||||||
visibility={visibility}
|
setContextAction("renameAsset");
|
||||||
onRename={() => handleAssetRename()}
|
setIsRenameMode(true);
|
||||||
onFocus={() => handleAssetFocus()}
|
};
|
||||||
onFlipX={() => console.log("Flip to X")}
|
const handleAssetFocus = () => {
|
||||||
onFlipZ={() => console.log("Flip to Z")}
|
setCanRender(false);
|
||||||
onMove={() => handleAssetMove()}
|
if (controls) {
|
||||||
onRotate={() => handleAssetRotate()}
|
(controls as CameraControls).enabled = true;
|
||||||
onDuplicate={() => handleAssetDuplicate()}
|
}
|
||||||
onCopy={() => handleAssetCopy()}
|
setContextAction("focusAsset");
|
||||||
onPaste={() => handleAssetPaste()}
|
};
|
||||||
onGroup={() => console.log("Group")}
|
const handleAssetMove = () => {
|
||||||
onArray={() => console.log("Array")}
|
setCanRender(false);
|
||||||
onDelete={() => handleAssetDelete()}
|
if (controls) {
|
||||||
/>
|
(controls as CameraControls).enabled = true;
|
||||||
</Html>
|
}
|
||||||
</ScreenSpace>
|
setContextAction("moveAsset");
|
||||||
)}
|
};
|
||||||
</>
|
const handleAssetRotate = () => {
|
||||||
);
|
setCanRender(false);
|
||||||
|
if (controls) {
|
||||||
|
(controls as CameraControls).enabled = true;
|
||||||
|
}
|
||||||
|
setContextAction("rotateAsset");
|
||||||
|
};
|
||||||
|
const handleAssetCopy = () => {
|
||||||
|
setCanRender(false);
|
||||||
|
if (controls) {
|
||||||
|
(controls as CameraControls).enabled = true;
|
||||||
|
}
|
||||||
|
setContextAction("copyAsset");
|
||||||
|
};
|
||||||
|
const handleAssetPaste = () => {
|
||||||
|
setCanRender(false);
|
||||||
|
if (controls) {
|
||||||
|
(controls as CameraControls).enabled = true;
|
||||||
|
}
|
||||||
|
setContextAction("pasteAsset");
|
||||||
|
};
|
||||||
|
const handleAssetDelete = () => {
|
||||||
|
setCanRender(false);
|
||||||
|
if (controls) {
|
||||||
|
(controls as CameraControls).enabled = true;
|
||||||
|
}
|
||||||
|
setContextAction("deleteAsset");
|
||||||
|
};
|
||||||
|
const handleAssetDuplicate = () => {
|
||||||
|
setCanRender(false);
|
||||||
|
if (controls) {
|
||||||
|
(controls as CameraControls).enabled = true;
|
||||||
|
}
|
||||||
|
setContextAction("duplicateAsset");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{canRender && (
|
||||||
|
<ScreenSpace depth={1}>
|
||||||
|
<Html
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: menuPosition.y,
|
||||||
|
left: menuPosition.x,
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContextMenu
|
||||||
|
visibility={visibility}
|
||||||
|
onRename={() => handleAssetRename()}
|
||||||
|
onFocus={() => handleAssetFocus()}
|
||||||
|
onFlipX={() => console.log("Flip to X")}
|
||||||
|
onFlipZ={() => console.log("Flip to Z")}
|
||||||
|
onMove={() => handleAssetMove()}
|
||||||
|
onRotate={() => handleAssetRotate()}
|
||||||
|
onDuplicate={() => handleAssetDuplicate()}
|
||||||
|
onCopy={() => handleAssetCopy()}
|
||||||
|
onPaste={() => handleAssetPaste()}
|
||||||
|
onGroup={() => console.log("Group")}
|
||||||
|
onArray={() => console.log("Array")}
|
||||||
|
onDelete={() => handleAssetDelete()}
|
||||||
|
/>
|
||||||
|
</Html>
|
||||||
|
</ScreenSpace>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContextControls;
|
export default ContextControls;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import ContextControls from "./contextControls/contextControls";
|
|||||||
import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D";
|
import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D";
|
||||||
import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls";
|
import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls";
|
||||||
import UndoRedo3DControls from "./undoRedoControls/undoRedo3D/undoRedo3DControls";
|
import UndoRedo3DControls from "./undoRedoControls/undoRedo3D/undoRedo3DControls";
|
||||||
|
import { useCameraShortcuts } from "../../../hooks/useCameraShortcuts";
|
||||||
|
|
||||||
export default function Controls() {
|
export default function Controls() {
|
||||||
const controlsRef = useRef<CameraControls>(null);
|
const controlsRef = useRef<CameraControls>(null);
|
||||||
@@ -116,6 +117,7 @@ export default function Controls() {
|
|||||||
stopInterval();
|
stopInterval();
|
||||||
};
|
};
|
||||||
}, [toggleView, state, socket]);
|
}, [toggleView, state, socket]);
|
||||||
|
useCameraShortcuts(controlsRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ function MoveControls3D({ boundingBoxRef }: any) {
|
|||||||
setContextAction(null);
|
setContextAction(null);
|
||||||
moveAssets()
|
moveAssets()
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [contextAction])
|
}, [contextAction])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -176,6 +177,7 @@ function MoveControls3D({ boundingBoxRef }: any) {
|
|||||||
canvasElement.removeEventListener("keydown", onKeyDown);
|
canvasElement.removeEventListener("keydown", onKeyDown);
|
||||||
canvasElement?.removeEventListener("keyup", onKeyUp);
|
canvasElement?.removeEventListener("keyup", onKeyUp);
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [camera, controls, scene, toggleView, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates]);
|
}, [camera, controls, scene, toggleView, selectedAssets, socket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates]);
|
||||||
|
|
||||||
const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => {
|
const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => {
|
||||||
@@ -223,6 +225,7 @@ function MoveControls3D({ boundingBoxRef }: any) {
|
|||||||
setDragOffset(newOffset);
|
setDragOffset(newOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [axisConstraint, camera, movedObjects])
|
}, [axisConstraint, camera, movedObjects])
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const SelectionControls3D: React.FC = () => {
|
|||||||
setContextAction(null);
|
setContextAction(null);
|
||||||
deleteSelection()
|
deleteSelection()
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [contextAction])
|
}, [contextAction])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -222,12 +223,14 @@ const SelectionControls3D: React.FC = () => {
|
|||||||
helper.enabled = false;
|
helper.enabled = false;
|
||||||
helper.dispose();
|
helper.dispose();
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule, toolMode]);
|
}, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule, toolMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) {
|
if (activeModule !== "builder" || toolMode !== 'cursor' || toggleView) {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [activeModule, toolMode, toggleView]);
|
}, [activeModule, toolMode, toggleView]);
|
||||||
|
|
||||||
const selectAssets = useCallback(() => {
|
const selectAssets = useCallback(() => {
|
||||||
@@ -362,7 +365,7 @@ const SelectionControls3D: React.FC = () => {
|
|||||||
removeAsset(uuid);
|
removeAsset(uuid);
|
||||||
});
|
});
|
||||||
|
|
||||||
echo.success("Selected models removed!");
|
echo.warn("Selected models removed!");
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getAllProjects } from "../../services/dashboard/getAllProjects";
|
|||||||
import { getUserData } from "../../functions/getUserData";
|
import { getUserData } from "../../functions/getUserData";
|
||||||
import { useLoadingProgress, useSocketStore } from "../../store/builder/store";
|
import { useLoadingProgress, useSocketStore } from "../../store/builder/store";
|
||||||
import { Color, SRGBColorSpace } from "three";
|
import { Color, SRGBColorSpace } from "three";
|
||||||
|
import { compressImage } from "../../utils/compressImage";
|
||||||
|
|
||||||
export default function Scene({ layout }: { readonly layout: "Main Layout" | "Comparison Layout"; }) {
|
export default function Scene({ layout }: { readonly layout: "Main Layout" | "Comparison Layout"; }) {
|
||||||
const map = useMemo(() => [
|
const map = useMemo(() => [
|
||||||
@@ -31,23 +32,24 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
|
|||||||
const { loadingProgress } = useLoadingProgress();
|
const { loadingProgress } = useLoadingProgress();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectId && loadingProgress > 1) return;
|
if (!projectId || loadingProgress !== 0) return;
|
||||||
getAllProjects(userId, organization).then((projects) => {
|
getAllProjects(userId, organization).then((projects) => {
|
||||||
if (!projects || !projects.Projects) return;
|
if (!projects || !projects.Projects) return;
|
||||||
let project = projects.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId);
|
let project = projects.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId);
|
||||||
const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName("canvas")[0];
|
const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName("canvas")[0];
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png");
|
compressImage((canvas as HTMLCanvasElement)?.toDataURL("image/png")).then((screenshotDataUrl) => {
|
||||||
const updateProjects = {
|
const updateProjects = {
|
||||||
projectId: project?._id,
|
projectId: project?._id,
|
||||||
organization,
|
organization,
|
||||||
userId,
|
userId,
|
||||||
projectName: project?.projectName,
|
projectName: project?.projectName,
|
||||||
thumbnail: screenshotDataUrl,
|
thumbnail: screenshotDataUrl,
|
||||||
};
|
};
|
||||||
if (projectSocket) {
|
if (projectSocket) {
|
||||||
projectSocket.emit("v1:project:update", updateProjects);
|
projectSocket.emit("v1:project:update", updateProjects);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
@@ -64,7 +66,7 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
|
|||||||
onContextMenu={(e) => { e.preventDefault(); }}
|
onContextMenu={(e) => { e.preventDefault(); }}
|
||||||
performance={{ min: 0.9, max: 1.0 }}
|
performance={{ min: 0.9, max: 1.0 }}
|
||||||
onCreated={(e) => { e.scene.background = layout === "Main Layout" ? null : new Color(0x19191d); }}
|
onCreated={(e) => { e.scene.background = layout === "Main Layout" ? null : new Color(0x19191d); }}
|
||||||
gl={{ outputColorSpace: SRGBColorSpace, powerPreference: "high-performance", antialias: true }}
|
gl={{ outputColorSpace: SRGBColorSpace, powerPreference: "high-performance", antialias: true, preserveDrawingBuffer: true }}
|
||||||
>
|
>
|
||||||
<Setup />
|
<Setup />
|
||||||
<Collaboration />
|
<Collaboration />
|
||||||
|
|||||||
@@ -1,241 +1,272 @@
|
|||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, 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 [axisLock, setAxisLock] = useState<"x" | "y" | "z" | null>(null);
|
||||||
);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const keysPressed = useRef<Set<string>>(new Set());
|
||||||
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;
|
// Axis lock key handling
|
||||||
const MIN_CONE_RADIUS = 0.01, MAX_CONE_RADIUS = 0.4;
|
useEffect(() => {
|
||||||
const MIN_CONE_HEIGHT = 0.035, MAX_CONE_HEIGHT = 2.0;
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.altKey) {
|
||||||
|
if (e.key.toLowerCase() === "x") keysPressed.current.add("x");
|
||||||
|
else if (e.key.toLowerCase() === "y") keysPressed.current.add("y");
|
||||||
|
else if (e.key.toLowerCase() === "z") keysPressed.current.add("z");
|
||||||
|
|
||||||
useEffect(() => {
|
if (keysPressed.current.has("x")) setAxisLock("x");
|
||||||
const canvasElement = gl.domElement;
|
else if (keysPressed.current.has("y")) setAxisLock("y");
|
||||||
let drag = false;
|
else if (keysPressed.current.has("z")) setAxisLock("z");
|
||||||
let isLeftMouseDown = false;
|
} else if (e.key === "Escape") {
|
||||||
|
setPoints([]);
|
||||||
const onMouseDown = () => {
|
setLinePoints(null);
|
||||||
isLeftMouseDown = true;
|
setAxisLock(null);
|
||||||
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 handleKeyUp = (e: KeyboardEvent) => {
|
||||||
setTubeGeometry(null);
|
keysPressed.current.delete(e.key.toLowerCase());
|
||||||
setStartConePosition(null);
|
if (keysPressed.current.has("x")) setAxisLock("x");
|
||||||
setEndConePosition(null);
|
else if (keysPressed.current.has("y")) setAxisLock("y");
|
||||||
|
else if (keysPressed.current.has("z")) setAxisLock("z");
|
||||||
|
else setAxisLock(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getArrowOrientation = (start: THREE.Vector3, end: THREE.Vector3) => {
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
const direction = new THREE.Vector3()
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
.subVectors(end, start)
|
return () => {
|
||||||
.normalize()
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
.negate();
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
const quaternion = new THREE.Quaternion();
|
};
|
||||||
quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
|
}, []);
|
||||||
return quaternion;
|
|
||||||
|
const getLineColor = useCallback(() => {
|
||||||
|
switch (axisLock) {
|
||||||
|
case "x":
|
||||||
|
return "#d94522"; // Red for X axis
|
||||||
|
case "y":
|
||||||
|
return "#22ab2e"; // Green for Y axis
|
||||||
|
case "z":
|
||||||
|
return "#227bd9"; // Blue for Z axis
|
||||||
|
default:
|
||||||
|
return "#b18ef1"; // Default purple
|
||||||
|
}
|
||||||
|
}, [axisLock]);
|
||||||
|
|
||||||
|
// Apply axis lock to a point
|
||||||
|
const applyAxisLock = useCallback(
|
||||||
|
(point: THREE.Vector3, referencePoint: THREE.Vector3) => {
|
||||||
|
const lockedPoint = point.clone();
|
||||||
|
|
||||||
|
switch (axisLock) {
|
||||||
|
case "x":
|
||||||
|
lockedPoint.y = referencePoint.y;
|
||||||
|
lockedPoint.z = referencePoint.z;
|
||||||
|
break;
|
||||||
|
case "y":
|
||||||
|
lockedPoint.x = referencePoint.x;
|
||||||
|
lockedPoint.z = referencePoint.z;
|
||||||
|
break;
|
||||||
|
case "z":
|
||||||
|
lockedPoint.x = referencePoint.x;
|
||||||
|
lockedPoint.y = referencePoint.y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lockedPoint;
|
||||||
|
},
|
||||||
|
[axisLock]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvasElement = gl.domElement;
|
||||||
|
let drag = false;
|
||||||
|
let isLeftMouseDown = false;
|
||||||
|
|
||||||
|
const onMouseDown = () => {
|
||||||
|
isLeftMouseDown = true;
|
||||||
|
drag = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const onMouseUp = (evt: any) => {
|
||||||
if (points.length === 2) {
|
isLeftMouseDown = false;
|
||||||
// console.log(points[0].distanceTo(points[1]));
|
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) {
|
||||||
|
let intersectionPoint = intersects[0].point.clone();
|
||||||
|
if (axisLock && points.length > 0) {
|
||||||
|
intersectionPoint = applyAxisLock(
|
||||||
|
intersectionPoint,
|
||||||
|
points[points.length - 1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (points.length < 2) {
|
||||||
|
setPoints([...points, intersectionPoint]);
|
||||||
|
} else {
|
||||||
|
setPoints([intersectionPoint]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [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, axisLock]);
|
||||||
);
|
|
||||||
|
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) {
|
||||||
|
let tempEnd = intersects[0].point.clone();
|
||||||
|
if (axisLock) {
|
||||||
|
tempEnd = applyAxisLock(tempEnd, points[0]);
|
||||||
|
}
|
||||||
|
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={getLineColor()}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -241,21 +241,18 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
|
|||||||
{Object.keys(zonesData).length !== 0 ? (
|
{Object.keys(zonesData).length !== 0 ? (
|
||||||
<>
|
<>
|
||||||
{Object.values(zonesData).map((zone, index) => (
|
{Object.values(zonesData).map((zone, index) => (
|
||||||
<>
|
<div
|
||||||
{ }
|
key={`${index}_${zone.zoneName}`}
|
||||||
<div
|
className={`zone ${selectedZone.zoneUuid === zone.zoneUuid ? "active" : ""
|
||||||
key={index}
|
}`}
|
||||||
className={`zone ${selectedZone.zoneUuid === zone.zoneUuid ? "active" : ""
|
onClick={() => {
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
|
|
||||||
handleSelect2dZoneData(zonesData[zone.zoneUuid]?.zoneUuid, zone.zoneName)
|
handleSelect2dZoneData(zonesData[zone.zoneUuid]?.zoneUuid, zone.zoneName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{zone.zoneName}
|
{zone.zoneName}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -436,6 +436,16 @@ export const useZoneAssetId = create<ZoneAssetState>((set) => ({
|
|||||||
setZoneAssetId: (asset) => set({ zoneAssetId: asset }),
|
setZoneAssetId: (asset) => set({ zoneAssetId: asset }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
interface ResourceManagementState {
|
||||||
|
resourceManagementId: string;
|
||||||
|
setResourceManagementId: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useResourceManagementId = create<ResourceManagementState>((set) => ({
|
||||||
|
resourceManagementId: "", // default value
|
||||||
|
setResourceManagementId: (id: string) => set({ resourceManagementId: id }),
|
||||||
|
}));
|
||||||
|
|
||||||
// version visible hidden
|
// version visible hidden
|
||||||
interface VersionHistoryState {
|
interface VersionHistoryState {
|
||||||
viewVersionHistory: boolean;
|
viewVersionHistory: boolean;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
border-radius: #{$border-radius-large};
|
border-radius: #{$border-radius-large};
|
||||||
outline: 1px solid var(--border-color);
|
outline: 1px solid var(--border-color);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
.header {
|
.header {
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@@ -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
@@ -516,6 +516,7 @@
|
|||||||
width: 50px;
|
width: 50px;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
outline: 1px solid var(--accent-color);
|
outline: 1px solid var(--accent-color);
|
||||||
}
|
}
|
||||||
@@ -586,6 +587,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
outline: 1px solid #aaaaaa29;
|
outline: 1px solid #aaaaaa29;
|
||||||
@@ -600,6 +602,7 @@
|
|||||||
|
|
||||||
.kebab-icon {
|
.kebab-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
transform: rotate(90deg) scale(0.8);
|
transform: rotate(90deg) scale(0.8);
|
||||||
}
|
}
|
||||||
@@ -1434,6 +1437,11 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: #{$border-radius-large};
|
border-radius: #{$border-radius-large};
|
||||||
|
|
||||||
|
outline: 1px solid var(--border-color);
|
||||||
|
outline-offset: -1px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--background-color);
|
||||||
|
|
||||||
.compare-simulations-header {
|
.compare-simulations-header {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
}
|
}
|
||||||
@@ -1598,8 +1606,13 @@
|
|||||||
.animations-lists {
|
.animations-lists {
|
||||||
max-height: 210px;
|
max-height: 210px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
.no-animation {
|
||||||
|
padding: 6px 8px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
.animations-list-wrapper {
|
.animations-list-wrapper {
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
|
|
||||||
.animations-list {
|
.animations-list {
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
@@ -1789,7 +1802,8 @@
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
.input-toggle-container{
|
|
||||||
|
.input-toggle-container {
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,8 +171,8 @@
|
|||||||
.messages-wrapper {
|
.messages-wrapper {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
max-height: 50vh;
|
max-height: 36vh;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
.edit-container {
|
.edit-container {
|
||||||
.input-container {
|
.input-container {
|
||||||
textarea {
|
textarea {
|
||||||
|
|||||||
@@ -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%);
|
||||||
|
}
|
||||||
|
|||||||
39
app/src/utils/compressImage.ts
Normal file
39
app/src/utils/compressImage.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export async function compressImage(
|
||||||
|
dataUrl: string,
|
||||||
|
maxWidth = 400,
|
||||||
|
maxHeight = 400
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = dataUrl;
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
const { width, height } = img;
|
||||||
|
|
||||||
|
// Calculate aspect ratio preserving resize
|
||||||
|
let newWidth = width;
|
||||||
|
let newHeight = height;
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
if (width > maxWidth) {
|
||||||
|
newWidth = maxWidth;
|
||||||
|
newHeight = (height * maxWidth) / width;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (height > maxHeight) {
|
||||||
|
newHeight = maxHeight;
|
||||||
|
newWidth = (width * maxHeight) / height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const offCanvas = document.createElement("canvas");
|
||||||
|
const ctx = offCanvas.getContext("2d");
|
||||||
|
|
||||||
|
offCanvas.width = newWidth;
|
||||||
|
offCanvas.height = newHeight;
|
||||||
|
|
||||||
|
ctx?.drawImage(img, 0, 0, newWidth, newHeight);
|
||||||
|
resolve(offCanvas.toDataURL("image/png"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -221,8 +221,12 @@ const KeyPressListener: React.FC = () => {
|
|||||||
|
|
||||||
// Shortcuts specific for sidebar visibility toggle and others specific to sidebar if added
|
// Shortcuts specific for sidebar visibility toggle and others specific to sidebar if added
|
||||||
handleSidebarShortcuts(keyCombination);
|
handleSidebarShortcuts(keyCombination);
|
||||||
|
|
||||||
|
|
||||||
// Active module selection (builder, simulation, etc.)
|
// Active module selection (builder, simulation, etc.)
|
||||||
handleModuleSwitch(keyCombination);
|
if (event.location === 0) { // Location 0 = standard keyboard (not numpad)
|
||||||
|
handleModuleSwitch(keyCombination);
|
||||||
|
}
|
||||||
// Common editing tools: cursor | delete | free-hand
|
// Common editing tools: cursor | delete | free-hand
|
||||||
handlePrimaryTools(keyCombination);
|
handlePrimaryTools(keyCombination);
|
||||||
// Shortcuts specific to the builder module (e.g., drawing and measurement tools)
|
// Shortcuts specific to the builder module (e.g., drawing and measurement tools)
|
||||||
|
|||||||
Reference in New Issue
Block a user