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

This commit is contained in:
2025-08-26 09:24:05 +05:30
29 changed files with 1694 additions and 1099 deletions

View File

@@ -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,8 +328,7 @@ 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) => (

View File

@@ -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 />
}
</> </>
); );

View File

@@ -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">

View File

@@ -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 />}

View File

@@ -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();
const { selectedProduct } = selectedProductStore();
const { setResourceManagementId } = useResourceManagementId();
useEffect(() => {
if (selectedProduct) {
const productDetails = getProductById(selectedProduct.productUuid);
const workerDetails = productDetails?.eventDatas || [];
const formattedWorkers = workerDetails
.filter((worker: any) => worker.type === "human")
.map((worker: any, index: number) => ({
employee: {
image: "", image: "",
"name": "John Doe", name: worker.modelName,
"employee_id": "HR-204", modelId: worker.modelUuid,
"status": "Active", employee_id: `HR-${204 + index}`,
status: "Active",
}, },
"task": { task: {
"status": "Ongoing", status: "Ongoing",
"title": "Inspecting Machine X", title: worker.taskTitle || "No Task Assigned",
"location": { location: {
"floor": 4, floor: worker.floor || 0,
"zone": "B" zone: worker.zone || "N/A"
}, },
"planned_time_hours": 6, planned_time_hours: worker.plannedTime || 0,
"time_spent_hours": 2, time_spent_hours: worker.timeSpent || 0,
"total_tasks": 12, total_tasks: worker.totalTasks || 0,
"completed_tasks": 3 completed_tasks: worker.completedTasks || 0
}, },
"actions": [ actions: [
"Assign Task", "Assign Task",
"Reassign Task", "Reassign Task",
"Pause", "Pause",
"Emergency Stop" "Emergency Stop"
], ],
"location": "Floor 4 . Zone B" location: `Floor ${worker.floor || "-"} . Zone ${worker.zone || "-"}`
}, }));
{
"employee": {
image: "",
"name": "Alice Smith",
"employee_id": "HR-205",
"status": "Active",
}, setWorkers(formattedWorkers);
"task": { }
"status": "Ongoing", }, [selectedProduct, getProductById]);
"title": "Calibrating Sensor Y",
"location": { useEffect(() => {
"floor": 2, //
"zone": "A" }, [workers]);
},
"planned_time_hours": 4,
"time_spent_hours": 1.5,
"total_tasks": 10,
"completed_tasks": 2 // const employee_details = [
}, // {
"actions": [ // "employee": {
"Assign Task", // image: "",
"Reassign Task", // "name": "John Doe",
"Pause", // "employee_id": "HR-204",
"Emergency Stop" // "status": "Active",
],
"location": "Floor 4 . Zone B" // },
}, // "task": {
{ // "status": "Ongoing",
"employee": { // "title": "Inspecting Machine X",
image: "", // "location": {
"name": "Michael Lee", // "floor": 4,
"employee_id": "HR-206", // "zone": "B"
"status": "Active", // },
// "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">

View File

@@ -1,49 +1,113 @@
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',
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.modelUuid,
name: asset.modelName,
model: asset.modelCode || "N/A",
status: asset.status || "Online",
usageRate: asset.usageRate || 15,
level: asset.level || "Level 1",
image: assetImage, image: assetImage,
description: 'Electric forklift used for moving goods and materials in warehouse operations.', description: asset.description || "No description",
cost: 122000, cost: asset.cost || 0,
count: 5, count: 1,
}, };
{ } else {
id: '2', grouped[asset.modelName].count += 1;
name: 'Warehouse Robot WR-300', }
model: 'WRB-3001', });
status: 'Online',
usageRate: 50, setAssets(Object.values(grouped));
level: 'Level 2', }
image: assetImage, }, [selectedProduct]);
description: 'Automated robot for handling packages and inventory in the warehouse.',
cost: 85000, function handleRenameAsset(newName: string) {
count: 3, //
}, // if (expandedAssetId) {
{ // setAssets(prevAssets =>
id: '3', // prevAssets.map(asset =>
name: 'Conveyor Belt System CB-150', // asset.id === expandedAssetId ? { ...asset, name: newName } : asset
model: 'CBS-150X', // )
status: 'Online', // );
usageRate: 95, // }
level: 'Level 3', }
image: assetImage,
description: 'High-speed conveyor belt system for efficient material handling.', useEffect(() => {
cost: 45000,
count: 2,
}, }, [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 (
@@ -55,19 +119,27 @@ const AssetManagement = () => {
/> */} /> */}
<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 ?
<>
<div className="drop-icon" onClick={() => setExpandedAssetId(null)}></div>
<img className='asset-image' src={asset.image} alt="" /> <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>

View File

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

View File

@@ -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
{!isEditableThread && (
<button
className="option" className="option"
onClick={() => { onClick={() => {
handleDeleteAction((val as Reply).replyId); handleDeleteAction((val as Reply).replyId);
}} }}
> >
Delete Delete
</button>} </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 >
)} )}

View File

@@ -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);

View File

@@ -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,8 +258,7 @@ 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">
@@ -301,8 +303,7 @@ 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)}
> >

View 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]);
};

View File

@@ -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("");
} }
}; };

View File

@@ -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);

View File

@@ -1,13 +1,31 @@
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({
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 [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const { selectedAssets } = useSelectedAssets(); const { selectedAssets } = useSelectedAssets();
const { setContextAction } = useContextActionStore(); const { setContextAction } = useContextActionStore();
@@ -93,7 +111,10 @@ function ContextControls() {
event.preventDefault(); event.preventDefault();
if (rightDrag.current) return; if (rightDrag.current) return;
if (selectedAssets.length > 0) { if (selectedAssets.length > 0) {
setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2 }); setMenuPosition({
x: event.clientX - gl.domElement.width / 2,
y: event.clientY - gl.domElement.height / 2,
});
setCanRender(true); setCanRender(true);
if (controls) { if (controls) {
(controls as CameraControls).enabled = false; (controls as CameraControls).enabled = false;
@@ -107,10 +128,10 @@ function ContextControls() {
}; };
if (selectedAssets.length > 0) { if (selectedAssets.length > 0) {
canvasElement.addEventListener('pointerdown', onPointerDown); canvasElement.addEventListener("pointerdown", onPointerDown);
canvasElement.addEventListener('pointermove', onPointerMove); canvasElement.addEventListener("pointermove", onPointerMove);
canvasElement.addEventListener('pointerup', onPointerUp); canvasElement.addEventListener("pointerup", onPointerUp);
canvasElement.addEventListener('contextmenu', handleContextClick) canvasElement.addEventListener("contextmenu", handleContextClick);
} else { } else {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
@@ -120,12 +141,12 @@ function ContextControls() {
} }
return () => { return () => {
canvasElement.removeEventListener('pointerdown', onPointerDown); canvasElement.removeEventListener("pointerdown", onPointerDown);
canvasElement.removeEventListener('pointermove', onPointerMove); canvasElement.removeEventListener("pointermove", onPointerMove);
canvasElement.removeEventListener('pointerup', onPointerUp); canvasElement.removeEventListener("pointerup", onPointerUp);
canvasElement.removeEventListener('contextmenu', handleContextClick); canvasElement.removeEventListener("contextmenu", handleContextClick);
}; };
}, [gl, selectedAssets]); }, [controls, gl, selectedAssets]);
const handleAssetRename = () => { const handleAssetRename = () => {
setCanRender(false); setCanRender(false);
@@ -134,67 +155,67 @@ function ContextControls() {
} }
setContextAction("renameAsset"); setContextAction("renameAsset");
setIsRenameMode(true); setIsRenameMode(true);
} };
const handleAssetFocus = () => { const handleAssetFocus = () => {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
(controls as CameraControls).enabled = true; (controls as CameraControls).enabled = true;
} }
setContextAction("focusAsset"); setContextAction("focusAsset");
} };
const handleAssetMove = () => { const handleAssetMove = () => {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
(controls as CameraControls).enabled = true; (controls as CameraControls).enabled = true;
} }
setContextAction("moveAsset") setContextAction("moveAsset");
} };
const handleAssetRotate = () => { const handleAssetRotate = () => {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
(controls as CameraControls).enabled = true; (controls as CameraControls).enabled = true;
} }
setContextAction("rotateAsset") setContextAction("rotateAsset");
} };
const handleAssetCopy = () => { const handleAssetCopy = () => {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
(controls as CameraControls).enabled = true; (controls as CameraControls).enabled = true;
} }
setContextAction("copyAsset") setContextAction("copyAsset");
} };
const handleAssetPaste = () => { const handleAssetPaste = () => {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
(controls as CameraControls).enabled = true; (controls as CameraControls).enabled = true;
} }
setContextAction("pasteAsset") setContextAction("pasteAsset");
} };
const handleAssetDelete = () => { const handleAssetDelete = () => {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
(controls as CameraControls).enabled = true; (controls as CameraControls).enabled = true;
} }
setContextAction("deleteAsset") setContextAction("deleteAsset");
} };
const handleAssetDuplicate = () => { const handleAssetDuplicate = () => {
setCanRender(false); setCanRender(false);
if (controls) { if (controls) {
(controls as CameraControls).enabled = true; (controls as CameraControls).enabled = true;
} }
setContextAction("duplicateAsset") setContextAction("duplicateAsset");
} };
return ( return (
<> <>
{canRender && ( {canRender && (
<ScreenSpace depth={1} > <ScreenSpace depth={1}>
<Html <Html
style={{ style={{
position: 'fixed', position: "fixed",
top: menuPosition.y, top: menuPosition.y,
left: menuPosition.x, left: menuPosition.x,
zIndex: 1000 zIndex: 1000,
}} }}
> >
<ContextMenu <ContextMenu

View File

@@ -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 (
<> <>

View File

@@ -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(() => {

View File

@@ -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();
} }
}; };

View File

@@ -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,13 +32,13 @@ 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,
@@ -48,6 +49,7 @@ export default function Scene({ layout }: { readonly layout: "Main Layout" | "Co
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 />

View File

@@ -1,34 +1,90 @@
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 [startConePosition, setStartConePosition] = const keysPressed = useRef<Set<string>>(new Set());
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");
if (keysPressed.current.has("x")) setAxisLock("x");
else if (keysPressed.current.has("y")) setAxisLock("y");
else if (keysPressed.current.has("z")) setAxisLock("z");
} else if (e.key === "Escape") {
setPoints([]);
setLinePoints(null);
setAxisLock(null);
}
};
const handleKeyUp = (e: KeyboardEvent) => {
keysPressed.current.delete(e.key.toLowerCase());
if (keysPressed.current.has("x")) setAxisLock("x");
else if (keysPressed.current.has("y")) setAxisLock("y");
else if (keysPressed.current.has("z")) setAxisLock("z");
else setAxisLock(null);
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
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(() => { useEffect(() => {
const canvasElement = gl.domElement; const canvasElement = gl.domElement;
@@ -54,14 +110,22 @@ const MeasurementTool = () => {
!intersect.object.name.includes("zonePlane") && !intersect.object.name.includes("zonePlane") &&
!intersect.object.name.includes("SelectionGroup") && !intersect.object.name.includes("SelectionGroup") &&
!intersect.object.name.includes("selectionAssetGroup") && !intersect.object.name.includes("selectionAssetGroup") &&
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") && !intersect.object.name.includes(
"SelectionGroupBoundingBoxLine"
) &&
!intersect.object.name.includes("SelectionGroupBoundingBox") && !intersect.object.name.includes("SelectionGroupBoundingBox") &&
!intersect.object.name.includes("SelectionGroupBoundingLine") && !intersect.object.name.includes("SelectionGroupBoundingLine") &&
intersect.object.type !== "GridHelper" intersect.object.type !== "GridHelper"
); );
if (intersects.length > 0) { if (intersects.length > 0) {
const intersectionPoint = intersects[0].point.clone(); let intersectionPoint = intersects[0].point.clone();
if (axisLock && points.length > 0) {
intersectionPoint = applyAxisLock(
intersectionPoint,
points[points.length - 1]
);
}
if (points.length < 2) { if (points.length < 2) {
setPoints([...points, intersectionPoint]); setPoints([...points, intersectionPoint]);
} else { } else {
@@ -78,9 +142,8 @@ const MeasurementTool = () => {
const onContextMenu = (evt: any) => { const onContextMenu = (evt: any) => {
evt.preventDefault(); evt.preventDefault();
if (!drag) { if (!drag) {
evt.preventDefault();
setPoints([]); setPoints([]);
setTubeGeometry(null); setLinePoints(null);
} }
}; };
@@ -90,8 +153,8 @@ const MeasurementTool = () => {
canvasElement.addEventListener("pointerup", onMouseUp); canvasElement.addEventListener("pointerup", onMouseUp);
canvasElement.addEventListener("contextmenu", onContextMenu); canvasElement.addEventListener("contextmenu", onContextMenu);
} else { } else {
resetMeasurement();
setPoints([]); setPoints([]);
setLinePoints(null);
} }
return () => { return () => {
@@ -100,10 +163,12 @@ const MeasurementTool = () => {
canvasElement.removeEventListener("pointerup", onMouseUp); canvasElement.removeEventListener("pointerup", onMouseUp);
canvasElement.removeEventListener("contextmenu", onContextMenu); canvasElement.removeEventListener("contextmenu", onContextMenu);
}; };
}, [toolMode, camera, raycaster, pointer, scene, points]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [toolMode, camera, raycaster, pointer, scene, points, axisLock]);
useFrame(() => { useFrame(() => {
if (points.length === 1) { if (points.length === 1) {
// live preview for second point
raycaster.setFromCamera(pointer, camera); raycaster.setFromCamera(pointer, camera);
const intersects = raycaster const intersects = raycaster
.intersectObjects(scene.children, true) .intersectObjects(scene.children, true)
@@ -122,116 +187,82 @@ const MeasurementTool = () => {
); );
if (intersects.length > 0) { if (intersects.length > 0) {
updateMeasurement(points[0], intersects[0].point); let tempEnd = intersects[0].point.clone();
if (axisLock) {
tempEnd = applyAxisLock(tempEnd, points[0]);
}
updateMeasurement(points[0], tempEnd);
} }
} else if (points.length === 2) { } else if (points.length === 2) {
// second point already fixed
updateMeasurement(points[0], points[1]); updateMeasurement(points[0], points[1]);
} else { } else {
resetMeasurement(); setLinePoints(null);
} }
}); });
const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => { const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => {
const distance = start.distanceTo(end); setLinePoints([start.clone(), end.clone()]);
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 = () => {
setTubeGeometry(null);
setStartConePosition(null);
setEndConePosition(null);
};
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]));
}
}, [points]);
return ( return (
<group ref={groupRef} name="MeasurementGroup"> <group ref={groupRef} name="MeasurementGroup">
{startConePosition && ( {linePoints && (
<mesh <>
name="MeasurementReference" {/* Outline line */}
position={startConePosition} <Line
quaternion={startConeQuaternion} points={linePoints}
> color="black"
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} /> lineWidth={6} // thicker than main line
<meshBasicMaterial color="yellow" /> depthTest={false}
</mesh> depthWrite={false}
)} renderOrder={998} // render behind main line
{endConePosition && ( />
<mesh
name="MeasurementReference" {/* Main line */}
position={endConePosition} <Line
quaternion={endConeQuaternion} points={linePoints}
> color={getLineColor()}
<coneGeometry args={[coneSize.radius, coneSize.height, 16]} /> lineWidth={2} // actual line width
<meshBasicMaterial color="yellow" /> depthTest={false}
</mesh> depthWrite={false}
)} transparent={false}
{tubeGeometry && ( opacity={1}
<mesh name="MeasurementReference" geometry={tubeGeometry}> renderOrder={999} // render on top
<meshBasicMaterial color="yellow" /> />
</mesh> </>
)} )}
{startConePosition && endConePosition && ( {points.map((point, index) => (
<Html <Html
scale={THREE.MathUtils.clamp( key={index}
startConePosition.distanceTo(endConePosition) * 0.25, position={point}
0, scale={0.5}
10 wrapperClass="measurement-label-wrapper"
)} className="measurement-label"
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]} zIndexRange={[1, 0]}
prepend prepend
sprite sprite
> >
<div> <div className="measurement-point"></div>
{(startConePosition.distanceTo(endConePosition) + (coneSize.height)).toFixed(2)} m </Html>
</div> ))}
{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> </Html>
)} )}
</group> </group>

View File

@@ -241,10 +241,8 @@ 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 <div
key={index} key={`${index}_${zone.zoneName}`}
className={`zone ${selectedZone.zoneUuid === zone.zoneUuid ? "active" : "" className={`zone ${selectedZone.zoneUuid === zone.zoneUuid ? "active" : ""
}`} }`}
onClick={() => { onClick={() => {
@@ -255,7 +253,6 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
> >
{zone.zoneName} {zone.zoneName}
</div> </div>
</>
))} ))}
</> </>
) : ( ) : (

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -1,15 +1,12 @@
@use "../abstracts/variables" as *; @use "../abstracts/variables" as *;
@use "../abstracts/mixins" as *; @use "../abstracts/mixins" as *;
.resourceManagement-container { .resourceManagement-container {
.navigation-wrapper { .navigation-wrapper {
@include flex-space-between; @include flex-space-between;
justify-content: space-around; justify-content: space-around;
.navigation { .navigation {
padding: 4px 12px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
text-wrap: nowrap; text-wrap: nowrap;
@@ -20,7 +17,6 @@
background: var(--background-color-button); background: var(--background-color-button);
} }
} }
} }
.search-container { .search-container {
@@ -74,7 +70,6 @@
border-radius: 100px; border-radius: 100px;
background: var(--background-color-button); background: var(--background-color-button);
} }
} }
} }
} }
@@ -104,13 +99,13 @@
background: var(--background-color); background: var(--background-color);
&.active { &.active {
outline: 1px solid var(--Color-Hover, #CCACFF); outline: 1px solid var(--Color-Hover, #ccacff);
} }
header { header {
position: relative; position: relative;
@include flex-space-between; @include flex-space-between;
padding: 3px 0; padding-bottom: 6px;
.user-details { .user-details {
display: flex; display: flex;
@@ -127,29 +122,31 @@
border-radius: 50%; border-radius: 50%;
width: 6px; width: 6px;
height: 6px; height: 6px;
outline: 1px solid #2F2C32; outline: 1px solid #2f2c32;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0; right: 0;
&.Active { &.Active {
background-color: #44E5C6; background-color: #44e5c6;
} }
} }
} }
.details { .details {
max-width: 144px;
.input-value{
max-width: 120px;
}
.employee-id { .employee-id {
color: #B7B7C6; color: #b7b7c6;
font-size: $tiny; font-size: $tiny;
} }
} }
} }
.see-more { .see-more {
padding: 4px 12px; padding: 4px 12px;
border-radius: 20px; border-radius: 20px;
text-wrap: nowrap; text-wrap: nowrap;
@@ -165,7 +162,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 1px; height: 1px;
background-color: #6F6F7A; background-color: #6f6f7a;
} }
} }
@@ -191,7 +188,7 @@
gap: 3px; gap: 3px;
.label-text { .label-text {
color: #B7B7C6; color: #b7b7c6;
} }
} }
} }
@@ -205,10 +202,12 @@
.stat-item { .stat-item {
border-radius: 100px; border-radius: 100px;
@include flex-space-between; @include flex-space-between;
background: linear-gradient(162.53deg, background: linear-gradient(
162.53deg,
rgba(51, 51, 51, 0.7) 0%, rgba(51, 51, 51, 0.7) 0%,
rgba(45, 36, 55, 0.7) 106.84%); rgba(45, 36, 55, 0.7) 106.84%
border: 1px solid #FFFFFF0D; );
border: 1px solid #ffffff0d;
padding: 6px; padding: 6px;
.stat-wrapper { .stat-wrapper {
@@ -241,7 +240,7 @@
.header { .header {
font-size: 12px; font-size: 12px;
color: #B7B7C6; color: #b7b7c6;
} }
} }
} }
@@ -255,16 +254,15 @@
button { button {
line-height: 133%; line-height: 133%;
font-size: 11px; font-size: 11px;
border: 1px solid var(--Linear-Border, #564B69); border: 1px solid var(--Linear-Border, #564b69);
border-radius: 100px; border-radius: 100px;
padding: 4px 0; padding: 4px 0;
&:last-child { &:last-child {
background-color: #CC2C1E; background-color: #cc2c1e;
} }
} }
} }
} }
} }
} }
@@ -277,17 +275,14 @@
.assetManagement-card-wrapper { .assetManagement-card-wrapper {
padding: 16px; padding: 16px;
border: 1px solid #564B69; border: 1px solid #564b69;
border-radius: 20px; border-radius: 20px;
gap: 10px; gap: 10px;
header { header {
border-bottom: 1px solid #595965; border-bottom: 1px solid #595965;
padding-bottom: 8px; padding-bottom: 8px;
.header-wrapper { .header-wrapper {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -312,20 +307,23 @@
@include flex-space-between; @include flex-space-between;
.asset-details { .asset-details {
display: flex;
// .asset-name{ gap: 4px;
// overflow: hidden; width: 100%;
// } max-width: 160px;
.input-value {
width: fit-content;
}
.asset-model { .asset-model {
color: var(--text-disabled); color: var(--text-disabled);
display: none;
} }
} }
} }
.asset-status-wrapper { .asset-status-wrapper {
padding: 4px 8px; padding: 4px 8px;
border: 1px solid var(--text-color-dark, #F3F3FDD9); border: 1px solid var(--text-color-dark, #f3f3fdd9);
border-radius: 100px; border-radius: 100px;
@include flex-space-between; @include flex-space-between;
gap: 4px; gap: 4px;
@@ -336,7 +334,7 @@
border-radius: 100%; border-radius: 100%;
&.Online { &.Online {
background-color: #44E5C6; background-color: #44e5c6;
} }
} }
@@ -384,7 +382,7 @@
width: 100%; width: 100%;
height: 5px; height: 5px;
border-radius: 20px; border-radius: 20px;
background-color: #6F6F7A; background-color: #6f6f7a;
position: relative; position: relative;
.filled-value { .filled-value {
@@ -393,7 +391,7 @@
left: 0; left: 0;
height: 100%; height: 100%;
width: 10px; width: 10px;
background-color: #CCACFF; background-color: #ccacff;
border-radius: 20px; border-radius: 20px;
} }
} }
@@ -407,7 +405,7 @@
gap: 10px; gap: 10px;
&__label { &__label {
color: #B7B7C6; color: #b7b7c6;
font-size: 14px; font-size: 14px;
} }
@@ -448,7 +446,7 @@
&.openViewMore { &.openViewMore {
outline-offset: -1px; outline-offset: -1px;
outline: 1px solid var(--Color-Hover, #CCACFF); outline: 1px solid var(--Color-Hover, #ccacff);
header { header {
display: flex; display: flex;
@@ -456,7 +454,227 @@
gap: 6px; gap: 6px;
.header-wrapper { .header-wrapper {
gap: 14px;
.asset-details-container {
flex-direction: column;
align-items: start;
justify-content: start;
.asset-details {
display: flex;
flex-direction: column;
gap: 4px;
max-width: 144px;
.input-value {
text-wrap: wrap;
font-size: 1rem;
}
}
.asset-status-wrapper {
margin-top: 8px;
}
}
}
}
}
}
}
}
// ASSET MANAGEMENT
.assetManagement-container {
display: flex;
flex-direction: column;
gap: 6px;
position: relative;
.assetManagement-card-wrapper {
padding: 16px;
border: 1px solid #564b69;
border-radius: 20px;
gap: 10px;
position: relative;
header {
border-bottom: 1px solid #595965;
padding-bottom: 8px;
.header-wrapper {
display: flex;
gap: 8px;
.icon {
min-width: 28px;
height: 28px;
border-radius: 7px;
@include flex-center;
background: var(--background-color-button);
}
.drop-icon {
position: absolute;
top: 18px;
right: 16px;
cursor: pointer;
}
.asset-image {
width: 114px;
height: 112px;
border-radius: 15.2px;
object-fit: cover;
}
.asset-details-container {
width: 100%;
@include flex-space-between;
.asset-details {
// .asset-name{
// overflow: hidden;
// }
.asset-model {
color: var(--text-disabled);
}
}
}
.asset-status-wrapper {
padding: 4px 8px;
border: 1px solid var(--text-color-dark, #f3f3fdd9);
border-radius: 100px;
@include flex-space-between;
gap: 4px;
.indication {
width: 6px;
height: 6px;
border-radius: 100%;
&.Online {
background-color: #44e5c6;
}
}
.status {
font-size: $small;
}
}
}
}
.asset-contents {
display: flex;
flex-direction: column;
gap: 3px;
.asset-wrapper {
@include flex-space-between;
padding: 6px 0;
gap: 20px;
.key-wrapper,
.viewMore {
display: flex;
align-items: center;
gap: 6px;
.icon {
@include flex-center;
}
}
.viewMore {
padding: 8px;
border-radius: 100px;
background: var(--background-color-button);
cursor: pointer;
}
.progress-wrapper {
flex: 1;
@include flex-space-between;
gap: 4px;
.progress-bar {
width: 100%;
height: 5px;
border-radius: 20px;
background-color: #6f6f7a;
position: relative;
.filled-value {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 10px;
background-color: #ccacff;
border-radius: 20px;
}
}
}
}
}
.asset-estimate {
margin-top: 5px;
display: flex;
flex-direction: column;
gap: 10px;
&__label {
color: #b7b7c6;
font-size: 14px;
}
&__value {
font-weight: 500;
font-size: 16px;
}
&__unit-cost {
display: flex;
flex-direction: column;
gap: 4px;
}
&__breakdown {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 2px;
}
&__view-button {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
background-color: var(--background-color-button);
border-radius: 20px;
padding: 8px 0;
}
&__view-text {
font-weight: 500;
// color: #4A4AFF;
}
}
&.openViewMore {
outline-offset: -1px;
outline: 1px solid var(--Color-Hover, #ccacff);
header {
display: flex;
flex-direction: column;
gap: 6px;
.header-wrapper {
gap: 20px; gap: 20px;
.asset-details-container { .asset-details-container {
@@ -480,12 +698,6 @@
} }
} }
} }
}
} }
} }
} }

View File

@@ -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;
} }

View File

@@ -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 {

View File

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

View 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"));
};
});
}

View File

@@ -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.)
if (event.location === 0) { // Location 0 = standard keyboard (not numpad)
handleModuleSwitch(keyCombination); 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)