diff --git a/app/package-lock.json b/app/package-lock.json index 485c1cb..536f4c4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -39,6 +39,7 @@ "mqtt": "^5.10.4", "postprocessing": "^6.36.4", "prompt-sync": "^4.2.0", + "r3f-perf": "^7.2.3", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", @@ -3378,6 +3379,15 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", @@ -7094,6 +7104,23 @@ "react": ">= 16.8.0" } }, + "node_modules/@utsubo/events": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@utsubo/events/-/events-0.1.7.tgz", + "integrity": "sha512-WB/GEj/0h27Bz8rJ0+CBtNz5mLT79ne1OjB7PUM4n0qLBqEDwm6yBzZC3j6tasHjlBPJDYZiBVIA1glaMlgZ5g==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.7" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -17915,6 +17942,64 @@ "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", "license": "ISC" }, + "node_modules/r3f-perf": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/r3f-perf/-/r3f-perf-7.2.3.tgz", + "integrity": "sha512-4+P/N/bnO9D8nzdm3suL/NjPZK/HHdjwpvajhi8j7eB41i2ECN6lX9RXiKSpHzpsDi2ui1tBj6q7/sz5opoqXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-icons": "^1.3.0", + "@react-three/drei": "^9.103.0", + "@stitches/react": "^1.2.8", + "@utsubo/events": "^0.1.7", + "zustand": "~4.5.2" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.133" + }, + "peerDependenciesMeta": { + "@react-three/fiber": { + "optional": true + }, + "dom": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/r3f-perf/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", diff --git a/app/package.json b/app/package.json index 8514111..d69d329 100644 --- a/app/package.json +++ b/app/package.json @@ -34,6 +34,7 @@ "mqtt": "^5.10.4", "postprocessing": "^6.36.4", "prompt-sync": "^4.2.0", + "r3f-perf": "^7.2.3", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", diff --git a/app/src/app.tsx b/app/src/app.tsx index b03d4df..9685c13 100644 --- a/app/src/app.tsx +++ b/app/src/app.tsx @@ -6,6 +6,7 @@ import Project from "./pages/Project"; import UserAuth from "./pages/UserAuth"; import "./styles/main.scss"; import { LoggerProvider } from "./components/ui/log/LoggerContext"; +import ForgotPassword from "./pages/ForgotPassword"; const App: React.FC = () => { @@ -19,8 +20,9 @@ const App: React.FC = () => { } /> + } /> } /> - } /> + } /> diff --git a/app/src/assets/cursors/close.svg b/app/src/assets/cursors/close.svg index 6fdebed..5b809eb 100644 --- a/app/src/assets/cursors/close.svg +++ b/app/src/assets/cursors/close.svg @@ -1,6 +1,6 @@ - + diff --git a/app/src/assets/cursors/pointing.svg b/app/src/assets/cursors/pointing.svg index 4a50a4b..1daa471 100644 --- a/app/src/assets/cursors/pointing.svg +++ b/app/src/assets/cursors/pointing.svg @@ -1,6 +1,6 @@ - + diff --git a/app/src/assets/gltf-glb/ui/human-ui-assembly.glb b/app/src/assets/gltf-glb/ui/human-ui-manufacture.glb similarity index 100% rename from app/src/assets/gltf-glb/ui/human-ui-assembly.glb rename to app/src/assets/gltf-glb/ui/human-ui-manufacture.glb diff --git a/app/src/assets/image/asset-image.png b/app/src/assets/image/asset-image.png new file mode 100644 index 0000000..851c7aa Binary files /dev/null and b/app/src/assets/image/asset-image.png differ diff --git a/app/src/assets/image/categories/decal.png b/app/src/assets/image/categories/decal.png new file mode 100644 index 0000000..ece13a7 Binary files /dev/null and b/app/src/assets/image/categories/decal.png differ diff --git a/app/src/assets/image/sampleDecal.png b/app/src/assets/image/sampleDecal.png new file mode 100644 index 0000000..ed8c9fd Binary files /dev/null and b/app/src/assets/image/sampleDecal.png differ diff --git a/app/src/components/Dashboard/DashboardCard.tsx b/app/src/components/Dashboard/DashboardCard.tsx index 0f75d19..8ac05b6 100644 --- a/app/src/components/Dashboard/DashboardCard.tsx +++ b/app/src/components/Dashboard/DashboardCard.tsx @@ -75,7 +75,7 @@ const DashboardCard: React.FC = ({ setLoadingProgress(1); setProjectName(projectName); - navigate(`/${projectId}`); + navigate(`/projects/${projectId}`); } catch {} }; @@ -108,7 +108,7 @@ const DashboardCard: React.FC = ({ setIsKebabOpen(false); } } catch (error) {} - window.open(`/${projectId}`, "_blank"); + window.open(`/projects/${projectId}`, "_blank"); break; case "rename": setIsRenaming(true); diff --git a/app/src/components/Dashboard/SidePannel.tsx b/app/src/components/Dashboard/SidePannel.tsx index 89ce5ea..db7d203 100644 --- a/app/src/components/Dashboard/SidePannel.tsx +++ b/app/src/components/Dashboard/SidePannel.tsx @@ -14,7 +14,6 @@ import lightThemeImage from "../../assets/image/lightThemeProject.png"; import { SettingsIcon, TrashIcon } from "../icons/ExportCommonIcons"; import { getUserData } from "../../functions/getUserData"; import { useLoadingProgress, useSocketStore } from "../../store/builder/store"; -import { createProject } from "../../services/dashboard/createProject"; interface SidePannelProps { setActiveTab: React.Dispatch>; @@ -38,11 +37,13 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { const handleCreateNewProject = async () => { const token = localStorage.getItem("token"); - const refreshToken = localStorage.getItem("refreshToken") - console.log('refreshToken: ', refreshToken); + const refreshToken = localStorage.getItem("refreshToken"); + console.log("refreshToken: ", refreshToken); try { const projectId = generateProjectId(); - useSocketStore.getState().initializeSocket(email, organization, token, refreshToken); + useSocketStore + .getState() + .initializeSocket(email, organization, token, refreshToken); //API for creating new Project // const project = await createProject( @@ -59,12 +60,12 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { projectUuid: projectId, }; - console.log('projectSocket: ', projectSocket); + console.log("projectSocket: ", projectSocket); if (projectSocket) { const handleResponse = (data: any) => { if (data.message === "Project created successfully") { setLoadingProgress(1) - navigate(`/${data.data.projectId}`); + navigate(`/projects/${data.data.projectId}`); } projectSocket.off("v1-project:response:add", handleResponse); // Clean up }; @@ -88,7 +89,8 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => {
{userName - ? userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase() + ? userName.charAt(0).toUpperCase() + + userName.slice(1).toLowerCase() : "Anonymous"}
@@ -162,10 +164,14 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { Settings -
{ - localStorage.clear(); - navigate("/"); - }}> +
{ + localStorage.clear(); + navigate("/"); + }} + > Log out
@@ -179,4 +185,4 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => { ); }; -export default SidePannel; \ No newline at end of file +export default SidePannel; diff --git a/app/src/components/footer/shortcutHelper.tsx b/app/src/components/footer/shortcutHelper.tsx index 405963e..230dff2 100644 --- a/app/src/components/footer/shortcutHelper.tsx +++ b/app/src/components/footer/shortcutHelper.tsx @@ -30,7 +30,11 @@ import { DuplicateInstanceIcon, PlayIcon, } from "../icons/ShortcutIcons"; -import { CloseIcon, EyeCloseIcon } from "../icons/ExportCommonIcons"; +import { + CloseIcon, + EyeCloseIcon, + PerformanceStatsIcon, +} from "../icons/ExportCommonIcons"; interface ShortcutItem { keys: string[]; @@ -51,6 +55,7 @@ interface ShortcutHelperProps { const ShortcutHelper: React.FC = ({ setShowShortcuts, }) => { + const shortcuts: ShortcutGroup[] = [ // Essential { @@ -76,8 +81,8 @@ const ShortcutHelper: React.FC = ({ }, { keys: ["CTRL", "+", "H"], - name: "Help", - description: "Open Help", + name: "Version History", + description: "Open Version History", icon: , }, { @@ -278,6 +283,12 @@ const ShortcutHelper: React.FC = ({ description: "Hide Simulation Player", icon: , }, + { + keys: ["F1"], + name: "Stats", + description: "Show Scene Stats", + icon: , + }, ], }, ]; @@ -300,6 +311,7 @@ const ShortcutHelper: React.FC = ({ > +
{shortcuts.map((group) => ( @@ -316,9 +328,8 @@ const ShortcutHelper: React.FC = ({
{activeShortcuts.map((item) => (
{ + return ( +
+
+

Successfully

+

Your password has been reset successfully

+ Login +
+ ); +}; + +export default ConfirmationMessage; diff --git a/app/src/components/forgotPassword/EmailInput.tsx b/app/src/components/forgotPassword/EmailInput.tsx new file mode 100644 index 0000000..4df4d19 --- /dev/null +++ b/app/src/components/forgotPassword/EmailInput.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +interface Props { + email: string; + setEmail: (value: string) => void; + onSubmit: (e: React.FormEvent) => void; +} + +const EmailInput: React.FC = ({ email, setEmail, onSubmit }) => { + return ( +
+

Forgot password

+

+ Enter your email for the verification process, we will send a 4-digit + code to your email. +

+
{ + e.preventDefault(); + onSubmit(e); + }} + > + setEmail(e.target.value)} + required + /> + +
+
+ ); +}; + +export default EmailInput; diff --git a/app/src/components/forgotPassword/OTPInput.tsx b/app/src/components/forgotPassword/OTPInput.tsx new file mode 100644 index 0000000..09d4537 --- /dev/null +++ b/app/src/components/forgotPassword/OTPInput.tsx @@ -0,0 +1,73 @@ +import React, { useState, useRef, useEffect } from "react"; + +const OTPInput: React.FC<{ + length?: number; + onComplete: (otp: string) => void; + code: string | number; +}> = ({ length = 4, onComplete, code }) => { + const [otpValues, setOtpValues] = useState(Array(length).fill("")); + const inputsRef = useRef<(HTMLInputElement | null)[]>([]); + + // ✅ Pre-fill inputs if code is passed + useEffect(() => { + if (code) { + const codeString = String(code); + const filled = codeString.split("").slice(0, length); + const padded = filled.concat(Array(length - filled.length).fill("")); + setOtpValues(padded); + if (filled.length === length) { + onComplete(filled.join("")); + } + } + }, [code, length, onComplete]); + + // ✅ Focus first input on mount + useEffect(() => { + inputsRef.current[0]?.focus(); + }, []); + + const handleChange = (value: string, index: number) => { + if (/^[0-9]?$/.test(value)) { + const newOtp = [...otpValues]; + newOtp[index] = value; + setOtpValues(newOtp); + + if (value && index < length - 1) { + inputsRef.current[index + 1]?.focus(); + } + + // ✅ Only trigger onComplete when all digits are filled + if (newOtp.every((digit) => digit !== "")) { + onComplete(newOtp.join("")); + } + } + }; + + const handleKeyDown = ( + e: React.KeyboardEvent, + index: number + ) => { + if (e.key === "Backspace" && !otpValues[index] && index > 0) { + inputsRef.current[index - 1]?.focus(); + } + }; + + return ( +
+ {otpValues.map((value, index) => ( + handleChange(e.target.value, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + ref={(el) => (inputsRef.current[index] = el)} + /> + ))} +
+ ); +}; + +export default OTPInput; diff --git a/app/src/components/forgotPassword/OTP_Verification.tsx b/app/src/components/forgotPassword/OTP_Verification.tsx new file mode 100644 index 0000000..450f210 --- /dev/null +++ b/app/src/components/forgotPassword/OTP_Verification.tsx @@ -0,0 +1,77 @@ +import React, { FormEvent, useState } from "react"; +import OTPInput from "./OTPInput"; + +interface Props { + email: string; + code: string; + timer: number; + setCode: (value: string) => void; + onSubmit: (e: React.FormEvent) => void; + resendCode: () => void; +} + +const OTPVerification: React.FC = ({ + email, + timer, + setCode, + onSubmit, + resendCode, + code, +}) => { + const [otp, setOtp] = useState(""); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + if (otp.length === 4) { + onSubmit(e); + } else { + alert("Please enter the 4-digit code"); + } + }; + + return ( +
+

Verification

+

+ Enter the 4-digit code sent to {email}. +

+
+ { + setOtp(code); + setCode(codes); + }} + code={code} + /> +
+ {timer > 0 + ? `${String(Math.floor(timer / 60)).padStart(2, "0")}:${String( + timer % 60 + ).padStart(2, "0")}` + : ""} +
+ + +
0 ? "disabled" : ""}`} + onClick={timer === 0 ? resendCode : undefined} + style={{ + cursor: timer === 0 ? "pointer" : "not-allowed", + opacity: timer === 0 ? 1 : 0.5, + }} + > + If you didn’t receive a code, Resend +
+
+ ); +}; + +export default OTPVerification; diff --git a/app/src/components/forgotPassword/PasswordSetup.tsx b/app/src/components/forgotPassword/PasswordSetup.tsx new file mode 100644 index 0000000..f8f5658 --- /dev/null +++ b/app/src/components/forgotPassword/PasswordSetup.tsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import { EyeIcon } from "../icons/ExportCommonIcons"; + +interface Props { + newPassword: string; + confirmPassword: string; + setNewPassword: (value: string) => void; + setConfirmPassword: (value: string) => void; + onSubmit: (e: React.FormEvent) => void; +} + +const PasswordSetup: React.FC = ({ + newPassword, + confirmPassword, + setNewPassword, + setConfirmPassword, + onSubmit, +}) => { + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + return ( +
+

New Password

+

+ Set the new password for your account so you can login and access all + features. +

+
{ + e.preventDefault(); + if (newPassword !== confirmPassword) { + alert("Passwords do not match"); + return; + } + onSubmit(e); + }} + > +
+ setNewPassword(e.target.value)} + required + /> + +
+ +
+ setConfirmPassword(e.target.value)} + required + /> + +
+ + +
+
+ ); +}; + +export default PasswordSetup; diff --git a/app/src/components/icons/ContextMenuIcons.tsx b/app/src/components/icons/ContextMenuIcons.tsx index 210f1b9..a48adf2 100644 --- a/app/src/components/icons/ContextMenuIcons.tsx +++ b/app/src/components/icons/ContextMenuIcons.tsx @@ -164,7 +164,7 @@ export function RenameIcon() { export function FocusIcon() { return ( - + ); @@ -173,7 +173,7 @@ export function FocusIcon() { export function TransformIcon() { return ( - + @@ -189,10 +189,10 @@ export function TransformIcon() { export function DublicateIcon() { return ( - + - + @@ -207,7 +207,7 @@ export function DublicateIcon() { export function CopyIcon() { return ( - + @@ -223,7 +223,7 @@ export function CopyIcon() { export function PasteIcon() { return ( - + @@ -239,7 +239,7 @@ export function PasteIcon() { export function ModifiersIcon() { return ( - + ); @@ -248,8 +248,8 @@ export function ModifiersIcon() { export function DeleteIcon() { return ( - - + + @@ -263,8 +263,8 @@ export function DeleteIcon() { export function MoveIcon() { return ( - - + + @@ -278,7 +278,7 @@ export function MoveIcon() { export function RotateIcon() { return ( - + ); } @@ -286,7 +286,7 @@ export function RotateIcon() { export function GroupIcon() { return ( - + ); } diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index 6f9a1fb..72f9ca0 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -710,6 +710,27 @@ export const ExpandIcon = ({ isActive }: { isActive: boolean }) => { ); }; +export const ExpandIcon2 = () => { + return ( + + + + + ); +}; + export const StartIcon = () => { return ( { ); }; + +export const SuccessIcon = () => { + return ( + + + + + ); +}; + +export const AlertIcon = () => { + return ( + + + + + ); +}; +export const NavigationIcon = () => { + return ( + + + + + + ); +}; +export const HangTagIcon = () => { + return ( + + + + + + + + + + + ); +}; +export const DecalInfoIcon = () => { + return ( + + + + + + ); +}; + +export const LayeringBottomIcon = () => { + return ( + + + + + + ); +}; + +export const LayeringTopIcon = () => { + return ( + + + + + + ); +}; + +export const ValueUpdateIcon = () => { + return ( + + + + + ); +}; + +export const ListTaskIcon = () => { + return ( + + + + ); +}; + +export const LocationPinIcon = () => { + return ( + + + + + ); +}; + +export const ClockThreeIcon = () => { + return ( + + + + ); +}; + +export const SlectedTickIcon = () => { + return ( + + + + + ); +}; + +export const HourGlassIcon = () => { + return ( + + + + ); +}; + +export const TargetIcon = () => { + return ( + + + + ); +}; + +export const ForkLiftIcon = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; +export const RightHalfFillCircleIcon = () => { + return ( + + + + ); +}; + +export const PerformanceStatsIcon = () => { + return ( + + + + ); +}; diff --git a/app/src/components/icons/SimulationIcons.tsx b/app/src/components/icons/SimulationIcons.tsx index 9c09869..95844ad 100644 --- a/app/src/components/icons/SimulationIcons.tsx +++ b/app/src/components/icons/SimulationIcons.tsx @@ -835,3 +835,29 @@ export const LayoutIcon = () => { ); }; + + +export function FilePackageIcon({ isActive }: Readonly<{ isActive: boolean }>) { + + return ( + + + + + + + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/app/src/components/layout/scenes/MainScene.tsx b/app/src/components/layout/scenes/MainScene.tsx index e283986..350c9e9 100644 --- a/app/src/components/layout/scenes/MainScene.tsx +++ b/app/src/components/layout/scenes/MainScene.tsx @@ -59,7 +59,7 @@ function MainScene() { const { setFloatingWidget } = useFloatingWidget(); const { clearComparisonProduct } = useComparisonProduct(); const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem(); - const { selectedAssets,setSelectedAssets } = useSelectedAssets(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const { assetStore, productStore } = useSceneContext(); const { products } = productStore(); const { setName } = assetStore(); @@ -75,13 +75,13 @@ function MainScene() { clearComparisonProduct(); setIsVersionSaved(false); } - }, [activeModule]) + }, [activeModule, clearComparisonProduct, setIsVersionSaved]) useEffect(() => { if (versionHistory.length > 0) { setSelectedVersion(versionHistory[0]) } - }, [versionHistory]) + }, [setSelectedVersion, versionHistory]) const handleSelectVersion = (option: string) => { const version = versionHistory.find((version) => version.versionName === option); @@ -101,7 +101,7 @@ function MainScene() { if (!projectId) return if (selectedFloorItem) { console.log('selectedFloorItem.userData.modelUuid: ', selectedFloorItem.userData.modelUuid); - console.log(' newName: ', newName); + console.log(' newName: ', newName); console.log('projectId: ', projectId); setAssetsApi({ modelUuid: selectedFloorItem.userData.modelUuid, @@ -155,7 +155,7 @@ function MainScene() { )} {(isPlaying) && activeModule === "simulation" && - loadingProgress == 0 && } + loadingProgress === 0 && } {(isPlaying) && activeModule !== "simulation" && } @@ -212,7 +212,11 @@ function MainScene() { {activeModule !== "market" && !selectedUser &&
} - {(commentPositionState !== null || selectedComment !== null) && } + + { + (commentPositionState !== null || selectedComment !== null) && + + } ); diff --git a/app/src/components/layout/sidebarLeft/Assets.tsx b/app/src/components/layout/sidebarLeft/Assets.tsx index 8c8b4d7..c08ef78 100644 --- a/app/src/components/layout/sidebarLeft/Assets.tsx +++ b/app/src/components/layout/sidebarLeft/Assets.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Search from "../../ui/inputs/Search"; import { getCategoryAsset } from "../../../services/factoryBuilder/asset/assets/getCategoryAsset"; import { fetchAssets } from "../../../services/marketplace/fetchAssets"; -import { useSelectedItem } from "../../../store/builder/store"; +import { useDecalStore, useSelectedItem } from "../../../store/builder/store"; // images ------------------- import vehicle from "../../../assets/image/categories/vehicles.png"; @@ -13,246 +13,295 @@ import storage from "../../../assets/image/categories/storage.png"; import office from "../../../assets/image/categories/office.png"; import safety from "../../../assets/image/categories/safety.png"; import feneration from "../../../assets/image/categories/feneration.png"; +import decal from "../../../assets/image/categories/decal.png"; import SkeletonUI from "../../templates/SkeletonUI"; +import { + AlertIcon, + DecalInfoIcon, + HangTagIcon, + NavigationIcon, +} from "../../icons/ExportCommonIcons"; // ------------------------------------- interface AssetProp { - filename: string; - thumbnail?: string; - category: string; - description?: string; - tags: string; - url?: string; - uploadDate?: number; - isArchieve?: boolean; - animated?: boolean; - price?: number; - CreatedBy?: string; + filename: string; + thumbnail?: string; + category: string; + description?: string; + tags: string; + url?: string; + uploadDate?: number; + isArchieve?: boolean; + animated?: boolean; + price?: number; + CreatedBy?: string; } interface CategoryListProp { - assetImage?: string; - assetName?: string; - categoryImage: string; - category: string; + assetImage?: string; + assetName?: string; + categoryImage: string; + category: string; } const Assets: React.FC = () => { - const { setSelectedItem } = useSelectedItem(); - const [searchValue, setSearchValue] = useState(""); - const [selectedCategory, setSelectedCategory] = useState(null); - const [categoryAssets, setCategoryAssets] = useState([]); - const [filtereredAssets, setFiltereredAssets] = useState([]); - const [categoryList, setCategoryList] = useState([]); - const [isLoading, setisLoading] = useState(false); // Loading state for assets + const { setSelectedItem } = useSelectedItem(); + const [searchValue, setSearchValue] = useState(""); + const [selectedCategory, setSelectedCategory] = useState(null); + const [categoryAssets, setCategoryAssets] = useState([]); + const [filtereredAssets, setFiltereredAssets] = useState([]); + const [categoryList, setCategoryList] = useState([]); + const [isLoading, setisLoading] = useState(false); // Loading state for assets - const handleSearchChange = (value: string) => { - const searchTerm = value.toLowerCase(); - setSearchValue(value); - if (searchTerm.trim() === "" && !selectedCategory) { - setCategoryAssets([]); - return; - } - const filteredModels = filtereredAssets?.filter((model) => { - if (!model?.tags || !model?.filename || !model?.category) return false; - if (searchTerm.startsWith(":") && searchTerm.length > 1) { - const tagSearchTerm = searchTerm.slice(1); - return model.tags.toLowerCase().includes(tagSearchTerm); - } else if (selectedCategory) { - return ( - model.category - .toLowerCase() - .includes(selectedCategory.toLowerCase()) && - model.filename.toLowerCase().includes(searchTerm) - ); - } else { - return model.filename.toLowerCase().includes(searchTerm); - } - }); + const handleSearchChange = (value: string) => { + const searchTerm = value.toLowerCase(); + setSearchValue(value); + if (searchTerm.trim() === "" && !selectedCategory) { + setCategoryAssets([]); + return; + } + const filteredModels = filtereredAssets?.filter((model) => { + if (!model?.tags || !model?.filename || !model?.category) return false; + if (searchTerm.startsWith(":") && searchTerm.length > 1) { + const tagSearchTerm = searchTerm.slice(1); + return model.tags.toLowerCase().includes(tagSearchTerm); + } else if (selectedCategory) { + return ( + model.category + .toLowerCase() + .includes(selectedCategory.toLowerCase()) && + model.filename.toLowerCase().includes(searchTerm) + ); + } else { + return model.filename.toLowerCase().includes(searchTerm); + } + }); - setCategoryAssets(filteredModels); + setCategoryAssets(filteredModels); + }; + + useEffect(() => { + const filteredAssets = async () => { + try { + const filt = await fetchAssets(); + setFiltereredAssets(filt); + } catch { + echo.error("Filter asset not found"); + } }; + filteredAssets(); + }, [categoryAssets]); - useEffect(() => { - const filteredAssets = async () => { - try { - const filt = await fetchAssets(); - setFiltereredAssets(filt); - } catch { - echo.error("Filter asset not found"); + useEffect(() => { + setCategoryList([ + { category: "Fenestration", categoryImage: feneration }, + { category: "Decals", categoryImage: decal }, + { category: "Vehicles", categoryImage: vehicle }, + { category: "Workstation", categoryImage: workStation }, + { category: "Machines", categoryImage: machines }, + { category: "Workers", categoryImage: worker }, + { category: "Storage", categoryImage: storage }, + { category: "Safety", categoryImage: safety }, + { category: "Office", categoryImage: office }, + ]); + }, []); + + const fetchCategoryAssets = async (asset: any) => { + setisLoading(true); + setSelectedCategory(asset); + try { + const res = await getCategoryAsset(asset); + setCategoryAssets(res); + setFiltereredAssets(res); + setisLoading(false); // End loading + // eslint-disable-next-line + } catch (error) { + echo.error("failed to fetch assets"); + setisLoading(false); + } + }; + + const activeSubcategories = [ + { name: "Safety", icon: }, + { name: "Navigation", icon: }, + { name: "Branding", icon: }, + { name: "Informational", icon: }, + ]; + + const { selectedSubCategory, setSelectedSubCategory } = useDecalStore(); + return ( +
+ +
+
+ {(() => { + if (isLoading) { + return ; // Show skeleton when loading } - }; - filteredAssets(); - }, [categoryAssets]); + if (searchValue) { + return ( +
+
+
+

Results for {searchValue}

+
+
+ {categoryAssets?.map((asset: any, index: number) => ( +
+ {asset.filename} { + setSelectedItem({ + name: asset.filename, + id: asset.AssetID, + type: + asset.type === "undefined" + ? undefined + : asset.type, + }); + }} + /> - useEffect(() => { - setCategoryList([ - { category: "Fenestration", categoryImage: feneration }, - { category: "Vehicles", categoryImage: vehicle }, - { category: "Workstation", categoryImage: workStation }, - { category: "Machines", categoryImage: machines }, - { category: "Workers", categoryImage: worker }, - { category: "Storage", categoryImage: storage }, - { category: "Safety", categoryImage: safety }, - { category: "Office", categoryImage: office }, - ]); - }, []); +
+ {asset.filename + .split("_") + .map( + (word: any) => + word.charAt(0).toUpperCase() + word.slice(1) + ) + .join(" ")} +
+
+ ))} +
+
+
+ ); + } - const fetchCategoryAssets = async (asset: any) => { - setisLoading(true); - setSelectedCategory(asset); - try { - const res = await getCategoryAsset(asset); - setCategoryAssets(res); - setFiltereredAssets(res); - setisLoading(false); // End loading - // eslint-disable-next-line - } catch (error) { - echo.error("failed to fetch assets"); - setisLoading(false); - } - }; + if (selectedCategory) { + return ( +
+

+ {selectedCategory} + +

- return ( -
- -
-
- {(() => { - if (isLoading) { - return ; // Show skeleton when loading - } - if (searchValue) { - return ( -
-
-
-

Results for {searchValue}

-
-
- {categoryAssets?.map((asset: any, index: number) => ( -
- {asset.filename} { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: asset.type === "undefined" ? undefined : asset.type - }); - }} - /> + {selectedCategory === "Decals" && ( + <> +
+ {activeSubcategories.map((cat, index) => ( +
setSelectedSubCategory(cat.name)} + > +
{cat.icon}
+
{cat.name}
+
+ ))} +
+ + )} +
+ {categoryAssets?.map((asset: any, index: number) => ( +
+ {asset.filename} { + setSelectedItem({ + name: asset.filename, + id: asset.AssetID, + type: + asset.type === "undefined" + ? undefined + : asset.type, + category: asset.category, + subType: asset.subType, + }); + }} + /> +
+ {asset.filename + .split("_") + .map( + (word: any) => + word.charAt(0).toUpperCase() + word.slice(1) + ) + .join(" ")} +
+
+ ))} + {categoryAssets.length === 0 && ( +
+ 🚧 The asset shelf is empty. We're working on filling it + up! +
+ )} +
+
+ ); + } -
- {asset.filename - .split("_") - .map( - (word: any) => - word.charAt(0).toUpperCase() + word.slice(1) - ) - .join(" ")} -
-
- ))} -
-
-
- ); - } - - if (selectedCategory) { - return ( -
-

- {selectedCategory} - -

-
- {categoryAssets?.map((asset: any, index: number) => ( -
- {asset.filename} { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: asset.type === "undefined" ? undefined : asset.type, - category: asset.category, - subType: asset.subType - }); - }} - /> -
- {asset.filename.split("_").map((word: any) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")} -
-
- ))} - {categoryAssets.length === 0 && ( -
- 🚧 The asset shelf is empty. We're working on filling it up! -
- )} -
-
- ); - } - - return ( -
-

Categories

-
- {Array.from( - new Set(categoryList.map((asset) => asset.category)) - ).map((category, index) => { - const categoryInfo = categoryList.find( - (asset) => asset.category === category - ); - return ( -
fetchCategoryAssets(category)} - > - {category} -
{category}
-
- ); - })} -
-
- ); - })()} -
-
-
- ); + return ( +
+

Categories

+
+ {Array.from( + new Set(categoryList.map((asset) => asset.category)) + ).map((category, index) => { + const categoryInfo = categoryList.find( + (asset) => asset.category === category + ); + return ( +
fetchCategoryAssets(category)} + > + {category} +
{category}
+
+ ); + })} +
+
+ ); + })()} + +
+
+ ); }; export default Assets; diff --git a/app/src/components/layout/sidebarLeft/Outline.tsx b/app/src/components/layout/sidebarLeft/Outline.tsx index c23e77c..ad47614 100644 --- a/app/src/components/layout/sidebarLeft/Outline.tsx +++ b/app/src/components/layout/sidebarLeft/Outline.tsx @@ -4,54 +4,92 @@ import DropDownList from "../../ui/list/DropDownList"; import { useSceneContext } from "../../../modules/scene/sceneContext"; import { isPointInsidePolygon } from "../../../functions/isPointInsidePolygon"; +interface AssetData { + id: string; + name: string; + position?: []; + rotation?: {}; +} + interface ZoneData { id: string; name: string; - assets: { id: string; name: string; position?: []; rotation?: {} }[]; + assets: AssetData[]; } const Outline: React.FC = () => { const [searchValue, setSearchValue] = useState(""); - const [zoneDataList, setZoneDataList] = useState([]); - const [buildingsList, setBuildingsList] = useState<{ id: string; name: string }[]>([]); - const [isLayersOpen, setIsLayersOpen] = useState(true); - const [isBuildingsOpen, setIsBuildingsOpen] = useState(false); - const [isZonesOpen, setIsZonesOpen] = useState(false); + const [sceneAssetsDataList, setSceneAssetsDataList] = useState< + ZoneData[] | AssetData[] + >([]); + // const [buildingsList, setBuildingsList] = useState<{ id: string; name: string }[]>([]); + // const [isLayersOpen, setIsLayersOpen] = useState(true); + // const [isBuildingsOpen, setIsBuildingsOpen] = useState(false); + const [isZonesOpen, setIsZonesOpen] = useState(true); const { assetStore, zoneStore } = useSceneContext(); const { assets } = assetStore(); const { zones } = zoneStore(); - useEffect(() => { - const updatedZoneList: ZoneData[] = zones?.map((zone: any) => { - const polygon2D = zone.points.map((p: any) => [p.position[0], p.position[2]]); - const assetsInZone = assets.filter((item: any) => { - const [x, , z] = item.position; - return isPointInsidePolygon([x, z], polygon2D as [number, number][]); - }) - .map((item: any) => ({ - id: item.modelUuid, - name: item.modelName, - position: item.position, - rotation: item.rotation, - })); + const assignedAssets = new Set(); - - return { - id: zone.zoneUuid, - name: zone.zoneName, - assets: assetsInZone, - }; - }); + const updatedZoneList: ZoneData[] = + zones?.map((zone: any) => { + const polygon2D = zone.points.map((p: any) => [ + p.position[0], + p.position[2], + ]); + const assetsInZone = assets + .filter((item: any) => { + const [x, , z] = item.position; + const inside = isPointInsidePolygon( + [x, z], + polygon2D as [number, number][] + ); + if (inside) assignedAssets.add(item.modelUuid); + return inside; + }) + .map((item: any) => ({ + id: item.modelUuid, + name: item.modelName, + position: item.position, + rotation: item.rotation, + })); - setZoneDataList(updatedZoneList); + return { + id: zone.zoneUuid, + name: zone.zoneName, + assets: assetsInZone, + }; + }) ?? []; + + // Collect unassigned assets + const unassignedAssets = assets + .filter((item: any) => !assignedAssets.has(item.modelUuid)) + .map((item: any) => ({ + id: item.modelUuid, + name: item.modelName, + position: item.position, + rotation: item.rotation, + })); + + // Add as a separate "zone" + if (unassignedAssets.length > 0) { + updatedZoneList.push({ + id: "unassigned-zone", + name: "Unassigned", + assets: unassignedAssets, + }); + } + + setSceneAssetsDataList(updatedZoneList); }, [zones, assets]); const handleSearchChange = (value: string) => { setSearchValue(value); }; - const dropdownItems = [{ id: "1", name: "Ground Floor" }]; + // const dropdownItems = [{ id: "1", name: "Ground Floor" }]; return (
@@ -63,7 +101,7 @@ const Outline: React.FC = () => {
) : (
-
+ {/*
{ showFocusIcon={true} remove /> -
+
*/}
- setIsBuildingsOpen((prev) => !prev)} showKebabMenu={false} showAddIcon={false} - /> + /> */} setIsZonesOpen((prev) => !prev)} showKebabMenu={false} diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index ba55f6f..9caafc2 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -1,24 +1,28 @@ import React, { useEffect, useState } from "react"; import Header from "./Header"; -import useModuleStore, { useSubModuleStore } from "../../../store/useModuleStore"; +import useModuleStore, { + useSubModuleStore, +} from "../../../store/useModuleStore"; import { - AnalysisIcon, - MechanicsIcon, - PropertiesIcon, - SimulationIcon, + AnalysisIcon, + FilePackageIcon, + MechanicsIcon, + PropertiesIcon, + SimulationIcon, } from "../../icons/SimulationIcons"; import { useToggleStore } from "../../../store/useUIToggleStore"; import Visualization from "./visualization/Visualization"; import Analysis from "./analysis/Analysis"; import Simulations from "./simulation/Simulations"; import useVersionHistoryVisibleStore, { - useSaveVersion, - useSelectedFloorItem, - useToolMode, + useDecalStore, + useSaveVersion, + useSelectedFloorItem, + useToolMode, } from "../../../store/builder/store"; import { - useSelectedEventData, - useSelectedEventSphere, + useSelectedEventData, + useSelectedEventSphere, } from "../../../store/simulation/useSimulationStore"; import { useBuilderStore } from "../../../store/builder/useBuilderStore"; import GlobalProperties from "./properties/GlobalProperties"; @@ -31,6 +35,8 @@ import WallProperties from "./properties/WallProperties"; import FloorProperties from "./properties/FloorProperties"; import SelectedWallProperties from "./properties/SelectedWallProperties"; import SelectedFloorProperties from "./properties/SelectedFloorProperties"; +import ResourceManagement from "./resourceManagement/ResourceManagement"; +import DecalProperties from "./properties/DecalProperties"; import ColliderProperties from "../../../modules/scene/physics/ui/ColliderProperties"; import { useSceneContext } from "../../../modules/scene/sceneContext"; @@ -49,6 +55,8 @@ type DisplayComponent = | "analysis" | "visualization" | "colliderProperties" + | "selectedDecalProperties" + | "resourceManagement" | "none"; const SideBarRight: React.FC = () => { @@ -62,36 +70,40 @@ const SideBarRight: React.FC = () => { const { selectedEventSphere } = useSelectedEventSphere(); const { viewVersionHistory, setVersionHistoryVisible } = useVersionHistoryVisibleStore(); const { isVersionSaved } = useSaveVersion(); + const { selectedSubCategory } = useDecalStore(); const { colliderStore } = useSceneContext(); const { selectedCollider } = colliderStore(); + const [displayComponent, setDisplayComponent] = + useState("none"); - const [displayComponent, setDisplayComponent] = useState("none"); + useEffect(() => { + if (activeModule !== "simulation") setSubModule("properties"); + if (activeModule === "simulation") setSubModule("simulations"); + }, [activeModule, setSubModule]); - useEffect(() => { - if (activeModule !== "simulation") setSubModule("properties"); - if (activeModule === "simulation") setSubModule("simulations"); - }, [activeModule, setSubModule]); + useEffect(() => { + if ( + activeModule !== "mechanics" && + selectedEventData && + selectedEventSphere + ) { + setSubModule("mechanics"); + } else if (!selectedEventData && !selectedEventSphere) { + if (activeModule === "simulation") { + setSubModule("simulations"); + } + } + if (activeModule !== "simulation") { + setSubModule("properties"); + } + }, [activeModule, selectedEventData, selectedEventSphere, setSubModule]); - useEffect(() => { - if (activeModule !== "mechanics" && selectedEventData && selectedEventSphere) { - setSubModule("mechanics"); - } else if (!selectedEventData && !selectedEventSphere) { - if (activeModule === "simulation") { - setSubModule("simulations"); - } - } - if (activeModule !== "simulation") { - setSubModule("properties"); - } - }, [activeModule, selectedEventData, selectedEventSphere, setSubModule]); - - useEffect(() => { - - if (activeModule === "visualization") { - setDisplayComponent("visualization"); - return; - } + useEffect(() => { + if (activeModule === "visualization") { + setDisplayComponent("visualization"); + return; + } if (!isVersionSaved && activeModule === "simulation") { if (subModule === "simulations") { @@ -105,168 +117,247 @@ const SideBarRight: React.FC = () => { if (subModule === "analysis") { setDisplayComponent("analysis"); return; - } - } + }if (subModule === "resourceManagement") { + setDisplayComponent("resourceManagement"); + return; + } + } - if (!isVersionSaved && activeModule === "builder") { + if (activeModule === "simulation" || activeModule === "builder") { + if (subModule === "resourceManagement") { + setDisplayComponent("resourceManagement"); + return; + } + }if (!isVersionSaved && activeModule === "builder") { if (selectedCollider) { setDisplayComponent("colliderProperties"); return; } } - if (subModule === "properties" && activeModule !== "visualization") { - if (selectedFloorItem) { - setDisplayComponent("assetProperties"); - return; - } - if (!selectedFloorItem && !selectedFloor && !selectedAisle && selectedWall) { - setDisplayComponent("selectedWallProperties"); - return; - } - if (!selectedFloorItem && !selectedWall && !selectedAisle && selectedFloor) { - setDisplayComponent("selectedFloorProperties"); - return; - } - if (viewVersionHistory) { - setDisplayComponent("versionHistory"); - return; - } - if (!selectedFloorItem && !selectedFloor && !selectedWall) { - if (toolMode === "Aisle") { - setDisplayComponent("aisleProperties"); - return; - } - if (toolMode === "Wall") { - setDisplayComponent("wallProperties"); - return; - } - if (toolMode === "Floor") { - setDisplayComponent("floorProperties"); - return; - } - setDisplayComponent("globalProperties"); - return; - } + if (subModule === "properties" && activeModule !== "visualization") { + if (selectedFloorItem) { + setDisplayComponent("assetProperties"); + return; + } + if ( + !selectedFloorItem && + !selectedFloor && + !selectedAisle && + selectedWall + ) { + setDisplayComponent("selectedWallProperties"); + return; + } + if ( + !selectedFloorItem && + !selectedWall && + !selectedAisle && + selectedFloor + ) { + setDisplayComponent("selectedFloorProperties"); + return; + } + if (viewVersionHistory) { + setDisplayComponent("versionHistory"); + return; + } + if (selectedSubCategory) { + setDisplayComponent("selectedDecalProperties"); + return; + } + if ( + !selectedFloorItem && + !selectedFloor && + !selectedWall && + !selectedSubCategory + ) { + if (toolMode === "Aisle") { + setDisplayComponent("aisleProperties"); + return; } - - if (subModule === "zoneProperties" && (activeModule === "builder" || activeModule === "simulation")) { - setDisplayComponent("zoneProperties"); - return; + if (toolMode === "Wall") { + setDisplayComponent("wallProperties"); + return; } + if (toolMode === "Floor") { + setDisplayComponent("floorProperties"); + return; + } + setDisplayComponent("globalProperties"); + return; + } + } - setDisplayComponent("none"); - }, [viewVersionHistory, activeModule, subModule, isVersionSaved, selectedFloorItem, selectedWall, selectedFloor, selectedAisle, toolMode, selectedCollider]); + if ( + subModule === "zoneProperties" && + (activeModule === "builder" || activeModule === "simulation") + ) { + setDisplayComponent("zoneProperties"); + return; + } - const renderComponent = () => { - switch (displayComponent) { - case "versionHistory": - return ; - case "globalProperties": - return ; - case "aisleProperties": - return ; - case "wallProperties": - return ; - case "floorProperties": - return ; - case "assetProperties": - return ; - case "selectedWallProperties": - return ; - case "selectedFloorProperties": - return ; - case "zoneProperties": - return ; - case "simulations": - return ; - case "mechanics": - return ; - case "analysis": - return ; - case "visualization": - return ; + setDisplayComponent("none"); + }, [ + viewVersionHistory, + activeModule, + subModule, + isVersionSaved, + selectedFloorItem, + selectedWall, + selectedFloor, + selectedAisle, + toolMode, + selectedSubCategory,selectedCollider + ]); + + const renderComponent = () => { + switch (displayComponent) { + case "versionHistory": + return ; + case "globalProperties": + return ; + case "aisleProperties": + return ; + case "wallProperties": + return ; + case "floorProperties": + return ; + case "assetProperties": + return ; + case "selectedWallProperties": + return ; + case "selectedFloorProperties": + return ; + case "zoneProperties": + return ; + case "simulations": + return ; + case "mechanics": + return ; + case "analysis": + return ; + case "visualization": + return ; + case "selectedDecalProperties": + return ; + case "resourceManagement": + return ; case "colliderProperties": return ; - default: - return null; - } - }; + default: + return null; + } + }; - return ( -
-
- {toggleUIRight && ( + return ( +
+
+ {toggleUIRight && ( + <> + {(!isVersionSaved || activeModule !== "simulation") && ( +
+ {activeModule !== "simulation" && ( <> - {(!isVersionSaved || activeModule !== "simulation") && ( -
- {activeModule !== "simulation" && ( - - )} - {activeModule === "simulation" && ( - <> - - - - - )} -
- )} - - {displayComponent !== "none" && ( -
-
- {renderComponent()} -
-
- )} + - )} -
- ); + )} + + {activeModule === "simulation" && ( + <> + + + + + )} + + {(activeModule === "builder" || + activeModule === "simulation") && ( + + )} +
+ )} + + {displayComponent !== "none" && ( +
+
+ {renderComponent()} + {/* */} +
+
+ )} + + )} +
+ ); }; -export default SideBarRight; \ No newline at end of file +export default SideBarRight; diff --git a/app/src/components/layout/sidebarRight/customInput/Vector3Input.tsx b/app/src/components/layout/sidebarRight/customInput/Vector3Input.tsx index 05fee8c..cc473c2 100644 --- a/app/src/components/layout/sidebarRight/customInput/Vector3Input.tsx +++ b/app/src/components/layout/sidebarRight/customInput/Vector3Input.tsx @@ -1,6 +1,4 @@ import React from "react"; -import { EyeDroperIcon } from "../../../icons/ExportCommonIcons"; -// import { useThree } from "@react-three/fiber"; interface PositionInputProps { onChange: (value: [number, number, number]) => void; // Callback for value change @@ -24,7 +22,7 @@ const Vector3Input: React.FC = ({ if (!value) return; const updatedValue = [...value] as [number, number, number]; updatedValue[index] = parseFloat(newValue) || 0; - // console.log('updatedValue: ', updatedValue); + console.log('updatedValue: ', updatedValue); onChange(updatedValue); }; @@ -42,8 +40,8 @@ const Vector3Input: React.FC = ({ handleChange(i, e.target.value)} + value={value?.[i] !== undefined ? value[i].toFixed(1) : ""} + onChange={(e) => handleChange(i, e.target.value)} placeholder={placeholder} disabled={disabled} /> diff --git a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx index 6607937..a62abda 100644 --- a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx @@ -4,130 +4,141 @@ import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; import { RemoveIcon } from "../../../icons/ExportCommonIcons"; import PositionInput from "../customInput/PositionInputs"; import RotationInput from "../customInput/RotationInput"; -import { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/builder/store"; +import { + useSelectedFloorItem, + useObjectPosition, + useObjectRotation, +} from "../../../../store/builder/store"; import { useSceneContext } from "../../../../modules/scene/sceneContext"; import { useBuilderStore } from "../../../../store/builder/useBuilderStore"; interface UserData { - id: number; - label: string; - value: string; + id: number; + label: string; + value: string; } const AssetProperties: React.FC = () => { - const [userData, setUserData] = useState([]); - const { selectedFloorItem } = useSelectedFloorItem(); - const { objectPosition } = useObjectPosition(); - const { objectRotation } = useObjectRotation(); - const { assetStore } = useSceneContext(); - const { assets, setCurrentAnimation } = assetStore(); - const { loopAnimation } = useBuilderStore(); - const [hoveredIndex, setHoveredIndex] = useState(null); + const [userData, setUserData] = useState([]); + const { selectedFloorItem } = useSelectedFloorItem(); + const { objectPosition } = useObjectPosition(); + const { objectRotation } = useObjectRotation(); + const { assetStore } = useSceneContext(); + const { assets, setCurrentAnimation } = assetStore(); + const { loopAnimation } = useBuilderStore(); + const [hoveredIndex, setHoveredIndex] = useState(null); - const handleAddUserData = () => { - }; + const handleAddUserData = () => { + setUserData([]); + }; - const handleUserDataChange = (id: number, newValue: string) => { - }; + const handleUserDataChange = (id: number, newValue: string) => {}; - const handleRemoveUserData = (id: number) => { - }; + const handleRemoveUserData = (id: number) => {}; - const handleAnimationClick = (animation: string) => { - if (selectedFloorItem) { - setCurrentAnimation(selectedFloorItem.uuid, animation, true, loopAnimation, true); - } + const handleAnimationClick = (animation: string) => { + if (selectedFloorItem) { + setCurrentAnimation( + selectedFloorItem.uuid, + animation, + true, + loopAnimation, + true + ); } + }; - if (!selectedFloorItem) return null; + if (!selectedFloorItem) return null; - return ( -
- {/* Name */} -
{selectedFloorItem.userData.modelName}
-
- {objectPosition && - { }} - value1={parseFloat(objectPosition.x.toFixed(5))} - value2={parseFloat(objectPosition.z.toFixed(5))} - /> - } - {objectRotation && - { }} - value={parseFloat(objectRotation.y.toFixed(5))} - /> - } -
+ return ( +
+ {/* Name */} +
{selectedFloorItem.userData.modelName}
+
+ {objectPosition && ( + {}} + value1={parseFloat(objectPosition.x.toFixed(5))} + value2={parseFloat(objectPosition.z.toFixed(5))} + /> + )} + {objectRotation && ( + {}} + value={parseFloat(objectRotation.y.toFixed(5))} + /> + )} +
-
-
Render settings
- - -
+
Render settings
+
+ + +
-
-
User Data
- {userData.map((data) => ( -
- handleUserDataChange(data.id, newValue)} - /> -
handleRemoveUserData(data.id)} - > - -
-
- ))} - - {/* Add new user data */} -
- + Add -
-
-
- {selectedFloorItem.uuid &&
Animations
} - {assets.map((asset) => ( -
- {asset.modelUuid === selectedFloorItem.uuid && - asset.animations && - asset.animations.length > 0 && - asset.animations.map((animation, index) => ( -
-
handleAnimationClick(animation)} - onMouseEnter={() => setHoveredIndex(index)} - onMouseLeave={() => setHoveredIndex(null)} - style={{ - height: "20px", - width: "100%", - borderRadius: "5px", - background: - hoveredIndex === index - ? "#7b4cd3" - : "transparent", - }} - > - {animation.charAt(0).toUpperCase() + - animation.slice(1).toLowerCase()} -
-
- ))} -
- ))} +
+
User Data
+ {userData.map((data, i) => ( +
+ handleUserDataChange(data.id, newValue)} + /> +
handleRemoveUserData(data.id)} + > +
+
+ ))} + + {/* Add new user data */} +
+ + Add
- ); +
+
Animations
+
+ {assets.map((asset, i) => { + if (asset.modelUuid !== selectedFloorItem.uuid || !asset.animations) + return ( + i === 0 && ( +
+ Looks like there are no preset animations yet. Stay tuned for + future additions! +
+ ) + ); + + return asset.animations.map((animation, index) => ( +
+
handleAnimationClick(animation)} + onMouseEnter={() => setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + className="animations-list" + style={{ + background: + hoveredIndex === index + ? "var(--background-color-button)" + : "var(--background-color)", + color: + hoveredIndex === index ? "var(--text-button-color)" : "", + }} + > + {animation.charAt(0).toUpperCase() + + animation.slice(1).toLowerCase()} +
+
+ )); + })} +
+
+ ); }; export default AssetProperties; diff --git a/app/src/components/layout/sidebarRight/properties/DecalProperties.tsx b/app/src/components/layout/sidebarRight/properties/DecalProperties.tsx new file mode 100644 index 0000000..4163ea5 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/DecalProperties.tsx @@ -0,0 +1,53 @@ +import { + LayeringBottomIcon, + LayeringTopIcon, +} from "../../../icons/ExportCommonIcons"; +import InputRange from "../../../ui/inputs/InputRange"; +import RotationInput from "../customInput/RotationInput"; +import Vector3Input from "../customInput/Vector3Input"; + +const DecalProperties = () => { + return ( +
+
Decal Propertis
+
+ {}} + value={10} + /> + console.log(value)} + header="Scale" + value={[0, 0, 0] as [number, number, number]} + /> +
+ +
+ console.log(value)} + key={"6"} + /> + +
+
Layering
+ +
+ + +
+
+
+
+ ); +}; + +export default DecalProperties; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/actions/AssemblyAction.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/ManufactureAction.tsx similarity index 74% rename from app/src/components/layout/sidebarRight/properties/eventProperties/actions/AssemblyAction.tsx rename to app/src/components/layout/sidebarRight/properties/eventProperties/actions/ManufactureAction.tsx index e371f6f..5d66ceb 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/actions/AssemblyAction.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/ManufactureAction.tsx @@ -3,7 +3,7 @@ import InputRange from "../../../../../ui/inputs/InputRange"; import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown"; import SwapAction from "./SwapAction"; -interface AssemblyActionProps { +interface ManufactureActionProps { processTime: { value: number; min: number; @@ -11,7 +11,7 @@ interface AssemblyActionProps { disabled?: boolean, onChange: (value: number) => void; }; - assemblyCount: { + manufactureCount: { value: number; min: number; max: number; @@ -26,9 +26,9 @@ interface AssemblyActionProps { clearPoints: () => void; } -const AssemblyAction: React.FC = ({ +const ManufactureAction: React.FC = ({ processTime, - assemblyCount, + manufactureCount, swapOptions, swapDefaultOption, onSwapSelect, @@ -46,18 +46,18 @@ const AssemblyAction: React.FC = ({ onChange={processTime.onChange} /> - {assemblyCount && ( + {manufactureCount && ( { }} - onChange={(value) => assemblyCount.onChange(parseInt(value))} + onChange={(value) => manufactureCount.onChange(parseInt(value))} /> )} = ({ ); }; -export default AssemblyAction; +export default ManufactureAction; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/actions/StorageAction.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/StorageAction.tsx index 4d280d2..4046d89 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/actions/StorageAction.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/StorageAction.tsx @@ -3,53 +3,51 @@ import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown"; import LabledDropdown from "../../../../../ui/inputs/LabledDropdown"; interface StorageActionProps { - type: "store" | "spawn" | "default"; - value: string; + maxCapacity: string; + spawnedCount: string; min: number; max?: number; - defaultValue: string; + maxCapacityDefault: string; + spawnedCountCefault: string; currentMaterialType: string; handleCapacityChange: (value: string) => void; + handleSpawnCountChange: (value: string) => void; handleMaterialTypeChange: (value: string) => void; } -const StorageAction: React.FC = ({ type, value, min, max, defaultValue, currentMaterialType, handleCapacityChange, handleMaterialTypeChange }) => { +const StorageAction: React.FC = ({ maxCapacity, spawnedCount, min, max, maxCapacityDefault, spawnedCountCefault, currentMaterialType, handleCapacityChange, handleSpawnCountChange, handleMaterialTypeChange }) => { return ( <> - {type === 'store' && - { }} - onChange={handleCapacityChange} - /> - } - {type === 'spawn' && - <> - { }} - onChange={handleCapacityChange} - /> - - - } + { }} + onChange={handleCapacityChange} + /> + { }} + onChange={handleSpawnCountChange} + /> + ); }; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx index 990d046..20ce08a 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx @@ -125,26 +125,6 @@ function ConveyorMechanics() { } }; - const handleRenameAction = (newName: string) => { - if (!selectedPointData) return; - - setActionName(newName); - const event = updateAction( - selectedProduct.productUuid, - selectedPointData.action.actionUuid, - { actionName: newName } - ); - - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productUuid, - projectId || '', - event - ); - } - }; - const handleSpawnCountChange = (value: string) => { if (!selectedPointData) return; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx index f5d1229..3df0401 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx @@ -17,7 +17,7 @@ function CraneMechanics() { const { selectedEventData } = useSelectedEventData(); const { productStore } = useSceneContext(); - const { getPointByUuid, updateAction, addAction, removeAction } = productStore(); + const { getPointByUuid, addAction, removeAction } = productStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { selectedAction, setSelectedAction, clearSelectedAction } = useSelectedAction(); @@ -62,36 +62,6 @@ function CraneMechanics() { }); }; - const handleRenameAction = (newName: string) => { - if (!selectedAction.actionId || !selectedPointData) return; - - const event = updateAction( - selectedProduct.productUuid, - selectedAction.actionId, - { actionName: newName } - ); - - const updatedActions = selectedPointData.actions.map(action => - action.actionUuid === selectedAction.actionId - ? { ...action, actionName: newName } - : action - ); - - setSelectedPointData({ - ...selectedPointData, - actions: updatedActions, - }); - - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productUuid, - projectId || '', - event - ); - } - }; - const handleAddAction = () => { if (!selectedEventData || !selectedPointData) return; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx index 4b29f8c..99c1a0c 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx @@ -7,7 +7,7 @@ import LabledDropdown from "../../../../../ui/inputs/LabledDropdown"; import Trigger from "../trigger/Trigger"; import ActionsList from "../components/ActionsList"; import WorkerAction from "../actions/WorkerAction"; -import AssemblyAction from "../actions/AssemblyAction"; +import ManufactureAction from "../actions/ManufactureAction"; import { useSelectedEventData, useSelectedAction } from "../../../../../../store/simulation/useSimulationStore"; import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi"; @@ -17,10 +17,10 @@ import { useSceneContext } from "../../../../../../modules/scene/sceneContext"; import { useParams } from "react-router-dom"; function HumanMechanics() { - const [activeOption, setActiveOption] = useState<"worker" | "assembly" | "operator">("worker"); + const [activeOption, setActiveOption] = useState<"worker" | "manufacturer" | "operator" | "assembler">("worker"); const [speed, setSpeed] = useState("0.5"); const [loadCount, setLoadCount] = useState(0); - const [assemblyCount, setAssemblyCount] = useState(0); + const [manufactureCount, setManufactureCount] = useState(0); const [loadCapacity, setLoadCapacity] = useState("1"); const [processTime, setProcessTime] = useState(10); const [swappedMaterial, setSwappedMaterial] = useState("Default material"); @@ -47,6 +47,7 @@ function HumanMechanics() { if (point?.actions?.length) { setSelectedPointData(point); const firstAction = point.actions[0]; + setSelectedAction(firstAction.actionUuid, firstAction.actionName); setCurrentAction(firstAction); setSpeed(( getEventByModelUuid( @@ -57,7 +58,7 @@ function HumanMechanics() { setLoadCapacity(firstAction.loadCapacity.toString()); setActiveOption(firstAction.actionType); setLoadCount(firstAction.loadCount || 0); - setAssemblyCount(firstAction.assemblyCount || 0); + setManufactureCount(firstAction.manufactureCount || 0); setProcessTime(firstAction.processTime || 10); setSwappedMaterial(firstAction.swapMaterial || "Default material"); } @@ -78,7 +79,7 @@ function HumanMechanics() { const newCurrentAction = getActionByUuid(selectedProduct.productUuid, actionUuid); - if (newCurrentAction && (newCurrentAction.actionType === 'assembly' || newCurrentAction?.actionType === 'worker' || newCurrentAction?.actionType === "operator")) { + if (newCurrentAction && (newCurrentAction.actionType === 'manufacturer' || newCurrentAction?.actionType === 'worker' || newCurrentAction?.actionType === "operator")) { if (!selectedAction.actionId) { setSelectedAction(newCurrentAction.actionUuid, newCurrentAction.actionName); } @@ -86,9 +87,9 @@ function HumanMechanics() { setActiveOption(newCurrentAction.actionType); setLoadCapacity(newCurrentAction.loadCapacity.toString()); setLoadCount(newCurrentAction.loadCount || 0); - setAssemblyCount(newCurrentAction.assemblyCount || 0); + setManufactureCount(newCurrentAction.manufactureCount || 0); - if (newCurrentAction.actionType === 'assembly') { + if (newCurrentAction.actionType === 'manufacturer') { setProcessTime(newCurrentAction.processTime || 10); setSwappedMaterial(newCurrentAction.swapMaterial || "Default material"); } @@ -117,7 +118,7 @@ function HumanMechanics() { const handleSelectActionType = (actionType: string) => { if (!selectedAction.actionId || !currentAction || !selectedPointData) return; - const updatedAction = { ...currentAction, actionType: actionType as "worker" | "assembly" | "operator" }; + const updatedAction = { ...currentAction, actionType: actionType as "worker" | "manufacturer" | "operator" | "assembler" }; const updatedActions = selectedPointData.actions.map(action => action.actionUuid === updatedAction.actionUuid ? updatedAction : action); const updatedPoint = { ...selectedPointData, actions: updatedActions }; @@ -142,7 +143,6 @@ function HumanMechanics() { if (isNaN(numericValue)) return; const updatedEvent = { - ...selectedEventData.data, speed: numericValue } as HumanEventSchema; @@ -203,10 +203,10 @@ function HumanMechanics() { setLoadCount(value); }; - const handleAssemblyCountChange = (value: number) => { + const handleManufactureCountChange = (value: number) => { if (!currentAction || !selectedPointData || !selectedAction.actionId) return; - const updatedAction = { ...currentAction, assemblyCount: value }; + const updatedAction = { ...currentAction, manufactureCount: value }; const updatedActions = selectedPointData.actions.map(action => action.actionUuid === updatedAction.actionUuid ? updatedAction : action); const updatedPoint = { ...selectedPointData, actions: updatedActions }; @@ -222,7 +222,7 @@ function HumanMechanics() { setCurrentAction(updatedAction); setSelectedPointData(updatedPoint); - setAssemblyCount(value); + setManufactureCount(value); }; const handleProcessTimeChange = (value: number) => { @@ -274,8 +274,8 @@ function HumanMechanics() { const updatedAction: HumanAction = JSON.parse(JSON.stringify(currentAction)); - if (updatedAction.actionType === 'assembly') { - updatedAction.assemblyPoint = { position: null, rotation: null, } + if (updatedAction.actionType === 'manufacturer') { + updatedAction.manufacturePoint = { position: null, rotation: null, } } else { updatedAction.pickUpPoint = { position: null, rotation: null, }; updatedAction.dropPoint = { position: null, rotation: null, } @@ -307,6 +307,11 @@ function HumanMechanics() { actionType: "worker", loadCount: 1, assemblyCount: 1, + assemblyCondition: { + conditionType: 'material', + materialType: "Default material" + }, + manufactureCount: 1, loadCapacity: 1, processTime: 10, triggers: [], @@ -397,7 +402,7 @@ function HumanMechanics() { @@ -425,22 +430,22 @@ function HumanMechanics() { clearPoints={handleClearPoints} /> } - {currentAction.actionType === 'assembly' && - { - if (selectedEventData) { + if (selectedEventData && selectedEventData.data.type === 'roboticArm') { const point = getPointByUuid( selectedProduct.productUuid, selectedEventData.data.modelUuid, @@ -71,36 +71,6 @@ function RoboticArmMechanics() { }); }; - const handleRenameAction = (newName: string) => { - if (!selectedAction.actionId || !selectedPointData) return; - - const event = updateAction( - selectedProduct.productUuid, - selectedAction.actionId, - { actionName: newName } - ); - - const updatedActions = selectedPointData.actions.map(action => - action.actionUuid === selectedAction.actionId - ? { ...action, actionName: newName } - : action - ); - - setSelectedPointData({ - ...selectedPointData, - actions: updatedActions, - }); - - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productUuid, - projectId || '', - event - ); - } - }; - const handleSpeedChange = (value: string) => { if (!selectedEventData) return; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx index 796dbc5..bf264fb 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; +import { MathUtils } from "three"; import RenameInput from "../../../../../ui/inputs/RenameInput"; import LabledDropdown from "../../../../../ui/inputs/LabledDropdown"; import Trigger from "../trigger/Trigger"; @@ -6,58 +7,78 @@ import StorageAction from "../actions/StorageAction"; import ActionsList from "../components/ActionsList"; import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi"; import { useSelectedAction, useSelectedEventData } from "../../../../../../store/simulation/useSimulationStore"; -import * as THREE from 'three'; import { useProductContext } from "../../../../../../modules/simulation/products/productContext"; import { useParams } from "react-router-dom"; import { useVersionContext } from "../../../../../../modules/builder/version/versionContext"; import { useSceneContext } from "../../../../../../modules/scene/sceneContext"; function StorageMechanics() { - const [activeOption, setActiveOption] = useState<"default" | "store" | "spawn">("default"); + const [activeOption, setActiveOption] = useState<"store" | "spawn">("store"); + const [currentCapacity, setCurrentCapacity] = useState("1"); + const [spawnedCount, setSpawnedCount] = useState("0"); + const [spawnedMaterial, setSpawnedMaterial] = useState("Default material"); const [selectedPointData, setSelectedPointData] = useState(); const { selectedEventData } = useSelectedEventData(); const { productStore } = useSceneContext(); - const { getPointByUuid, updateAction } = productStore(); + const { getPointByUuid, updateAction, updateEvent, getEventByModelUuid, getActionByUuid, addAction, removeAction } = productStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); - const { setSelectedAction, clearSelectedAction } = useSelectedAction(); + const { selectedAction, setSelectedAction, clearSelectedAction } = useSelectedAction(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { projectId } = useParams(); - const updateSelectedPointData = () => { - if (selectedEventData && selectedProduct) { - const point = getPointByUuid( - selectedProduct.productUuid, - selectedEventData?.data.modelUuid, - selectedEventData?.selectedPoint - ) as StoragePointSchema | undefined; - if (point && "action" in point) { - setSelectedPointData(point); - const uiOption = point.action.actionType === "retrieve" ? "spawn" : point.action.actionType; - setActiveOption(uiOption as "store" | "spawn"); - setSelectedAction(point.action.actionUuid, point.action.actionName); - } - } - }; - useEffect(() => { - if (selectedEventData) { + if (selectedEventData && selectedEventData.data.type === "storageUnit") { const point = getPointByUuid( selectedProduct.productUuid, - selectedEventData?.data.modelUuid, - selectedEventData?.selectedPoint + selectedEventData.data.modelUuid, + selectedEventData.selectedPoint ) as StoragePointSchema | undefined; - if (point && "action" in point) { + + if (point?.actions?.length) { setSelectedPointData(point); - const uiOption = point.action.actionType === "retrieve" ? "spawn" : point.action.actionType; - setActiveOption(uiOption as "store" | "spawn"); - setSelectedAction(point.action.actionUuid, point.action.actionName); + const firstAction = point.actions[0]; + + const eventData = getEventByModelUuid( + selectedProduct.productUuid, + selectedEventData.data.modelUuid + ) as StorageEventSchema | undefined; + + setCurrentCapacity(eventData?.storageCapacity?.toString() || "1"); + setSpawnedCount(eventData?.storageCount?.toString() || "0"); + setSpawnedMaterial(eventData?.materialType?.toString() || "Default material"); + setSelectedAction(firstAction.actionUuid, firstAction.actionName); + setActiveOption(firstAction.actionType === "retrieve" ? "spawn" : "store"); } } else { clearSelectedAction(); } - }, [selectedProduct, selectedEventData]); + }, [selectedEventData, selectedProduct]); + + useEffect(() => { + if (selectedEventData && selectedEventData.data.type === "storageUnit" && selectedAction.actionId) { + const point = getPointByUuid( + selectedProduct.productUuid, + selectedEventData.data.modelUuid, + selectedEventData.selectedPoint + ) as StoragePointSchema | undefined; + + const actionUuid = selectedAction.actionId || point?.actions[0].actionUuid || ''; + + const newCurrentAction = getActionByUuid(selectedProduct.productUuid, actionUuid); + + if (newCurrentAction && (newCurrentAction.actionType === 'store' || newCurrentAction.actionType === 'retrieve')) { + if (!selectedAction.actionId) { + setSelectedAction(newCurrentAction.actionUuid, newCurrentAction.actionName); + } + const uiOption = newCurrentAction.actionType === "retrieve" ? "spawn" : "store"; + setActiveOption(uiOption); + } else { + clearSelectedAction(); + } + } + }, [selectedAction, selectedProduct, selectedEventData]); const updateBackend = ( productName: string, @@ -75,48 +96,63 @@ function StorageMechanics() { } const handleActionTypeChange = (option: string) => { - if (!selectedEventData || !selectedPointData) return; - const internalOption = actionTypeMap[option as keyof typeof actionTypeMap] as "store" | "retrieve"; + if (!selectedAction.actionId || !selectedPointData) return; + const internalOption = option === "spawn" ? "retrieve" : "store"; + + const updatedAction = { + actionType: internalOption as "store" | "retrieve" + }; + + const updatedActions = selectedPointData.actions.map(action => + action.actionUuid === selectedAction.actionId ? { + ...action, + actionType: updatedAction.actionType + } : action + ); + + const updatedPoint = { ...selectedPointData, actions: updatedActions }; + + const event = updateAction( + selectedProduct.productUuid, + selectedAction.actionId, + updatedAction + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + } + + setSelectedPointData(updatedPoint); setActiveOption(option as "store" | "spawn"); - - const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, { - actionType: internalOption, - }); - - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productUuid, - projectId || '', - event - ); - updateSelectedPointData(); - } - }; - - const handleRenameAction = (newName: string) => { - if (!selectedPointData) return; - const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, { actionName: newName }); - - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productUuid, - projectId || '', - event - ); - updateSelectedPointData(); - } }; const handleCapacityChange = (value: string) => { - if (!selectedEventData || !selectedPointData) return; - const newCapacity = parseInt(value); + if (!selectedEventData) return; - const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, { - storageCapacity: newCapacity, - }); + const numericValue = parseInt(value); + if (isNaN(numericValue)) return; + + const updatedEvent = { + storageCapacity: numericValue + } as StorageEventSchema; + + const currentCount = parseInt(spawnedCount); + if (currentCount > numericValue) { + updatedEvent.storageCount = numericValue; + setSpawnedCount(numericValue.toString()); + } + + const event = updateEvent( + selectedProduct.productUuid, + selectedEventData.data.modelUuid, + updatedEvent + ); if (event) { updateBackend( @@ -125,25 +161,54 @@ function StorageMechanics() { projectId || '', event ); - updateSelectedPointData(); } + + setCurrentCapacity(value); }; - const createNewMaterial = (materialType: string): { materialType: string; materialId: string } | null => { - if (!selectedEventData || !selectedPointData) return null; - const materialId = THREE.MathUtils.generateUUID(); - return { - materialType, - materialId - }; + const handleSpawnCountChange = (value: string) => { + if (!selectedEventData) return; + + const numericValue = parseInt(value); + if (isNaN(numericValue)) return; + + const maxCapacity = parseInt(currentCapacity); + if (numericValue > maxCapacity) return; + + const updatedEvent = { + storageCount: numericValue + } as StorageEventSchema; + + const event = updateEvent( + selectedProduct.productUuid, + selectedEventData.data.modelUuid, + updatedEvent + ); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productUuid, + projectId || '', + event + ); + } + + setSpawnedCount(value); }; const handleMaterialTypeChange = (value: string) => { - if (!selectedEventData || !selectedPointData) return; + if (!selectedEventData) return; - const event = updateAction(selectedProduct.productUuid, selectedPointData.action.actionUuid, { - materialType: value, - }); + const updatedEvent = { + materialType: value + } as StorageEventSchema; + + const event = updateEvent( + selectedProduct.productUuid, + selectedEventData.data.modelUuid, + updatedEvent + ); if (event) { updateBackend( @@ -152,70 +217,114 @@ function StorageMechanics() { projectId || '', event ); - updateSelectedPointData(); } + + setSpawnedMaterial(value); }; - const currentActionName = useMemo(() => - selectedPointData ? selectedPointData.action.actionName : "Action Name", - [selectedPointData] - ); + const handleAddAction = () => { + if (!selectedEventData || !selectedPointData) return; - const currentCapacity = useMemo(() => - selectedPointData ? selectedPointData.action.storageCapacity.toString() : "0", - [selectedPointData] - ); + const newAction: StorageAction = { + actionUuid: MathUtils.generateUUID(), + actionName: `Action ${selectedPointData.actions.length + 1}`, + actionType: "store", + triggers: [], + }; - const currentMaterialType = useMemo(() => - selectedPointData?.action.materialType || "Default material", - [selectedPointData] - ); + const updatedActions = [...(selectedPointData.actions || []), newAction]; + const updatedPoint = { ...selectedPointData, actions: updatedActions }; - const availableActions = { - defaultOption: "store", - options: ["store", "spawn"], + const event = addAction( + selectedProduct.productUuid, + selectedEventData.data.modelUuid, + selectedEventData.selectedPoint, + newAction + ); + + if (event) { + updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event); + } + + setSelectedPointData(updatedPoint); + setSelectedAction(newAction.actionUuid, newAction.actionName); }; - const actionTypeMap = { - spawn: "retrieve", - store: "store" + const handleDeleteAction = (actionUuid: string) => { + if (!selectedPointData || !actionUuid) return; + + const updatedActions = selectedPointData.actions.filter(action => action.actionUuid !== actionUuid); + const updatedPoint = { ...selectedPointData, actions: updatedActions }; + + const event = removeAction( + selectedProduct.productUuid, + actionUuid + ); + + if (event) { + updateBackend(selectedProduct.productName, selectedProduct.productUuid, projectId || '', event); + } + + setSelectedPointData(updatedPoint); + + const index = selectedPointData.actions.findIndex((a) => a.actionUuid === selectedAction.actionId); + const nextAction = updatedPoint.actions[index] || updatedPoint.actions[index - 1]; + if (nextAction) { + setSelectedAction(nextAction.actionUuid, nextAction.actionName); + } else { + clearSelectedAction(); + } }; return ( <> - {selectedEventData && ( -
- -
-
- -
-
- - -
-
-
- -
-
+ {selectedEventData && selectedEventData.data.type === "storageUnit" && ( + <> +
+ +
+
+ + + {selectedAction.actionId && ( +
+
+ +
+
+ +
+
+ +
+
+ )} +
+ )} ); diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx index fada871..181252b 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx @@ -119,26 +119,6 @@ function VehicleMechanics() { } }; - const handleRenameAction = (newName: string) => { - if (!selectedPointData) return; - - setActionName(newName); - const event = updateAction( - selectedProduct.productUuid, - selectedPointData.action.actionUuid, - { actionName: newName } - ); - - if (event) { - updateBackend( - selectedProduct.productName, - selectedProduct.productUuid, - projectId || '', - event - ); - } - }; - const handleLoadCapacityChange = (value: string) => { if (!selectedPointData) return; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx index 0697c7b..23c060c 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx @@ -13,7 +13,7 @@ import { useSceneContext } from "../../../../../../modules/scene/sceneContext"; type TriggerProps = { selectedPointData?: PointsScheme | undefined; - type?: "Conveyor" | "Vehicle" | "RoboticArm" | "Machine" | "StorageUnit" | "Human"; + type?: "Conveyor" | "Vehicle" | "RoboticArm" | "Machine" | "StorageUnit" | "Human" | "Crane"; }; const Trigger = ({ selectedPointData, type }: TriggerProps) => { @@ -36,9 +36,9 @@ const Trigger = ({ selectedPointData, type }: TriggerProps) => { let actionUuid: string | undefined; - if (type === "Conveyor" || type === "Vehicle" || type === "Machine" || type === "StorageUnit") { - actionUuid = (selectedPointData as | ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema).action?.actionUuid; - } else if ((type === "RoboticArm" || type === "Human") && selectedAction.actionId) { + if (type === "Conveyor" || type === "Vehicle" || type === "Machine") { + actionUuid = (selectedPointData as | ConveyorPointSchema | VehiclePointSchema | MachinePointSchema).action?.actionUuid; + } else if ((type === "RoboticArm" || type === "Human" || type === "StorageUnit" || type === 'Crane') && selectedAction.actionId) { actionUuid = selectedAction.actionId; } diff --git a/app/src/components/layout/sidebarRight/resourceManagement/NavigateCatagory.tsx b/app/src/components/layout/sidebarRight/resourceManagement/NavigateCatagory.tsx new file mode 100644 index 0000000..dca4399 --- /dev/null +++ b/app/src/components/layout/sidebarRight/resourceManagement/NavigateCatagory.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +interface Props { + category: string[]; + selectedCategory: string; + setSelectedCategory: (cat: string) => void; +} + +const NavigateCategory = ({ category, selectedCategory, setSelectedCategory }: Props) => { + return ( +
+ {category.map((cat) => ( +
setSelectedCategory(cat)} + > + {cat} +
+ ))} +
+ ); +}; + +export default NavigateCategory; diff --git a/app/src/components/layout/sidebarRight/resourceManagement/ResourceManagement.tsx b/app/src/components/layout/sidebarRight/resourceManagement/ResourceManagement.tsx new file mode 100644 index 0000000..e5c0442 --- /dev/null +++ b/app/src/components/layout/sidebarRight/resourceManagement/ResourceManagement.tsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react' + +import Search from '../../../ui/inputs/Search' +import RegularDropDown from '../../../ui/inputs/RegularDropDown' +import Hrm from './hrm/Hrm' +import AssetManagement from './hrm/assetManagement/AssetManagement' + +const ResourceManagement = () => { + type DisplayType = "hrm" | "asset"; + const [selectType, setSelectType] = useState("assetManagement") + + const [display, setDisplay] = useState("asset"); + + return ( +
+
+
setSelectType("assetManagement")} + > + Asset Management +
+
setSelectType("peopleOperation")} + > + People Operations +
+
+ +
+ { }} /> +
+ + {selectType === "assetManagement" ? : } +
+ ) +} + +export default ResourceManagement diff --git a/app/src/components/layout/sidebarRight/resourceManagement/hrm/Hrm.tsx b/app/src/components/layout/sidebarRight/resourceManagement/hrm/Hrm.tsx new file mode 100644 index 0000000..99828ae --- /dev/null +++ b/app/src/components/layout/sidebarRight/resourceManagement/hrm/Hrm.tsx @@ -0,0 +1,280 @@ +import { useEffect, useState } from 'react' +import { ClockThreeIcon, LocationPinIcon, TargetIcon } from '../../../../icons/ExportCommonIcons' +import { useSceneContext } from '../../../../../modules/scene/sceneContext'; +import { useProductContext } from '../../../../../modules/simulation/products/productContext'; +import RenameInput from '../../../../ui/inputs/RenameInput'; +import { useResourceManagementId } from '../../../../../store/builder/store'; +import { set } from 'immer/dist/internal'; +// import NavigateCatagory from '../NavigateCatagory' + +const Hrm = () => { + const [selectedCard, setSelectedCard] = useState(0); + const [workers, setWorkers] = useState([]); + + const { productStore } = useSceneContext(); + const { products, getProductById } = productStore(); + 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: "", + name: worker.modelName, + modelId: worker.modelUuid, + employee_id: `HR-${204 + index}`, + status: "Active", + }, + task: { + status: "Ongoing", + title: worker.taskTitle || "No Task Assigned", + location: { + floor: worker.floor || 0, + zone: worker.zone || "N/A" + }, + planned_time_hours: worker.plannedTime || 0, + time_spent_hours: worker.timeSpent || 0, + total_tasks: worker.totalTasks || 0, + completed_tasks: worker.completedTasks || 0 + }, + actions: [ + "Assign Task", + "Reassign Task", + "Pause", + "Emergency Stop" + ], + location: `Floor ${worker.floor || "-"} . Zone ${worker.zone || "-"}` + })); + + setWorkers(formattedWorkers); + } + }, [selectedProduct, getProductById]); + + useEffect(() => { + // + }, [workers]); + + + + + // const employee_details = [ + // { + // "employee": { + // image: "", + // "name": "John Doe", + // "employee_id": "HR-204", + // "status": "Active", + + // }, + // "task": { + // "status": "Ongoing", + // "title": "Inspecting Machine X", + // "location": { + // "floor": 4, + // "zone": "B" + // }, + // "planned_time_hours": 6, + // "time_spent_hours": 2, + // "total_tasks": 12, + // "completed_tasks": 3 + // }, + // "actions": [ + // "Assign Task", + // "Reassign Task", + // "Pause", + // "Emergency Stop" + // ], + // "location": "Floor 4 . Zone B" + // }, + // { + // "employee": { + // image: "", + // "name": "Alice Smith", + // "employee_id": "HR-205", + // "status": "Active", + + // }, + // "task": { + // "status": "Ongoing", + // "title": "Calibrating Sensor Y", + // "location": { + // "floor": 2, + // "zone": "A" + // }, + // "planned_time_hours": 4, + // "time_spent_hours": 1.5, + // "total_tasks": 10, + // "completed_tasks": 2 + // }, + // "actions": [ + // "Assign Task", + // "Reassign Task", + // "Pause", + // "Emergency Stop" + // ], + // "location": "Floor 4 . Zone B" + // }, + // { + // "employee": { + // image: "", + // "name": "Michael Lee", + // "employee_id": "HR-206", + // "status": "Active", + + // }, + // "task": { + // "status": "Ongoing", + // "title": "Testing Conveyor Belt Z", + // "location": { + // "floor": 5, + // "zone": "C" + // }, + // "planned_time_hours": 5, + // "time_spent_hours": 3, + // "total_tasks": 8, + // "completed_tasks": 5 + // }, + // "actions": [ + // "Assign Task", + // "Reassign Task", + // "Pause", + // "Emergency Stop" + // ], + // "location": "Floor 4 . Zone B" + // }, + // ] + function handleRenameWorker(newName: string) { + // + + } + function handleHumanClick(employee: any) { + if (employee.modelId) { + setResourceManagementId(employee.modelId); + } + } + + + return ( + <> + {/* */} + +
+ {workers.map((employee, index) => ( +
setSelectedCard(index)} + key={index} + > +
+
+
+ +
+
+
+ {/*
{employee.employee.name}
*/} + +
{employee.employee.employee_id}
+
+
+ +
{ handleHumanClick(employee.employee) }}>View in Scene
+
+ +
+ {/*
+
+
+ + Ongoing Task: +
+
{employee.task.title}
+
+ + +
+
+ + Location: +
+
+ Floor {employee.task.location.floor}. Zone {employee.task.location.zone} +
+
+
*/} + +
+
+ +
+ + Planned time: +
+ + {employee.task.planned_time_hours} hr +
+ {/*
+ +
+ + Total Tasks: +
+ + {employee.task.total_tasks} +
+
+ +
+ + Time Spent: +
+ + {employee.task.time_spent_hours} hr +
*/} +
+ +
+ + Cost per hr: +
+ + {employee.task.completed_tasks} +
+
+ +
+
+
+ +
+
Location:
+
+
{employee.location}
+
+
+ {/* + */} + + +
+
+ +
+ ))} +
+ + ) +} + +export default Hrm diff --git a/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx b/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx new file mode 100644 index 0000000..faa6015 --- /dev/null +++ b/app/src/components/layout/sidebarRight/resourceManagement/hrm/assetManagement/AssetManagement.tsx @@ -0,0 +1,231 @@ +import { useEffect, useState } from 'react' +// import NavigateCatagory from '../../NavigateCatagory' +import { EyeIcon, ForkLiftIcon, KebabIcon, LocationPinIcon, RightHalfFillCircleIcon } from '../../../../../icons/ExportCommonIcons'; +import assetImage from "../../../../../../assets/image/asset-image.png" +import { useSceneContext } from '../../../../../../modules/scene/sceneContext'; +import { useProductContext } from '../../../../../../modules/simulation/products/productContext'; +import RenameInput from '../../../../../ui/inputs/RenameInput'; +import { useResourceManagementId } from '../../../../../../store/builder/store'; +const AssetManagement = () => { + // const [selectedCategory, setSelectedCategory] = useState("All Assets"); + const [expandedAssetId, setExpandedAssetId] = useState(null); + const [assets, setAssets] = useState([]); + + const { productStore } = useSceneContext(); + const { products, getProductById } = productStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); + const { setResourceManagementId } = useResourceManagementId(); + + + + useEffect(() => { + if (selectedProduct) { + const productDetails = getProductById(selectedProduct.productUuid); + const productAssets = productDetails?.eventDatas || []; + const grouped: Record = {}; + 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, + description: asset.description || "No description", + cost: asset.cost || 0, + count: 1, + }; + } else { + grouped[asset.modelName].count += 1; + } + }); + + setAssets(Object.values(grouped)); + } + }, [selectedProduct]); + + function handleRenameAsset(newName: string) { + // + // if (expandedAssetId) { + // setAssets(prevAssets => + // prevAssets.map(asset => + // asset.id === expandedAssetId ? { ...asset, name: newName } : asset + // ) + // ); + // } + } + + useEffect(() => { + + + }, [assets]); + + function handleAssetClick(id: string) { + + + setResourceManagementId(id); + } + + // const dummyAssets = [ + // { + // id: '1', + // name: 'Forklift Model X200', + // model: 'FLK-0025', + // status: 'Online', + // usageRate: 15, + // level: 'Level 1', + // image: assetImage, + // description: 'Electric forklift used for moving goods and materials in warehouse operations.', + // cost: 122000, + // count: 5, + // }, + // { + // id: '2', + // name: 'Warehouse Robot WR-300', + // model: 'WRB-3001', + // status: 'Online', + // usageRate: 50, + // level: 'Level 2', + // image: assetImage, + // description: 'Automated robot for handling packages and inventory in the warehouse.', + // cost: 85000, + // count: 3, + // }, + // { + // id: '3', + // name: 'Conveyor Belt System CB-150', + // model: 'CBS-150X', + // status: 'Online', + // usageRate: 95, + // level: 'Level 3', + // image: assetImage, + // description: 'High-speed conveyor belt system for efficient material handling.', + // cost: 45000, + // count: 2, + // }, + // ]; + + + return ( + <> + {/* */} + +
+ {assets.map((asset, index) => ( +
+
+
+ + {expandedAssetId === asset.id ? + <> +
setExpandedAssetId(null)}>▾
+ + + : +
+ } +
+
+ {/*
{asset.name}
*/} + + {asset.count !== 1 &&
+ x + {asset.count} +
} +
{asset.model}
+
+
+
+
{asset.status}
+
+
+ +
+ {expandedAssetId === asset.id &&
{asset.description}
} +
+ + {expandedAssetId === asset.id ? ( +
+
+
Cost of Asset
+
₹ 1,22,000
+
+ +
+
+
In Scene
+
5 items
+
+
+
Total Cost
+
₹ 6,10,000
+
+
+ +
+ +
handleAssetClick(asset.id)}>View in Scene
+
+
+ + + ) : ( +
+
+
+
+
Usage rate
+
+
+
+
+
+
{asset.usageRate}%
+
+
+ +
+
+
+
{asset.level}
+
+ +
+ setExpandedAssetId(expandedAssetId === asset.id ? null : asset.id) + } + > +
{expandedAssetId === asset.id ? "View Less" : "View More"}
+
+
+ +
+ + +
+ + )} + +
+ + ))} +
+ + ) +} + +export default AssetManagement + + diff --git a/app/src/components/templates/CreateNewWindow.tsx b/app/src/components/templates/CreateNewWindow.tsx new file mode 100644 index 0000000..91085ab --- /dev/null +++ b/app/src/components/templates/CreateNewWindow.tsx @@ -0,0 +1,183 @@ +import React, { ReactNode, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +type NewWindowProps = { + children: ReactNode; + title?: string; + width?: number; + height?: number; + left?: number; + top?: number; + center?: boolean; + features?: Partial<{ + toolbar: boolean; + menubar: boolean; + scrollbars: boolean; + resizable: boolean; + location: boolean; + status: boolean; + }>; + onClose?: () => void; + copyStyles?: boolean; + noopener?: boolean; + className?: string; + theme?: string | null; +}; + +export const RenderInNewWindow: React.FC = ({ + children, + title = "New Window", + width = 900, + height = 700, + left, + top, + center = true, + features, + onClose, + copyStyles = true, + noopener = true, + className, + theme = "light", +}) => { + const [mounted, setMounted] = useState(false); + const childWindowRef = useRef(null); + const containerElRef = useRef(null); + + useEffect(() => { + if (typeof window === "undefined") return; + + const screenLeft = window.screenLeft ?? window.screenX ?? 0; + const screenTop = window.screenTop ?? window.screenY ?? 0; + const availWidth = window.outerWidth ?? window.innerWidth; + const availHeight = window.outerHeight ?? window.innerHeight; + + const finalLeft = + center && availWidth + ? Math.max(0, screenLeft + (availWidth - width) / 2) + : left ?? 100; + + const finalTop = + center && availHeight + ? Math.max(0, screenTop + (availHeight - height) / 2) + : top ?? 100; + + const baseFeatures = [ + `width=${Math.floor(width)}`, + `height=${Math.floor(height)}`, + `left=${Math.floor(finalLeft)}`, + `top=${Math.floor(finalTop)}`, + ]; + + const featureFlags = features ?? { + toolbar: false, + menubar: false, + scrollbars: true, + resizable: true, + location: false, + status: false, + }; + + Object.entries(featureFlags).forEach(([k, v]) => + baseFeatures.push(`${k}=${v ? "yes" : "no"}`) + ); + + const newWin = window.open("", "_blank", baseFeatures.join(",")); + if (!newWin) { + console.warn("Popup blocked or failed to open window."); + onClose?.(); + return; + } + + if (noopener) { + try { + newWin.opener = null; + } catch {} + } + + newWin.document.open(); + newWin.document.write(` + + + + ${title} + + + +`); + newWin.document.close(); + + if (copyStyles) { + const head = newWin.document.head; + Array.from(document.styleSheets).forEach((styleSheet) => { + try { + if ((styleSheet as CSSStyleSheet).cssRules) { + const newStyleEl = newWin.document.createElement("style"); + const rules = Array.from( + (styleSheet as CSSStyleSheet).cssRules + ).map((r) => r.cssText); + newStyleEl.appendChild( + newWin.document.createTextNode(rules.join("\n")) + ); + head.appendChild(newStyleEl); + } + } catch { + const ownerNode = styleSheet.ownerNode as HTMLElement | null; + if (ownerNode && ownerNode.tagName === "LINK") { + const link = ownerNode as HTMLLinkElement; + const newLink = newWin.document.createElement("link"); + newLink.rel = link.rel; + newLink.href = link.href; + newLink.media = link.media; + head.appendChild(newLink); + } + } + }); + } + + const container = newWin.document.createElement("div"); + if (className) container.className = className; + newWin.document.body.appendChild(container); + + newWin.document.title = title; + + // Handle child window close + const handleChildUnload = () => { + onClose?.(); + }; + newWin.addEventListener("beforeunload", handleChildUnload); + + // 👇 Handle parent refresh/close → auto close child + const handleParentUnload = () => { + try { + newWin.close(); + } catch {} + }; + window.addEventListener("beforeunload", handleParentUnload); + + childWindowRef.current = newWin; + containerElRef.current = container; + setMounted(true); + + return () => { + newWin.removeEventListener("beforeunload", handleChildUnload); + window.removeEventListener("beforeunload", handleParentUnload); + try { + newWin.close(); + } catch {} + childWindowRef.current = null; + containerElRef.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const w = childWindowRef.current; + if (w && !w.closed) { + w.document.title = title; + } + }, [title]); + + if (!mounted || !containerElRef.current) return null; + + return createPortal(children, containerElRef.current); +}; diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index e834f28..d5d49e4 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -36,6 +36,7 @@ import { } from "../../store/visualization/useDroppedObjectsStore"; import { useParams } from "react-router-dom"; import { useVersionContext } from "../../modules/builder/version/versionContext"; +import { MoveIcon, RotateIcon } from "../icons/ShortcutIcons"; // Utility component const ToolButton = ({ @@ -65,12 +66,8 @@ const Tools: React.FC = () => { const { isPlaying, setIsPlaying } = usePlayButtonStore(); const { showShortcuts } = useShortcutStore(); - const { - activeTool, - setActiveTool, - setToolMode, - setAddAction, - } = useStoreHooks(); + const { activeTool, setActiveTool, setToolMode, setAddAction } = + useStoreHooks(); const { setActiveSubTool, activeSubTool } = useActiveSubTool(); const { setSelectedWallItem } = useSelectedWallItem(); @@ -81,14 +78,15 @@ const Tools: React.FC = () => { const { selectedZone } = useSelectedZoneStore(); const { floatingWidget } = useFloatingWidget(); const { widgets3D } = use3DWidget(); - const { visualizationSocket } = useSocketStore(); - const dropdownRef = useRef(null); - const [openDrop, setOpenDrop] = useState(false); + const { visualizationSocket } = useSocketStore(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { projectId } = useParams(); + const dropdownRef = useRef(null); + const [openDrop, setOpenDrop] = useState(false); + // 1. Set UI toggles on initial render useEffect(() => { setToggleUI( @@ -155,7 +153,7 @@ const Tools: React.FC = () => { if (!is2D) setAddAction("Pillar"); break; case "delete": - is2D ? setToolMode('2D-Delete') : setToolMode('3D-Delete'); + is2D ? setToolMode("2D-Delete") : setToolMode("3D-Delete"); break; } }; @@ -251,7 +249,7 @@ const Tools: React.FC = () => { templates, visualizationSocket, projectId, - versionId: selectedVersion?.versionId || '' + versionId: selectedVersion?.versionId || "", }) } /> @@ -278,6 +276,10 @@ const Tools: React.FC = () => { return FreeMoveIcon; case "delete": return DeleteIcon; + case "move": + return MoveIcon; + case "rotate": + return RotateIcon; default: return CursorIcon; } @@ -304,6 +306,10 @@ const Tools: React.FC = () => { return ; case "delete": return ; + case "move": + return ; + case "rotate": + return ; default: return null; } @@ -362,6 +368,24 @@ const Tools: React.FC = () => { )}
+ {activeModule !== "visualization" && ( + <> +
+
+ {["move", "rotate"].map((tool) => ( + setActiveTool(tool)} + /> + ))} +
+ + )} +
{activeModule === "builder" && renderBuilderTools()} {activeModule === "simulation" && renderSimulationTools()} diff --git a/app/src/components/ui/collaboration/Messages.tsx b/app/src/components/ui/collaboration/Messages.tsx index b3f37e1..11e87b2 100644 --- a/app/src/components/ui/collaboration/Messages.tsx +++ b/app/src/components/ui/collaboration/Messages.tsx @@ -231,7 +231,10 @@ const Messages: React.FC = ({ val, i, setMessages, mode, setIsEdit
{isEditableThread ? getRelativeTime(val.createdAt) : val.createdAt}
{(val as Reply).creatorId === userId && ( -
+
setOpenOptions(false)} + > + {openOptions && (
- {!(isEditableThread) && } + + {!isEditableThread && ( + + )}
)}
)} +
{"comment" in val ? val.comment : val.threadTitle}
+
)} diff --git a/app/src/components/ui/collaboration/ThreadChat.tsx b/app/src/components/ui/collaboration/ThreadChat.tsx index 8e4b24b..5370689 100644 --- a/app/src/components/ui/collaboration/ThreadChat.tsx +++ b/app/src/components/ui/collaboration/ThreadChat.tsx @@ -131,9 +131,10 @@ const ThreadChat: React.FC = () => { if (dragging) updatePosition(e, true); }; - useEffect(() => { - updatePosition({ clientX: position.x, clientY: position.y }, true); - }, [selectedComment]); + // Commented this useEffect to prevent offset after user saved the comment + // useEffect(() => { + // updatePosition({ clientX: position.x, clientY: position.y }, true); + // }, [selectedComment]); const handlePointerUp = (event: React.PointerEvent) => { @@ -144,6 +145,10 @@ const ThreadChat: React.FC = () => { }; 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(); try { // const createComments = await addCommentsApi(projectId, value, selectedComment?.threadId, selectedVersion?.versionId || "")/ @@ -163,6 +168,7 @@ const ThreadChat: React.FC = () => { // } + if (threadSocket && mode === "create") { const addComment = { versionId: selectedVersion?.versionId || "", @@ -190,7 +196,7 @@ const ThreadChat: React.FC = () => { // removeComment(selectedComment?.threadId) // setSelectedComment([]) // } - console.log('threadSocket:threadChat ', threadSocket); + if (threadSocket) { // projectId, userId, organization, threadId const deleteThread = { @@ -258,7 +264,7 @@ const ThreadChat: React.FC = () => { }; if (threadSocket) { - console.log('createThread: ', createThread); + setInputActive(false); threadSocket.emit("v1:thread:create", createThread); diff --git a/app/src/components/ui/inputs/InputRange.tsx b/app/src/components/ui/inputs/InputRange.tsx index 1a2f6f5..372edaf 100644 --- a/app/src/components/ui/inputs/InputRange.tsx +++ b/app/src/components/ui/inputs/InputRange.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from "react"; -import * as CONSTANTS from "../../../types/world/worldConstants"; + interface InputToggleProps { - label: string; // Represents the toggle state (on/off) + label: string; min?: number; max?: number; - onClick?: () => void; // Function to handle toggle clicks - onChange?: (value: number) => void; // Function to handle toggle clicks + step?: number; // Added step prop + onClick?: () => void; + onChange?: (value: number) => void; disabled?: boolean; value?: number; onPointerUp?: (value: number) => void; @@ -17,54 +18,46 @@ const InputRange: React.FC = ({ onChange, min, max, + step = 1, // default step disabled, value, onPointerUp, }) => { - const [rangeValue, setRangeValue] = useState(value ? value : 5); + const [rangeValue, setRangeValue] = useState(value ?? 5); function handleChange(e: React.ChangeEvent) { - const newValue = parseInt(e.target.value); // Parse the value to an integer + const newValue = parseFloat(e.target.value); + setRangeValue(newValue); - setRangeValue(newValue); // Update the local state - - if (onChange) { - onChange(newValue); // Call the onChange function if it exists - } + if (onChange) onChange(newValue); } + useEffect(() => { - value && setRangeValue(value); + if (value !== undefined) setRangeValue(value); }, [value]); + function handlePointerUp(e: React.PointerEvent) { - const newValue = parseInt(e.currentTarget.value, 10); // Parse value correctly - - if (onPointerUp) { - onPointerUp(newValue); // Call the callback function if it exists - } + const newValue = parseFloat(e.currentTarget.value); + if (onPointerUp) onPointerUp(newValue); } - function handlekey(e: React.KeyboardEvent) { - const newValue = parseInt(e.currentTarget.value, 10); // Parse value correctly - if (onPointerUp) { - onPointerUp(newValue); // Call the callback function if it exists - } + function handleKey(e: React.KeyboardEvent) { + const newValue = parseFloat(e.currentTarget.value); + if (onPointerUp) onPointerUp(newValue); } return (
-