diff --git a/app/docs/documents/projectStructure.md b/app/docs/documents/projectStructure.md index 1e12346..234dabb 100644 --- a/app/docs/documents/projectStructure.md +++ b/app/docs/documents/projectStructure.md @@ -1,10 +1,13 @@ -# Project Folder Structure +# **Project Folder Structure Maintenance** -This document provides a detailed description of the purpose of each folder in the project by root level, along with the folder hierarchy. +This document outlines the project’s folder structure, describing the purpose of each folder and file. It also includes guidelines for development, collaboration, and scalability. -## Folder Hierarchy +--- + +## **Folder Hierarchy** ``` +πŸ“ .github πŸ“ src β”œβ”€β”€ πŸ“ assets β”œβ”€β”€ πŸ“ components @@ -14,94 +17,195 @@ This document provides a detailed description of the purpose of each folder in t β”‚ β”œβ”€β”€ πŸ“ builder β”‚ β”œβ”€β”€ πŸ“ simulation β”‚ └── πŸ“ visualization +β”œβ”€β”€ πŸ“ pages β”œβ”€β”€ πŸ“ services β”œβ”€β”€ πŸ“ store β”œβ”€β”€ πŸ“ styles β”œβ”€β”€ πŸ“ tests β”œβ”€β”€ πŸ“ types β”œβ”€β”€ πŸ“ utils +β”œβ”€β”€ πŸ“ temp β”œβ”€β”€ App.css β”œβ”€β”€ App.tsx β”œβ”€β”€ index.css β”œβ”€β”€ main.tsx -└── vite-env.d.ts +β”œβ”€β”€ Dockerfile +└── nginx.conf ``` --- -## Description of Each Folder +## **Folder and File Descriptions** -### πŸ“ `src` -The root directory for all source code related to the application. - -#### πŸ“ `assets` -- **Purpose:** Contains static assets such as images, icons, fonts, or other resources. -- **Example:** - - `react.svg`: A static SVG file used in the project. - -#### πŸ“ `components` -- **Purpose:** Contains reusable React components, serving as building blocks for the user interface. -- **Example:** - - Buttons, modals, headers, and forms. - -#### πŸ“ `functions` -- **Purpose:** Stores pure functions or logic that can be reused across the application. -- **Example:** - - Utility functions for data transformation or computation. - -#### πŸ“ `hooks` -- **Purpose:** Holds custom React hooks for managing specific logic or behaviors. -- **Example:** - - Hooks for state management, API calls, or reusable effects. - -#### πŸ“ `modules` -- **Purpose:** Organizes high-level, feature-specific code into subdirectories. - - **πŸ“ builder:** Manages functionalities and components related to building or configuring features. - - **πŸ“ simulation:** Handles processes or logic related to simulations or dynamic scenarios. - - **πŸ“ visualization:** Focuses on displaying data visually, such as charts, graphs, or interactive UI elements. - -#### πŸ“ `services` -- **Purpose:** Encapsulates external service interactions, such as API calls or library integrations. -- **Example:** - - REST API clients or authentication handlers. - -#### πŸ“ `store` -- **Purpose:** Contains state management logic and configurations for tools like Redux or Zustand. -- **Example:** - - Redux slices, context providers, or global state stores. - -#### πŸ“ `styles` -- **Purpose:** Includes global CSS, SCSS, or theming resources. -- **Example:** - - Global styles, theme variables, or resets. - -#### πŸ“ `tests` -- **Purpose:** Stores test files for unit testing, integration testing, and mock data. -- **Example:** - - Test files (`*.test.tsx`, `*.spec.ts`) or mock utilities. - -#### πŸ“ `types` -- **Purpose:** Contains shared TypeScript type definitions and interfaces. -- **Example:** - - Type declarations for props, data models, or API responses. - -#### πŸ“ `utils` -- **Purpose:** Includes general-purpose utility files that don’t fit into other specific folders. -- **Example:** - - Helper functions like debouncers or date formatters. +### πŸ“ **`src`** +The root directory for all application source code. --- -## Root-Level Files +### πŸ“ **`assets`** +- **Purpose:** Static assets like images, fonts, and icons. +- **Examples:** + - Subfolders for `images`, `icons`, and `fonts`. + - Use descriptive file names (e.g., `logo.svg`, `Roboto-Regular.ttf`). -### `App.tsx` -- **Purpose:** The root React component, initializing the main application layout and logic. +--- -### `index.css` -- **Purpose:** Contains global styles applied throughout the application. +### πŸ“ **`components`** +- **Purpose:** Reusable React components forming the building blocks of the UI. +- **Examples:** + - Buttons, modals, input fields, and headers. + - Organize larger components into folders (e.g., `Button/`). -### `main.tsx` -- **Purpose:** The entry point of the app, rendering the React application and setting up the React DOM. +--- -### `vite-env.d.ts` -- **Purpose:** TypeScript environment configuration file for Vite. +### πŸ“ **`functions`** +- **Purpose:** Pure functions and reusable logic. +- **Examples:** + - `formatDate.ts`: Format dates into readable strings. + - `calculateTotal.ts`: Logic for cart total calculation. + +--- + +### πŸ“ **`hooks`** +- **Purpose:** Custom React hooks encapsulating reusable logic. +- **Examples:** + - `useAuth.ts`: Manages authentication logic. + - `useFetch.ts`: Fetches data from APIs. + +--- + +### πŸ“ **`modules`** +- **Purpose:** Feature-specific code organized into subfolders. +- **Subfolders:** + - **`builder`**: For UI components and logic related to building features. + - **`simulation`**: Code for running simulations. + - **`visualization`**: Visualization components like charts and graphs. + +--- + +### πŸ“ **`pages`** +- **Purpose:** Pages that represent routes in the application. +- **Examples:** + - `HomePage.tsx`: Home route. + - `DashboardPage.tsx`: Dashboard route. + +--- + +### πŸ“ **`services`** +- **Purpose:** External API interactions and third-party service logic. +- **Examples:** + - `apiClient.ts`: Wrapper for HTTP requests. + - `authService.ts`: Authentication services. + +--- + +### πŸ“ **`store`** +- **Purpose:** State management logic using tools like Redux or Context API. +- **Examples:** + - `authSlice.ts`: Authentication-related state management. + - `cartSlice.ts`: Shopping cart state management. + +--- + +### πŸ“ **`styles`** +- **Purpose:** Global and modular styles. +- **Examples:** + - `global.css`: Global styles. + - `theme.scss`: Theme variables. + +--- + +### πŸ“ **`tests`** +- **Purpose:** Test files for unit, integration, and E2E testing. +- **Examples:** + - `Button.test.tsx`: Tests for the `Button` component. + - `mockData.ts`: Mock API responses. + +--- + +### πŸ“ **`types`** +- **Purpose:** Shared TypeScript type definitions and interfaces. +- **Examples:** + - `User.ts`: User-related type definitions. + - `ApiResponse.ts`: API response types. + +--- + +### πŸ“ **`utils`** +- **Purpose:** General-purpose utility functions and constants. +- **Examples:** + - `debounce.ts`: Debounce function. + - `constants.ts`: Shared constants. + +--- + +### πŸ“ **`temp`** +- **Purpose:** A temporary directory for experimental or work-in-progress code. +- **Guidelines:** + - This folder is included in `.gitignore` to prevent its contents from affecting the main project. + - Move any experimental work here to isolate it from production-ready code. + +--- + +### **Root-Level Files** + +- **`App.tsx`**: Root React component. +- **`main.tsx`**: Entry point for the app. +- **`Dockerfile`**: Docker configuration for containerizing the application. +- **`nginx.conf`**: Configuration for the Nginx server. + +--- + +## **Development Guidelines** + +### **Environment Management** +- **`.env.local`**: Use this file for testing and development-specific variables. +- **`.env`**: Should only contain variables required for deployed services (e.g., API base URLs). + +### **Collaboration Best Practices** +1. **Use the `temp` Folder for Experiments:** + Any experimental work should be isolated in the `temp` folder. Avoid committing temporary code to the main repository. + +2. **Documentation:** + - Read the shared documentation in the `docify` and `.github` folders before starting work. + - Follow any guidelines or standards outlined in these documents. + +3. **Branching Rules:** + - Do not merge other branches into your branch without proper code review or necessity. + - Use meaningful branch names (e.g., `feature/auth-module`, `fix/header-bug`). + +4. **Code Reviews:** + - Ensure all code undergoes peer review before merging. + - Use PR templates provided in the `.github` folder to document changes. + +--- + +## **Additional Instructions for Large-Scale Projects** + +1. **Folder Depth:** + - Avoid excessive nesting of folders to maintain simplicity. + - Refactor large folders into modules or domains as the project scales. + +2. **Dependency Management:** + - Regularly review and update dependencies. + - Remove unused or deprecated libraries to reduce technical debt. + +3. **Automate Workflows:** + - Use CI/CD pipelines to automate testing, building, and deployment processes. + - Integrate tools like Prettier, ESLint, and Husky to enforce code quality. + +4. **Versioning and Change Logs:** + - Maintain a changelog to track major updates. + - Use semantic versioning (`x.y.z`) for releases. + +5. **Performance Monitoring:** + - Regularly monitor and optimize app performance. + - Use tools like Lighthouse, React DevTools, and browser profilers. + +6. **Error Handling:** + - Centralize error handling using utilities or middleware. + - Log errors in both the frontend and backend for debugging. + +7. **Documentation:** + - Continuously update this document to reflect structural or procedural changes. + - Ensure all team members are familiar with the documentation. diff --git a/app/package-lock.json b/app/package-lock.json index a820896..8be748e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -29,6 +29,7 @@ "chartjs-plugin-annotation": "^3.1.0", "glob": "^11.0.0", "gsap": "^3.12.5", + "html2canvas": "^1.4.1", "leva": "^0.10.0", "mqtt": "^5.10.4", "postprocessing": "^6.36.4", @@ -2019,7 +2020,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -2031,7 +2032,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4134,26 +4135,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -4265,25 +4246,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "node_modules/@turf/along": { "version": "7.2.0", @@ -8047,6 +8028,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -9017,7 +9007,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -9102,6 +9092,15 @@ "postcss": "^8.4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -9885,7 +9884,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.3.1" } @@ -12489,6 +12488,19 @@ } } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -15235,7 +15247,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -20434,6 +20446,15 @@ "node": "*" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -20694,7 +20715,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -20737,7 +20758,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, + "dev": true, "dependencies": { "acorn": "^8.11.0" }, @@ -20749,7 +20770,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -21220,6 +21241,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -21236,7 +21266,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "node_modules/v8-to-istanbul": { "version": "8.1.1", @@ -22295,7 +22325,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } diff --git a/app/package.json b/app/package.json index 66e3b39..ce5c7d3 100644 --- a/app/package.json +++ b/app/package.json @@ -24,6 +24,7 @@ "chartjs-plugin-annotation": "^3.1.0", "glob": "^11.0.0", "gsap": "^3.12.5", + "html2canvas": "^1.4.1", "leva": "^0.10.0", "mqtt": "^5.10.4", "postprocessing": "^6.36.4", diff --git a/app/src/assets/image/feneration.png b/app/src/assets/image/categories/feneration.png similarity index 100% rename from app/src/assets/image/feneration.png rename to app/src/assets/image/categories/feneration.png diff --git a/app/src/assets/image/categories/machines.png b/app/src/assets/image/categories/machines.png new file mode 100644 index 0000000..a441d48 Binary files /dev/null and b/app/src/assets/image/categories/machines.png differ diff --git a/app/src/assets/image/vehicles.png b/app/src/assets/image/categories/vehicles.png similarity index 100% rename from app/src/assets/image/vehicles.png rename to app/src/assets/image/categories/vehicles.png diff --git a/app/src/assets/image/categories/workStation.png b/app/src/assets/image/categories/workStation.png new file mode 100644 index 0000000..43221ae Binary files /dev/null and b/app/src/assets/image/categories/workStation.png differ diff --git a/app/src/assets/image/categories/worker.png b/app/src/assets/image/categories/worker.png new file mode 100644 index 0000000..e2287e7 Binary files /dev/null and b/app/src/assets/image/categories/worker.png differ diff --git a/app/src/assets/image/machines.png b/app/src/assets/image/machines.png deleted file mode 100644 index 199870d..0000000 Binary files a/app/src/assets/image/machines.png and /dev/null differ diff --git a/app/src/assets/image/userImage.png b/app/src/assets/image/userImage.png deleted file mode 100644 index 51af26c..0000000 Binary files a/app/src/assets/image/userImage.png and /dev/null differ diff --git a/app/src/assets/image/workStation.png b/app/src/assets/image/workStation.png deleted file mode 100644 index 6345487..0000000 Binary files a/app/src/assets/image/workStation.png and /dev/null differ diff --git a/app/src/assets/image/worker.png b/app/src/assets/image/worker.png deleted file mode 100644 index 7067644..0000000 Binary files a/app/src/assets/image/worker.png and /dev/null differ diff --git a/app/src/components/layout/sidebarLeft/Assets.tsx b/app/src/components/layout/sidebarLeft/Assets.tsx index 67bf969..9e04615 100644 --- a/app/src/components/layout/sidebarLeft/Assets.tsx +++ b/app/src/components/layout/sidebarLeft/Assets.tsx @@ -1,16 +1,20 @@ import React, { useEffect, useMemo, useState } from "react"; import Search from "../../ui/inputs/Search"; -import vehicle from "../../../assets/image/vehicles.png"; -import workStation from "../../../assets/image/workStation.png"; -import machines from "../../../assets/image/machines.png"; -import feneration from "../../../assets/image/feneration.png"; -import worker from "../../../assets/image/worker.png"; import { getCategoryAsset } from "../../../services/factoryBuilder/assest/assets/getCategoryAsset"; import arch from "../../../assets/gltf-glb/arch.glb"; import door from "../../../assets/gltf-glb/door.glb"; import window from "../../../assets/gltf-glb/window.glb"; import { fetchAssets } from "../../../services/marketplace/fetchAssets"; import { useSelectedItem } from "../../../store/store"; + +// images ------------------- +import vehicle from "../../../assets/image/categories/vehicles.png"; +import workStation from "../../../assets/image/categories/workStation.png"; +import machines from "../../../assets/image/categories/machines.png"; +import feneration from "../../../assets/image/categories/feneration.png"; +import worker from "../../../assets/image/categories/worker.png"; +// ------------------------------------- + interface AssetProp { filename: string; thumbnail?: string; diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index 8fa95a9..5938aa3 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -115,17 +115,10 @@ const Tools: React.FC = () => { setOpenDrop(false); // Close the dropdown } }; - const handleEscKeyPress = (event: KeyboardEvent) => { - if (event.key === "Escape") { - setIsPlaying(false); // Set isPlaying to false when Escape key is pressed - } - }; document.addEventListener("mousedown", handleOutsideClick); - document.addEventListener("keydown", handleEscKeyPress); // Listen for ESC key return () => { document.removeEventListener("mousedown", handleOutsideClick); - document.removeEventListener("keydown", handleEscKeyPress); // Clean up the event listener }; }, []); useEffect(() => { diff --git a/app/src/modules/builder/agv/agv.tsx b/app/src/modules/builder/agv/agv.tsx index 48f4306..5b8a2e4 100644 --- a/app/src/modules/builder/agv/agv.tsx +++ b/app/src/modules/builder/agv/agv.tsx @@ -1,68 +1,111 @@ import { useEffect, useState } from "react"; import { Line } from "@react-three/drei"; -import { useNavMesh, useSimulationStates } from "../../../store/store"; +import { + useNavMesh, + usePlayAgv, + useSimulationStates, +} from "../../../store/store"; import PathNavigator from "./pathNavigator"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; type PathPoints = { - modelUuid: string; - modelSpeed: number; - bufferTime: number; - points: { x: number; y: number; z: number }[]; - hitCount: number; + modelUuid: string; + modelSpeed: number; + bufferTime: number; + points: { x: number; y: number; z: number }[]; + hitCount: number; }; +interface ProcessContainerProps { + processes: any[]; + agvRef: any; + MaterialRef: any; +} -const Agv = () => { - const [pathPoints, setPathPoints] = useState([]); - const { simulationStates } = useSimulationStates(); - const { navMesh } = useNavMesh(); - const { isPlaying } = usePlayButtonStore(); +const Agv: React.FC = ({ + processes, + agvRef, + MaterialRef, +}) => { + const [pathPoints, setPathPoints] = useState([]); + const { simulationStates } = useSimulationStates(); + const { navMesh } = useNavMesh(); + const { isPlaying } = usePlayButtonStore(); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - useEffect(() => { - if (simulationStates.length > 0) { - const agvModels = simulationStates.filter((val) => val.modelName === "agv" && val.type === "Vehicle"); - const newPathPoints = agvModels.filter((model: any) => model.points && model.points.actions && typeof model.points.actions.start === "object" && typeof model.points.actions.end === "object" && "x" in model.points.actions.start && "y" in model.points.actions.start && "x" in model.points.actions.end && "y" in model.points.actions.end) - .map((model: any) => ({ - modelUuid: model.modeluuid, - modelSpeed: model.points.speed, - bufferTime: model.points.actions.buffer, - hitCount: model.points.actions.hitCount, - points: [ - { x: model.position[0], y: model.position[1], z: model.position[2] }, - { x: model.points.actions.start.x, y: 0, z: model.points.actions.start.y }, - { x: model.points.actions.end.x, y: 0, z: model.points.actions.end.y }, - ], - })); + useEffect(() => { + if (simulationStates.length > 0) { + const agvModels = simulationStates.filter( + (val) => val.modelName === "agv" && val.type === "Vehicle" + ); - setPathPoints(newPathPoints); - } - }, [simulationStates]); + const newPathPoints = agvModels + .filter( + (model: any) => + model.points && + model.points.actions && + typeof model.points.actions.start === "object" && + typeof model.points.actions.end === "object" && + "x" in model.points.actions.start && + "y" in model.points.actions.start && + "x" in model.points.actions.end && + "y" in model.points.actions.end + ) + .map((model: any) => ({ + modelUuid: model.modeluuid, + modelSpeed: model.points.speed, + bufferTime: model.points.actions.buffer, + hitCount: model.points.actions.hitCount, + points: [ + { + x: model.position[0], + y: model.position[1], + z: model.position[2], + }, + { + x: model.points.actions.start.x, + y: 0, + z: model.points.actions.start.y, + }, + { + x: model.points.actions.end.x, + y: 0, + z: model.points.actions.end.y, + }, + ], + })); - return ( - <> - {pathPoints.map((pair, i) => ( - - + setPathPoints(newPathPoints); + } + }, [simulationStates]); - {pair.points.slice(1).map((point, idx) => ( - - - - - ))} - - ))} - - ); + return ( + <> + {pathPoints.map((pair, i) => ( + + + + {pair.points.slice(1).map((point, idx) => ( + + + + + ))} + + ))} + + ); }; export default Agv; diff --git a/app/src/modules/builder/agv/pathNavigator.tsx b/app/src/modules/builder/agv/pathNavigator.tsx index 6369a87..d68da57 100644 --- a/app/src/modules/builder/agv/pathNavigator.tsx +++ b/app/src/modules/builder/agv/pathNavigator.tsx @@ -1,216 +1,485 @@ -import React, { useEffect, useState, useRef } from "react"; +import React, { useEffect, useState, useRef, useMemo } from "react"; import * as THREE from "three"; import { useFrame, useThree } from "@react-three/fiber"; import { NavMeshQuery } from "@recast-navigation/core"; import { Line } from "@react-three/drei"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import { usePlayAgv } from "../../../store/store"; +import crate from "../../../assets/gltf-glb/crate_box.glb"; interface PathNavigatorProps { - navMesh: any; - pathPoints: any; - id: string; - speed: number; - bufferTime: number; - hitCount: number; + navMesh: any; + pathPoints: any; + id: string; + speed: number; + bufferTime: number; + hitCount: number; + processes: any[]; + agvRef: any; + MaterialRef: any; } - +interface AGVData { + processId: string; + vehicleId: string; + hitCount: number; + totalHits: number; +} +type Phase = "initial" | "toDrop" | "toPickup"; +type MaterialType = "Box" | "Crate"; export default function PathNavigator({ - navMesh, - pathPoints, - id, - speed, - bufferTime, - hitCount + navMesh, + pathPoints, + id, + speed, + bufferTime, + hitCount, + processes, + agvRef, + MaterialRef, }: PathNavigatorProps) { - const [path, setPath] = useState<[number, number, number][]>([]); - const [currentPhase, setCurrentPhase] = useState<'initial' | 'loop'>('initial'); - const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>([]); - const [pickupDropPath, setPickupDropPath] = useState<[number, number, number][]>([]); - const [dropPickupPath, setDropPickupPath] = useState<[number, number, number][]>([]); - const [initialPosition, setInitialPosition] = useState(null); - const [initialRotation, setInitialRotation] = useState(null); - const [targetPosition] = useState(new THREE.Vector3()); - const [smoothPosition] = useState(new THREE.Vector3()); - const [targetQuaternion] = useState(new THREE.Quaternion()); - const distancesRef = useRef([]); - const totalDistanceRef = useRef(0); - const progressRef = useRef(0); - const isWaiting = useRef(false); - const timeoutRef = useRef(null); - const pathTransitionProgress = useRef(0); + const [currentPhase, setCurrentPhase] = useState("initial"); + // - const { scene } = useThree(); - const { isPlaying } = usePlayButtonStore(); + const [path, setPath] = useState<[number, number, number][]>([]); + const PickUpDrop = useRef([]); - useEffect(() => { - const object = scene.getObjectByProperty("uuid", id); - if (object) { - setInitialPosition(object.position.clone()); - setInitialRotation(object.rotation.clone()); - smoothPosition.copy(object.position.clone()); - targetPosition.copy(object.position.clone()); - targetQuaternion.setFromEuler(object.rotation.clone()); - } - }, [scene, id]); + // const [currentPhase, setCurrentPhase] = useState<"initial" | "loop">( + // "initial" + // ); + const [toPickupPath, setToPickupPath] = useState<[number, number, number][]>( + [] + ); - const computePath = (start: any, end: any) => { - try { - const navMeshQuery = new NavMeshQuery(navMesh); - const { path: segmentPath } = navMeshQuery.computePath(start, end); - return segmentPath?.map(({ x, y, z }) => [x, y + 0.1, z] as [number, number, number]) || []; - } catch { - return []; - } - }; + const [pickupDropPath, setPickupDropPath] = useState< + [number, number, number][] + >([]); + const [dropPickupPath, setDropPickupPath] = useState< + [number, number, number][] + >([]); + const [initialPosition, setInitialPosition] = useState( + null + ); + const [initialRotation, setInitialRotation] = useState( + null + ); + const [boxVisible, setBoxVisible] = useState(false); - const resetState = () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } + const distancesRef = useRef([]); + const totalDistanceRef = useRef(0); + const progressRef = useRef(0); + const isWaiting = useRef(false); + const timeoutRef = useRef(null); + const hasStarted = useRef(false); - setPath([]); - setCurrentPhase('initial'); - distancesRef.current = []; - totalDistanceRef.current = 0; - progressRef.current = 0; - isWaiting.current = false; - pathTransitionProgress.current = 0; + const { scene } = useThree(); + const { isPlaying } = usePlayButtonStore(); + const { PlayAgv, setPlayAgv } = usePlayAgv(); - const object = scene.getObjectByProperty("uuid", id); - if (object && initialPosition && initialRotation) { - object.position.copy(initialPosition); - object.rotation.copy(initialRotation); - smoothPosition.copy(initialPosition); - targetPosition.copy(initialPosition); - targetQuaternion.setFromEuler(initialRotation); - } - }; + useEffect(() => { + const object = scene.getObjectByProperty("uuid", id); + if (object) { + setInitialPosition(object.position.clone()); + setInitialRotation(object.rotation.clone()); + } + }, [scene, id]); + const computePath = (start: any, end: any) => { + try { + const navMeshQuery = new NavMeshQuery(navMesh); + const { path: segmentPath } = navMeshQuery.computePath(start, end); + return ( + segmentPath?.map( + ({ x, y, z }) => [x, y + 0.1, z] as [number, number, number] + ) || [] + ); + } catch { + return []; + } + }; - useEffect(() => { - if (!isPlaying) { - resetState(); - } + const resetState = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } - if (!navMesh || pathPoints.length < 2) return; + setPath([]); + setCurrentPhase("initial"); + setPickupDropPath([]); + setDropPickupPath([]); + distancesRef.current = []; + totalDistanceRef.current = 0; + progressRef.current = 0; + isWaiting.current = false; - const [pickup, drop] = pathPoints.slice(-2); - const object = scene.getObjectByProperty("uuid", id); - if (!object) return; + if (initialPosition && initialRotation) { + const object = scene.getObjectByProperty("uuid", id); + if (object) { + object.position.copy(initialPosition); + object.rotation.copy(initialRotation); + } + } + }; - const currentPosition = { x: object.position.x, y: object.position.y, z: object.position.z }; + useEffect(() => { + if (!isPlaying) { + resetState(); + } - const toPickupPath = computePath(currentPosition, pickup); - const pickupToDropPath = computePath(pickup, drop); - const dropToPickupPath = computePath(drop, pickup); + if (!navMesh || pathPoints.length < 2) return; - if (toPickupPath.length && pickupToDropPath.length && dropToPickupPath.length) { - setPickupDropPath(pickupToDropPath); - setDropPickupPath(dropToPickupPath); - setToPickupPath(toPickupPath); - setPath(toPickupPath); - setCurrentPhase('initial'); - } - }, [navMesh, pathPoints, hitCount, isPlaying]); + const [pickup, drop] = pathPoints.slice(-2); - useEffect(() => { - if (path.length < 2) return; + PickUpDrop.current = pathPoints.slice(-2); - let total = 0; - const segmentDistances = path.slice(0, -1).map((point, i) => { - const dist = new THREE.Vector3(...point).distanceTo(new THREE.Vector3(...path[i + 1])); - total += dist; - return dist; - }); + const object = scene.getObjectByProperty("uuid", id); - distancesRef.current = segmentDistances; - totalDistanceRef.current = total; - progressRef.current = 0; - isWaiting.current = false; - }, [path]); + if (!object) return; - useFrame((_, delta) => { - if (!isPlaying || path.length < 2 || !scene || !id) return; + const currentPosition = { + x: object.position.x, + y: object.position.y, + z: object.position.z, + }; - const object = scene.getObjectByProperty("uuid", id); - if (!object) return; + const toPickupPath = computePath(currentPosition, pickup); + const pickupToDropPath = computePath(pickup, drop); + const dropToPickupPath = computePath(drop, pickup); - const speedFactor = speed; - progressRef.current += delta * speedFactor; + if ( + toPickupPath.length && + pickupToDropPath.length && + dropToPickupPath.length + ) { + setPickupDropPath(pickupToDropPath); + setDropPickupPath(dropToPickupPath); + setToPickupPath(toPickupPath); + setPath(toPickupPath); + setCurrentPhase("initial"); + } + }, [navMesh, pathPoints, hitCount, isPlaying, PlayAgv]); - let covered = progressRef.current; - let accumulated = 0; - let index = 0; + useEffect(() => { + if (path.length < 2) return; - while ( - index < distancesRef.current.length && - covered > accumulated + distancesRef.current[index] - ) { - accumulated += distancesRef.current[index]; - index++; - } + let total = 0; + const segmentDistances = path.slice(0, -1).map((point, i) => { + const dist = new THREE.Vector3(...point).distanceTo( + new THREE.Vector3(...path[i + 1]) + ); + total += dist; + return dist; + }); - if (index >= distancesRef.current.length) { - progressRef.current = totalDistanceRef.current; + distancesRef.current = segmentDistances; + totalDistanceRef.current = total; + progressRef.current = 0; + isWaiting.current = false; + }, [path]); - if (!isWaiting.current) { - isWaiting.current = true; + // Add these refs outside the useFrame if not already present: + const startPointReached = useRef(false); - timeoutRef.current = setTimeout(() => { - if (currentPhase === 'initial') { - setPath(pickupDropPath); - setCurrentPhase('loop'); - } else { - setPath(prevPath => - prevPath === pickupDropPath ? dropPickupPath : pickupDropPath - ); - } + const baseMaterials = useMemo( + () => ({ + Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), + Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + // Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + Default: new THREE.MeshStandardMaterial(), + }), + [] + ); - progressRef.current = 0; - isWaiting.current = false; - }, bufferTime * 1000); - } - return; - } + // Example usage: + const targetModelUUID = id; // Replace with your target ID + const matchedProcess = findProcessByTargetModelUUID( + processes, + targetModelUUID + ); - const start = new THREE.Vector3(...path[index]); - const end = new THREE.Vector3(...path[index + 1]); - const dist = distancesRef.current[index]; - const t = THREE.MathUtils.clamp((covered - accumulated) / dist, 0, 1); + function findProcessByTargetModelUUID(processes: any, targetModelUUID: any) { + for (const process of processes) { + for (const path of process.paths) { + for (const point of path.points) { + if ( + point.connections?.targets?.some( + (target: any) => target.modelUUID === targetModelUUID + ) + ) { + // + return process.id; // Return the process if a match is found + } + } + } + } + return null; // Return null if no match is found + } - targetPosition.copy(start).lerp(end, t); + useFrame((_, delta) => {}); + const boxRef = useRef(null); - smoothPosition.lerp(targetPosition, 0.1); - object.position.copy(smoothPosition); + useEffect(() => { + if (!scene || !boxRef || !processes) return; - const direction = new THREE.Vector3().subVectors(end, start).normalize(); - const targetRotationY = Math.atan2(direction.x, direction.z); - targetQuaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), targetRotationY); - object.quaternion.slerp(targetQuaternion, 0.1); - }); + // 1. Find the existing object by UUID + const existingObject = scene.getObjectByProperty("uuid", id); + if (!existingObject) return; - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - }; - }, []); + // 2. Find the process that targets this object's modelUUID + if (boxVisible) { + const matchedProcess = findProcessByTargetModelUUID(processes, id); - return ( - - {toPickupPath.length > 0 && ( - - )} + // 3. Determine the material (from materialref) if a process is matched + let materialType = "Default"; - {pickupDropPath.length > 0 && ( - - )} + if (matchedProcess) { + const materialEntry = MaterialRef.current.find((item: any) => + item.objects.some((obj: any) => obj.processId === matchedProcess) + ); + console.log("materialEntry: ", materialEntry); + if (materialEntry) { + materialType = materialEntry.material; // "Box" or "Crate" + } + } - {dropPickupPath.length > 0 && ( - - )} - - ); -} \ No newline at end of file + // 4. Create the box mesh with the assigned material + const boxGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5); + const boxMaterial = baseMaterials[materialType as MaterialType]; + const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial); + boxMesh.position.y = 1; + boxMesh.name = `box-${id}`; + + // 5. Add to scene and cleanup + existingObject.add(boxMesh); + boxRef.current = boxMesh; + } + + if (!startPointReached.current && boxVisible) { + setBoxVisible(false); + } + return () => { + if (boxRef.current?.parent) { + boxRef.current.parent.remove(boxRef.current); + } + boxRef.current = null; + }; + }, [ + processes, + MaterialRef, + boxVisible, + findProcessByTargetModelUUID, + startPointReached, + ]); + + useFrame((_, delta) => { + const currentAgv = (agvRef.current || []).find( + (agv: AGVData) => agv.vehicleId === id + ); + + if (!scene || !id || !isPlaying) return; + + const object = scene.getObjectByProperty("uuid", id); + if (!object) return; + + if (isPlaying && !hasStarted.current) { + hasStarted.current = false; + startPointReached.current = false; + progressRef.current = 0; + isWaiting.current = false; + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + } + + const isAgvReady = () => { + if (!agvRef.current || agvRef.current.length === 0) return false; + if (!currentAgv) return false; + return hitCount === currentAgv.expectedHitCount; + }; + + // Step 1: Snap to start point on first play + if (isPlaying && !hasStarted.current && toPickupPath.length > 0) { + setBoxVisible(false); + const startPoint = new THREE.Vector3(...toPickupPath[0]); + object.position.copy(startPoint); + + if (toPickupPath.length > 1) { + const nextPoint = new THREE.Vector3(...toPickupPath[1]); + const direction = nextPoint.clone().sub(startPoint).normalize(); + object.rotation.y = Math.atan2(direction.x, direction.z); + } + + hasStarted.current = true; + startPointReached.current = true; + progressRef.current = 0; + + setToPickupPath(toPickupPath.slice(-1)); + + return; + } + + // Step 2: Wait at start point for AGV readiness + if (isPlaying && startPointReached.current && path.length === 0) { + if (!isAgvReady()) { + return; // Prevent transitioning to the next phase if AGV is not ready + } + + setBoxVisible(true); + + setPath([...toPickupPath]); // Start path transition once the AGV is ready + setCurrentPhase("toDrop"); + progressRef.current = 0; + console.log("startPointReached: ", startPointReached.current); + startPointReached.current = false; + + return; + } + + if (path.length < 2) return; + + progressRef.current += delta * speed; + + let covered = progressRef.current; + let accumulated = 0; + let index = 0; + + if (distancesRef.current.length !== path.length - 1) { + distancesRef.current = []; + totalDistanceRef.current = 0; + + for (let i = 0; i < path.length - 1; i++) { + const start = new THREE.Vector3(...path[i]); + const end = new THREE.Vector3(...path[i + 1]); + const distance = start.distanceTo(end); + distancesRef.current.push(distance); + totalDistanceRef.current += distance; + } + } + + while ( + index < distancesRef.current.length && + covered > accumulated + distancesRef.current[index] + ) { + accumulated += distancesRef.current[index]; + index++; + } + + // AGV has completed its path + if (index >= distancesRef.current.length) { + progressRef.current = totalDistanceRef.current; + + timeoutRef.current = setTimeout(() => { + if (!isAgvReady()) { + isWaiting.current = false; + return; + } + + let nextPath = []; + let nextPhase = currentPhase; + + if (currentPhase === "toDrop") { + nextPath = dropPickupPath; + nextPhase = "toPickup"; + setBoxVisible(false); + } else if (currentPhase === "toPickup") { + // When returning to start point (toPickup phase completed) + // Set position to toPickupPath[1] instead of [0] + if (toPickupPath.length > 1) { + object.position.copy(new THREE.Vector3(...toPickupPath[1])); + // Also set rotation towards the next point if available + if (toPickupPath.length > 2) { + const nextPoint = new THREE.Vector3(...toPickupPath[2]); + const direction = nextPoint + .clone() + .sub(object.position) + .normalize(); + object.rotation.y = Math.atan2(direction.x, direction.z); + } + } + + // Reset the path and mark start point as reached + setPath([]); + startPointReached.current = true; + progressRef.current = 0; + distancesRef.current = []; + + // Stop the AGV by setting isplaying to false + if (currentAgv) { + currentAgv.isplaying = false; + } + + // Clear timeout and return to prevent further movement + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + return; + } else { + nextPath = pickupDropPath; + nextPhase = "toDrop"; + setBoxVisible(true); + } + + setPath([...nextPath]); + setCurrentPhase(nextPhase); + progressRef.current = 0; + isWaiting.current = false; + distancesRef.current = []; + + // Reset hit count for the next cycle + if (agvRef.current) { + agvRef.current = agvRef.current.map((agv: AGVData) => + agv.vehicleId === id ? { ...agv, hitCount: 0 } : agv + ); + } + }, bufferTime * 1000); + + return; + } + + // Step 4: Interpolate position and rotation + const start = new THREE.Vector3(...path[index]); + const end = new THREE.Vector3(...path[index + 1]); + const dist = distancesRef.current[index]; + + if (!dist || dist === 0) return; + + const t = THREE.MathUtils.clamp((covered - accumulated) / dist, 0, 1); + object.position.copy(start.clone().lerp(end, t)); + + const direction = new THREE.Vector3().subVectors(end, start).normalize(); + object.rotation.y = Math.atan2(direction.x, direction.z); + }); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + return ( + + {toPickupPath.length > 0 && ( + + )} + + {pickupDropPath.length > 0 && ( + + )} + + {dropPickupPath.length > 0 && ( + + )} + + ); +} diff --git a/app/src/modules/builder/geomentries/assets/addAssetModel.ts b/app/src/modules/builder/geomentries/assets/addAssetModel.ts index d109eb8..1b1c959 100644 --- a/app/src/modules/builder/geomentries/assets/addAssetModel.ts +++ b/app/src/modules/builder/geomentries/assets/addAssetModel.ts @@ -150,6 +150,7 @@ async function handleModelLoad( const organization = email ? email.split("@")[1].split(".")[0] : ""; getAssetEventType(selectedItem.id, organization).then(async (res) => { + console.log('res: ', res); if (res.type === "Conveyor") { const pointUUIDs = res.points.map(() => THREE.MathUtils.generateUUID()); @@ -224,6 +225,7 @@ async function handleModelLoad( eventData as Types.ConveyorEventsSchema ]); + console.log('data: ', data); socket.emit("v2:model-asset:add", data); } else if (res.type === "Vehicle") { @@ -453,6 +455,7 @@ async function handleModelLoad( return updatedItems; }); + console.log('data: ', data); socket.emit("v2:model-asset:add", data); } diff --git a/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx b/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx index 39162ad..307d51f 100644 --- a/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx +++ b/app/src/modules/builder/geomentries/lines/distanceText/distanceText.tsx @@ -1,143 +1,143 @@ -import { useEffect, useState } from "react"; -import { getLines } from "../../../../../services/factoryBuilder/lines/getLinesApi"; -import * as THREE from "three"; -import { - useActiveLayer, - useDeletedLines, - useNewLines, - useToggleView, -} from "../../../../../store/store"; -import objectLinesToArray from "../lineConvertions/objectLinesToArray"; -import { Html } from "@react-three/drei"; -import * as Types from "../../../../../types/world/worldTypes"; - -const DistanceText = () => { - const [lines, setLines] = useState< - { - distance: string; - position: THREE.Vector3; - userData: Types.Line; - layer: string; - }[] - >([]); - const { activeLayer } = useActiveLayer(); - const { toggleView } = useToggleView(); - const { newLines, setNewLines } = useNewLines(); - const { deletedLines, setDeletedLines } = useDeletedLines(); - - useEffect(() => { - const email = localStorage.getItem("email"); - if (!email) return; - const organization = email.split("@")[1].split(".")[0]; - - getLines(organization).then((data) => { - data = objectLinesToArray(data); - - const lines = data - .filter((line: Types.Line) => line[0][2] === activeLayer) - .map((line: Types.Line) => { - const point1 = new THREE.Vector3( - line[0][0].x, - line[0][0].y, - line[0][0].z - ); - const point2 = new THREE.Vector3( - line[1][0].x, - line[1][0].y, - line[1][0].z - ); - const distance = point1.distanceTo(point2); - const midpoint = new THREE.Vector3() - .addVectors(point1, point2) - .divideScalar(2); - return { - distance: distance.toFixed(1), - position: midpoint, - userData: line, - layer: activeLayer, - }; - }); - setLines(lines); - }); - }, [activeLayer]); - - useEffect(() => { - if (newLines.length > 0) { - if (newLines[0][0][2] !== activeLayer) return; - const newLinesData = newLines.map((line: Types.Line) => { - const point1 = new THREE.Vector3( - line[0][0].x, - line[0][0].y, - line[0][0].z - ); - const point2 = new THREE.Vector3( - line[1][0].x, - line[1][0].y, - line[1][0].z - ); - const distance = point1.distanceTo(point2); - const midpoint = new THREE.Vector3() - .addVectors(point1, point2) - .divideScalar(2); - - return { - distance: distance.toFixed(1), - position: midpoint, - userData: line, - layer: activeLayer, - }; - }); - setLines((prevLines) => [...prevLines, ...newLinesData]); - setNewLines([]); - } - }, [newLines, activeLayer]); - - useEffect(() => { - if ((deletedLines as Types.Lines).length > 0) { - setLines((prevLines) => - prevLines.filter( - (line) => - !deletedLines.some( - (deletedLine: any) => - deletedLine[0][1] === line.userData[0][1] && - deletedLine[1][1] === line.userData[1][1] - ) - ) - ); - setDeletedLines([]); - } - }, [deletedLines]); - - return ( - <> - {toggleView && ( - - {lines.map((text) => ( - -
- {text.distance} m -
- - ))} -
- )} - - ); -}; - -export default DistanceText; +import { useEffect, useState } from "react"; +import { getLines } from "../../../../../services/factoryBuilder/lines/getLinesApi"; +import * as THREE from "three"; +import { + useActiveLayer, + useDeletedLines, + useNewLines, + useToggleView, +} from "../../../../../store/store"; +import objectLinesToArray from "../lineConvertions/objectLinesToArray"; +import { Html } from "@react-three/drei"; +import * as Types from "../../../../../types/world/worldTypes"; + +const DistanceText = () => { + const [lines, setLines] = useState< + { + distance: string; + position: THREE.Vector3; + userData: Types.Line; + layer: string; + }[] + >([]); + const { activeLayer } = useActiveLayer(); + const { toggleView } = useToggleView(); + const { newLines, setNewLines } = useNewLines(); + const { deletedLines, setDeletedLines } = useDeletedLines(); + + useEffect(() => { + const email = localStorage.getItem("email"); + if (!email) return; + const organization = email.split("@")[1].split(".")[0]; + + getLines(organization).then((data) => { + data = objectLinesToArray(data); + + const lines = data + .filter((line: Types.Line) => line[0][2] === activeLayer) + .map((line: Types.Line) => { + const point1 = new THREE.Vector3( + line[0][0].x, + line[0][0].y, + line[0][0].z + ); + const point2 = new THREE.Vector3( + line[1][0].x, + line[1][0].y, + line[1][0].z + ); + const distance = point1.distanceTo(point2); + const midpoint = new THREE.Vector3() + .addVectors(point1, point2) + .divideScalar(2); + return { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + }); + setLines(lines); + }); + }, [activeLayer]); + + useEffect(() => { + if (newLines.length > 0) { + if (newLines[0][0][2] !== activeLayer) return; + const newLinesData = newLines.map((line: Types.Line) => { + const point1 = new THREE.Vector3( + line[0][0].x, + line[0][0].y, + line[0][0].z + ); + const point2 = new THREE.Vector3( + line[1][0].x, + line[1][0].y, + line[1][0].z + ); + const distance = point1.distanceTo(point2); + const midpoint = new THREE.Vector3() + .addVectors(point1, point2) + .divideScalar(2); + + return { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + }); + setLines((prevLines) => [...prevLines, ...newLinesData]); + setNewLines([]); + } + }, [newLines, activeLayer]); + + useEffect(() => { + if ((deletedLines as Types.Lines).length > 0) { + setLines((prevLines) => + prevLines.filter( + (line) => + !deletedLines.some( + (deletedLine: any) => + deletedLine[0][1] === line.userData[0][1] && + deletedLine[1][1] === line.userData[1][1] + ) + ) + ); + setDeletedLines([]); + } + }, [deletedLines]); + + return ( + <> + {toggleView && ( + + {lines.map((text) => ( + +
+ {text.distance} m +
+ + ))} +
+ )} + + ); +}; + +export default DistanceText; diff --git a/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx b/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx index 039d30f..7342b1e 100644 --- a/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx +++ b/app/src/modules/builder/geomentries/lines/distanceText/referenceDistanceText.tsx @@ -1,71 +1,71 @@ -import * as THREE from "three"; -import { Html } from "@react-three/drei"; -import { useState, useEffect } from "react"; -import { useActiveLayer } from "../../../../../store/store"; - -const ReferenceDistanceText = ({ line }: { line: any }) => { - interface TextState { - distance: string; - position: THREE.Vector3; - userData: any; - layer: any; - } - - const [text, setTexts] = useState(null); - const { activeLayer } = useActiveLayer(); - - useEffect(() => { - if (line) { - if (line.parent === null) { - setTexts(null); - return; - } - const distance = line.userData.linePoints.cursorPosition.distanceTo( - line.userData.linePoints.startPoint - ); - const midpoint = new THREE.Vector3() - .addVectors( - line.userData.linePoints.cursorPosition, - line.userData.linePoints.startPoint - ) - .divideScalar(2); - const newTexts = { - distance: distance.toFixed(1), - position: midpoint, - userData: line, - layer: activeLayer, - }; - setTexts(newTexts); - } - }); - - return ( - - - {text !== null && ( - -
- {text.distance} m -
- - )} -
-
- ); -}; - -export default ReferenceDistanceText; +import * as THREE from "three"; +import { Html } from "@react-three/drei"; +import { useState, useEffect } from "react"; +import { useActiveLayer } from "../../../../../store/store"; + +const ReferenceDistanceText = ({ line }: { line: any }) => { + interface TextState { + distance: string; + position: THREE.Vector3; + userData: any; + layer: any; + } + + const [text, setTexts] = useState(null); + const { activeLayer } = useActiveLayer(); + + useEffect(() => { + if (line) { + if (line.parent === null) { + setTexts(null); + return; + } + const distance = line.userData.linePoints.cursorPosition.distanceTo( + line.userData.linePoints.startPoint + ); + const midpoint = new THREE.Vector3() + .addVectors( + line.userData.linePoints.cursorPosition, + line.userData.linePoints.startPoint + ) + .divideScalar(2); + const newTexts = { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + setTexts(newTexts); + } + }); + + return ( + + + {text !== null && ( + +
+ {text.distance} m +
+ + )} +
+
+ ); +}; + +export default ReferenceDistanceText; diff --git a/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts index ea5ea74..c93b660 100644 --- a/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts +++ b/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts @@ -20,6 +20,7 @@ async function loadInitialFloorItems( const organization = (email!.split("@")[1]).split(".")[0]; const items = await getFloorAssets(organization); + console.log('items: ', items); localStorage.setItem("FloorItems", JSON.stringify(items)); await initializeDB(); diff --git a/app/src/modules/scene/camera/camMode.tsx b/app/src/modules/scene/camera/camMode.tsx index ba1aa0f..b4a59b0 100644 --- a/app/src/modules/scene/camera/camMode.tsx +++ b/app/src/modules/scene/camera/camMode.tsx @@ -5,6 +5,7 @@ import { useCamMode, useToggleView } from '../../../store/store'; import { useKeyboardControls } from '@react-three/drei'; import switchToThirdPerson from './switchToThirdPerson'; import switchToFirstPerson from './switchToFirstPerson'; +import { detectModifierKeys } from '../../../utils/shortcutkeys/detectModifierKeys'; const CamMode: React.FC = () => { const { camMode, setCamMode } = useCamMode(); @@ -37,7 +38,9 @@ const CamMode: React.FC = () => { const handleKeyPress = async (event: any) => { if (!state.controls) return; - if (event.key === "/" && !isTransitioning && !toggleView) { + const keyCombination = detectModifierKeys(event); + + if (keyCombination === "/" && !isTransitioning && !toggleView) { setIsTransitioning(true); state.controls.mouseButtons.left = CONSTANTS.controlsTransition.leftMouse; state.controls.mouseButtons.right = CONSTANTS.controlsTransition.rightMouse; @@ -81,9 +84,7 @@ const CamMode: React.FC = () => { } }); - return ( - <> - ); + return null; // This component does not render any UI }; export default CamMode; \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/copyPasteControls.tsx b/app/src/modules/scene/controls/selection/copyPasteControls.tsx index 5af10c0..5c07649 100644 --- a/app/src/modules/scene/controls/selection/copyPasteControls.tsx +++ b/app/src/modules/scene/controls/selection/copyPasteControls.tsx @@ -5,6 +5,7 @@ import { useFloorItems, useSelectedAssets, useSimulationStates, useSocketStore, import { toast } from "react-toastify"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; import * as Types from "../../../../types/world/worldTypes"; +import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, selectionGroup, setDuplicatedObjects, movedObjects, setMovedObjects, rotatedObjects, setRotatedObjects, boundingBoxRef }: any) => { const { camera, controls, gl, scene, pointer, raycaster } = useThree(); @@ -38,10 +39,12 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas }; const onKeyDown = (event: KeyboardEvent) => { - if (event.ctrlKey && event.key.toLowerCase() === "c" && movedObjects.length === 0 && rotatedObjects.length === 0) { + const keyCombination = detectModifierKeys(event); + + if (keyCombination === "Ctrl+C" && movedObjects.length === 0 && rotatedObjects.length === 0) { copySelection(); } - if (event.ctrlKey && event.key.toLowerCase() === "v" && copiedObjects.length > 0 && pastedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + if (keyCombination === "Ctrl+V" && copiedObjects.length > 0 && pastedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { pasteCopiedObjects(); } }; @@ -263,7 +266,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -341,7 +344,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -419,7 +422,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -570,9 +573,7 @@ const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pas setSelectedAssets([]); } - return ( - <> - ); + return null; // No visible output, but the component handles copy-paste functionality }; export default CopyPasteControls; \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/duplicationControls.tsx b/app/src/modules/scene/controls/selection/duplicationControls.tsx index 2964181..a127a30 100644 --- a/app/src/modules/scene/controls/selection/duplicationControls.tsx +++ b/app/src/modules/scene/controls/selection/duplicationControls.tsx @@ -6,6 +6,7 @@ import { toast } from "react-toastify"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; import * as Types from "../../../../types/world/worldTypes"; import { setFloorItemApi } from "../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi"; +import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedObjects, setpastedObjects, selectionGroup, movedObjects, setMovedObjects, rotatedObjects, setRotatedObjects, boundingBoxRef }: any) => { const { camera, controls, gl, scene, pointer, raycaster } = useThree(); @@ -39,12 +40,11 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb }; const onKeyDown = (event: KeyboardEvent) => { - if (event.key.toLowerCase() === "d") { - event.preventDefault(); - if (event.ctrlKey && event.key.toLowerCase() === "d" && selectedAssets.length > 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + const keyCombination = detectModifierKeys(event); + + if (keyCombination === "Ctrl+D" && selectedAssets.length > 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { duplicateSelection(); } - } }; if (!toggleView) { @@ -245,7 +245,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -323,7 +323,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -401,7 +401,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb return { uuid: pointUUID, position: vehiclePoint?.position, - rotation: vehiclePoint?.rotation, + // rotation: vehiclePoint?.rotation, actions: hasActions ? { ...vehiclePoint.actions, @@ -552,9 +552,7 @@ const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedOb setSelectedAssets([]); } - return ( - <> - ); + return null; // This component does not render any UI }; export default DuplicationControls; \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/moveControls.tsx b/app/src/modules/scene/controls/selection/moveControls.tsx index 2a11c53..47a3747 100644 --- a/app/src/modules/scene/controls/selection/moveControls.tsx +++ b/app/src/modules/scene/controls/selection/moveControls.tsx @@ -5,6 +5,7 @@ import { useFloorItems, useSelectedAssets, useSimulationStates, useSocketStore, // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; import { toast } from "react-toastify"; import * as Types from "../../../../types/world/worldTypes"; +import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, duplicatedObjects, setDuplicatedObjects, selectionGroup, rotatedObjects, setRotatedObjects, boundingBoxRef }: any) { const { camera, controls, gl, scene, pointer, raycaster } = useThree(); @@ -56,14 +57,16 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje }; const onKeyDown = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); + if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0) return; - if (event.key.toLowerCase() === "g") { + if (keyCombination === "G") { if (selectedAssets.length > 0) { moveAssets(); itemsData.current = floorItems.filter((item: { modeluuid: string }) => selectedAssets.some((asset: any) => asset.uuid === item.modeluuid)); } } - if (event.key.toLowerCase() === "escape") { + if (keyCombination === "ESCAPE") { event.preventDefault(); clearSelection(); @@ -453,9 +456,7 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje setSelectedAssets([]); } - return ( - <> - ) + return null; // No need to return anything, as this component is used for its side effects } export default MoveControls \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/rotateControls.tsx b/app/src/modules/scene/controls/selection/rotateControls.tsx index 90143ca..bef64a2 100644 --- a/app/src/modules/scene/controls/selection/rotateControls.tsx +++ b/app/src/modules/scene/controls/selection/rotateControls.tsx @@ -457,9 +457,7 @@ function RotateControls({ rotatedObjects, setRotatedObjects, movedObjects, setMo setSelectedAssets([]); } - return ( - <> - ) + return null; // No need to return anything, as this component is used for its side effects } export default RotateControls \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/selectionControls.tsx b/app/src/modules/scene/controls/selection/selectionControls.tsx index 11c2dfb..acaed3a 100644 --- a/app/src/modules/scene/controls/selection/selectionControls.tsx +++ b/app/src/modules/scene/controls/selection/selectionControls.tsx @@ -14,6 +14,7 @@ import CopyPasteControls from "./copyPasteControls"; import MoveControls from "./moveControls"; import RotateControls from "./rotateControls"; import useModuleStore from "../../../../store/useModuleStore"; +import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; const SelectionControls: React.FC = () => { const { camera, controls, gl, scene, pointer } = useThree(); @@ -101,12 +102,13 @@ const SelectionControls: React.FC = () => { }; const onKeyDown = (event: KeyboardEvent) => { + const keyCombination = detectModifierKeys(event); if (movedObjects.length > 0 || rotatedObjects.length > 0) return; - if (event.key.toLowerCase() === "escape") { + if (keyCombination === "ESCAPE") { event.preventDefault(); clearSelection(); } - if (event.key.toLowerCase() === "delete") { + if (keyCombination === "DELETE") { event.preventDefault(); deleteSelection(); } diff --git a/app/src/modules/simulation/path/pathCreation.tsx b/app/src/modules/simulation/path/pathCreation.tsx index 839cf39..345fff3 100644 --- a/app/src/modules/simulation/path/pathCreation.tsx +++ b/app/src/modules/simulation/path/pathCreation.tsx @@ -16,6 +16,7 @@ import { useFrame, useThree } from "@react-three/fiber"; import { useSubModuleStore } from "../../../store/useModuleStore"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; import { setEventApi } from "../../../services/factoryBuilder/assest/floorAsset/setEventsApt"; +import { detectModifierKeys } from "../../../utils/shortcutkeys/detectModifierKeys"; function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObject; }) { const { isPlaying } = usePlayButtonStore(); @@ -40,11 +41,12 @@ function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObjec useEffect(() => { setTransformMode(null); const handleKeyDown = (e: KeyboardEvent) => { + const keyCombination = detectModifierKeys(e); if (!selectedActionSphere) return; - if (e.key === "g") { + if (keyCombination === "G") { setTransformMode((prev) => (prev === "translate" ? null : "translate")); } - if (e.key === "r") { + if (keyCombination === "R") { setTransformMode((prev) => (prev === "rotate" ? null : "rotate")); } }; @@ -333,7 +335,6 @@ function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObjec key={path.modeluuid} ref={(el) => (groupRefs.current[path.modeluuid] = el!)} position={path.position} - rotation={path.rotation} onClick={(e) => { if (isConnecting || eyeDropMode) return; e.stopPropagation(); @@ -387,7 +388,6 @@ function PathCreation({ pathsGroupRef, }: { pathsGroupRef: React.MutableRefObjec key={path.modeluuid} ref={(el) => (groupRefs.current[path.modeluuid] = el!)} position={path.position} - rotation={path.rotation} onClick={(e) => { if (isConnecting || eyeDropMode) return; e.stopPropagation(); diff --git a/app/src/modules/simulation/process/mesh.tsx b/app/src/modules/simulation/process/mesh.tsx deleted file mode 100644 index f6b94bb..0000000 --- a/app/src/modules/simulation/process/mesh.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const Mesh: React.FC = () => { - return ; -}; - -export default Mesh; diff --git a/app/src/modules/simulation/process/processAnimator.tsx b/app/src/modules/simulation/process/processAnimator.tsx index c8cf07f..7b906d2 100644 --- a/app/src/modules/simulation/process/processAnimator.tsx +++ b/app/src/modules/simulation/process/processAnimator.tsx @@ -1,502 +1,119 @@ -import React, { useRef, useState, useEffect, useMemo } from "react"; -import { - useAnimationPlaySpeed, - usePauseButtonStore, - usePlayButtonStore, - useResetButtonStore, -} from "../../../store/usePlayButtonStore"; -import { GLTFLoader } from "three-stdlib"; +import React, { useRef, useEffect, useMemo } from "react"; import { useLoader, useFrame } from "@react-three/fiber"; +import { GLTFLoader } from "three-stdlib"; import * as THREE from "three"; import { GLTF } from "three-stdlib"; import crate from "../../../assets/gltf-glb/crate_box.glb"; -import box from "../../../assets/gltf-glb/box.glb"; -interface PointAction { - uuid: string; - name: string; - type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap"; - objectType: string; - material: string; - delay: string | number; - spawnInterval: string | number; - isUsed: boolean; +import { useProcessAnimation } from "./useProcessAnimations"; +import ProcessObject from "./processObject"; +import { ProcessData } from "./types"; +import { useSimulationStates } from "../../../store/store"; +import { retrieveGLTF } from "../../../utils/indexDB/idbUtils"; + +interface ProcessContainerProps { + processes: ProcessData[]; + setProcesses: React.Dispatch>; + agvRef: any; + MaterialRef: any; } -interface ProcessPoint { - uuid: string; - position: number[]; - rotation: number[]; - actions: PointAction[]; - connections: { - source: { modelUUID: string; pointUUID: string }; - targets: { modelUUID: string; pointUUID: string }[]; - }; -} - -interface ProcessPath { - modeluuid: string; - modelName: string; - points: ProcessPoint[]; - pathPosition: number[]; - pathRotation: number[]; - speed: number; -} - -interface ProcessData { - id: string; - paths: ProcessPath[]; - animationPath: { x: number; y: number; z: number }[]; - pointActions: PointAction[][]; - speed: number; - customMaterials?: Record; - renderAs?: "box" | "custom"; -} - -interface AnimationState { - currentIndex: number; - progress: number; - isAnimating: boolean; - speed: number; - isDelaying: boolean; - delayStartTime: number; - currentDelayDuration: number; - delayComplete: boolean; - currentPathIndex: number; -} - -interface SpawnedObject { - ref: React.RefObject; - state: AnimationState; - visible: boolean; - material: THREE.Material; - spawnTime: number; - currentMaterialType: string; - position: THREE.Vector3; -} - -interface ProcessAnimationState { - spawnedObjects: { [objectId: string]: SpawnedObject }; - nextSpawnTime: number; - objectIdCounter: number; - isProcessDelaying: boolean; - processDelayStartTime: number; - processDelayDuration: number; -} - -const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ +const ProcessAnimator: React.FC = ({ processes, + setProcesses, + agvRef, + MaterialRef, }) => { const gltf = useLoader(GLTFLoader, crate) as GLTF; - const { isPlaying, setIsPlaying } = usePlayButtonStore(); - const { isPaused, setIsPaused } = usePauseButtonStore(); - const { isReset, setReset } = useResetButtonStore(); const groupRef = useRef(null); - const debugRef = useRef(false); - const clockRef = useRef(new THREE.Clock()); - const pauseTimeRef = useRef(0); - const elapsedBeforePauseRef = useRef(0); - const animationStatesRef = useRef>({}); - const { speed, setSpeed } = useAnimationPlaySpeed(); - const prevIsPlaying = useRef(null); - const [internalResetFlag, setInternalResetFlag] = useState(false); - const [animationStates, setAnimationStates] = useState< - Record - >({}); - // Store the speed in a ref to access the latest value in animation frames - const speedRef = useRef(speed); + const { + animationStates, + setAnimationStates, + clockRef, + elapsedBeforePauseRef, + speedRef, + debugRef, + findSpawnPoint, + createSpawnedObject, + handlePointActions, + hasNonInheritActions, + getPointDataForAnimationIndex, + processes: processedProcesses, + checkAndCountTriggers, + } = useProcessAnimation(processes, setProcesses, agvRef); - // Update the ref when speed changes - useEffect(() => { - speedRef.current = speed; - }, [speed]); - - useEffect(() => { - if (prevIsPlaying.current !== null) { - setAnimationStates({}); - } - - // Update ref to current isPlaying after effect - prevIsPlaying.current = isPlaying; - - // setAnimationStates({}); - }, [isPlaying]); - - // Sync ref with state - useEffect(() => { - animationStatesRef.current = animationStates; - }, [animationStates]); - - // Base materials const baseMaterials = useMemo( () => ({ Box: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), Crate: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), - Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + // Default: new THREE.MeshStandardMaterial({ color: 0x00ff00 }), + Default: new THREE.MeshStandardMaterial(), }), [] ); - // Replace your reset effect with this: useEffect(() => { - if (isReset) { - // 1. Mark that we're doing an internal reset - setInternalResetFlag(true); - - // 2. Pause the animation first - setIsPlaying(false); - setIsPaused(false); - - // 3. Reset all animation states - setAnimationStates({}); - animationStatesRef.current = {}; - - // 4. Reset timing references - clockRef.current = new THREE.Clock(); - elapsedBeforePauseRef.current = 0; - pauseTimeRef.current = 0; - - // 5. Clear the external reset flag - setReset(false); - - // 6. After state updates are complete, restart - setTimeout(() => { - setInternalResetFlag(false); - setIsPlaying(true); - }, 0); - } - }, [isReset, setReset, setIsPlaying, setIsPaused]); - - // Handle pause state changes - useEffect(() => { - if (isPaused) { - pauseTimeRef.current = clockRef.current.getElapsedTime(); - } else if (pauseTimeRef.current > 0) { - const pausedDuration = - clockRef.current.getElapsedTime() - pauseTimeRef.current; - elapsedBeforePauseRef.current += pausedDuration; - } - }, [isPaused]); - - // Initialize animation states when processes or play state changes - useEffect(() => { - if (isPlaying && !internalResetFlag) { - const newStates: Record = {}; - processes.forEach((process) => { - newStates[process.id] = { - spawnedObjects: {}, - nextSpawnTime: 0, - objectIdCounter: 0, - isProcessDelaying: false, - processDelayStartTime: 0, - processDelayDuration: 0, + // Update material references for all spawned objects + Object.entries(animationStates).forEach(([processId, processState]) => { + Object.keys(processState.spawnedObjects).forEach((objectId) => { + const entry = { + processId, + objectId, }; - }); - setAnimationStates(newStates); - animationStatesRef.current = newStates; - clockRef.current.start(); - } - }, [isPlaying, processes, internalResetFlag]); - const findSpawnPoint = (process: ProcessData): ProcessPoint | null => { - for (const path of process.paths || []) { - for (const point of path.points || []) { - const spawnAction = point.actions?.find( - (a) => a.isUsed && a.type === "Spawn" + const materialType = + processState.spawnedObjects[objectId]?.currentMaterialType; + + if (!materialType) { + return; + } + + const matRefArray = MaterialRef.current; + + // Find existing material group + const existing = matRefArray.find( + (entryGroup: { material: string; objects: any[] }) => + entryGroup.material === materialType ); - if (spawnAction) { - return point; - } - } - } - return null; - }; - const findAnimationPathPoint = ( - process: ProcessData, - spawnPoint: ProcessPoint - ): THREE.Vector3 => { - if (process.animationPath && process.animationPath.length > 0) { - let pointIndex = 0; - for (const path of process.paths || []) { - for (let i = 0; i < (path.points?.length || 0); i++) { - const point = path.points?.[i]; - if (point && point.uuid === spawnPoint.uuid) { - if (process.animationPath[pointIndex]) { - const p = process.animationPath[pointIndex]; - return new THREE.Vector3(p.x, p.y, p.z); - } + if (existing) { + // Check if this processId + objectId already exists + const alreadyExists = existing.objects.some( + (o: any) => + o.processId === entry.processId && o.objectId === entry.objectId + ); + + if (!alreadyExists) { + existing.objects.push(entry); } - pointIndex++; + } else { + // Create new group for this material type + matRefArray.push({ + material: materialType, + objects: [entry], + }); } - } - } - return new THREE.Vector3( - spawnPoint.position[0], - spawnPoint.position[1], - spawnPoint.position[2] - ); - }; - - const createSpawnedObject = ( - process: ProcessData, - currentTime: number, - materialType: string, - spawnPoint: ProcessPoint - ): SpawnedObject => { - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const spawnPosition = findAnimationPathPoint(process, spawnPoint); - const material = - processMaterials[materialType as keyof typeof processMaterials] || - baseMaterials.Default; - - if (debugRef.current) { - console.log(`Creating object with material: ${materialType}`, material); - } - - return { - ref: React.createRef(), - state: { - currentIndex: 0, - progress: 0, - isAnimating: true, - speed: process.speed || 1, // Process base speed (will be multiplied by global speed) - isDelaying: false, - delayStartTime: 0, - currentDelayDuration: 0, - delayComplete: false, - currentPathIndex: 0, - }, - visible: true, - material: material, - currentMaterialType: materialType, - spawnTime: currentTime, - position: spawnPosition, - }; - }; - - const handleMaterialSwap = ( - processId: string, - objectId: string, - materialType: string - ) => { - if (debugRef.current) { - console.log(`Attempting material swap to: ${materialType}`); - } - - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState || !processState.spawnedObjects[objectId]) { - if (debugRef.current) console.log("Object not found for swap"); - return prev; - } - - const process = processes.find((p) => p.id === processId); - if (!process) { - if (debugRef.current) console.log("Process not found"); - return prev; - } - - const processMaterials = { - ...baseMaterials, - ...(process.customMaterials || {}), - }; - - const newMaterial = - processMaterials[materialType as keyof typeof processMaterials]; - if (!newMaterial) { - if (debugRef.current) console.log(`Material ${materialType} not found`); - return prev; - } - - if (debugRef.current) { - console.log(`Swapping material for ${objectId} to ${materialType}`); - } - - return { - ...prev, - [processId]: { - ...processState, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - material: newMaterial, - currentMaterialType: materialType, - }, - }, - }, - }; + }); }); - }; + }, [animationStates, MaterialRef, agvRef]); - const handlePointActions = ( - processId: string, - objectId: string, - actions: PointAction[] = [], - currentTime: number - ): boolean => { - let shouldStopAnimation = false; - - actions.forEach((action) => { - if (!action.isUsed) return; - - if (debugRef.current) { - console.log(`Processing action: ${action.type} for ${objectId}`); - } - - switch (action.type) { - case "Delay": - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState || processState.isProcessDelaying) { - return prev; - } - - const delayDuration = - typeof action.delay === "number" - ? action.delay - : parseFloat(action.delay as string) || 0; - - if (delayDuration > 0) { - return { - ...prev, - [processId]: { - ...processState, - isProcessDelaying: true, - processDelayStartTime: currentTime, - processDelayDuration: delayDuration, - spawnedObjects: { - ...processState.spawnedObjects, - [objectId]: { - ...processState.spawnedObjects[objectId], - state: { - ...processState.spawnedObjects[objectId].state, - isAnimating: false, - isDelaying: true, - delayStartTime: currentTime, - currentDelayDuration: delayDuration, - delayComplete: false, - }, - }, - }, - }, - }; - } - return prev; - }); - shouldStopAnimation = true; - break; - - case "Despawn": - setAnimationStates((prev) => { - const processState = prev[processId]; - if (!processState) return prev; - - const newSpawnedObjects = { ...processState.spawnedObjects }; - delete newSpawnedObjects[objectId]; - - return { - ...prev, - [processId]: { - ...processState, - spawnedObjects: newSpawnedObjects, - }, - }; - }); - shouldStopAnimation = true; - break; - - case "Swap": - if (action.material) { - handleMaterialSwap(processId, objectId, action.material); - } - break; - - default: - break; - } - }); - - return shouldStopAnimation; - }; - - const hasNonInheritActions = (actions: PointAction[] = []): boolean => { - return actions.some((action) => action.isUsed && action.type !== "Inherit"); - }; - - const getPointDataForAnimationIndex = ( - process: ProcessData, - index: number - ): ProcessPoint | null => { - if (!process.paths) return null; - - let cumulativePoints = 0; - for (const path of process.paths) { - const pointCount = path.points?.length || 0; - - if (index < cumulativePoints + pointCount) { - const pointIndex = index - cumulativePoints; - return path.points?.[pointIndex] || null; - } - - cumulativePoints += pointCount; - } - - return null; - }; + // In processAnimator.tsx - only the relevant spawn logic part that needs fixes useFrame(() => { - if (!isPlaying || isPaused) return; - + // Spawn logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; - processes.forEach((process) => { + processedProcesses.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; if (processState.isProcessDelaying) { - // Apply global speed to delays (faster speed = shorter delays) - const effectiveDelayTime = - processState.processDelayDuration / speedRef.current; - - if ( - currentTime - processState.processDelayStartTime >= - effectiveDelayTime - ) { - newStates[process.id] = { - ...processState, - isProcessDelaying: false, - spawnedObjects: Object.entries( - processState.spawnedObjects - ).reduce( - (acc, [id, obj]) => ({ - ...acc, - [id]: { - ...obj, - state: { - ...obj.state, - isDelaying: false, - delayComplete: true, - isAnimating: true, - progress: - obj.state.progress === 0 ? 0.001 : obj.state.progress, - }, - }, - }), - {} - ), - }; - } + // Existing delay handling logic... return; } @@ -513,7 +130,14 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ ? spawnAction.spawnInterval : parseFloat(spawnAction.spawnInterval as string) || 0; - // Apply global speed to spawn intervals (faster speed = more frequent spawns) + // Check if this is a zero interval spawn and we already spawned an object + if ( + spawnInterval === 0 && + processState.hasSpawnedZeroIntervalObject === true + ) { + return; // Don't spawn more objects for zero interval + } + const effectiveSpawnInterval = spawnInterval / speedRef.current; if (currentTime >= processState.nextSpawnTime) { @@ -522,9 +146,11 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ process, currentTime, spawnAction.material || "Default", - spawnPoint + spawnPoint, + baseMaterials ); + // Update state with the new object and flag for zero interval newStates[process.id] = { ...processState, spawnedObjects: { @@ -533,6 +159,11 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ }, objectIdCounter: processState.objectIdCounter + 1, nextSpawnTime: currentTime + effectiveSpawnInterval, + // Mark that we've spawned an object for zero interval case + hasSpawnedZeroIntervalObject: + spawnInterval === 0 + ? true + : processState.hasSpawnedZeroIntervalObject, }; } }); @@ -542,20 +173,18 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ }); useFrame((_, delta) => { - if (!isPlaying || isPaused) return; - + // Animation logic frame const currentTime = clockRef.current.getElapsedTime() - elapsedBeforePauseRef.current; setAnimationStates((prev) => { const newStates = { ...prev }; - processes.forEach((process) => { + processedProcesses.forEach((process) => { const processState = newStates[process.id]; if (!processState) return; if (processState.isProcessDelaying) { - // Apply global speed to delays (faster speed = shorter delays) const effectiveDelayTime = processState.processDelayDuration / speedRef.current; @@ -617,7 +246,6 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ const stateRef = obj.state; if (stateRef.isDelaying) { - // Apply global speed to delays (faster speed = shorter delays) const effectiveDelayTime = stateRef.currentDelayDuration / speedRef.current; @@ -654,18 +282,15 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ stateRef.currentIndex ); + // Handle point actions when first arriving at point if (stateRef.progress === 0 && currentPointData?.actions) { - if (debugRef.current) { - console.log( - `At point ${stateRef.currentIndex} with actions:`, - currentPointData.actions - ); - } const shouldStop = handlePointActions( process.id, objectId, currentPointData.actions, - currentTime + currentTime, + processedProcesses, + baseMaterials ); if (shouldStop) { updatedObjects[objectId] = { ...obj, state: { ...stateRef } }; @@ -676,14 +301,39 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ const nextPointIdx = stateRef.currentIndex + 1; const isLastPoint = nextPointIdx >= path.length; + // if (isLastPoint) { + // if (currentPointData?.actions) { + // const shouldStop = !hasNonInheritActions( + // currentPointData.actions + // ); + // if (shouldStop) { + // return; + // } + // } + // } + if (isLastPoint) { if (currentPointData?.actions) { - const shouldStop = !hasNonInheritActions( + const hasNonInherit = hasNonInheritActions( currentPointData.actions ); - if (shouldStop) { + if (!hasNonInherit) { + // Remove the object if all actions are inherit + updatedObjects[objectId] = { + ...obj, + visible: false, + state: { ...stateRef, isAnimating: false }, + }; return; } + } else { + // No actions at last point - remove the object + updatedObjects[objectId] = { + ...obj, + visible: false, + state: { ...stateRef, isAnimating: false }, + }; + return; } } @@ -691,8 +341,6 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ const nextPoint = path[nextPointIdx]; const distance = path[stateRef.currentIndex].distanceTo(nextPoint); - - // Apply both process-specific speed and global speed multiplier const effectiveSpeed = stateRef.speed * speedRef.current; const movement = effectiveSpeed * delta; @@ -708,17 +356,19 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ stateRef.progress = 0; currentRef.position.copy(nextPoint); + // TRIGGER CHECK - When object arrives at new point + checkAndCountTriggers( + process.id, + objectId, + stateRef.currentIndex, // The new point index + processedProcesses, + currentTime + ); + const newPointData = getPointDataForAnimationIndex( process, stateRef.currentIndex ); - - if (newPointData?.actions && debugRef.current) { - console.log( - `Reached new point with actions:`, - newPointData.actions - ); - } } else { currentRef.position.lerpVectors( path[stateRef.currentIndex], @@ -742,56 +392,32 @@ const ProcessAnimator: React.FC<{ processes: ProcessData[] }> = ({ }); }); - if (!processes || processes.length === 0) { + if (!processedProcesses || processedProcesses.length === 0) { return null; } return ( - <> + {Object.entries(animationStates).flatMap(([processId, processState]) => Object.entries(processState.spawnedObjects) .filter(([_, obj]) => obj.visible) .map(([objectId, obj]) => { - const process = processes.find((p) => p.id === processId); + const process = processedProcesses.find((p) => p.id === processId); + const renderAs = process?.renderAs || "custom"; - if (renderAs === "box") { - return ( - } - material={obj.material} - position={obj.position} - > - - - ); - } - - if (gltf?.scene) { - // Clone the scene and apply the material to all meshes - const clonedScene = gltf.scene.clone(); - clonedScene.traverse((child) => { - if (child instanceof THREE.Mesh) { - child.material = obj.material; - } - }); - - return ( - } - position={obj.position} - > - - - ); - } - - return null; + return ( + + ); }) )} - + ); }; diff --git a/app/src/modules/simulation/process/processContainer.tsx b/app/src/modules/simulation/process/processContainer.tsx index bf8fa48..1cbc75b 100644 --- a/app/src/modules/simulation/process/processContainer.tsx +++ b/app/src/modules/simulation/process/processContainer.tsx @@ -2,15 +2,28 @@ import React, { useState } from "react"; import ProcessCreator from "./processCreator"; import ProcessAnimator from "./processAnimator"; -const ProcessContainer: React.FC = () => { - const [processes, setProcesses] = useState([]); - +interface ProcessContainerProps { + processes: any[]; + setProcesses: React.Dispatch>; + agvRef: any; + MaterialRef: any; +} +const ProcessContainer: React.FC = ({ + processes, + setProcesses, + agvRef, + MaterialRef, +}) => { return ( <> - {processes.length > 0 && } - + ); }; diff --git a/app/src/modules/simulation/process/processCreator.tsx b/app/src/modules/simulation/process/processCreator.tsx index 4403a71..cae152b 100644 --- a/app/src/modules/simulation/process/processCreator.tsx +++ b/app/src/modules/simulation/process/processCreator.tsx @@ -24,16 +24,26 @@ // isUsed: boolean; // } +// export interface PointTrigger { +// uuid: string; +// bufferTime: number; +// name: string; +// type: string; +// isUsed: boolean; +// } + // export interface PathPoint { // uuid: string; // position: [number, number, number]; // actions: PointAction[]; +// triggers: PointTrigger[]; // connections: { // targets: Array<{ modelUUID: string }>; // }; // } // export interface SimulationPath { +// type: string; // modeluuid: string; // points: PathPoint[]; // pathPosition: [number, number, number]; @@ -45,7 +55,9 @@ // paths: SimulationPath[]; // animationPath: THREE.Vector3[]; // pointActions: PointAction[][]; +// pointTriggers: PointTrigger[][]; // speed: number; +// isplaying: boolean; // } // interface ProcessCreatorProps { @@ -58,18 +70,33 @@ // ): SimulationPath { // const { modeluuid } = path; -// // Simplified normalizeAction function that preserves exact original properties +// // Normalized action handler // const normalizeAction = (action: any): PointAction => { // return { ...action }; // Return exact copy with no modifications // }; +// // Normalized trigger handler +// const normalizeTrigger = (trigger: any): PointTrigger => { +// return { ...trigger }; // Return exact copy with no modifications +// }; + // if (path.type === "Conveyor") { // return { +// type: path.type, // modeluuid, // points: path.points.map((point) => ({ // uuid: point.uuid, // position: point.position, -// actions: point.actions.map(normalizeAction), // Preserve exact actions +// actions: Array.isArray(point.actions) +// ? point.actions.map(normalizeAction) +// : point.actions +// ? [normalizeAction(point.actions)] +// : [], +// triggers: Array.isArray(point.triggers) +// ? point.triggers.map(normalizeTrigger) +// : point.triggers +// ? [normalizeTrigger(point.triggers)] +// : [], // connections: { // targets: point.connections.targets.map((target) => ({ // modelUUID: target.modelUUID, @@ -83,44 +110,44 @@ // : path.speed || 1, // }; // } else { +// // For vehicle paths, handle the case where triggers might not exist // return { +// type: path.type, // modeluuid, // points: [ // { -// uuid: path.point.uuid, -// position: path.point.position, -// actions: Array.isArray(path.point.actions) -// ? path.point.actions.map(normalizeAction) -// : [normalizeAction(path.point.actions)], +// uuid: path.points.uuid, +// position: path.points.position, +// actions: Array.isArray(path.points.actions) +// ? path.points.actions.map(normalizeAction) +// : path.points.actions +// ? [normalizeAction(path.points.actions)] +// : [], +// // For vehicle paths, since triggers might not exist in the schema, +// // we always define default to an empty array +// triggers: [], // connections: { -// targets: path.point.connections.targets.map((target) => ({ +// targets: path.points.connections.targets.map((target) => ({ // modelUUID: target.modelUUID, // })), // }, // }, // ], // pathPosition: path.position, -// speed: path.point.speed || 1, +// speed: path.points.speed || 1, // }; // } // } -// // Custom shallow comparison for arrays -// const areArraysEqual = (a: any[], b: any[]) => { -// if (a.length !== b.length) return false; -// for (let i = 0; i < a.length; i++) { -// if (a[i] !== b[i]) return false; -// } -// return true; -// }; - // // Helper function to create an empty process // const createEmptyProcess = (): Process => ({ // id: `process-${Math.random().toString(36).substring(2, 11)}`, // paths: [], // animationPath: [], // pointActions: [], +// pointTriggers: [], // Added point triggers array // speed: 1, +// isplaying: false, // }); // // Enhanced connection checking function @@ -156,12 +183,38 @@ // return connectsToLast && !connectsToFirst; // } +// // Check if a point has a spawn action +// function hasSpawnAction(point: PathPoint): boolean { +// return point.actions.some((action) => action.type.toLowerCase() === "spawn"); +// } + +// // Ensure spawn point is always at the beginning of the path +// function ensureSpawnPointIsFirst(path: SimulationPath): SimulationPath { +// if (path.points.length !== 3) return path; + +// // If the third point has spawn action and first doesn't, reverse the array +// if (hasSpawnAction(path.points[2]) && !hasSpawnAction(path.points[0])) { +// return { +// ...path, +// points: [...path.points].reverse(), +// }; +// } + +// return path; +// } + // // Updated path adjustment function // function adjustPathPointsOrder(paths: SimulationPath[]): SimulationPath[] { -// if (paths.length < 2) return paths; +// if (paths.length < 1) return paths; // const adjustedPaths = [...paths]; +// // First ensure all paths have spawn points at the beginning +// for (let i = 0; i < adjustedPaths.length; i++) { +// adjustedPaths[i] = ensureSpawnPointIsFirst(adjustedPaths[i]); +// } + +// // Then handle connections between paths // for (let i = 0; i < adjustedPaths.length - 1; i++) { // const currentPath = adjustedPaths[i]; // const nextPath = adjustedPaths[i + 1]; @@ -189,6 +242,7 @@ // const [processes, setProcesses] = useState([]); // const hasSpawnAction = useCallback((path: SimulationPath): boolean => { +// if (path.type !== "Conveyor") return false; // return path.points.some((point) => // point.actions.some((action) => action.type.toLowerCase() === "spawn") // ); @@ -202,19 +256,23 @@ // const animationPath: THREE.Vector3[] = []; // const pointActions: PointAction[][] = []; +// const pointTriggers: PointTrigger[][] = []; // Added point triggers collection // const processSpeed = paths[0]?.speed || 1; // for (const path of paths) { // for (const point of path.points) { -// const obj = scene.getObjectByProperty("uuid", point.uuid); -// if (!obj) { -// console.warn(`Object with UUID ${point.uuid} not found in scene`); -// continue; -// } +// if (path.type === "Conveyor") { +// const obj = scene.getObjectByProperty("uuid", point.uuid); +// if (!obj) { +// console.warn(`Object with UUID ${point.uuid} not found in scene`); +// continue; +// } -// const position = obj.getWorldPosition(new THREE.Vector3()); -// animationPath.push(position.clone()); -// pointActions.push(point.actions); +// const position = obj.getWorldPosition(new THREE.Vector3()); +// animationPath.push(position.clone()); +// pointActions.push(point.actions); +// pointTriggers.push(point.triggers); // Collect triggers for each point +// } // } // } @@ -223,7 +281,9 @@ // paths, // animationPath, // pointActions, +// pointTriggers, // speed: processSpeed, +// isplaying: false, // }; // }, // [scene] @@ -326,21 +386,35 @@ // ); // }, [simulationStates]); +// // Enhanced dependency tracking that includes action and trigger types // const pathsDependency = useMemo(() => { // if (!convertedPaths) return null; // return convertedPaths.map((path) => ({ // id: path.modeluuid, -// hasSpawn: path.points.some((p: PathPoint) => -// p.actions.some((a: PointAction) => a.type.toLowerCase() === "spawn") -// ), +// // Track all action types for each point +// actionSignature: path.points +// .map((point, index) => +// point.actions.map((action) => `${index}-${action.type}`).join("|") +// ) +// .join(","), +// // Track all trigger types for each point +// triggerSignature: path.points +// .map((point, index) => +// point.triggers +// .map((trigger) => `${index}-${trigger.type}`) +// .join("|") +// ) +// .join(","), // connections: path.points // .flatMap((p: PathPoint) => // p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID) // ) // .join(","), +// isplaying: false, // })); // }, [convertedPaths]); +// // Force process recreation when paths change // useEffect(() => { // if (!convertedPaths || convertedPaths.length === 0) { // if (prevProcessesRef.current.length > 0) { @@ -350,42 +424,16 @@ // return; // } -// if (areArraysEqual(prevPathsRef.current, convertedPaths)) { -// return; -// } - -// prevPathsRef.current = convertedPaths; +// // Always regenerate processes if the pathsDependency has changed +// // This ensures action and trigger type changes will be detected // const newProcesses = createProcessesFromPaths(convertedPaths); +// prevPathsRef.current = convertedPaths; -// // console.log("--- Action Types in Paths ---"); -// // convertedPaths.forEach((path) => { -// // path.points.forEach((point) => { -// // point.actions.forEach((action) => { -// // console.log( -// // `Path ${path.modeluuid}, Point ${point.uuid}: ${action.type}` -// // ); -// // }); -// // }); -// // }); -// // console.log("New processes:", newProcesses); - -// if ( -// newProcesses.length !== prevProcessesRef.current.length || -// !newProcesses.every( -// (proc, i) => -// proc.paths.length === prevProcessesRef.current[i]?.paths.length && -// proc.paths.every( -// (path, j) => -// path.modeluuid === -// prevProcessesRef.current[i]?.paths[j]?.modeluuid -// ) -// ) -// ) { -// onProcessesCreated(newProcesses); -// // prevProcessesRef.current = newProcesses; -// } +// // Always update processes when action or trigger types change +// onProcessesCreated(newProcesses); +// prevProcessesRef.current = newProcesses; // }, [ -// pathsDependency, +// pathsDependency, // This now includes action and trigger types // onProcessesCreated, // convertedPaths, // createProcessesFromPaths, @@ -423,10 +471,19 @@ export interface PointAction { isUsed: boolean; } +export interface PointTrigger { + uuid: string; + bufferTime: number; + name: string; + type: string; + isUsed: boolean; +} + export interface PathPoint { uuid: string; position: [number, number, number]; actions: PointAction[]; + triggers: PointTrigger[]; connections: { targets: Array<{ modelUUID: string }>; }; @@ -438,6 +495,7 @@ export interface SimulationPath { points: PathPoint[]; pathPosition: [number, number, number]; speed?: number; + isplaying: boolean; } export interface Process { @@ -445,7 +503,9 @@ export interface Process { paths: SimulationPath[]; animationPath: THREE.Vector3[]; pointActions: PointAction[][]; + pointTriggers: PointTrigger[][]; speed: number; + isplaying: boolean; } interface ProcessCreatorProps { @@ -458,11 +518,16 @@ function convertToSimulationPath( ): SimulationPath { const { modeluuid } = path; - // Simplified normalizeAction function that preserves exact original properties + // Normalized action handler const normalizeAction = (action: any): PointAction => { return { ...action }; // Return exact copy with no modifications }; + // Normalized trigger handler + const normalizeTrigger = (trigger: any): PointTrigger => { + return { ...trigger }; // Return exact copy with no modifications + }; + if (path.type === "Conveyor") { return { type: path.type, @@ -470,7 +535,16 @@ function convertToSimulationPath( points: path.points.map((point) => ({ uuid: point.uuid, position: point.position, - actions: point.actions.map(normalizeAction), // Preserve exact actions + actions: Array.isArray(point.actions) + ? point.actions.map(normalizeAction) + : point.actions + ? [normalizeAction(point.actions)] + : [], + triggers: Array.isArray(point.triggers) + ? point.triggers.map(normalizeTrigger) + : point.triggers + ? [normalizeTrigger(point.triggers)] + : [], connections: { targets: point.connections.targets.map((target) => ({ modelUUID: target.modelUUID, @@ -482,8 +556,10 @@ function convertToSimulationPath( typeof path.speed === "string" ? parseFloat(path.speed) || 1 : path.speed || 1, + isplaying: false, // Added missing property }; } else { + // For vehicle paths, handle the case where triggers might not exist return { type: path.type, modeluuid, @@ -493,7 +569,10 @@ function convertToSimulationPath( position: path.points.position, actions: Array.isArray(path.points.actions) ? path.points.actions.map(normalizeAction) - : [normalizeAction(path.points.actions)], + : path.points.actions + ? [normalizeAction(path.points.actions)] + : [], + triggers: [], connections: { targets: path.points.connections.targets.map((target) => ({ modelUUID: target.modelUUID, @@ -503,26 +582,20 @@ function convertToSimulationPath( ], pathPosition: path.position, speed: path.points.speed || 1, + isplaying: false, }; } } -// Custom shallow comparison for arrays -const areArraysEqual = (a: any[], b: any[]) => { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; -}; - // Helper function to create an empty process const createEmptyProcess = (): Process => ({ id: `process-${Math.random().toString(36).substring(2, 11)}`, paths: [], animationPath: [], pointActions: [], + pointTriggers: [], // Added point triggers array speed: 1, + isplaying: false, }); // Enhanced connection checking function @@ -631,19 +704,23 @@ export function useProcessCreation() { const animationPath: THREE.Vector3[] = []; const pointActions: PointAction[][] = []; + const pointTriggers: PointTrigger[][] = []; // Added point triggers collection const processSpeed = paths[0]?.speed || 1; for (const path of paths) { for (const point of path.points) { - const obj = scene.getObjectByProperty("uuid", point.uuid); - if (!obj) { - console.warn(`Object with UUID ${point.uuid} not found in scene`); - continue; - } + if (path.type === "Conveyor") { + const obj = scene.getObjectByProperty("uuid", point.uuid); + if (!obj) { + console.warn(`Object with UUID ${point.uuid} not found in scene`); + continue; + } - const position = obj.getWorldPosition(new THREE.Vector3()); - animationPath.push(position.clone()); - pointActions.push(point.actions); + const position = obj.getWorldPosition(new THREE.Vector3()); + animationPath.push(position.clone()); + pointActions.push(point.actions); + pointTriggers.push(point.triggers); // Collect triggers for each point + } } } @@ -652,7 +729,9 @@ export function useProcessCreation() { paths, animationPath, pointActions, + pointTriggers, speed: processSpeed, + isplaying: false, }; }, [scene] @@ -755,7 +834,7 @@ const ProcessCreator: React.FC = React.memo( ); }, [simulationStates]); - // Enhanced dependency tracking that includes action types + // Enhanced dependency tracking that includes action and trigger types const pathsDependency = useMemo(() => { if (!convertedPaths) return null; return convertedPaths.map((path) => ({ @@ -766,11 +845,20 @@ const ProcessCreator: React.FC = React.memo( point.actions.map((action) => `${index}-${action.type}`).join("|") ) .join(","), + // Track all trigger types for each point + triggerSignature: path.points + .map((point, index) => + point.triggers + .map((trigger) => `${index}-${trigger.type}`) + .join("|") + ) + .join(","), connections: path.points .flatMap((p: PathPoint) => p.connections.targets.map((t: { modelUUID: string }) => t.modelUUID) ) .join(","), + isplaying: false, })); }, [convertedPaths]); @@ -785,15 +873,15 @@ const ProcessCreator: React.FC = React.memo( } // Always regenerate processes if the pathsDependency has changed - // This ensures action type changes will be detected + // This ensures action and trigger type changes will be detected const newProcesses = createProcessesFromPaths(convertedPaths); prevPathsRef.current = convertedPaths; - // Always update processes when action types change + // Always update processes when action or trigger types change onProcessesCreated(newProcesses); prevProcessesRef.current = newProcesses; }, [ - pathsDependency, // This now includes action types + pathsDependency, // This now includes action and trigger types onProcessesCreated, convertedPaths, createProcessesFromPaths, diff --git a/app/src/modules/simulation/process/processObject.tsx b/app/src/modules/simulation/process/processObject.tsx new file mode 100644 index 0000000..01ef41d --- /dev/null +++ b/app/src/modules/simulation/process/processObject.tsx @@ -0,0 +1,58 @@ +import React, { useMemo } from "react"; +import * as THREE from "three"; +import { GLTF } from "three-stdlib"; +import { SpawnedObject } from "./types"; + +interface ProcessObjectProps { + objectId: string; + obj: SpawnedObject; + renderAs?: "box" | "custom"; + gltf?: GLTF; +} + +const ProcessObject: React.FC = ({ + objectId, + obj, + renderAs = "custom", + gltf, +}) => { + const renderedObject = useMemo(() => { + if (renderAs === "box") { + return ( + } + material={obj.material} + position={obj.position} + > + + + ); + } + + if (gltf?.scene) { + const clonedScene = gltf.scene.clone(); + clonedScene.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.material = obj.material; + } + }); + + return ( + } + position={obj.position} + > + + + ); + } + + return null; + }, [objectId, obj, renderAs, gltf]); + + return renderedObject; +}; + +export default ProcessObject; diff --git a/app/src/modules/simulation/process/processObjectRender.tsx b/app/src/modules/simulation/process/processObjectRender.tsx deleted file mode 100644 index 7f5a0cc..0000000 --- a/app/src/modules/simulation/process/processObjectRender.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useRef, useEffect } from "react"; -import { useFrame } from "@react-three/fiber"; -import * as THREE from "three"; -import { GLTF } from "three-stdlib"; -import { Box3Helper } from "three"; -import { SpawnedObject, ProcessData } from "./types"; - -interface ProcessObjectRendererProps { - objectId: string; - object: SpawnedObject; - process: ProcessData; - gltf: GLTF; - showBoundingBox?: boolean; -} - -export const ProcessObjectRenderer: React.FC = ({ - objectId, - object, - process, - gltf, - showBoundingBox = false, -}) => { - const meshRef = useRef(null); - const boxHelperRef = useRef(null); - const boundingBoxRef = useRef(new THREE.Box3()); - - // Issue 1: Can't assign to ref.current as it's read-only - useEffect(() => { - if (object.ref && meshRef.current) { - // Instead of direct assignment, we need to store the mesh reference another way - // Option 1: If you can modify the SpawnedObject interface, add a setMesh method - if (typeof (object as any).setMesh === 'function') { - (object as any).setMesh(meshRef.current); - } - - // Option 2: Store the mesh in a property that isn't ref.current - // This requires modifying your SpawnedObject interface to include this property - (object as any).meshInstance = meshRef.current; - - // Option 3: If you need to maintain compatibility, you could use Object.defineProperty - // But this is a hack and not recommended - // Object.defineProperty(object.ref, 'current', { value: meshRef.current, writable: true }); - } - }, [object.ref]); - - // Create a bounding box helper for visualization - useFrame(() => { - if (meshRef.current && showBoundingBox) { - // Update the bounding box to match the mesh position - if (!boxHelperRef.current) { - // Get the size of the mesh - const size = new THREE.Vector3(1, 1, 1); - - // If the mesh has geometry, use its dimensions - if (meshRef.current.geometry) { - const box = new THREE.Box3().setFromObject(meshRef.current); - box.getSize(size); - } - - // Create a new bounding box centered on the mesh - boundingBoxRef.current = new THREE.Box3().setFromCenterAndSize( - meshRef.current.position, - size - ); - - // Create a helper to visualize the box - boxHelperRef.current = new Box3Helper( - boundingBoxRef.current, - new THREE.Color(0xff0000) - ); - - // Add the helper to the scene - meshRef.current.parent?.add(boxHelperRef.current); - } else { - // Update the box position to match the mesh - boundingBoxRef.current.setFromCenterAndSize( - meshRef.current.position, - boundingBoxRef.current.getSize(new THREE.Vector3()) - ); - - // Force the helper to update - boxHelperRef.current.updateMatrixWorld(true); - } - } - }); - - if (gltf?.scene) { - return ( - - ); - } - - // Issue 2: Material color type problem - return ( - - - - - ); -}; \ No newline at end of file diff --git a/app/src/modules/simulation/process/types.ts b/app/src/modules/simulation/process/types.ts index 64a3e6a..086b620 100644 --- a/app/src/modules/simulation/process/types.ts +++ b/app/src/modules/simulation/process/types.ts @@ -1,79 +1,86 @@ import * as THREE from "three"; -import React from "react"; -export interface ProcessPoint { +export interface Trigger { uuid: string; - position: number[]; - actions?: PointAction[]; + name: string; + type: string; + bufferTime: number; + isUsed: boolean; } export interface PointAction { - type: string; + uuid: string; + name: string; + type: "Inherit" | "Spawn" | "Despawn" | "Delay" | "Swap"; + objectType: string; + material: string; + delay: string | number; + spawnInterval: string | number; isUsed: boolean; - spawnInterval?: number | string; - material?: string; - delay?: number | string; + hitCount?: number; +} + +export interface ProcessPoint { + uuid: string; + position: number[]; + rotation: number[]; + actions: PointAction[]; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + triggers?: Trigger[]; +} +export interface ProcessPath { + modeluuid: string; + modelName: string; + points: ProcessPoint[]; + pathPosition: number[]; + pathRotation: number[]; + speed: number; + type: "Conveyor" | "Vehicle"; + isplaying: boolean } export interface ProcessData { id: string; - name: string; - paths?: { - points?: ProcessPoint[]; - }[]; - animationPath?: { x: number; y: number; z: number }[]; - speed?: number; + paths: ProcessPath[]; + animationPath: { x: number; y: number; z: number }[]; + pointActions: PointAction[][]; + speed: number; customMaterials?: Record; + renderAs?: "box" | "custom"; + pointTriggers: []; +} + +export interface AnimationState { + currentIndex: number; + progress: number; + isAnimating: boolean; + speed: number; + isDelaying: boolean; + delayStartTime: number; + currentDelayDuration: number; + delayComplete: boolean; + currentPathIndex: number; +} + +export interface SpawnedObject { + ref: React.RefObject; + state: AnimationState; + visible: boolean; + material: THREE.Material; + spawnTime: number; + currentMaterialType: string; + position: THREE.Vector3; } export interface ProcessAnimationState { - spawnedObjects: Record; + spawnedObjects: { [objectId: string]: SpawnedObject }; nextSpawnTime: number; objectIdCounter: number; isProcessDelaying: boolean; processDelayStartTime: number; processDelayDuration: number; - isCollisionPaused?: boolean; + hasSpawnedZeroIntervalObject?: boolean; } - -export interface SpawnedObject { - ref: React.RefObject; - state: { - currentIndex: number; - progress: number; - isAnimating: boolean; - speed: number; - isDelaying: boolean; - delayStartTime: number; - currentDelayDuration: number; - delayComplete: boolean; - currentPathIndex: number; - }; - visible: boolean; - material: THREE.Material; - currentMaterialType: string; - spawnTime: number; - position: THREE.Vector3; - collision?: { - boundingBox: THREE.Box3; - isColliding: boolean; - colliding: boolean; // Added this property - }; -} - -// For use in your processAnimator.tsx -// Update the CollisionState interface to include all required properties -interface CollisionState { - boundingBox: THREE.Box3; - isColliding: boolean; - colliding: boolean; // This was missing - collidingWith: string[]; - } -export interface SpawnedObjectWithCollision extends SpawnedObject { - collision: { - boundingBox: THREE.Box3; - isColliding: boolean; - colliding: boolean; - collidingWith: string[]; - }; - } diff --git a/app/src/modules/simulation/process/useProcessAnimations.tsx b/app/src/modules/simulation/process/useProcessAnimations.tsx new file mode 100644 index 0000000..7525c29 --- /dev/null +++ b/app/src/modules/simulation/process/useProcessAnimations.tsx @@ -0,0 +1,540 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import * as THREE from "three"; +import { + ProcessData, + ProcessAnimationState, + SpawnedObject, + AnimationState, + ProcessPoint, + PointAction, + Trigger, +} from "./types"; +import { + useAnimationPlaySpeed, + usePauseButtonStore, + usePlayButtonStore, + useResetButtonStore, +} from "../../../store/usePlayButtonStore"; +import { usePlayAgv } from "../../../store/store"; + +// Enhanced ProcessAnimationState with trigger tracking +interface EnhancedProcessAnimationState extends ProcessAnimationState { + triggerCounts: Record; + triggerLogs: Array<{ + timestamp: number; + pointId: string; + objectId: string; + triggerId: string; + }>; +} + +interface ProcessContainerProps { + processes: ProcessData[]; + setProcesses: React.Dispatch>; + agvRef: any; +} + +interface PlayAgvState { + playAgv: Record; + setPlayAgv: (data: any) => void; +} + +export const useProcessAnimation = ( + processes: ProcessData[], + setProcesses: React.Dispatch>, + agvRef: any +) => { + // State and refs initialization + const { isPlaying, setIsPlaying } = usePlayButtonStore(); + const { isPaused, setIsPaused } = usePauseButtonStore(); + const { isReset, setReset } = useResetButtonStore(); + const debugRef = useRef(false); + const clockRef = useRef(new THREE.Clock()); + const pauseTimeRef = useRef(0); + const elapsedBeforePauseRef = useRef(0); + const animationStatesRef = useRef< + Record + >({}); + const { speed } = useAnimationPlaySpeed(); + const prevIsPlaying = useRef(null); + const [internalResetFlag, setInternalResetFlag] = useState(false); + const [animationStates, setAnimationStates] = useState< + Record + >({}); + const speedRef = useRef(speed); + const { PlayAgv, setPlayAgv } = usePlayAgv(); + + // Effect hooks + useEffect(() => { + speedRef.current = speed; + }, [speed]); + + useEffect(() => { + if (prevIsPlaying.current !== null) { + setAnimationStates({}); + } + prevIsPlaying.current = isPlaying; + }, [isPlaying]); + + useEffect(() => { + animationStatesRef.current = animationStates; + }, [animationStates]); + + // Reset handler + useEffect(() => { + if (isReset) { + setInternalResetFlag(true); + setIsPlaying(false); + setIsPaused(false); + setAnimationStates({}); + animationStatesRef.current = {}; + clockRef.current = new THREE.Clock(); + elapsedBeforePauseRef.current = 0; + pauseTimeRef.current = 0; + setReset(false); + setTimeout(() => { + setInternalResetFlag(false); + setIsPlaying(true); + }, 0); + } + }, [isReset, setReset, setIsPlaying, setIsPaused]); + + // Pause handler + useEffect(() => { + if (isPaused) { + pauseTimeRef.current = clockRef.current.getElapsedTime(); + } else if (pauseTimeRef.current > 0) { + const pausedDuration = + clockRef.current.getElapsedTime() - pauseTimeRef.current; + elapsedBeforePauseRef.current += pausedDuration; + } + }, [isPaused]); + + // Initialize animation states with trigger tracking + useEffect(() => { + if (isPlaying && !internalResetFlag) { + const newStates: Record = {}; + + processes.forEach((process) => { + const triggerCounts: Record = {}; + + // Initialize trigger counts for all On-Hit triggers + process.paths?.forEach((path) => { + path.points?.forEach((point) => { + point.triggers?.forEach((trigger: Trigger) => { + if (trigger.type === "On-Hit" && trigger.isUsed) { + triggerCounts[`${point.uuid}-${trigger.uuid}`] = 0; + } + }); + }); + }); + + newStates[process.id] = { + spawnedObjects: {}, + nextSpawnTime: 0, + objectIdCounter: 0, + isProcessDelaying: false, + processDelayStartTime: 0, + processDelayDuration: 0, + triggerCounts, + triggerLogs: [], + }; + }); + + setAnimationStates(newStates); + animationStatesRef.current = newStates; + clockRef.current.start(); + } + }, [isPlaying, processes, internalResetFlag]); + + // Helper functions + const findSpawnPoint = (process: ProcessData): ProcessPoint | null => { + for (const path of process.paths || []) { + for (const point of path.points || []) { + const spawnAction = point.actions?.find( + (a) => a.isUsed && a.type === "Spawn" + ); + if (spawnAction) { + return point; + } + } + } + return null; + }; + + const findAnimationPathPoint = ( + process: ProcessData, + spawnPoint: ProcessPoint + ): THREE.Vector3 => { + if (process.animationPath && process.animationPath.length > 0) { + let pointIndex = 0; + for (const path of process.paths || []) { + for (let i = 0; i < (path.points?.length || 0); i++) { + const point = path.points?.[i]; + if (point && point.uuid === spawnPoint.uuid) { + if (process.animationPath[pointIndex]) { + const p = process.animationPath[pointIndex]; + return new THREE.Vector3(p.x, p.y, p.z); + } + } + pointIndex++; + } + } + } + return new THREE.Vector3( + spawnPoint.position[0], + spawnPoint.position[1], + spawnPoint.position[2] + ); + }; + + // Optimized object creation + const createSpawnedObject = useCallback( + ( + process: ProcessData, + currentTime: number, + materialType: string, + spawnPoint: ProcessPoint, + baseMaterials: Record + ): SpawnedObject => { + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; + + const spawnPosition = findAnimationPathPoint(process, spawnPoint); + const material = + processMaterials[materialType as keyof typeof processMaterials] || + baseMaterials.Default; + + return { + ref: { current: null }, + state: { + currentIndex: 0, + progress: 0, + isAnimating: true, + speed: process.speed || 1, + isDelaying: false, + delayStartTime: 0, + currentDelayDuration: 0, + delayComplete: false, + currentPathIndex: 0, + }, + visible: true, + material: material, + currentMaterialType: materialType, + spawnTime: currentTime, + position: spawnPosition, + }; + }, + [] + ); + + // Material handling + const handleMaterialSwap = useCallback( + ( + processId: string, + objectId: string, + materialType: string, + processes: ProcessData[], + baseMaterials: Record + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || !processState.spawnedObjects[objectId]) + return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const processMaterials = { + ...baseMaterials, + ...(process.customMaterials || {}), + }; + + const newMaterial = + processMaterials[materialType as keyof typeof processMaterials]; + if (!newMaterial) return prev; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + material: newMaterial, + currentMaterialType: materialType, + }, + }, + }, + }; + }); + }, + [] + ); + + // Point action handler with trigger counting + const handlePointActions = useCallback( + ( + processId: string, + objectId: string, + actions: PointAction[] = [], + currentTime: number, + processes: ProcessData[], + baseMaterials: Record + ): boolean => { + let shouldStopAnimation = false; + + actions.forEach((action) => { + if (!action.isUsed) return; + + switch (action.type) { + case "Delay": + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState || processState.isProcessDelaying) return prev; + + const delayDuration = + typeof action.delay === "number" + ? action.delay + : parseFloat(action.delay as string) || 0; + + if (delayDuration > 0) { + return { + ...prev, + [processId]: { + ...processState, + isProcessDelaying: true, + processDelayStartTime: currentTime, + processDelayDuration: delayDuration, + spawnedObjects: { + ...processState.spawnedObjects, + [objectId]: { + ...processState.spawnedObjects[objectId], + state: { + ...processState.spawnedObjects[objectId].state, + isAnimating: false, + isDelaying: true, + delayStartTime: currentTime, + currentDelayDuration: delayDuration, + delayComplete: false, + }, + }, + }, + }, + }; + } + return prev; + }); + shouldStopAnimation = true; + break; + + case "Despawn": + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const newSpawnedObjects = { ...processState.spawnedObjects }; + delete newSpawnedObjects[objectId]; + + return { + ...prev, + [processId]: { + ...processState, + spawnedObjects: newSpawnedObjects, + }, + }; + }); + shouldStopAnimation = true; + break; + + case "Swap": + if (action.material) { + handleMaterialSwap( + processId, + objectId, + action.material, + processes, + baseMaterials + ); + } + break; + + default: + break; + } + }); + + return shouldStopAnimation; + }, + [handleMaterialSwap] + ); + + // Trigger counting system + const checkAndCountTriggers = useCallback( + ( + processId: string, + objectId: string, + currentPointIndex: number, + processes: ProcessData[], + currentTime: number + ) => { + setAnimationStates((prev) => { + const processState = prev[processId]; + if (!processState) return prev; + + const process = processes.find((p) => p.id === processId); + if (!process) return prev; + + const point = getPointDataForAnimationIndex(process, currentPointIndex); + if (!point?.triggers) return prev; + + const onHitTriggers = point.triggers.filter( + (t: Trigger) => t.type === "On-Hit" && t.isUsed + ); + if (onHitTriggers.length === 0) return prev; + + const newTriggerCounts = { ...processState.triggerCounts }; + const newTriggerLogs = [...processState.triggerLogs]; + let shouldLog = false; + + // Update counts for all valid triggers + onHitTriggers.forEach((trigger: Trigger) => { + const triggerKey = `${point.uuid}-${trigger.uuid}`; + newTriggerCounts[triggerKey] = + (newTriggerCounts[triggerKey] || 0) + 1; + shouldLog = true; + newTriggerLogs.push({ + timestamp: currentTime, + pointId: point.uuid, + objectId, + triggerId: trigger.uuid, + }); + }); + + const processTotalHits = Object.values(newTriggerCounts).reduce( + (a, b) => a + b, + 0 + ); + + if (shouldLog) { + const vehiclePaths = process.paths.filter( + (path) => path.type === "Vehicle" + ); + + vehiclePaths.forEach((vehiclePath) => { + if (vehiclePath.points?.length > 0) { + const vehiclePoint = vehiclePath.points[0]; + const action = vehiclePoint.actions?.[0]; + let expectedHitCount = action?.hitCount; + + if (expectedHitCount !== undefined) { + const vehicleId = vehiclePath.modeluuid; + + let vehicleEntry = agvRef.current.find( + (v: any) => + v.vehicleId === vehicleId && v.processId === processId + ); + + if (!vehicleEntry) { + vehicleEntry = { + processId, + vehicleId, + expectedHitCount, + isplaying: false, + hitCount: 0, // Initialize hitCount + }; + agvRef.current.push(vehicleEntry); + } + + // Increment expectedHitCount if not playing + if (!vehicleEntry.isplaying) { + vehicleEntry.expectedHitCount = processTotalHits; + } + + // Set vehicle's hitCount to the processTotalHits + vehicleEntry.hitCount = processTotalHits; + vehicleEntry.lastUpdated = currentTime; + + // If hitCount matches expectedHitCount, set isplaying to true + if (vehicleEntry.hitCount >= vehicleEntry.expectedHitCount) { + vehicleEntry.isplaying = true; + } + } + } + }); + } + + return { + ...prev, + [processId]: { + ...processState, + triggerCounts: newTriggerCounts, + triggerLogs: newTriggerLogs, + totalHits: processTotalHits, + }, + }; + }); + }, + [] + ); + + // Utility functions + const hasNonInheritActions = useCallback( + (actions: PointAction[] = []): boolean => { + return actions.some( + (action) => action.isUsed && action.type !== "Inherit" + ); + }, + [] + ); + + const getPointDataForAnimationIndex = useCallback( + (process: ProcessData, index: number): ProcessPoint | null => { + if (!process.paths) return null; + + let cumulativePoints = 0; + for (const path of process.paths) { + const pointCount = path.points?.length || 0; + + if (index < cumulativePoints + pointCount) { + const pointIndex = index - cumulativePoints; + return path.points?.[pointIndex] || null; + } + + cumulativePoints += pointCount; + } + + return null; + }, + [] + ); + + const getTriggerCounts = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerCounts || {}; + }, []); + + const getTriggerLogs = useCallback((processId: string) => { + return animationStatesRef.current[processId]?.triggerLogs || []; + }, []); + + return { + animationStates, + setAnimationStates, + clockRef, + elapsedBeforePauseRef, + speedRef, + debugRef, + findSpawnPoint, + createSpawnedObject, + handlePointActions, + hasNonInheritActions, + getPointDataForAnimationIndex, + checkAndCountTriggers, + getTriggerCounts, + getTriggerLogs, + processes, + }; +}; diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index 98fd3dd..5bcf7d1 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -16,23 +16,9 @@ function Simulation() { const { activeModule } = useModuleStore(); const pathsGroupRef = useRef() as React.MutableRefObject; const { simulationStates, setSimulationStates } = useSimulationStates(); - const [processes, setProcesses] = useState([]); - - useEffect(() => { - // console.log('simulationStates: ', simulationStates); - }, [simulationStates]); - - // useEffect(() => { - // if (selectedActionSphere) { - // console.log('selectedActionSphere: ', selectedActionSphere); - // } - // }, [selectedActionSphere]); - - // useEffect(() => { - // if (selectedPath) { - // console.log('selectedPath: ', selectedPath); - // } - // }, [selectedPath]); + const [processes, setProcesses] = useState([]); + const agvRef = useRef([]); + const MaterialRef = useRef([]); return ( <> @@ -41,8 +27,17 @@ function Simulation() { <> - - + + )} diff --git a/app/src/store/store.ts b/app/src/store/store.ts index 8810dbe..3210afc 100644 --- a/app/src/store/store.ts +++ b/app/src/store/store.ts @@ -70,9 +70,8 @@ export const useZones = create((set: any) => ({ zones: [], setZones: (callback: any) => set((state: any) => ({ - zones: - typeof callback === 'function' ? callback(state.zones) : callback - })) + zones: typeof callback === "function" ? callback(state.zones) : callback, + })), })); interface ZonePointsState { @@ -314,9 +313,7 @@ export const useActiveUsers = create((set: any) => ({ setActiveUsers: (callback: (prev: any[]) => any[] | any[]) => set((state: { activeUsers: any[] }) => ({ activeUsers: - typeof callback === "function" - ? callback(state.activeUsers) - : callback, + typeof callback === "function" ? callback(state.activeUsers) : callback, })), })); @@ -356,12 +353,33 @@ export const useSelectedPath = create((set: any) => ({ })); interface SimulationPathsStore { - simulationStates: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[]; + simulationStates: ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[]; setSimulationStates: ( paths: - | (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[] - | ((prev: (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[] - ) => (Types.ConveyorEventsSchema | Types.VehicleEventsSchema | Types.StaticMachineEventsSchema | Types.ArmBotEventsSchema)[]) + | ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[] + | (( + prev: ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[] + ) => ( + | Types.ConveyorEventsSchema + | Types.VehicleEventsSchema + | Types.StaticMachineEventsSchema + | Types.ArmBotEventsSchema + )[]) ) => void; } @@ -372,7 +390,7 @@ export const useSimulationStates = create((set) => ({ simulationStates: typeof paths === "function" ? paths(state.simulationStates) : paths, })), -})) +})); export const useNavMesh = create((set: any) => ({ navMesh: null, @@ -456,6 +474,11 @@ export const useTileDistance = create((set: any) => ({ })), })); +export const usePlayAgv = create((set, get) => ({ + PlayAgv: [], + setPlayAgv: (updateFn: (prev: any[]) => any[]) => + set({ PlayAgv: updateFn(get().PlayAgv) }), +})); // Define the Asset type type Asset = { id: string; @@ -474,4 +497,4 @@ type ZoneAssetState = { export const useZoneAssetId = create((set) => ({ zoneAssetId: null, setZoneAssetId: (asset) => set({ zoneAssetId: asset }), -})); \ No newline at end of file +})); diff --git a/app/src/styles/abstracts/mixins.scss b/app/src/styles/abstracts/mixins.scss index e3c57a2..5090d5b 100644 --- a/app/src/styles/abstracts/mixins.scss +++ b/app/src/styles/abstracts/mixins.scss @@ -9,3 +9,25 @@ justify-content: space-between; align-items: center; } + +// Array of base colors +$colors: ( + #f5550b, + #1bac1b, + #0099ff, + #d4c927, + #8400ff, + #13e9b3, + #df1dcf +); + +@mixin gradient-by-child($index) { + // Get the color based on the index passed + $base-color: nth($colors, $index); + // Apply gradient using the same color with different alpha values + background: linear-gradient( + 144.19deg, + rgba($base-color, 0.2) 16.62%, // 20% opacity + rgba($base-color, 0.08) 85.81% // 80% opacity + ); +} diff --git a/app/src/styles/components/input.scss b/app/src/styles/components/input.scss index 40cff59..990d243 100644 --- a/app/src/styles/components/input.scss +++ b/app/src/styles/components/input.scss @@ -7,14 +7,28 @@ input { width: 100%; padding: 2px 4px; border-radius: #{$border-radius-small}; - border: 1px solid var(--border-color); - outline: none; + outline: 2px solid var(--border-color); + outline-offset: -2px; + border: none; background: transparent; color: var(--input-text-color); &:focus, &:active { - border: 1px solid var(--accent-color); + outline: 1px solid var(--accent-color); + } + + &:-webkit-autofill, + &:-webkit-autofill:hover, + &:-webkit-autofill:focus, + &:-webkit-autofill:active { + // Text styles + -webkit-text-fill-color: var(--input-text-color) !important; + caret-color: var(--input-text-color); + + // Background styles + background-color: var(--background-color) !important; + -webkit-box-shadow: 0 0 0px 1000px var(--background-color) inset !important; } } @@ -617,6 +631,7 @@ input { input { border: none; + outline: none; background: transparent; &::placeholder { diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index b021833..aca8474 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -130,7 +130,6 @@ grid-column: 1 / -1; } - .widget-left-sideBar { .widgets-wrapper { @@ -542,7 +541,6 @@ .floating { width: 100%; - } } @@ -1074,24 +1072,62 @@ .category-name { position: relative; z-index: 3; - font-size: var(--font-size-large); + font-size: var(--font-size-regular); // -webkit-text-fill-color: transparent; // -webkit-text-stroke: 1px black; } &::after { content: ""; - width: 50px; - height: 50px; + width: 60px; + height: 60px; border-radius: 50%; background-color: var(--circle-color, #000); position: absolute; - top: 50%; + top: 60%; right: -10px; transform: translate(0, -50%); - background: linear-gradient(144.19deg, - #f1e7cd 16.62%, - #fffaef 85.81%); + } + &:nth-child(1), &:nth-child(9) { + &::after { + @include gradient-by-child(1); // First child uses the first color + } + } + + &:nth-child(2), &:nth-child(10) { + &::after { + @include gradient-by-child(2); // Second child uses the second color + } + } + + &:nth-child(3), &:nth-child(11) { + &::after { + @include gradient-by-child(3); // Third child uses the third color + } + } + + &:nth-child(4), &:nth-child(12) { + &::after { + @include gradient-by-child(4); // Fourth child uses the fourth color + } + } + + &:nth-child(5), &:nth-child(13) { + &::after { + @include gradient-by-child(5); // Fifth child uses the fifth color + } + } + + &:nth-child(6), &:nth-child(14) { + &::after { + @include gradient-by-child(6); // Fifth child uses the fifth color + } + } + + &:nth-child(7), &:nth-child(15) { + &::after { + @include gradient-by-child(7); // Fifth child uses the fifth color + } } .category-image { @@ -1118,30 +1154,47 @@ .assets { width: 117px; height: 95px; - border-radius: 3.59px; + border-radius: #{$border-radius-small}; background-color: var(--background-color-gray); - padding: 8px; - padding-top: 12px; font-weight: $medium-weight; position: relative; overflow: hidden; + padding: 0; + &:hover { + .asset-name { + opacity: 1; + } + } .asset-name { - position: relative; + position: absolute; + top: 0; z-index: 3; + padding: 8px; + width: 100%; font-size: var(--font-size-regular); + background: color-mix( + in srgb, + var(--background-color) 40%, + transparent + ); + backdrop-filter: blur(5px); + opacity: 0; + transition: opacity 0.3s ease; + + /* Added properties for ellipsis */ + display: -webkit-box; /* Necessary for multiline truncation */ + -webkit-line-clamp: 2; /* Number of lines to show */ + -webkit-box-orient: vertical; /* Box orientation for the ellipsis */ + overflow: hidden; /* Hide overflowing content */ + text-overflow: ellipsis; /* Add ellipsis for truncated content */ } .asset-image { height: 100%; width: 100%; - position: absolute; - // top: 50%; - // right: 5px; - // transform: translate(0, -50%); - top: 0; - left: 0; z-index: 2; + object-fit: cover; } } } @@ -1204,4 +1257,4 @@ .assets-wrapper { margin: 0; } -} \ No newline at end of file +} diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/realTimeViz.scss index aa42fd1..61cb32a 100644 --- a/app/src/styles/pages/realTimeViz.scss +++ b/app/src/styles/pages/realTimeViz.scss @@ -765,20 +765,19 @@ .editWidgetOptions { position: absolute; - // top: 50%; - // left: 50%; - // transform: translate(-50%, -50%); background-color: var(--background-color); z-index: 3; display: flex; flex-direction: column; border-radius: 6px; overflow: hidden; + padding: 4px; min-width: 150px; .option { - padding: 8px 10px; + padding: 4px 10px; + border-radius: #{$border-radius-small}; color: var(--text-color); cursor: pointer; @@ -791,7 +790,8 @@ color: #f65648; &:hover { - background-color: #ffe3e0; + background-color: #f65648; + color: white; } } } diff --git a/app/src/types/world/worldTypes.d.ts b/app/src/types/world/worldTypes.d.ts index fefca2a..3197198 100644 --- a/app/src/types/world/worldTypes.d.ts +++ b/app/src/types/world/worldTypes.d.ts @@ -1,13 +1,13 @@ // Importing core classes and types from THREE.js and @react-three/fiber import * as THREE from "three"; -import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; -import { DragControls } from 'three/examples/jsm/controls/DragControls'; -import { IntersectionEvent } from '@react-three/fiber/dist/declarations/src/core/events'; +import { TransformControls } from "three/examples/jsm/controls/TransformControls"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; +import { DragControls } from "three/examples/jsm/controls/DragControls"; +import { IntersectionEvent } from "@react-three/fiber/dist/declarations/src/core/events"; import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events"; import { RootState } from "@react-three/fiber"; import { CSM } from "three/examples/jsm/csm/CSM"; -import { CSMHelper } from 'three/examples/jsm/csm/CSMHelper'; +import { CSMHelper } from "three/examples/jsm/csm/CSMHelper"; import { CameraControls } from "@react-three/drei"; /** Core THREE.js and React-Fiber Event Types **/ @@ -15,7 +15,6 @@ import { CameraControls } from "@react-three/drei"; // Event type specific to pointer events in @react-three/fiber export type ThreeEvent = ThreeEvent; - /** Vector and Reference Types **/ // 2D Vector type from THREE.js @@ -33,7 +32,6 @@ export type RefVector3 = React.MutableRefObject; // Quaternion type for rotations, using the base structure from THREE.js export type QuaternionType = THREE.QuaternionLike; - /** Basic Object Types for Scene Management **/ // THREE.js mesh object @@ -49,7 +47,9 @@ export type Shape = THREE.Shape; export type IntersectionEvent = THREE.Intersection; // Array type for intersections with objects in the scene -export type IntersectsType = THREE.Intersection>[]; +export type IntersectsType = THREE.Intersection< + THREE.Object3D +>[]; // Event type for mesh interactions export type MeshEvent = IntersectionEvent; @@ -60,7 +60,6 @@ export type DragEvent = DragEvent; // Generic type for user data attached to objects export type UserData = any; - /** React Mutable References for Scene Objects **/ // Mutable reference to the scene, used in React-based projects @@ -92,7 +91,6 @@ export type Vector3Array = THREE.Vector3[]; export type DragControl = DragControls | null; export type RefDragControl = React.MutableRefObject; - /** Primitive Types with Mutable References **/ export type String = string; @@ -109,15 +107,15 @@ export type NumberArray = number[]; export type RefRaycaster = React.MutableRefObject; // Camera reference, supporting both perspective and basic cameras -export type RefCamera = React.MutableRefObject; - +export type RefCamera = React.MutableRefObject< + THREE.Camera | THREE.PerspectiveCamera +>; /** Three.js Root State Management **/ // Root state of the @react-three/fiber instance, providing context of the scene export type ThreeState = RootState; - /** Point and Line Types for Spatial Geometry **/ // Defines a point in 3D space with metadata for unique identification @@ -132,11 +130,16 @@ export type RefLine = React.MutableRefObject; // Collection of lines for structured geometry export type Lines = Array; - /** Wall and Room Types for 3D Space Management **/ // Defines a wall with its geometry, position, rotation, material, and layer information -export type Wall = [THREE.ExtrudeGeometry, [number, number, number], [number, number, number], string, number]; +export type Wall = [ + THREE.ExtrudeGeometry, + [number, number, number], + [number, number, number], + string, + number +]; // Collection of walls, useful in scene construction export type Walls = Array; @@ -145,15 +148,22 @@ export type Walls = Array; export type RefWalls = React.MutableRefObject; // Room type, containing coordinates and layer metadata for spatial management -export type Rooms = Array<{ coordinates: Array<{ position: THREE.Vector3, uuid: string }>, layer: number }>; +export type Rooms = Array<{ + coordinates: Array<{ position: THREE.Vector3; uuid: string }>; + layer: number; +}>; // Reference for room objects, enabling updates within React components -export type RefRooms = React.MutableRefObject, layer: number }>>; +export type RefRooms = React.MutableRefObject< + Array<{ + coordinates: Array<{ position: THREE.Vector3; uuid: string }>; + layer: number; + }> +>; // Reference for lines, supporting React-based state changes export type RefLines = React.MutableRefObject; - /** Floor Line Types for Layered Structures **/ // Floor line type for single lines on the floor level @@ -168,18 +178,16 @@ export type OnlyFloorLines = Array; // Reference for multi-level floor lines, allowing structured updates export type RefOnlyFloorLines = React.MutableRefObject; - /** GeoJSON Line Integration **/ // Structure for representing GeoJSON lines, integrating external data sources export type GeoJsonLine = { - line: any; - uuids: [string, string]; - layer: number; - type: string; + line: any; + uuids: [string, string]; + layer: number; + type: string; }; - /** State Management Types for React Components **/ // Dispatch types for number and boolean states, commonly used in React hooks @@ -189,68 +197,71 @@ export type BooleanState = React.Dispatch>; // Mutable reference for TubeGeometry, allowing dynamic geometry updates export type RefTubeGeometry = React.MutableRefObject; - /** Floor Item Configuration **/ // Type for individual items placed on the floor, with positioning and rotation metadata export type FloorItemType = { - modeluuid: string; - modelname: string; - position: [number, number, number]; - rotation: { x: number; y: number; z: number }; - modelfileID: string; - isLocked: boolean; - isVisible: boolean; + modeluuid: string; + modelname: string; + position: [number, number, number]; + rotation: { x: number; y: number; z: number }; + modelfileID: string; + isLocked: boolean; + isVisible: boolean; }; // Array of floor items for managing multiple objects on the floor export type FloorItems = Array; // Dispatch type for setting floor item state in React -export type setFloorItemSetState = React.Dispatch>; - +export type setFloorItemSetState = React.Dispatch< + React.SetStateAction +>; /** Asset Configuration for Loading and Positioning **/ // Configuration for assets, allowing model URLs, scaling, positioning, and types interface AssetConfiguration { - modelUrl: string; - scale?: [number, number, number]; - csgscale?: [number, number, number]; - csgposition?: [number, number, number]; - positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number; - type?: "Fixed-Move" | "Free-Move"; + modelUrl: string; + scale?: [number, number, number]; + csgscale?: [number, number, number]; + csgposition?: [number, number, number]; + positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number; + type?: "Fixed-Move" | "Free-Move"; } // Collection of asset configurations, keyed by unique identifiers export type AssetConfigurations = { - [key: string]: AssetConfiguration; + [key: string]: AssetConfiguration; }; - /** Wall Item Configuration **/ // Configuration for wall items, including model, scale, position, and rotation interface WallItem { - type: "Fixed-Move" | "Free-Move" | undefined; - model?: THREE.Group; - modeluuid?: string - modelname?: string; - scale?: [number, number, number]; - csgscale?: [number, number, number]; - csgposition?: [number, number, number]; - position?: [number, number, number]; - quaternion?: Types.QuaternionType; + type: "Fixed-Move" | "Free-Move" | undefined; + model?: THREE.Group; + modeluuid?: string; + modelname?: string; + scale?: [number, number, number]; + csgscale?: [number, number, number]; + csgposition?: [number, number, number]; + position?: [number, number, number]; + quaternion?: Types.QuaternionType; } // Collection of wall items, allowing for multiple items in a scene export type wallItems = Array; // Dispatch for setting wall item state in React -export type setWallItemSetState = React.Dispatch>; +export type setWallItemSetState = React.Dispatch< + React.SetStateAction +>; // Dispatch for setting vector3 state in React -export type setVector3State = React.Dispatch>; +export type setVector3State = React.Dispatch< + React.SetStateAction +>; // Dispatch for setting euler state in React export type setEulerState = React.Dispatch>; @@ -258,147 +269,250 @@ export type setEulerState = React.Dispatch>; // Reference type for wall items, allowing direct access to the mutable array export type RefWallItems = React.MutableRefObject; - /** Wall and Item Selection State Management **/ // State management for selecting, removing, and indexing wall items export type setRemoveLayerSetState = (layer: number | null) => void; export type setSelectedWallItemSetState = (item: THREE.Object3D | null) => void; -export type setSelectedFloorItemSetState = (item: THREE.Object3D | null) => void; +export type setSelectedFloorItemSetState = ( + item: THREE.Object3D | null +) => void; export type setSelectedItemsIndexSetState = (index: number | null) => void; export type RefCSM = React.MutableRefObject; export type RefCSMHelper = React.MutableRefObject; interface PathConnection { - fromModelUUID: string; - fromUUID: string; - toConnections: { - toModelUUID: string; - toUUID: string; - }[]; + fromModelUUID: string; + fromUUID: string; + toConnections: { + toModelUUID: string; + toUUID: string; + }[]; } interface ConnectionStore { - connections: PathConnection[]; - setConnections: (connections: PathConnection[]) => void; - addConnection: (newConnection: PathConnection) => void; - removeConnection: (fromUUID: string, toUUID: string) => void; + connections: PathConnection[]; + setConnections: (connections: PathConnection[]) => void; + addConnection: (newConnection: PathConnection) => void; + removeConnection: (fromUUID: string, toUUID: string) => void; } interface ConveyorEventsSchema { - modeluuid: string; - modelName: string; - type: 'Conveyor'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | []; - triggers: { uuid: string; name: string; type: string; isUsed: boolean; bufferTime: number }[] | []; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }[]; + modeluuid: string; + modelName: string; + type: "Conveyor"; + points: { + uuid: string; position: [number, number, number]; rotation: [number, number, number]; - speed: number | string; + actions: + | { + uuid: string; + name: string; + type: string; + material: string; + delay: number | string; + spawnInterval: number | string; + isUsed: boolean; + }[] + | []; + triggers: + | { + uuid: string; + name: string; + type: string; + isUsed: boolean; + bufferTime: number; + }[] + | []; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }[]; + position: [number, number, number]; + rotation: [number, number, number]; + speed: number | string; } interface VehicleEventsSchema { - modeluuid: string; - modelName: string; - type: 'Vehicle'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; start: { x: number, y: number } | {}, hitCount: number, end: { x: number, y: number } | {}, buffer: number }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - speed: number; - }; + modeluuid: string; + modelName: string; + type: "Vehicle"; + points: { + uuid: string; position: [number, number, number]; - rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + type: string; + start: { x: number; y: number } | {}; + hitCount: number; + end: { x: number; y: number } | {}; + buffer: number; + }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + speed: number; + isPlaying: boolean; + }; + + position: [number, number, number]; } interface StaticMachineEventsSchema { - modeluuid: string; - modelName: string; - type: 'StaticMachine'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; buffer: number; material: string; }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; + modeluuid: string; + modelName: string; + type: "StaticMachine"; + points: { + uuid: string; position: [number, number, number]; - rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + buffer: number | string; + material: string; + isUsed: boolean; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }; + position: [number, number, number]; } interface ArmBotEventsSchema { - modeluuid: string; - modelName: string; - type: 'ArmBot'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[] }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; + modeluuid: string; + modelName: string; + type: "ArmBot"; + points: { + uuid: string; position: [number, number, number]; - rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + speed: number; + processes: { triggerId: string; startPoint: string; endPoint: string }[]; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }; + position: [number, number, number]; } export type EventData = { - modeluuid: string; - modelname: string; - position: [number, number, number]; - rotation: { x: number; y: number; z: number }; - modelfileID: string; - isLocked: boolean; - isVisible: boolean; - eventData?: { - type: 'Conveyor'; + modeluuid: string; + modelname: string; + position: [number, number, number]; + rotation: { x: number; y: number; z: number }; + modelfileID: string; + isLocked: boolean; + isVisible: boolean; + eventData?: + | { + type: "Conveyor"; points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | []; - triggers: { uuid: string; name: string; type: string; isUsed: boolean; bufferTime: number }[] | []; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: + | { + uuid: string; + name: string; + type: string; + material: string; + delay: number | string; + spawnInterval: number | string; + isUsed: boolean; + }[] + | []; + triggers: + | { + uuid: string; + name: string; + type: string; + isUsed: boolean; + bufferTime: number; + }[] + | []; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; }[]; speed: number | string; - } | { - type: 'Vehicle'; + } + | { + type: "Vehicle"; points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: { uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; type: string; start: { x: number, y: number } | {}, hitCount: number, end: { x: number, y: number } | {}, buffer: number }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; + name: string; + type: string; + start: { x: number; y: number } | {}; + hitCount: number; + end: { x: number; y: number } | {}; + buffer: number; + }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + speed: number; + }; + } + | { + type: "StaticMachine"; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: { + uuid: string; + name: string; + buffer: number; + material: string; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; + }; + } + | { + type: "ArmBot"; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + actions: { + uuid: string; + name: string; speed: number; + processes: { + triggerId: string; + startPoint: string; + endPoint: string; + }[]; + }; + triggers: { uuid: string; name: string; type: string }; + connections: { + source: { modelUUID: string; pointUUID: string }; + targets: { modelUUID: string; pointUUID: string }[]; + }; }; - } | { - type: 'StaticMachine'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; buffer: number; material: string; }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; - } | { - type: 'ArmBot'; - points: { - uuid: string; - position: [number, number, number]; - rotation: [number, number, number]; - actions: { uuid: string; name: string; speed: number; processes: { triggerId: string; startPoint: string; endPoint: string }[] }; - triggers: { uuid: string; name: string; type: string }; - connections: { source: { modelUUID: string; pointUUID: string }; targets: { modelUUID: string; pointUUID: string }[] }; - }; - }; -} + }; +}; + diff --git a/app/src/utils/shortcutkeys/detectModifierKeys.ts b/app/src/utils/shortcutkeys/detectModifierKeys.ts new file mode 100644 index 0000000..e5b7ded --- /dev/null +++ b/app/src/utils/shortcutkeys/detectModifierKeys.ts @@ -0,0 +1,30 @@ +// Function to detect if Shift, Ctrl, Alt, or combinations are pressed +// and return the corresponding key combination string +export const detectModifierKeys = (event: KeyboardEvent): string => { + const modifiers = [ + event.ctrlKey ? "Ctrl" : "", + event.altKey ? "Alt" : "", + event.shiftKey ? "Shift" : "", + event.metaKey ? "Meta" : "" // Add support for Command/Win key + ].filter(Boolean); + + // Ignore modifier keys when they're pressed alone + const isModifierKey = [ + "Control", "Shift", "Alt", "Meta", + "Ctrl", "AltGraph", "OS" // Additional modifier key aliases + ].includes(event.key); + + const mainKey = isModifierKey ? "" : event.key.toUpperCase(); + + // Handle special cases for keys with different representations + const normalizedKey = mainKey === " " ? "Space" : mainKey; + + // Build the combination string + if (modifiers.length > 0 && normalizedKey) { + return `${modifiers.join("+")}+${normalizedKey}`; + } else if (modifiers.length > 0) { + return modifiers.join("+"); + } else { + return normalizedKey; + } +}; diff --git a/app/src/utils/shortcutkeys/handleShortcutKeys.ts b/app/src/utils/shortcutkeys/handleShortcutKeys.ts index 57fdf66..43ca553 100644 --- a/app/src/utils/shortcutkeys/handleShortcutKeys.ts +++ b/app/src/utils/shortcutkeys/handleShortcutKeys.ts @@ -1,198 +1,198 @@ -// Importing React and useEffect from React library -import React, { useEffect } from "react"; -// Importing the necessary hooks and types from React and TypeScript -import useModuleStore, { useThreeDStore } from "../../store/useModuleStore"; -import useToggleStore from "../../store/useUIToggleStore"; -import { useActiveSubTool, useActiveTool, useAddAction, useDeleteTool, useSelectedWallItem, useToggleView, useToolMode } from "../../store/store"; -import { usePlayButtonStore } from "../../store/usePlayButtonStore"; - -const KeyPressListener: React.FC = () => { - // Function to detect if Shift, Ctrl, Alt, or combinations are pressed - const detectModifierKeys = (event: KeyboardEvent): string => { - const modifiers = [ - event.ctrlKey ? "Ctrl" : "", - event.altKey ? "Alt" : "", - event.shiftKey ? "Shift" : "", - event.metaKey ? "Meta" : "" // Add support for Command/Win key - ].filter(Boolean); - - // Ignore modifier keys when they're pressed alone - const isModifierKey = [ - "Control", "Shift", "Alt", "Meta", - "Ctrl", "AltGraph", "OS" // Additional modifier key aliases - ].includes(event.key); - - const mainKey = isModifierKey ? "" : event.key.toUpperCase(); - - // Handle special cases for keys with different representations - const normalizedKey = mainKey === " " ? "Space" : mainKey; - - // Build the combination string - if (modifiers.length > 0 && normalizedKey) { - return `${modifiers.join("+")}+${normalizedKey}`; - } else if (modifiers.length > 0) { - return modifiers.join("+"); - } else { - return normalizedKey; - } - }; - - // Importing the necessary hooks from the store - const { activeModule, setActiveModule } = useModuleStore(); - const { setActiveSubTool } = useActiveSubTool(); - const { toggleUI, setToggleUI } = useToggleStore(); - const { setToggleThreeD } = useThreeDStore(); - const { setToolMode } = useToolMode(); - const { setIsPlaying } = usePlayButtonStore(); - - // wall options - const { toggleView, setToggleView } = useToggleView(); - const { setDeleteTool } = useDeleteTool(); - const { setAddAction } = useAddAction(); - const { setSelectedWallItem } = useSelectedWallItem(); - const { setActiveTool } = useActiveTool(); - - useEffect(() => { - // Function to handle keydown events - const handleKeyPress = (event: KeyboardEvent) => { - - const activeElement = document.activeElement; - - const isTyping = - activeElement instanceof HTMLInputElement || - activeElement instanceof HTMLTextAreaElement || - (activeElement && activeElement.getAttribute('contenteditable') === 'true'); - - if (isTyping) { - return; // Don't trigger shortcuts while typing - } - - const keyCombination = detectModifierKeys(event); - - // Allow default behavior for F5 and F12 - if (["F5", "F11", "F12"].includes(event.key) || keyCombination === "Ctrl+R") { - return; - } - - // Prevent default action for the key press - event.preventDefault(); - - // Detect the key combination pressed - if (keyCombination) { - // Check for specific key combinations to switch modules - if (keyCombination === "1" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setActiveModule("builder"); - } - if (keyCombination === "2" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setActiveModule("simulation"); - } - if (keyCombination === "3" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setActiveModule("visualization"); - } - if (keyCombination === "4" && !toggleView) { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - setToggleUI(false); - setActiveModule("market"); - } - - // sidebar toggle - if (keyCombination === "Ctrl+." && activeModule !== "market") { - setToggleUI(!toggleUI); - } - - // tools toggle - if (keyCombination === "V") { - setActiveTool("cursor"); - setActiveSubTool("cursor"); - } - if (keyCombination === "X") { - setActiveTool("delete"); - setActiveSubTool("delete"); - } - if (keyCombination === "H") { - setActiveTool("free-hand"); - setActiveSubTool("free-hand"); - } - - // player toggle - if (keyCombination === "Ctrl+P" && !toggleView) { - setIsPlaying(true); - } - - // builder key combination - if (activeModule === "builder") { - if (keyCombination === "TAB") { - if (toggleView) { - setToggleView(false); - setToggleThreeD(true); - setActiveTool("cursor"); - } else { - setSelectedWallItem(null); - setDeleteTool(false); - setAddAction(null); - setToggleView(true); - setToggleThreeD(false); - setActiveTool("cursor"); - } - } - // builder tools - if (toggleView) { - if (keyCombination === "Q" || keyCombination === "6") { - setActiveTool("draw-wall"); - setToolMode("Wall"); - } - if (keyCombination === "R" || keyCombination === "7") { - setActiveTool("draw-aisle"); - setToolMode("Aisle"); - } - if (keyCombination === "E" || keyCombination === "8") { - setActiveTool("draw-zone"); - setToolMode("Zone"); - } - if (keyCombination === "T" || keyCombination === "9") { - setActiveTool("draw-floor"); - setToolMode("Floor"); - } - } - if (keyCombination === "M") { - setActiveTool("measure"); - setToolMode("MeasurementScale"); - } - } - - // Undo redo - if (keyCombination === "Ctrl+Z") { - // Handle undo action here - } - if (keyCombination === "Ctrl+Y" || keyCombination === "Ctrl+Shift+Z") { - // Handle redo action here - } - - // cleanup function to remove event listener - if (keyCombination === "ESCAPE") { - setActiveTool("cursor"); - setIsPlaying(false); - } - } - }; - - // Add event listener for keydown - window.addEventListener("keydown", handleKeyPress); - - // Cleanup function to remove event listener - return () => { - window.removeEventListener("keydown", handleKeyPress); - }; - }, [activeModule, toggleUI, toggleView]); // Empty dependency array ensures this runs only once - - return null; // No UI component to render, so return null -}; - -export default KeyPressListener; +// Importing React and useEffect from React library +import React, { useEffect } from "react"; + +// Importing the necessary hooks and types from the application's state management stores +import useModuleStore, { useThreeDStore } from "../../store/useModuleStore"; +import useToggleStore from "../../store/useUIToggleStore"; +import { + useActiveSubTool, + useActiveTool, + useAddAction, + useDeleteTool, + useSelectedWallItem, + useToggleView, + useToolMode, +} from "../../store/store"; +import { usePlayButtonStore } from "../../store/usePlayButtonStore"; + +// Utility function to detect modifier keys (Ctrl, Alt, Shift) and key combinations +import { detectModifierKeys } from "./detectModifierKeys"; + +// KeyPressListener component to handle global keyboard shortcuts +const KeyPressListener: React.FC = () => { + // Accessing state and actions from different stores + const { activeModule, setActiveModule } = useModuleStore(); // Module management (e.g., builder, simulation, visualization) + const { setActiveSubTool } = useActiveSubTool(); // Sub-tool management + const { toggleUI, setToggleUI } = useToggleStore(); // UI visibility toggle + const { setToggleThreeD } = useThreeDStore(); // 3D view toggle + const { setToolMode } = useToolMode(); // Tool mode management + const { setIsPlaying } = usePlayButtonStore(); // Play button state management + + // Wall and tool-related actions + const { toggleView, setToggleView } = useToggleView(); // 2D/3D toggle state + const { setDeleteTool } = useDeleteTool(); // Delete tool action + const { setAddAction } = useAddAction(); // Add action management + const { setSelectedWallItem } = useSelectedWallItem(); // Selected wall item management + const { setActiveTool } = useActiveTool(); // Active tool management + + // useEffect to manage global keyboard shortcuts + useEffect(() => { + // Function to handle keydown events + const handleKeyPress = (event: KeyboardEvent) => { + // Identify the currently focused element + const activeElement = document.activeElement; + + // Check if the user is typing in an input field, textarea, or contenteditable element + const isTyping = + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement || + (activeElement && activeElement.getAttribute("contenteditable") === "true"); + + if (isTyping) { + return; // Skip shortcut handling while typing + } + + // Detect the combination of keys pressed + const keyCombination = detectModifierKeys(event); + + // Allow browser default behavior for specific keys (e.g., F5 for refresh) + if (["F5", "F11", "F12"].includes(event.key) || keyCombination === "Ctrl+R") { + return; + } + + // Prevent the default browser action for other handled key presses + event.preventDefault(); + + if (keyCombination) { + // Switch between different modules (e.g., builder, simulation) + if (keyCombination === "1" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setActiveModule("builder"); + } + if (keyCombination === "2" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setActiveModule("simulation"); + } + if (keyCombination === "3" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setActiveModule("visualization"); + } + if (keyCombination === "4" && !toggleView) { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + setToggleUI(false); + setActiveModule("market"); + } + + // Toggle UI visibility + if (keyCombination === "Ctrl+." && activeModule !== "market") { + setToggleUI(!toggleUI); + } + + // Tool selection shortcuts + if (keyCombination === "V") { + setActiveTool("cursor"); + setActiveSubTool("cursor"); + } + if (keyCombination === "X") { + setActiveTool("delete"); + setActiveSubTool("delete"); + } + if (keyCombination === "H") { + setActiveTool("free-hand"); + setActiveSubTool("free-hand"); + } + + // Toggle play mode + if (keyCombination === "Ctrl+P" && !toggleView) { + setIsPlaying(true); + } + + // Builder-specific shortcuts + if (activeModule === "builder") { + if (keyCombination === "TAB") { + // Switch between 2D and 3D views + if (toggleView) { + setToggleView(false); + setToggleThreeD(true); + setActiveTool("cursor"); + } else { + setSelectedWallItem(null); + setDeleteTool(false); + setAddAction(null); + setToggleView(true); + setToggleThreeD(false); + setActiveTool("cursor"); + } + } + + // Wall-related tools + if (toggleView) { + if (keyCombination === "Q" || keyCombination === "6") { + setActiveTool("draw-wall"); + setToolMode("Wall"); + } + if (keyCombination === "R" || keyCombination === "7") { + setActiveTool("draw-aisle"); + setToolMode("Aisle"); + } + if (keyCombination === "E" || keyCombination === "8") { + setActiveTool("draw-zone"); + setToolMode("Zone"); + } + if (keyCombination === "T" || keyCombination === "9") { + setActiveTool("draw-floor"); + setToolMode("Floor"); + } + } + + // Measurement tool + if (keyCombination === "M") { + setActiveTool("measure"); + setToolMode("MeasurementScale"); + } + } + + // Undo and redo actions + if (keyCombination === "Ctrl+Z") { + // Handle undo action + } + if (keyCombination === "Ctrl+Y" || keyCombination === "Ctrl+Shift+Z") { + // Handle redo action + } + + // Helper actions + if (keyCombination === "Ctrl+H") { + // Open help + } + if (keyCombination === "Ctrl+F") { + // Open find functionality + } + if (keyCombination === "Ctrl+?") { + // Show shortcuts info + } + + // Reset to cursor tool and stop play mode + if (keyCombination === "ESCAPE") { + setActiveTool("cursor"); + setIsPlaying(false); + } + } + }; + + // Add keydown event listener + window.addEventListener("keydown", handleKeyPress); + + // Cleanup function to remove the event listener + return () => { + window.removeEventListener("keydown", handleKeyPress); + }; + }, [activeModule, toggleUI, toggleView]); // Dependencies to reapply effect if these values change + + return null; // This component does not render any UI +}; + +export default KeyPressListener;