Merge branch 'main' into realTimeVisulization

This commit is contained in:
gabriel 2025-04-11 18:07:36 +05:30
commit c059fc8fd2
68 changed files with 3727 additions and 2672 deletions

View File

@ -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 projects 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 dont 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.

98
app/package-lock.json generated
View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -168,7 +168,7 @@ const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/widget3D/${id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
@ -190,29 +190,23 @@ const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
}, [chartMeasurements, chartDuration, widgetName]);
useEffect(() => { }, [rotation]);
const rotationDegrees = {
x: (rotation[0] * 180) / Math.PI,
y: (rotation[1] * 180) / Math.PI,
z: (rotation[2] * 180) / Math.PI,
};
const transformStyle = {
transform: `rotateX(${rotationDegrees.x}deg) rotateY(${rotationDegrees.y}deg) rotateZ(${rotationDegrees.z}deg) translate(-50%, -50%)`,
};
return (
<Html
position={position}
scale={[0.5, 0.5, 0.5]}
rotation={rotation}
transform
sprite
sprite={false}
zIndexRange={[1, 0]}
style={{
transform: transformStyle.transform,
transformStyle: "preserve-3d",
transition: "transform 0.1s ease-out",
}}
// style={{
// transform: transformStyle.transform,
// transformStyle: "preserve-3d",
// transition: "transform 0.1s ease-out",
// }}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
@ -223,7 +217,7 @@ const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
}}
wrapperClass="pointer-none"
>
>
<div
className={`productionCapacity-wrapper card ${selectedChartId?.id === id ? "activeChart" : ""}`}
onClick={() => setSelectedChartId({ id: id, type: type })}
@ -232,7 +226,7 @@ const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
style={{
width: "300px", // Original width
height: "300px", // Original height
transform: transformStyle.transform,
// transform: transformStyle.transform,
transformStyle: "preserve-3d",
position: "absolute",
}}

View File

@ -233,15 +233,16 @@ const ReturnOfInvestment: React.FC<ReturnOfInvestmentProps> = ({
return (
<Html
position={[position[0], position[1], position[2]]}
rotation={rotation}
scale={[0.5, 0.5, 0.5]}
transform
zIndexRange={[1, 0]}
sprite
style={{
transform: transformStyle.transform,
transformStyle: "preserve-3d",
transition: "transform 0.1s ease-out",
}}
sprite={false}
// style={{
// transform: transformStyle.transform,
// transformStyle: "preserve-3d",
// transition: "transform 0.1s ease-out",
// }}
>
<div
className={`returnOfInvestment card ${

View File

@ -111,15 +111,16 @@ const StateWorking: React.FC<StateWorkingProps> = ({
return (
<Html
position={[position[0], position[1], position[2]]}
rotation={rotation}
scale={[0.5, 0.5, 0.5]}
transform
zIndexRange={[1, 0]}
sprite
style={{
transform: transformStyle.transform,
transformStyle: "preserve-3d",
transition: "transform 0.1s ease-out",
}}
sprite={false}
// style={{
// transform: transformStyle.transform,
// transformStyle: "preserve-3d",
// transition: "transform 0.1s ease-out",
// }}
>
<div
className={`stateWorking-wrapper card ${

View File

@ -211,15 +211,16 @@ const Throughput: React.FC<ThroughputProps> = ({
return (
<Html
position={[position[0], position[1], position[2]]}
rotation={rotation}
scale={[0.5, 0.5, 0.5]}
transform
zIndexRange={[1, 0]}
sprite
style={{
transform: transformStyle.transform,
transformStyle: "preserve-3d",
transition: "transform 0.1s ease-out",
}}
sprite={false}
// style={{
// transform: transformStyle.transform,
// transformStyle: "preserve-3d",
// transition: "transform 0.1s ease-out",
// }}
>
<div
className={`throughput-wrapper card ${

View File

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

View File

@ -26,9 +26,14 @@ const Templates = () => {
templateData();
}, []);
const handleDeleteTemplate = async (id: string) => {
const handleDeleteTemplate = async (
e: React.MouseEvent<HTMLButtonElement>,
id: string
) => {
try {
e.stopPropagation();
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
let deleteTemplate = {
organization: organization,
@ -49,7 +54,6 @@ const Templates = () => {
const handleLoadTemplate = async (template: any) => {
try {
if (selectedZone.zoneName === "") return;
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
@ -88,27 +92,27 @@ const Templates = () => {
return (
<div className="template-list">
{templates.map((template, index) => (
<div key={template.id} className="template-item">
<div
key={template.id}
className="template-item"
onClick={() => handleLoadTemplate(template)}
>
{template?.snapshot && (
<div className="template-image-container">
<img
src={template.snapshot}
alt={`${template.name} preview`}
className="template-image"
onClick={() => handleLoadTemplate(template)}
/>
</div>
)}
<div className="template-details">
<div
onClick={() => handleLoadTemplate(template)}
className="template-name"
>
<div className="template-name">
{/* {`Template ${index + 1}`} */}
<RenameInput value={`Template ${index + 1}`} />
</div>
<button
onClick={() => handleDeleteTemplate(template.id)}
onClick={(e) => handleDeleteTemplate(e, template.id)}
className="delete-button"
aria-label="Delete template"
>

View File

@ -107,7 +107,7 @@ const ProgressBarWidget = ({
const Widgets2D = () => {
return (
<div className="widget2D">
<div className="widget2D widgets-wrapper">
<div className="chart-container">
{chartTypes.map((type, index) => {
const widgetTitle = `Widget ${index + 1}`;

View File

@ -12,22 +12,21 @@ const Widgets3D = () => {
];
const { widgetSelect, setWidgetSelect } = useAsset3dWidget();
return (
<div className="widgets-container widget3D">
<div className="widgets-container widgets-wrapper widget3D">
{widgets?.map((widget, index) => (
<div
key={index}
className="widget-item"
draggable
onDragStart={(e) => {
let name = widget.name
let crt = e.target
let name = widget.name;
let crt = e.target;
if (crt instanceof HTMLElement) {
const widget = crt.cloneNode(true) as HTMLElement;
e.dataTransfer.setDragImage(widget, 0, 0)
e.dataTransfer.effectAllowed = "move"
e.dataTransfer.setData("text/plain", "ui-" + name)
e.dataTransfer.setDragImage(widget, 0, 0);
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", "ui-" + name);
}
}}
onPointerDown={() => {
@ -42,7 +41,7 @@ const Widgets3D = () => {
className="widget-image"
src={widget.img}
alt={widget.name}
draggable={false}
draggable={false}
/>
</div>
))}

View File

@ -33,7 +33,7 @@ const WidgetsFloating = () => {
};
return (
<div className="floatingWidgets-wrapper">
<div className="floatingWidgets-wrapper widgets-wrapper">
{/* {widgets.map((widget) => (
<div
key={widget.id}

View File

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

View File

@ -24,6 +24,7 @@ interface ButtonsProps {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneId: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
@ -41,6 +42,7 @@ interface ButtonsProps {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneId: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
@ -120,7 +122,6 @@ const AddButtons: React.FC<ButtonsProps> = ({
// // Update the selectedZone state
// setSelectedZone(updatedZone);
// }
};
// Function to clean all widgets from a panel
@ -168,6 +169,7 @@ const AddButtons: React.FC<ButtonsProps> = ({
// Function to handle "+" button click
const handlePlusButtonClick = async (side: Side) => {
if (selectedZone.activeSides.includes(side)) {
console.log("open");
// Panel already exists: Remove widgets from that side and update activeSides
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0]; // Fallback value
@ -195,6 +197,18 @@ const AddButtons: React.FC<ButtonsProps> = ({
}
setSelectedZone(updatedZone);
if (hiddenPanels[selectedZone.zoneId]?.includes(side)) {
setHiddenPanels(prev => ({
...prev,
[selectedZone.zoneId]: prev[selectedZone.zoneId].filter(s => s !== side)
}));
}
// if(hiddenPanels[selectedZone.zoneId].includes(side))
// API call to delete the panel
// try {
// const response = await deletePanelApi(selectedZone.zoneId, side, organization);
@ -240,7 +254,7 @@ const AddButtons: React.FC<ButtonsProps> = ({
// } else {
//
// }
} catch (error) { }
} catch (error) {}
}
};
return (
@ -250,8 +264,9 @@ const AddButtons: React.FC<ButtonsProps> = ({
<div key={side} className={`side-button-container ${side}`}>
{/* "+" Button */}
<button
className={`side-button ${side}${selectedZone.activeSides.includes(side) ? " active" : ""
}`}
className={`side-button ${side}${
selectedZone.activeSides.includes(side) ? " active" : ""
}`}
onClick={() => handlePlusButtonClick(side)}
title={
selectedZone.activeSides.includes(side)
@ -269,11 +284,10 @@ const AddButtons: React.FC<ButtonsProps> = ({
<div className="extra-Bs">
{/* Hide Panel */}
<div
className={`icon ${
hiddenPanels[selectedZone.zoneId]?.includes(side)
? "active"
: ""
}`}
className={`icon ${hiddenPanels[selectedZone.zoneId]?.includes(side)
? "active"
: ""
}`}
title={
hiddenPanels[selectedZone.zoneId]?.includes(side)
? "Show Panel"
@ -301,8 +315,9 @@ const AddButtons: React.FC<ButtonsProps> = ({
{/* Lock/Unlock Panel */}
<div
className={`icon ${selectedZone.lockedPanels.includes(side) ? "active" : ""
}`}
className={`icon ${
selectedZone.lockedPanels.includes(side) ? "active" : ""
}`}
title={
selectedZone.lockedPanels.includes(side)
? "Unlock Panel"

View File

@ -13,12 +13,17 @@ import { get3dWidgetZoneData } from "../../../services/realTimeVisulization/zone
// Define the type for `Side`
type Side = "top" | "bottom" | "left" | "right";
interface HiddenPanels {
[zoneId: string]: Side[];
}
interface DisplayZoneProps {
zonesData: {
[key: string]: {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
widgets: Widget[];
zoneId: string;
zoneViewPortTarget: number[];
@ -31,6 +36,7 @@ interface DisplayZoneProps {
panelOrder: Side[];
lockedPanels: Side[];
zoneId: string;
points: [];
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: {
@ -47,6 +53,7 @@ interface DisplayZoneProps {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneId: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
@ -59,12 +66,15 @@ interface DisplayZoneProps {
}[];
}>
>;
hiddenPanels: HiddenPanels; // Updated prop type
setHiddenPanels: React.Dispatch<React.SetStateAction<HiddenPanels>>; // Updated prop type
}
const DisplayZone: React.FC<DisplayZoneProps> = ({
zonesData,
selectedZone,
setSelectedZone,
hiddenPanels,
}) => {
// Ref for the container element
const containerRef = useRef<HTMLDivElement | null>(null);
@ -74,8 +84,8 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
const [showLeftArrow, setShowLeftArrow] = useState(false);
const [showRightArrow, setShowRightArrow] = useState(false);
const { floatingWidget, setFloatingWidget } = useFloatingWidget()
const{setSelectedChartId}=useWidgetStore()
const { setSelectedChartId } = useWidgetStore()
// Function to calculate overflow state
const updateOverflowState = useCallback(() => {
@ -152,13 +162,13 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
if (selectedZone?.zoneId === zoneId) {
return;
}
setSelectedChartId(null)
setSelectedChartId(null);
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
let response = await getSelect2dZoneData(zoneId, organization);
console.log('response: ', response);
let res = await getFloatingZoneData(zoneId, organization);
console.log('res: ', res);
setFloatingWidget(res);
// Set the selected zone in the store
@ -168,26 +178,32 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
useDroppedObjectsStore.getState().addObject(zoneName, val);
});
}
setSelectedZone({
zoneName,
activeSides: response.activeSides || [],
panelOrder: response.panelOrder || [],
lockedPanels: response.lockedPanels || [],
widgets: response.widgets || [],
points: response.points || [],
zoneId: zoneId,
zoneViewPortTarget: response.viewPortCenter || {},
zoneViewPortPosition: response.viewPortposition || {},
});
} catch (error) {
}
}
return (
<div
ref={containerRef}
className={`zone-wrapper ${selectedZone?.activeSides?.includes("bottom") ? "bottom" : ""}`}
className={`zone-wrapper ${
selectedZone?.activeSides?.includes("bottom") &&
!hiddenPanels[selectedZone.zoneId]?.includes("bottom")
? "bottom"
: ""
}`}
>
{/* Left Arrow */}
{showLeftArrow && (
@ -197,8 +213,8 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
)}
{/* Scrollable Zones Container */}
<div
ref={scrollContainerRef}
<div
ref={scrollContainerRef}
className="zones-wrapper"
style={{ overflowX: "auto", whiteSpace: "nowrap" }}
>
@ -207,8 +223,12 @@ const DisplayZone: React.FC<DisplayZoneProps> = ({
{Object.keys(zonesData).map((zoneName, index) => (
<div
key={index}
className={`zone ${selectedZone.zoneName === zoneName ? "active" : ""}`}
onClick={() => handleSelect2dZoneData(zonesData[zoneName]?.zoneId, zoneName)}
className={`zone ${
selectedZone.zoneName === zoneName ? "active" : ""
}`}
onClick={() =>
handleSelect2dZoneData(zonesData[zoneName]?.zoneId, zoneName)
}
>
{zoneName}
</div>

View File

@ -45,6 +45,7 @@ export const DraggableWidget = ({
zoneName: string;
zoneId: string;
activeSides: Side[];
points:[];
panelOrder: Side[];
lockedPanels: Side[];
widgets: Widget[];
@ -54,7 +55,7 @@ export const DraggableWidget = ({
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
points:[];
lockedPanels: Side[];
zoneId: string;
zoneViewPortTarget: number[];
@ -197,6 +198,7 @@ export const DraggableWidget = ({
},
id: `${widget.id}-copy-${Date.now()}`,
};
console.log('duplicatedWidget: ', duplicatedWidget);
let duplicateWidget = {
organization: organization,

View File

@ -1,5 +1,5 @@
import { useThree } from "@react-three/fiber";
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import {
useAsset3dWidget,
useSocketStore,
@ -61,6 +61,10 @@ export default function Dropped3dWidgets() {
const mouseStartRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const { setSelectedChartId } = useWidgetStore();
const { measurements, duration} = useChartStore();
let [floorPlanesVertical, setFloorPlanesVertical] = useState(
new THREE.Plane(new THREE.Vector3(0, 1, 0))
);
const activeZoneWidgets = zoneWidgetData[selectedZone.zoneId] || [];
useEffect(() => {
@ -77,8 +81,9 @@ export default function Dropped3dWidgets() {
);
setWidgets3D(result);
if (result.length < 0) return
const formattedWidgets = result.map((widget: WidgetData) => ({
const formattedWidgets = result?.map((widget: WidgetData) => ({
id: widget.id,
type: widget.type,
position: widget.position,
@ -176,7 +181,7 @@ export default function Dropped3dWidgets() {
};
const onDrop = (event: any) => {
console.log("onDrop called. hasEntered: ", hasEntered.current);
event.preventDefault();
event.stopPropagation();
@ -188,7 +193,15 @@ export default function Dropped3dWidgets() {
const newWidget = createdWidgetRef.current;
if (!newWidget || !widgetSelect.startsWith("ui")) return;
// ✅ Manual removal of the temp widget (same ID)
// ✅ Extract 2D drop position
const [x, , z] = newWidget.position;
// ✅ Prepare polygon from selectedZone.points
const points3D = selectedZone.points as Array<[number, number, number]>;
const zonePolygonXZ = points3D.map(([x, , z]) => [x, z] as [number, number]);
const isInside = isPointInPolygon([x, z], zonePolygonXZ);
// ✅ Remove temp widget
const prevWidgets = useZoneWidgetStore.getState().zoneWidgetData[selectedZone.zoneId] || [];
const cleanedWidgets = prevWidgets.filter(w => w.id !== newWidget.id);
useZoneWidgetStore.setState((state) => ({
@ -197,8 +210,12 @@ export default function Dropped3dWidgets() {
[selectedZone.zoneId]: cleanedWidgets,
},
}));
// ✅ Now re-add it as final
// if (!isInside) {
// createdWidgetRef.current = null;
// return; // Stop here
// }
// ✅ Add widget if inside polygon
addWidget(selectedZone.zoneId, newWidget);
const add3dWidget = {
@ -211,16 +228,10 @@ export default function Dropped3dWidgets() {
visualizationSocket.emit("v2:viz-3D-widget:add", add3dWidget);
}
setTimeout(() => {
let pointerDivs = document.getElementsByClassName("pointer-none");
Array.from(pointerDivs).forEach((el) => {
el.classList.remove("pointer-none");
});
}, 1000);
createdWidgetRef.current = null;
};
canvasElement.addEventListener("dragenter", handleDragEnter);
canvasElement.addEventListener("dragover", handleDragOver);
canvasElement.addEventListener("drop", onDrop);
@ -232,8 +243,6 @@ export default function Dropped3dWidgets() {
};
}, [widgetSelect, activeModule, selectedZone.zoneId, widgetSubOption, camera,]);
useEffect(() => {
if (!rightClickSelected) return;
const email = localStorage.getItem("email") || "";
@ -310,25 +319,61 @@ export default function Dropped3dWidgets() {
}
}, [rightSelect, rightClickSelected]);
function isPointInPolygon(
point: [number, number],
polygon: Array<[number, number]>
): boolean {
const [x, z] = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, zi] = polygon[i];
const [xj, zj] = polygon[j];
const intersect =
zi > z !== zj > z &&
x < ((xj - xi) * (z - zi)) / (zj - zi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
const [prevX, setPrevX] = useState(0);
useEffect(() => {
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const handleMouseDown = (event: MouseEvent) => {
if (!rightClickSelected || !rightSelect) return;
const cameraDirection = new THREE.Vector3();
camera.getWorldDirection(cameraDirection);
// Plane normal should be perpendicular to screen (XZ move), so use screen right direction
const right = new THREE.Vector3();
camera.getWorldDirection(cameraDirection);
cameraDirection.y = 0;
cameraDirection.normalize();
right.crossVectors(new THREE.Vector3(0, 1, 0), cameraDirection).normalize();
// Create a plane that allows vertical movement
const verticalPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(right, new THREE.Vector3(0, 0, 0));
setFloorPlanesVertical(verticalPlane);
if (rightSelect === "RotateX" || rightSelect === "RotateY") {
mouseStartRef.current = { x: event.clientX, y: event.clientY };
const selectedZone = Object.keys(zoneWidgetData).find(
const selectedZoneId = Object.keys(zoneWidgetData).find(
(zoneId: string) =>
zoneWidgetData[zoneId].some(
(widget: WidgetData) => widget.id === rightClickSelected
)
);
if (!selectedZone) return;
const selectedWidget = zoneWidgetData[selectedZone].find(
if (!selectedZoneId) return;
const selectedWidget = zoneWidgetData[selectedZoneId].find(
(widget: WidgetData) => widget.id === rightClickSelected
);
if (selectedWidget) {
@ -339,14 +384,14 @@ export default function Dropped3dWidgets() {
const handleMouseMove = (event: MouseEvent) => {
if (!rightClickSelected || !rightSelect) return;
const selectedZone = Object.keys(zoneWidgetData).find((zoneId: string) =>
const selectedZoneId = Object.keys(zoneWidgetData).find((zoneId: string) =>
zoneWidgetData[zoneId].some(
(widget: WidgetData) => widget.id === rightClickSelected
)
);
if (!selectedZone) return;
if (!selectedZoneId) return;
const selectedWidget = zoneWidgetData[selectedZone].find(
const selectedWidget = zoneWidgetData[selectedZoneId].find(
(widget: WidgetData) => widget.id === rightClickSelected
);
if (!selectedWidget) return;
@ -357,77 +402,101 @@ export default function Dropped3dWidgets() {
raycaster.setFromCamera(mouse, camera);
if (
rightSelect === "Horizontal Move" &&
raycaster.ray.intersectPlane(plane.current, planeIntersect.current)
) {
if (rightSelect === "Horizontal Move" &&raycaster.ray.intersectPlane(plane.current, planeIntersect.current)) {
const points3D = selectedZone.points as Array<[number, number, number]>;
const zonePolygonXZ = points3D.map(([x, , z]) => [x, z] as [number, number]);
const newPosition: [number, number, number] = [
planeIntersect.current.x,
selectedWidget.position[1],
planeIntersect.current.z,
];
updateWidgetPosition(selectedZone, rightClickSelected, newPosition);
const isInside = isPointInPolygon(
[newPosition[0], newPosition[2]],
zonePolygonXZ
);
// if (isInside) {
updateWidgetPosition(selectedZoneId, rightClickSelected, newPosition);
// }
}
if (rightSelect === "Vertical Move") {
if (
raycaster.ray.intersectPlane(
verticalPlane.current,
planeIntersect.current
)
) {
updateWidgetPosition(selectedZone, rightClickSelected, [
selectedWidget.position[0],
planeIntersect.current.y,
selectedWidget.position[2],
]);
if (raycaster.ray.intersectPlane(floorPlanesVertical, planeIntersect.current)) {
const currentY = selectedWidget.position[1];
const newY = planeIntersect.current.y;
console.log('planeIntersect.current: ', planeIntersect.current);
const deltaY = newY - currentY;
// Reject if jump is too large (safety check)
if (Math.abs(deltaY) > 200) return;
// Clamp jump or apply smoothing
const clampedY = currentY + THREE.MathUtils.clamp(deltaY, -10, 10);
if (clampedY > 0) {
updateWidgetPosition(selectedZoneId, rightClickSelected, [
selectedWidget.position[0],
clampedY,
selectedWidget.position[2],
]);
}
}
}
if (rightSelect === "RotateX") {
const deltaX = event.clientX - mouseStartRef.current.x;
const rotationSpeed = 0.03;
const newRotation: [number, number, number] = [
rotationStartRef.current[0] + deltaX * rotationSpeed,
rotationStartRef.current[1],
rotationStartRef.current[2],
];
updateWidgetRotation(selectedZone, rightClickSelected, newRotation);
const currentX = event.pageX;
const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
setPrevX(currentX);
if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
const newRotation: [number, number, number] = [
selectedWidget.rotation[0] + 0.05 * sign,
selectedWidget.rotation[1],
selectedWidget.rotation[2],
];
updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation);
}
}
if (rightSelect === "RotateY") {
const deltaY = event.clientY - mouseStartRef.current.y;
const rotationSpeed = 0.03;
const newRotation: [number, number, number] = [
rotationStartRef.current[0],
rotationStartRef.current[1] + deltaY * rotationSpeed,
rotationStartRef.current[2],
];
updateWidgetRotation(selectedZone, rightClickSelected, newRotation);
const currentX = event.pageX;
const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
setPrevX(currentX);
if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
const newRotation: [number, number, number] = [
selectedWidget.rotation[0] ,
selectedWidget.rotation[1]+ 0.05 * sign,
selectedWidget.rotation[2],
];
updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation);
}
}
if (rightSelect === "RotateZ") {
const deltaX = event.movementX;
const rotationSpeed = 0.03;
const currentRotation = selectedWidget.rotation || [0, 0, 0];
const newRotation: [number, number, number] = [
currentRotation[0],
currentRotation[1],
currentRotation[2] + deltaX * rotationSpeed,
];
updateWidgetRotation(selectedZone, rightClickSelected, newRotation);
const currentX = event.pageX;
const sign = currentX > prevX ? 1 : currentX < prevX ? -1 : 0;
setPrevX(currentX);
if (selectedWidget?.rotation && selectedWidget.rotation.length >= 3) {
const newRotation: [number, number, number] = [
selectedWidget.rotation[0] ,
selectedWidget.rotation[1],
selectedWidget.rotation[2]+ 0.05 * sign,
];
updateWidgetRotation(selectedZoneId, rightClickSelected, newRotation);
}
}
};
const handleMouseUp = () => {
if (!rightClickSelected || !rightSelect) return;
const selectedZone = Object.keys(zoneWidgetData).find((zoneId) =>
const selectedZoneId = Object.keys(zoneWidgetData).find((zoneId) =>
zoneWidgetData[zoneId].some(
(widget) => widget.id === rightClickSelected
)
);
if (!selectedZone) return;
if (!selectedZoneId) return;
const selectedWidget = zoneWidgetData[selectedZone].find(
const selectedWidget = zoneWidgetData[selectedZoneId].find(
(widget) => widget.id === rightClickSelected
);
if (!selectedWidget) return;
@ -444,7 +513,7 @@ export default function Dropped3dWidgets() {
number
];
// (async () => {
// let response = await update3dWidget(selectedZone, organization, rightClickSelected, lastPosition);
// let response = await update3dWidget(selectedZoneId, organization, rightClickSelected, lastPosition);
//
// if (response) {
//
@ -452,7 +521,7 @@ export default function Dropped3dWidgets() {
// })();
let updatingPosition = {
organization: organization,
zoneId: selectedZone,
zoneId: selectedZoneId,
id: rightClickSelected,
position: lastPosition,
};
@ -466,8 +535,9 @@ export default function Dropped3dWidgets() {
const rotation = selectedWidget.rotation || [0, 0, 0];
let lastRotation = formatValues(rotation) as [number, number, number];
// (async () => {
// let response = await update3dWidgetRotation(selectedZone, organization, rightClickSelected, lastRotation);
// let response = await update3dWidgetRotation(selectedZoneId, organization, rightClickSelected, lastRotation);
//
// if (response) {
//
@ -475,7 +545,7 @@ export default function Dropped3dWidgets() {
// })();
let updatingRotation = {
organization: organization,
zoneId: selectedZone,
zoneId: selectedZoneId,
id: rightClickSelected,
rotation: lastRotation,
};

View File

@ -49,6 +49,7 @@ const DroppedObjects: React.FC = () => {
const { visualizationSocket } = useSocketStore();
const { isPlaying } = usePlayButtonStore();
const zones = useDroppedObjectsStore((state) => state.zones);
const [openKebabId, setOpenKebabId] = useState<string | null>(null);
const updateObjectPosition = useDroppedObjectsStore(
(state) => state.updateObjectPosition
@ -70,6 +71,7 @@ const DroppedObjects: React.FC = () => {
vertical: "top" | "bottom";
horizontal: "left" | "right";
} | null>(null); // State to track active edges for distance lines
const [currentPosition, setCurrentPosition] = useState<{
top: number | "auto";
left: number | "auto";
@ -143,7 +145,7 @@ const DroppedObjects: React.FC = () => {
// if (res.message === "FloatingWidget deleted successfully") {
// deleteObject(zoneName, id, index); // Call the deleteObject method from the store
// }
} catch (error) { }
} catch (error) {}
}
const handlePointerDown = (event: React.PointerEvent, index: number) => {
@ -519,41 +521,46 @@ const DroppedObjects: React.FC = () => {
{zone.objects.map((obj, index) => {
const topPosition =
typeof obj.position.top === "number"
? `calc(${obj.position.top}px + ${isPlaying && selectedZone.activeSides.includes("top")
? `${heightMultiplier - 55}px`
: "0px"
})`
? `calc(${obj.position.top}px + ${
isPlaying && selectedZone.activeSides.includes("top")
? `${heightMultiplier - 55}px`
: "0px"
})`
: "auto";
const leftPosition =
typeof obj.position.left === "number"
? `calc(${obj.position.left}px + ${isPlaying && selectedZone.activeSides.includes("left")
? `${widthMultiplier - 100}px`
: "0px"
})`
? `calc(${obj.position.left}px + ${
isPlaying && selectedZone.activeSides.includes("left")
? `${widthMultiplier - 100}px`
: "0px"
})`
: "auto";
const rightPosition =
typeof obj.position.right === "number"
? `calc(${obj.position.right}px + ${isPlaying && selectedZone.activeSides.includes("right")
? `${widthMultiplier - 100}px`
: "0px"
})`
? `calc(${obj.position.right}px + ${
isPlaying && selectedZone.activeSides.includes("right")
? `${widthMultiplier - 100}px`
: "0px"
})`
: "auto";
const bottomPosition =
typeof obj.position.bottom === "number"
? `calc(${obj.position.bottom}px + ${isPlaying && selectedZone.activeSides.includes("bottom")
? `${heightMultiplier - 55}px`
: "0px"
})`
? `calc(${obj.position.bottom}px + ${
isPlaying && selectedZone.activeSides.includes("bottom")
? `${heightMultiplier - 55}px`
: "0px"
})`
: "auto";
return (
<div
key={`${zoneName}-${index}`}
className={`${obj.className} ${selectedChartId?.id === obj.id && "activeChart"
}`}
className={`${obj.className} ${
selectedChartId?.id === obj.id && "activeChart"
}`}
ref={chartWidget}
style={{
position: "absolute",
@ -561,6 +568,8 @@ const DroppedObjects: React.FC = () => {
left: leftPosition,
right: rightPosition,
bottom: bottomPosition,
pointerEvents: isPlaying ? "none" : "auto",
minHeight: `${obj.className === "warehouseThroughput" && "150px !important"} `
}}
onPointerDown={(event) => {
setSelectedChartId(obj);

View File

@ -22,6 +22,7 @@ interface PanelProps {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points:[];
zoneId: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
@ -33,6 +34,7 @@ interface PanelProps {
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points:[];
zoneId: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
@ -163,8 +165,8 @@ const Panel: React.FC<PanelProps> = ({
// Calculate panel capacity
const calculatePanelCapacity = (panel: Side) => {
const CHART_WIDTH = panelSize;
const CHART_HEIGHT = panelSize;
const CHART_WIDTH = panelSize - 10;
const CHART_HEIGHT = panelSize - 10;
const dimensions = panelDimensions[panel];
if (!dimensions) {

View File

@ -37,7 +37,7 @@ type FormattedZoneData = Record<
{
activeSides: Side[];
panelOrder: Side[];
points: [];
lockedPanels: Side[];
zoneId: string;
zoneViewPortTarget: number[];
@ -85,6 +85,7 @@ const RealTimeVisulization: React.FC = () => {
const organization = email?.split("@")[1]?.split(".")[0];
try {
const response = await getZone2dData(organization);
console.log('response: ', response);
if (!Array.isArray(response)) {
return;
@ -95,6 +96,7 @@ const RealTimeVisulization: React.FC = () => {
activeSides: [],
panelOrder: [],
lockedPanels: [],
points: zone.points,
zoneId: zone.zoneId,
zoneViewPortTarget: zone.viewPortCenter,
zoneViewPortPosition: zone.viewPortposition,
@ -105,7 +107,7 @@ const RealTimeVisulization: React.FC = () => {
{}
);
setZonesData(formattedData);
} catch (error) {}
} catch (error) { }
}
GetZoneData();
@ -121,6 +123,7 @@ const RealTimeVisulization: React.FC = () => {
activeSides: selectedZone.activeSides || [],
panelOrder: selectedZone.panelOrder || [],
lockedPanels: selectedZone.lockedPanels || [],
points:selectedZone.points||[],
zoneId: selectedZone.zoneId || "",
zoneViewPortTarget: selectedZone.zoneViewPortTarget || [],
zoneViewPortPosition: selectedZone.zoneViewPortPosition || [],
@ -283,6 +286,7 @@ const RealTimeVisulization: React.FC = () => {
"Horizontal Move",
"RotateX",
"RotateY",
"RotateZ",
"Delete",
]}
/>
@ -294,6 +298,8 @@ const RealTimeVisulization: React.FC = () => {
zonesData={zonesData}
selectedZone={selectedZone}
setSelectedZone={setSelectedZone}
hiddenPanels={hiddenPanels}
setHiddenPanels={setHiddenPanels}
/>
{!isPlaying && selectedZone?.zoneName !== "" && (

View File

@ -1,105 +1,96 @@
import React, { useState, useEffect } from 'react'
import { Line } from 'react-chartjs-2'
import useChartStore from '../../../../store/useChartStore';
import { useWidgetStore } from '../../../../store/useWidgetStore';
import axios from 'axios';
import React, { useState, useEffect } from "react";
import { Line } from "react-chartjs-2";
import useChartStore from "../../../../store/useChartStore";
import { useWidgetStore } from "../../../../store/useWidgetStore";
import axios from "axios";
import io from "socket.io-client";
import { WalletIcon } from '../../../icons/3dChartIcons';
import { WalletIcon } from "../../../icons/3dChartIcons";
const TotalCardComponent = ({ object }: any) => {
const [progress, setProgress] = useState<any>(0);
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState(object.header ? object.header : "");
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const { header, flotingDuration, flotingMeasurements } = useChartStore();
const { selectedChartId } = useWidgetStore();
const TotalCardComponent = ({
object
}: any) => {
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const [ progress, setProgress ] = useState<any>(0)
const [measurements, setmeasurements] = useState<any>(object?.Data?.measurements ? object.Data.measurements : {});
const [duration, setDuration] = useState("1h")
const [name, setName] = useState(object.header ? object.header : '')
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0]
const { header, flotingDuration, flotingMeasurements } = useChartStore();
const { selectedChartId } = useWidgetStore();
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0)
return;
const iotApiUrl = process.env.REACT_APP_IOT_SOCKET_SERVER_URL;
const socket = io(`http://${iotApiUrl}`);
useEffect(() => {
if (!iotApiUrl || !measurements || Object.keys(measurements).length === 0) return;
const socket = io(`http://${iotApiUrl}`);
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lastInput", inputData);
};
socket.on("connect", startStream);
socket.on("lastOutput", (response) => {
const responseData = response.input1;
if (typeof responseData === "number") {
setProgress(responseData);
}
});
return () => {
socket.off("lastOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async() => {
if (object?.id !== "") {
try {
const response = await axios.get(`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`);
if (response.status === 200) {
console.log(response);
setmeasurements(response.data.Data.measurements)
setDuration(response.data.Data.duration)
setName(response.data.header)
} else {
console.log("Unexpected response:", response);
}
} catch (error) {
console.error("There was an error!", error);
}
}
const inputData = {
measurements,
duration,
interval: 1000,
};
const startStream = () => {
socket.emit("lastInput", inputData);
};
socket.on("connect", startStream);
socket.on("lastOutput", (response) => {
const responseData = response.input1;
if (typeof responseData === "number") {
setProgress(responseData);
}
useEffect(() => {
fetchSavedInputes();
}, []);
useEffect(() => {
if (selectedChartId?.id === object?.id) {
fetchSavedInputes();
});
return () => {
socket.off("lastOutput");
socket.emit("stop_stream"); // Stop streaming when component unmounts
socket.disconnect();
};
}, [measurements, duration, iotApiUrl]);
const fetchSavedInputes = async () => {
if (object?.id !== "") {
try {
const response = await axios.get(
`http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}/api/v2/A_floatWidget/${object?.id}/${organization}`
);
if (response.status === 200) {
setmeasurements(response.data.Data.measurements);
setDuration(response.data.Data.duration);
setName(response.data.header);
} else {
}
}
,[header, flotingDuration, flotingMeasurements])
} catch (error) {}
}
};
return (
<>
<div className="header-wrapper" >
<div className="header">{name}</div>
<div className="data-values">
<div className="value">{progress}</div>
<div className="per">{object.per}</div>
</div>
</div>
<div className="icon">
<WalletIcon />
</div>
</>
)
}
useEffect(() => {
fetchSavedInputes();
}, []);
export default TotalCardComponent
useEffect(() => {
if (selectedChartId?.id === object?.id) {
fetchSavedInputes();
}
}, [header, flotingDuration, flotingMeasurements]);
return (
<>
<div className="header-wrapper">
<div className="header">{name}</div>
<div className="data-values">
<div className="value">{progress}</div>
<div className="per">{object.per}</div>
</div>
</div>
<div className="icon">
<WalletIcon />
</div>
</>
);
};
export default TotalCardComponent;

View File

@ -125,13 +125,13 @@ const WarehouseThroughput = () => {
lineGraphOptions, // ✅ Include chart options
});
event.dataTransfer.setData("text/plain", cardData);
// event.dataTransfer.effectAllowed = "move"; // Improve drag effect
};
return (
<div className="warehouseThroughput floating" draggable onDragStart={handleDragStart}>
<div className="warehouseThroughput floating" style={{ minHeight: "160px !important" }} draggable onDragStart={handleDragStart}>
<div className="header">
<h2>Warehouse Throughput</h2>
<p>

View File

@ -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<PathPoints[]>([]);
const { simulationStates } = useSimulationStates();
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
const Agv: React.FC<ProcessContainerProps> = ({
processes,
agvRef,
MaterialRef,
}) => {
const [pathPoints, setPathPoints] = useState<PathPoints[]>([]);
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) => (
<group key={i} visible={!isPlaying}>
<PathNavigator
navMesh={navMesh}
pathPoints={pair.points}
id={pair.modelUuid}
speed={pair.modelSpeed}
bufferTime={pair.bufferTime}
hitCount={pair.hitCount}
/>
setPathPoints(newPathPoints);
}
}, [simulationStates]);
{pair.points.slice(1).map((point, idx) => (
<mesh position={[point.x, point.y, point.z]} key={idx}>
<sphereGeometry args={[0.3, 15, 15]} />
<meshStandardMaterial color="red" />
</mesh>
))}
</group>
))}
</>
);
return (
<>
{pathPoints.map((pair, i) => (
<group key={i} visible={!isPlaying}>
<PathNavigator
navMesh={navMesh}
pathPoints={pair.points}
id={pair.modelUuid}
speed={pair.modelSpeed}
bufferTime={pair.bufferTime}
hitCount={pair.hitCount}
processes={processes}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
{pair.points.slice(1).map((point, idx) => (
<mesh position={[point.x, point.y, point.z]} key={idx}>
<sphereGeometry args={[0.3, 15, 15]} />
<meshStandardMaterial color="red" />
</mesh>
))}
</group>
))}
</>
);
};
export default Agv;

View File

@ -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<THREE.Vector3 | null>(null);
const [initialRotation, setInitialRotation] = useState<THREE.Euler | null>(null);
const [targetPosition] = useState(new THREE.Vector3());
const [smoothPosition] = useState(new THREE.Vector3());
const [targetQuaternion] = useState(new THREE.Quaternion());
const distancesRef = useRef<number[]>([]);
const totalDistanceRef = useRef(0);
const progressRef = useRef(0);
const isWaiting = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const pathTransitionProgress = useRef(0);
const [currentPhase, setCurrentPhase] = useState<Phase>("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<THREE.Vector3 | null>(
null
);
const [initialRotation, setInitialRotation] = useState<THREE.Euler | null>(
null
);
const [boxVisible, setBoxVisible] = useState(false);
const resetState = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
const distancesRef = useRef<number[]>([]);
const totalDistanceRef = useRef(0);
const progressRef = useRef(0);
const isWaiting = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(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<THREE.Mesh | null>(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 (
<group name="path-navigator-lines" visible={!isPlaying} >
{toPickupPath.length > 0 && (
<Line points={toPickupPath} color="blue" lineWidth={3} dashed dashSize={0.75} dashScale={2} />
)}
// 3. Determine the material (from materialref) if a process is matched
let materialType = "Default";
{pickupDropPath.length > 0 && (
<Line points={pickupDropPath} color="red" lineWidth={3} />
)}
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 && (
<Line points={dropPickupPath} color="red" lineWidth={3} />
)}
</group>
);
}
// 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 (
<group name="path-navigator-lines" visible={!isPlaying}>
{toPickupPath.length > 0 && (
<Line
points={toPickupPath}
color="blue"
lineWidth={3}
dashed
dashSize={0.75}
dashScale={2}
/>
)}
{pickupDropPath.length > 0 && (
<Line points={pickupDropPath} color="red" lineWidth={3} />
)}
{dropPickupPath.length > 0 && (
<Line points={dropPickupPath} color="red" lineWidth={3} />
)}
</group>
);
}

View File

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

View File

@ -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 && (
<group name="Distance_Text">
{lines.map((text) => (
<Html
// data
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
className={`distance line-${text.userData[0][1]}_${text.userData[1][1]}_${text.layer}`}
>
{text.distance} m
</div>
</Html>
))}
</group>
)}
</>
);
};
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 && (
<group name="Distance_Text">
{lines.map((text) => (
<Html
// data
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
key={`${text.userData[0][1]}_${text.userData[1][1]}`}
className={`distance line-${text.userData[0][1]}_${text.userData[1][1]}_${text.layer}`}
>
{text.distance} m
</div>
</Html>
))}
</group>
)}
</>
);
};
export default DistanceText;

View File

@ -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<TextState | null>(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 (
<group name="Reference_Distance_Text">
<mesh>
{text !== null && (
<Html
// data
key={text.distance}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
className={`Reference_Distance line-${text.userData.userData}`}
>
{text.distance} m
</div>
</Html>
)}
</mesh>
</group>
);
};
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<TextState | null>(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 (
<group name="Reference_Distance_Text">
<mesh>
{text !== null && (
<Html
// data
key={text.distance}
userData={text.userData}
position={[text.position.x, 1, text.position.z]}
// class
wrapperClass="distance-text-wrapper"
className="distance-text"
// other
zIndexRange={[1, 0]}
prepend
sprite
>
<div
className={`Reference_Distance line-${text.userData.userData}`}
>
{text.distance} m
</div>
</Html>
)}
</mesh>
</group>
);
};
export default ReferenceDistanceText;

View File

@ -10,190 +10,227 @@ import { useNavigate } from "react-router-dom";
import { Html } from "@react-three/drei";
import CollabUserIcon from "./collabUserIcon";
import { getAvatarColor } from "./users/functions/getAvatarColor";
import useModuleStore from "../../store/useModuleStore";
const CamModelsGroup = () => {
const navigate = useNavigate();
const groupRef = useRef<THREE.Group>(null);
const email = localStorage.getItem("email");
const { activeUsers, setActiveUsers } = useActiveUsers();
const { socket } = useSocketStore();
const navigate = useNavigate();
const groupRef = useRef<THREE.Group>(null);
const email = localStorage.getItem("email");
const { setActiveUsers } = useActiveUsers();
const { socket } = useSocketStore();
const { activeModule } = useModuleStore();
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
loader.setDRACOLoader(dracoLoader);
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
loader.setDRACOLoader(dracoLoader);
const [cams, setCams] = useState<any[]>([]);
const [models, setModels] = useState<Record<string, { targetPosition: THREE.Vector3; targetRotation: THREE.Euler }>>({});
const [cams, setCams] = useState<any[]>([]);
const [models, setModels] = useState<
Record<
string,
{ targetPosition: THREE.Vector3; targetRotation: THREE.Euler }
>
>({});
const dedupeCams = (cams: any[]) => {
const seen = new Set();
return cams.filter((cam) => {
if (seen.has(cam.uuid)) return false;
seen.add(cam.uuid);
return true;
});
};
const dedupeCams = (cams: any[]) => {
const seen = new Set();
return cams.filter((cam) => {
if (seen.has(cam.uuid)) return false;
seen.add(cam.uuid);
return true;
});
};
const dedupeUsers = (users: any[]) => {
const seen = new Set();
return users.filter((user) => {
if (seen.has(user._id)) return false;
seen.add(user._id);
return true;
});
};
const dedupeUsers = (users: any[]) => {
const seen = new Set();
return users.filter((user) => {
if (seen.has(user._id)) return false;
seen.add(user._id);
return true;
});
};
useEffect(() => {
if (!email) navigate("/");
useEffect(() => {
if (!email) navigate("/");
if (!socket) return;
const organization = email!.split("@")[1].split(".")[0];
if (!socket) return;
const organization = email!.split("@")[1].split(".")[0];
socket.on("userConnectResponse", (data: any) => {
if (!groupRef.current) return;
if (data.data.userData.email === email) return;
if (socket.id === data.socketId || organization !== data.organization) return;
socket.on("userConnectResponse", (data: any) => {
if (!groupRef.current) return;
if (data.data.userData.email === email) return;
if (socket.id === data.socketId || organization !== data.organization)
return;
const model = groupRef.current.getObjectByProperty("uuid", data.data.userData._id);
if (model) {
groupRef.current.remove(model);
}
const model = groupRef.current.getObjectByProperty(
"uuid",
data.data.userData._id
);
if (model) {
groupRef.current.remove(model);
}
loader.load(camModel, (gltf) => {
const newModel = gltf.scene.clone();
newModel.uuid = data.data.userData._id;
newModel.position.set(
data.data.position.x,
data.data.position.y,
data.data.position.z
);
newModel.rotation.set(
data.data.rotation.x,
data.data.rotation.y,
data.data.rotation.z
);
newModel.userData = data.data.userData;
loader.load(camModel, (gltf) => {
const newModel = gltf.scene.clone();
newModel.uuid = data.data.userData._id;
newModel.position.set(
data.data.position.x,
data.data.position.y,
data.data.position.z
);
newModel.rotation.set(
data.data.rotation.x,
data.data.rotation.y,
data.data.rotation.z
);
newModel.userData = data.data.userData;
setCams((prev) => dedupeCams([...prev, newModel]));
setActiveUsers((prev: any) =>
dedupeUsers([...prev, data.data.userData])
);
});
});
setCams((prev) => dedupeCams([...prev, newModel]));
setActiveUsers((prev: any) =>
dedupeUsers([...prev, data.data.userData])
);
});
});
socket.on("userDisConnectResponse", (data: any) => {
if (!groupRef.current) return;
if (socket.id === data.socketId || organization !== data.organization) return;
socket.on("userDisConnectResponse", (data: any) => {
if (!groupRef.current) return;
if (socket.id === data.socketId || organization !== data.organization)
return;
setCams((prev) =>
prev.filter((cam) => cam.uuid !== data.data.userData._id)
);
setActiveUsers((prev: any) =>
prev.filter((user: any) => user._id !== data.data.userData._id)
);
});
setCams((prev) =>
prev.filter((cam) => cam.uuid !== data.data.userData._id)
);
setActiveUsers((prev: any) =>
prev.filter((user: any) => user._id !== data.data.userData._id)
);
});
socket.on("cameraUpdateResponse", (data: any) => {
if (
!groupRef.current ||
socket.id === data.socketId ||
organization !== data.organization
)
return;
socket.on("cameraUpdateResponse", (data: any) => {
if (
!groupRef.current ||
socket.id === data.socketId ||
organization !== data.organization
)
return;
setModels((prev) => ({
...prev,
[data.data.userId]: {
targetPosition: new THREE.Vector3(
data.data.position.x,
data.data.position.y,
data.data.position.z
),
targetRotation: new THREE.Euler(
data.data.rotation.x,
data.data.rotation.y,
data.data.rotation.z
),
},
}));
});
setModels((prev) => ({
...prev,
[data.data.userId]: {
targetPosition: new THREE.Vector3(
data.data.position.x,
data.data.position.y,
data.data.position.z
),
targetRotation: new THREE.Euler(
data.data.rotation.x,
data.data.rotation.y,
data.data.rotation.z
),
},
}));
});
return () => {
socket.off("userConnectResponse");
socket.off("userDisConnectResponse");
socket.off("cameraUpdateResponse");
};
}, [socket]);
return () => {
socket.off("userConnectResponse");
socket.off("userDisConnectResponse");
socket.off("cameraUpdateResponse");
};
}, [socket]);
useFrame(() => {
if (!groupRef.current) return;
Object.keys(models).forEach((uuid) => {
const model = groupRef.current!.getObjectByProperty("uuid", uuid);
if (!model) return;
useFrame(() => {
if (!groupRef.current) return;
Object.keys(models).forEach((uuid) => {
const model = groupRef.current!.getObjectByProperty("uuid", uuid);
if (!model) return;
const { targetPosition, targetRotation } = models[uuid];
model.position.lerp(targetPosition, 0.1);
model.rotation.x = THREE.MathUtils.lerp(model.rotation.x, targetRotation.x, 0.1);
model.rotation.y = THREE.MathUtils.lerp(model.rotation.y, targetRotation.y, 0.1);
model.rotation.z = THREE.MathUtils.lerp(model.rotation.z, targetRotation.z, 0.1);
});
});
const { targetPosition, targetRotation } = models[uuid];
model.position.lerp(targetPosition, 0.1);
model.rotation.x = THREE.MathUtils.lerp(
model.rotation.x,
targetRotation.x,
0.1
);
model.rotation.y = THREE.MathUtils.lerp(
model.rotation.y,
targetRotation.y,
0.1
);
model.rotation.z = THREE.MathUtils.lerp(
model.rotation.z,
targetRotation.z,
0.1
);
});
});
useEffect(() => {
if (!groupRef.current) return;
const organization = email!.split("@")[1].split(".")[0];
useEffect(() => {
if (!groupRef.current) return;
const organization = email!.split("@")[1].split(".")[0];
getActiveUsersData(organization).then((data) => {
const filteredData = data.cameraDatas.filter(
(camera: any) => camera.userData.email !== email
);
getActiveUsersData(organization).then((data) => {
const filteredData = data.cameraDatas.filter(
(camera: any) => camera.userData.email !== email
);
if (filteredData.length > 0) {
loader.load(camModel, (gltf) => {
const newCams = filteredData.map((cam: any) => {
const newModel = gltf.scene.clone();
newModel.uuid = cam.userData._id;
newModel.position.set(cam.position.x, cam.position.y, cam.position.z);
newModel.rotation.set(cam.rotation.x, cam.rotation.y, cam.rotation.z);
newModel.userData = cam.userData;
return newModel;
});
if (filteredData.length > 0) {
loader.load(camModel, (gltf) => {
const newCams = filteredData.map((cam: any) => {
const newModel = gltf.scene.clone();
newModel.uuid = cam.userData._id;
newModel.position.set(
cam.position.x,
cam.position.y,
cam.position.z
);
newModel.rotation.set(
cam.rotation.x,
cam.rotation.y,
cam.rotation.z
);
newModel.userData = cam.userData;
return newModel;
});
const users = filteredData.map((cam: any) => cam.userData);
setActiveUsers((prev: any) => dedupeUsers([...prev, ...users]));
setCams((prev) => dedupeCams([...prev, ...newCams]));
});
}
});
}, []);
const users = filteredData.map((cam: any) => cam.userData);
setActiveUsers((prev: any) => dedupeUsers([...prev, ...users]));
setCams((prev) => dedupeCams([...prev, ...newCams]));
});
}
});
}, []);
return (
<group ref={groupRef} name="Cam-Model-Group">
{cams.map((cam, index) => (
<primitive key={cam.uuid} object={cam}>
<Html
as="div"
center
zIndexRange={[1, 0]}
sprite
style={{
color: "white",
textAlign: "center",
fontFamily: "Arial, sans-serif",
}}
position={[-0.015, 0, 0.7]}
>
<CollabUserIcon
userImage={cam.userData.userImage ||""}
userName={cam.userData.userName}
color={getAvatarColor(index, cam.userData.userName)}
/>
</Html>
</primitive>
))}
</group>
);
return (
<group
ref={groupRef}
name="Cam-Model-Group"
visible={activeModule !== "visualization" ? true : false}
>
{cams.map((cam, index) => (
<primitive key={cam.uuid} object={cam}>
<Html
as="div"
center
zIndexRange={[1, 0]}
sprite
style={{
color: "white",
textAlign: "center",
fontFamily: "Arial, sans-serif",
display: `${activeModule !== "visualization" ? "" : "none"}`,
}}
position={[-0.015, 0, 0.7]}
>
<CollabUserIcon
userImage={cam.userData.userImage || ""}
userName={cam.userData.userName}
color={getAvatarColor(index, cam.userData.userName)}
/>
</Html>
</primitive>
))}
</group>
);
};
export default CamModelsGroup;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<THREE.Group>; }) {
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();

View File

@ -1,7 +0,0 @@
import React from "react";
const Mesh: React.FC = () => {
return <mesh></mesh>;
};
export default Mesh;

View File

@ -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<React.SetStateAction<any[]>>;
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<string, THREE.Material>;
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<THREE.Group | THREE.Mesh>;
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<ProcessContainerProps> = ({
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<THREE.Group>(null);
const debugRef = useRef<boolean>(false);
const clockRef = useRef<THREE.Clock>(new THREE.Clock());
const pauseTimeRef = useRef<number>(0);
const elapsedBeforePauseRef = useRef<number>(0);
const animationStatesRef = useRef<Record<string, ProcessAnimationState>>({});
const { speed, setSpeed } = useAnimationPlaySpeed();
const prevIsPlaying = useRef<boolean | null>(null);
const [internalResetFlag, setInternalResetFlag] = useState(false);
const [animationStates, setAnimationStates] = useState<
Record<string, ProcessAnimationState>
>({});
// Store the speed in a ref to access the latest value in animation frames
const speedRef = useRef<number>(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<string, ProcessAnimationState> = {};
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 (
<>
<group ref={groupRef}>
{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 (
<mesh
key={objectId}
ref={obj.ref as React.RefObject<THREE.Mesh>}
material={obj.material}
position={obj.position}
>
<boxGeometry args={[1, 1, 1]} />
</mesh>
);
}
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 (
<group
key={objectId}
ref={obj.ref as React.RefObject<THREE.Group>}
position={obj.position}
>
<primitive object={clonedScene} />
</group>
);
}
return null;
return (
<ProcessObject
key={objectId}
objectId={objectId}
obj={obj}
renderAs={renderAs}
gltf={gltf}
/>
);
})
)}
</>
</group>
);
};

View File

@ -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<any[]>([]);
interface ProcessContainerProps {
processes: any[];
setProcesses: React.Dispatch<React.SetStateAction<any[]>>;
agvRef: any;
MaterialRef: any;
}
const ProcessContainer: React.FC<ProcessContainerProps> = ({
processes,
setProcesses,
agvRef,
MaterialRef,
}) => {
return (
<>
<ProcessCreator onProcessesCreated={setProcesses} />
{processes.length > 0 && <ProcessAnimator processes={processes} />}
<ProcessAnimator
processes={processes}
setProcesses={setProcesses}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
</>
);
};

View File

@ -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<Process[]>([]);
// 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<ProcessCreatorProps> = 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<ProcessCreatorProps> = 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<ProcessCreatorProps> = 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,

View File

@ -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<ProcessObjectProps> = ({
objectId,
obj,
renderAs = "custom",
gltf,
}) => {
const renderedObject = useMemo(() => {
if (renderAs === "box") {
return (
<mesh
key={objectId}
ref={obj.ref as React.RefObject<THREE.Mesh>}
material={obj.material}
position={obj.position}
>
<boxGeometry args={[1, 1, 1]} />
</mesh>
);
}
if (gltf?.scene) {
const clonedScene = gltf.scene.clone();
clonedScene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = obj.material;
}
});
return (
<group
key={objectId}
ref={obj.ref as React.RefObject<THREE.Group>}
position={obj.position}
>
<primitive object={clonedScene} />
</group>
);
}
return null;
}, [objectId, obj, renderAs, gltf]);
return renderedObject;
};
export default ProcessObject;

View File

@ -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<ProcessObjectRendererProps> = ({
objectId,
object,
process,
gltf,
showBoundingBox = false,
}) => {
const meshRef = useRef<THREE.Mesh>(null);
const boxHelperRef = useRef<THREE.Box3Helper | null>(null);
const boundingBoxRef = useRef<THREE.Box3>(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 (
<primitive
ref={meshRef}
object={gltf.scene.clone()}
position={[0, 0, 0]}
material={object.material}
/>
);
}
// Issue 2: Material color type problem
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial
// Fix the color property access
color={
object.material && 'color' in object.material
? (object.material as THREE.MeshStandardMaterial).color
: "#00ff00"
}
metalness={0.5}
roughness={0.3}
/>
</mesh>
);
};

View File

@ -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<string, THREE.Material>;
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<THREE.Group | THREE.Mesh>;
state: AnimationState;
visible: boolean;
material: THREE.Material;
spawnTime: number;
currentMaterialType: string;
position: THREE.Vector3;
}
export interface ProcessAnimationState {
spawnedObjects: Record<string, SpawnedObject | SpawnedObjectWithCollision>;
spawnedObjects: { [objectId: string]: SpawnedObject };
nextSpawnTime: number;
objectIdCounter: number;
isProcessDelaying: boolean;
processDelayStartTime: number;
processDelayDuration: number;
isCollisionPaused?: boolean;
hasSpawnedZeroIntervalObject?: boolean;
}
export interface SpawnedObject {
ref: React.RefObject<THREE.Object3D>;
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[];
};
}

View File

@ -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<string, number>;
triggerLogs: Array<{
timestamp: number;
pointId: string;
objectId: string;
triggerId: string;
}>;
}
interface ProcessContainerProps {
processes: ProcessData[];
setProcesses: React.Dispatch<React.SetStateAction<any[]>>;
agvRef: any;
}
interface PlayAgvState {
playAgv: Record<string, any>;
setPlayAgv: (data: any) => void;
}
export const useProcessAnimation = (
processes: ProcessData[],
setProcesses: React.Dispatch<React.SetStateAction<any[]>>,
agvRef: any
) => {
// State and refs initialization
const { isPlaying, setIsPlaying } = usePlayButtonStore();
const { isPaused, setIsPaused } = usePauseButtonStore();
const { isReset, setReset } = useResetButtonStore();
const debugRef = useRef<boolean>(false);
const clockRef = useRef<THREE.Clock>(new THREE.Clock());
const pauseTimeRef = useRef<number>(0);
const elapsedBeforePauseRef = useRef<number>(0);
const animationStatesRef = useRef<
Record<string, EnhancedProcessAnimationState>
>({});
const { speed } = useAnimationPlaySpeed();
const prevIsPlaying = useRef<boolean | null>(null);
const [internalResetFlag, setInternalResetFlag] = useState(false);
const [animationStates, setAnimationStates] = useState<
Record<string, EnhancedProcessAnimationState>
>({});
const speedRef = useRef<number>(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<string, EnhancedProcessAnimationState> = {};
processes.forEach((process) => {
const triggerCounts: Record<string, number> = {};
// 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<string, THREE.Material>
): 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<string, THREE.Material>
) => {
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<string, THREE.Material>
): 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,
};
};

View File

@ -16,23 +16,9 @@ function Simulation() {
const { activeModule } = useModuleStore();
const pathsGroupRef = useRef() as React.MutableRefObject<THREE.Group>;
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<any[]>([]);
const agvRef = useRef([]);
const MaterialRef = useRef([]);
return (
<>
@ -41,8 +27,17 @@ function Simulation() {
<>
<PathCreation pathsGroupRef={pathsGroupRef} />
<PathConnector pathsGroupRef={pathsGroupRef} />
<ProcessContainer />
<Agv />
<ProcessContainer
processes={processes}
setProcesses={setProcesses}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
<Agv
processes={processes}
agvRef={agvRef}
MaterialRef={MaterialRef}
/>
</>
)}
</>

View File

@ -1,22 +1,33 @@
// import html2canvas from "html2canvas";
import html2canvas from "html2canvas";
export const captureVisualization = async (): Promise<string | null> => {
const container = document.getElementById("real-time-vis-canvas");
if (!container) return null;
if (!container) {
console.error("Container element not found");
return null;
}
try {
// Use html2canvas to capture the container
// const canvas = await html2canvas(container, {
// scale: 1, // Adjust scale for higher/lower resolution
// });
// Hide any elements you don't want in the screenshot
const originalVisibility = container.style.visibility;
container.style.visibility = 'visible';
const canvas = await html2canvas(container, {
scale: 2, // Higher scale for better quality
logging: false, // Disable console logging
useCORS: true, // Handle cross-origin images
allowTaint: true, // Allow tainted canvas
backgroundColor: '#ffffff', // Set white background
removeContainer: true // Clean up temporary containers
});
// // Convert the canvas to a data URL (PNG format)
// const dataUrl = canvas.toDataURL("image/png");
// return dataUrl;
// Restore original visibility
container.style.visibility = originalVisibility;
return null;
// Convert to PNG with highest quality
return canvas.toDataURL('image/png', 1.0);
} catch (error) {
console.error("Error capturing visualization:", error);
return null;
}
};
};

View File

@ -1,5 +1,3 @@
import { saveTemplateApi } from "../../services/realTimeVisulization/zoneData/saveTempleteApi";
import { useSocketStore } from "../../store/store";
import { Template } from "../../store/useTemplateStore";
import { captureVisualization } from "./captureVisualization";
@ -28,7 +26,7 @@ export const handleSaveTemplate = async ({
templates = [],
visualizationSocket,
}: HandleSaveTemplateProps): Promise<void> => {
console.log('floatingWidget: ', floatingWidget);
console.log("floatingWidget: ", floatingWidget);
try {
// Check if the selected zone has any widgets
if (!selectedZone.panelOrder || selectedZone.panelOrder.length === 0) {
@ -49,12 +47,12 @@ export const handleSaveTemplate = async ({
}
// Capture visualization snapshot
// const snapshot = await captureVisualization();
const snapshot = null;
const snapshot = await captureVisualization();
if (!snapshot) {
return;
}
// if (!snapshot) {
// return;
// }
// Create a new template
const newTemplate: Template = {
id: generateUniqueId(),

View File

@ -1,5 +1,5 @@
// let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
let url_Backend_dwinzo = `http://192.168.0.102:5000`;
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
export const clearPanel = async (
zoneId: string,
organization: string,

View File

@ -18,7 +18,7 @@ export const loadTempleteApi = async (
);
if (!response.ok) {
throw new Error("Failed to add 3dwidget in the zone");
throw new Error("Failed to load Template in the zone");
}
const result = await response.json();

View File

@ -70,9 +70,8 @@ export const useZones = create<any>((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<any>((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<any>((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<SimulationPathsStore>((set) => ({
simulationStates:
typeof paths === "function" ? paths(state.simulationStates) : paths,
})),
}))
}));
export const useNavMesh = create<any>((set: any) => ({
navMesh: null,
@ -456,6 +474,11 @@ export const useTileDistance = create<any>((set: any) => ({
})),
}));
export const usePlayAgv = create<any>((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<ZoneAssetState>((set) => ({
zoneAssetId: null,
setZoneAssetId: (asset) => set({ zoneAssetId: asset }),
}));
}));

View File

@ -14,7 +14,7 @@ interface SelectedZoneState {
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
points: []
lockedPanels: Side[];
zoneId: string;
zoneViewPortTarget: number[];
@ -37,6 +37,7 @@ export const useSelectedZoneStore = create<SelectedZoneStore>((set) => ({
activeSides: [], // Empty array
panelOrder: [], // Empty array
lockedPanels: [], // Empty array
points: [],
zoneId: "",
zoneViewPortTarget: [],
zoneViewPortPosition: [],

View File

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

View File

@ -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;
}
}
@ -45,6 +59,7 @@ input {
.toggle-header-container {
@include flex-center;
padding: 6px 12px;
margin: 6px 0;
.toggle-header-item {
width: 100%;
@ -553,6 +568,7 @@ input {
.input-value {
width: 42px;
text-align: center;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
@ -615,6 +631,7 @@ input {
input {
border: none;
outline: none;
background: transparent;
&::placeholder {
@ -654,4 +671,4 @@ input {
.multi-email-invite-input.active {
border: 1px solid var(--accent-color);
}
}
}

View File

@ -1,507 +1,482 @@
@use '../../../abstracts/variables.scss' as *;
@use '../../../abstracts/mixins.scss' as *;
@use "../../../abstracts/variables.scss" as *;
@use "../../../abstracts/mixins.scss" as *;
.throughput-wrapper,
.card {
background-color: var(--background-color);
box-shadow: var(--box-shadow-heavy);
background-color: var(--background-color);
box-shadow: var(--box-shadow-heavy);
@include flex-center;
flex-direction: column;
gap: 6px;
border-radius: 17.22px;
max-width: 341px;
padding: 14px;
.header {
@include flex-center;
flex-direction: column;
gap: 6px;
border-radius: 17.22px;
max-width: 341px;
align-items: start;
font-weight: $medium-weight;
font-size: $large;
}
padding: 14px;
.display-value {
@include flex-center;
.header {
.left {
@include flex-center;
gap: 6px;
.icon {
width: 27px !important;
height: 27px !important;
background: var(--background-color);
border-radius: 50%;
@include flex-center;
align-items: start;
font-weight: $medium-weight;
font-size: $large;
}
}
.value-container {
display: flex;
flex-direction: column;
.display-value {
@include flex-center;
.value-wrapper {
display: flex;
gap: 6px;
.left {
@include flex-center;
gap: 6px;
.icon {
width: 27px !important;
height: 27px !important;
background: var(--background-color);
border-radius: 50%;
@include flex-center;
}
.value-container {
display: flex;
flex-direction: column;
.value-wrapper {
display: flex;
gap: 6px;
.value {
font-size: $small;
color: var(--accent-color);
font-weight: $bold-weight;
}
}
.total-sales {
display: flex;
gap: 6px;
}
}
.value {
font-size: $small;
color: var(--accent-color);
font-weight: $bold-weight;
}
}
.right {
.percent-increase {
width: 80px;
height: 24px;
border-radius: 7.75px;
background: var(--background-color);
color: #34C759;
@include flex-center;
}
.total-sales {
display: flex;
gap: 6px;
}
}
}
.footer {
text-align: center;
.right {
.percent-increase {
width: 80px;
height: 24px;
border-radius: 7.75px;
background: var(--background-color);
color: #34c759;
@include flex-center;
}
}
}
.footer {
text-align: center;
}
}
.returnOfInvestment {
gap: 10px;
gap: 10px;
min-width: 150px;
.charts {
width: 100%;
height: 200px;
min-width: 150px;
}
.charts {
width: 100%;
height: 200px;
min-width: 150px;
.returns-wrapper {
display: flex;
gap: 4px;
align-items: center;
.value {
font-size: var(--font-size-xxxlarge);
color: var(--accent-color);
}
}
.returns-wrapper {
display: flex;
gap: 4px;
align-items: center;
.footer {
text-align: start;
.value {
font-size: var(--font-size-xxxlarge);
color: var(--accent-color);
}
}
.footer {
text-align: start;
span {
font-weight: $bold-weight;
}
span {
font-weight: $bold-weight;
}
}
}
.productionCapacity-wrapper {
background-color: var(--background-color);
display: flex;
flex-direction: column;
gap: 6px;
width: 313px;
padding: 4.5px;
border-radius: 9.74px;
.headeproductionCapacityr-wrapper,
.bar-chart {
padding: 14px;
background-color: var(--background-color);
display: flex;
flex-direction: column;
gap: 6px;
width: 313px;
padding: 4.5px;
border-radius: 9.74px;
border-radius: 5.2px;
.headeproductionCapacityr-wrapper,
.bar-chart {
padding: 14px;
background-color: var(--background-color);
width: 100%;
height: 150px;
display: flex;
justify-content: center;
align-items: center;
.header {
font-size: $small;
text-align: start;
justify-content: start;
}
.production-capacity {
display: flex;
gap: 6px;
.current,
.target {
display: flex;
flex-direction: column;
gap: 6px;
border-radius: 5.2px;
width: 100%;
height: 150px;
display: flex;
justify-content: center;
align-items: center;
.header {
font-size: $small;
text-align: start;
justify-content: start;
.key,
.value {
font-size: $tiny;
}
}
.production-capacity {
display: flex;
gap: 6px;
.current,
.target {
display: flex;
gap: 6px;
align-items: center;
.key,
.value {
font-size: $tiny;
}
}
.current {
.value {
background: var(--background-color-secondary);
color: #5783F2;
border-radius: 12.99px;
padding: 3px 4px;
}
}
.target {
.value {
background: var(--background-color-secondary);
color: #9641A7;
border-radius: 12.99px;
padding: 3px 4px;
}
}
.current {
.value {
background: var(--background-color-secondary);
color: #5783f2;
border-radius: 12.99px;
padding: 3px 4px;
}
}
}
.bar-chart {
padding: 14px 0;
.target {
.value {
background: var(--background-color-secondary);
color: #9641a7;
border-radius: 12.99px;
padding: 3px 4px;
}
}
}
}
.bar-chart {
padding: 14px 0;
}
}
.stateWorking-wrapper {
min-width: 445px;
font-size: var(--font-size-small);
backdrop-filter: blur(40px);
background: var(--background-color-secondary);
border-radius: 20px;
color: var(--text-color);
background: #252525CC;
min-width: 445px;
font-size: var(--font-size-small);
backdrop-filter: blur(40px);
background: var(--background-color-secondary);
border-radius: 20px;
color: var(--text-color);
background: #252525cc;
.header-wrapper {
width: 100%;
display: flex;
justify-content: space-between;
.header-wrapper {
width: 100%;
display: flex;
justify-content: space-between;
.header {
display: flex;
flex-direction: column;
.header {
display: flex;
flex-direction: column;
span {
font-size: var(--font-size-xxlarge);
span {
font-size: var(--font-size-xxlarge);
&:first-child {
color: #FCFDFD;
}
&:last-child {
color: #34C759;
}
}
&:first-child {
color: #fcfdfd;
}
.img {
border-radius: 4px;
img {
border-radius: 4px;
}
&:last-child {
color: #34c759;
}
}
}
.data-wrapper {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
padding: 10px;
.img {
border-radius: 4px;
.data-table {
display: flex;
justify-content: space-between;
padding: 5px;
border-bottom: 1px dotted #ccc;
.data {
color: #FCFDFD;
font-weight: bold;
}
.key {
color: #FCFDFD;
text-align: right;
}
}
img {
border-radius: 4px;
}
}
}
.data-wrapper {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
padding: 10px;
.data-table {
display: flex;
justify-content: space-between;
padding: 5px;
border-bottom: 1px dotted #ccc;
.data {
color: #fcfdfd;
font-weight: bold;
}
.key {
color: #fcfdfd;
text-align: right;
}
}
}
}
.total-card {
min-height: auto !important;
background: var(--background-color);
min-height: 83px !important;
background: var(--background-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px !important;
.header-wrapper {
display: flex;
flex-direction: column;
gap: 5px;
.header {
color: #a0aec0;
}
.data-values {
display: flex;
align-items: center;
gap: 6px;
.value {
color: var(--text-color);
}
.per {
color: #01b574;
}
}
}
.icon {
width: 45px;
height: 45px;
background-color: var(--accent-color);
display: flex;
justify-content: center;
align-items: center;
justify-content: space-between;
padding: 18px !important;
.header-wrapper {
display: flex;
flex-direction: column;
gap: 5px;
.header {
color: #A0AEC0;
}
.data-values {
display: flex;
align-items: center;
gap: 6px;
.value {
color: var(--text-color);
}
.per {
color: #01B574;
}
}
}
.icon {
width: 45px;
height: 45px;
background-color: var(--accent-color);
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
}
border-radius: 12px;
}
}
.warehouseThroughput {
display: flex;
flex-direction: column;
gap: 6px;
background-color: var(--background-color);
padding: 14px;
.header {
p {
span {
color: var(--accent-color);
}
}
h2 {
color: var(--text-color);
}
min-height: 250px !important;
display: flex;
flex-direction: column;
gap: 6px;
background-color: var(--background-color);
padding: 14px;
.header {
p {
span {
color: var(--accent-color);
}
}
h2 {
color: var(--text-color);
}
}
}
.productivity-dashboard {
width: 100%;
background-color: var(--background-color);
color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
background-color: var(--background-color);
color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
header {
@include flex-space-between;
margin-bottom: 20px;
header {
@include flex-space-between;
margin-bottom: 20px;
h2 {
font-size: $regular;
margin: 0;
font-weight: 600;
}
.options {
background-color: #343b47;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #49505a;
}
}
h2 {
font-size: $regular;
margin: 0;
font-weight: 600;
}
main {
display: flex;
gap: 20px;
.options {
background-color: #343b47;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
cursor: pointer;
.metrics {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
.metric {
background-color: #2c3e50;
padding: 15px;
border-radius: 4px;
.label {
font-size: var(--font-size-regular);
margin: 0;
opacity: 0.7;
}
.value {
font-size: var(--font-size-xlarge);
font-weight: bold;
margin: 0;
}
}
}
.chart-section {
position: relative;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
.progress-circle {
transform: rotate(-90deg);
/* Adjust rotation for SVG */
width: 150px;
height: 150px;
circle {
transition: stroke-dashoffset 0.5s ease-in-out; // Smooth animation
}
}
.chart-details {
position: absolute;
text-align: center;
.title {
font-size: var(--font-size-large);
margin: 0;
opacity: 0.7;
}
.time {
font-size: var(--font-size-xxxlarge);
font-weight: bold;
margin: 0;
}
.subtitle {
font-size: var(--font-size-regular);
margin: 0;
opacity: 0.7;
}
}
}
&:hover {
background-color: #49505a;
}
}
}
main {
display: flex;
gap: 20px;
.metrics {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
.metric {
background-color: #2c3e50;
padding: 15px;
border-radius: 4px;
.label {
font-size: var(--font-size-regular);
margin: 0;
opacity: 0.7;
}
.value {
font-size: var(--font-size-xlarge);
font-weight: bold;
margin: 0;
}
}
}
.chart-section {
position: relative;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
.progress-circle {
transform: rotate(-90deg);
/* Adjust rotation for SVG */
width: 150px;
height: 150px;
circle {
transition: stroke-dashoffset 0.5s ease-in-out; // Smooth animation
}
}
.chart-details {
position: absolute;
text-align: center;
.title {
font-size: var(--font-size-large);
margin: 0;
opacity: 0.7;
}
.time {
font-size: var(--font-size-xxxlarge);
font-weight: bold;
margin: 0;
}
.subtitle {
font-size: var(--font-size-regular);
margin: 0;
opacity: 0.7;
}
}
}
}
}
/* FleetEfficiency.module.css */
.fleetEfficiency {
width: 100%;
min-height: 240px !important;
padding: 20px;
background: var(--background-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
width: 240px;
text-align: center;
display: flex;
flex-direction: column;
gap: 12px;
.progressContainer {
position: relative;
width: 100%;
padding: 20px;
background: var(--background-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
width: 240px;
text-align: center;
display: flex;
flex-direction: column;
gap: 12px;
height: 120px;
overflow: auto !important;
.progress {
width: 100%;
height: 100%;
.progressContainer {
position: relative;
.barOverflow {
width: 100%;
height: 120px;
overflow: auto !important;
.progress {
width: 100%;
height: 100%;
.barOverflow {
width: 100%;
}
}
}
}
}
}
.scaleLabels {
background-color: var(--background-color);
box-shadow: var(--box-shadow-heavy);
display: flex;
justify-content: space-between;
color: #718096;
font-size: var(--font-size-small);
padding: 18px 10px;
position: relative;
z-index: 100;
top: -32px;
background-color: var(--background-color);
box-shadow: var(--box-shadow-heavy);
display: flex;
justify-content: space-between;
color: #718096;
font-size: var(--font-size-small);
padding: 18px 10px;
position: relative;
z-index: 1;
top: -32px;
}
.barOverflow {
position: relative;
overflow: hidden;
width: 100%;
height: 100px;
margin-bottom: -14px;
position: relative;
overflow: hidden;
width: 100%;
height: 100px;
margin-bottom: -14px;
}
.bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200px;
border-radius: 50%;
box-sizing: border-box;
border: 25px solid #eee;
border-bottom-color: var(--accent-color);
border-right-color: var(--accent-color);
transition: transform 0.5s ease;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200px;
border-radius: 50%;
box-sizing: border-box;
border: 25px solid #eee;
border-bottom-color: var(--accent-color);
border-right-color: var(--accent-color);
transition: transform 0.5s ease;
}
// progress should be progress {progress}
// progress should be progress {progress}

View File

@ -66,9 +66,9 @@
.sidebar-left-content-container {
border-bottom: 1px solid var(--border-color);
// flex: 1;
height: calc(100% - 36px);
// height: calc(100% - 36px);
position: relative;
overflow: auto;
// overflow: auto;
.template-list {
display: flex;
@ -78,19 +78,19 @@
min-height: 50vh;
max-height: 60vh;
}
.template-item {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1rem;
transition: box-shadow 0.3s ease;
}
.template-image-container {
position: relative;
padding-bottom: 56.25%; // 16:9 aspect ratio
}
.template-image {
position: absolute;
width: 100%;
@ -100,19 +100,19 @@
cursor: pointer;
transition: transform 0.3s ease;
}
.template-details {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
}
.template-name {
cursor: pointer;
font-weight: 500;
}
.delete-button {
padding: 0.25rem 0.5rem;
background: #ff4444;
@ -122,18 +122,21 @@
cursor: pointer;
transition: opacity 0.3s ease;
}
.no-templates {
text-align: center;
color: #666;
padding: 2rem;
grid-column: 1 / -1;
}
.widget-left-sideBar {
min-height: 50vh;
max-height: 60vh;
.widgets-wrapper {
min-height: 50vh;
max-height: 60vh;
overflow: auto;
}
.widget2D {
overflow: auto;
@ -538,7 +541,6 @@
.floating {
width: 100%;
}
}
@ -1070,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 {
@ -1114,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;
}
}
}
@ -1200,4 +1257,4 @@
.assets-wrapper {
margin: 0;
}
}
}

View File

@ -32,7 +32,7 @@
min-width: 250px;
max-width: 300px;
min-height: 83px !important;
// min-height: 83px !important;
// max-height: 100px !important;
background: var(--background-color);
@ -362,7 +362,7 @@
.panel.hidePanel {
pointer-events: none;
opacity: 0.1;
opacity: 0;
}
}
@ -769,20 +769,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;
@ -795,7 +794,8 @@
color: #f65648;
&:hover {
background-color: #ffe3e0;
background-color: #f65648;
color: white;
}
}
}

View File

@ -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<PointerEvent>;
/** Vector and Reference Types **/
// 2D Vector type from THREE.js
@ -33,7 +32,6 @@ export type RefVector3 = React.MutableRefObject<THREE.Vector3 | null>;
// 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<THREE.Object3D<THREE.Object3DEventMap>>[];
export type IntersectsType = THREE.Intersection<
THREE.Object3D<THREE.Object3DEventMap>
>[];
// Event type for mesh interactions
export type MeshEvent = IntersectionEvent<MouseEvent>;
@ -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<DragControls | null>;
/** Primitive Types with Mutable References **/
export type String = string;
@ -109,15 +107,15 @@ export type NumberArray = number[];
export type RefRaycaster = React.MutableRefObject<THREE.Raycaster>;
// Camera reference, supporting both perspective and basic cameras
export type RefCamera = React.MutableRefObject<THREE.Camera | THREE.PerspectiveCamera>;
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<Line | [Point] | []>;
// Collection of lines for structured geometry
export type Lines = Array<Line>;
/** 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<Wall>;
@ -145,15 +148,22 @@ export type Walls = Array<Wall>;
export type RefWalls = React.MutableRefObject<Walls>;
// 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<Array<{ coordinates: Array<{ position: THREE.Vector3, uuid: string }>, 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<Lines>;
/** Floor Line Types for Layered Structures **/
// Floor line type for single lines on the floor level
@ -168,18 +178,16 @@ export type OnlyFloorLines = Array<Lines>;
// Reference for multi-level floor lines, allowing structured updates
export type RefOnlyFloorLines = React.MutableRefObject<OnlyFloorLines>;
/** 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<React.SetStateAction<boolean>>;
// Mutable reference for TubeGeometry, allowing dynamic geometry updates
export type RefTubeGeometry = React.MutableRefObject<THREE.TubeGeometry | null>;
/** 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<FloorItemType>;
// Dispatch type for setting floor item state in React
export type setFloorItemSetState = React.Dispatch<React.SetStateAction<FloorItems | null | undefined>>;
export type setFloorItemSetState = React.Dispatch<
React.SetStateAction<FloorItems | null | undefined>
>;
/** 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<WallItem>;
// Dispatch for setting wall item state in React
export type setWallItemSetState = React.Dispatch<React.SetStateAction<WallItem[]>>;
export type setWallItemSetState = React.Dispatch<
React.SetStateAction<WallItem[]>
>;
// Dispatch for setting vector3 state in React
export type setVector3State = React.Dispatch<React.SetStateAction<THREE.Vector3>>;
export type setVector3State = React.Dispatch<
React.SetStateAction<THREE.Vector3>
>;
// Dispatch for setting euler state in React
export type setEulerState = React.Dispatch<React.SetStateAction<THREE.Euler>>;
@ -258,147 +269,250 @@ export type setEulerState = React.Dispatch<React.SetStateAction<THREE.Euler>>;
// Reference type for wall items, allowing direct access to the mutable array
export type RefWallItems = React.MutableRefObject<WallItem[]>;
/** 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<CSM>;
export type RefCSMHelper = React.MutableRefObject<CSMHelper>;
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 }[] };
};
};
}
};
};

View File

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

View File

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