Merge branch 'main-demo' into dev-resourceManagement

This commit is contained in:
2025-08-26 09:36:42 +05:30
18 changed files with 465 additions and 259 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,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

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

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

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

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

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

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

@@ -1,5 +1,5 @@
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, Line } from "@react-three/drei"; import { Html, Line } from "@react-three/drei";
@@ -10,7 +10,81 @@ const MeasurementTool = () => {
const [points, setPoints] = useState<THREE.Vector3[]>([]); const [points, setPoints] = useState<THREE.Vector3[]>([]);
const [linePoints, setLinePoints] = useState<THREE.Vector3[] | null>(null); const [linePoints, setLinePoints] = useState<THREE.Vector3[] | 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());
// Axis lock key handling
useEffect(() => {
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;
@@ -45,7 +119,13 @@ const MeasurementTool = () => {
); );
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 {
@@ -84,7 +164,7 @@ const MeasurementTool = () => {
canvasElement.removeEventListener("contextmenu", onContextMenu); canvasElement.removeEventListener("contextmenu", onContextMenu);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [toolMode, camera, raycaster, pointer, scene, points]); }, [toolMode, camera, raycaster, pointer, scene, points, axisLock]);
useFrame(() => { useFrame(() => {
if (points.length === 1) { if (points.length === 1) {
@@ -107,7 +187,10 @@ const MeasurementTool = () => {
); );
if (intersects.length > 0) { if (intersects.length > 0) {
const tempEnd = intersects[0].point.clone(); let tempEnd = intersects[0].point.clone();
if (axisLock) {
tempEnd = applyAxisLock(tempEnd, points[0]);
}
updateMeasurement(points[0], tempEnd); updateMeasurement(points[0], tempEnd);
} }
} else if (points.length === 2) { } else if (points.length === 2) {
@@ -139,7 +222,7 @@ const MeasurementTool = () => {
{/* Main line */} {/* Main line */}
<Line <Line
points={linePoints} points={linePoints}
color="#b18ef1" color={getLineColor()}
lineWidth={2} // actual line width lineWidth={2} // actual line width
depthTest={false} depthTest={false}
depthWrite={false} depthWrite={false}

View File

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

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

@@ -105,7 +105,7 @@
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;
@@ -142,6 +142,9 @@
.details { .details {
max-width: 144px; max-width: 144px;
.input-value{
max-width: 120px;
}
.employee-id { .employee-id {
color: #b7b7c6; color: #b7b7c6;
font-size: $tiny; font-size: $tiny;

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

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