diff --git a/app/src/components/layout/scenes/ComparisonScene.tsx b/app/src/components/layout/scenes/ComparisonScene.tsx index ce8e455..f803dee 100644 --- a/app/src/components/layout/scenes/ComparisonScene.tsx +++ b/app/src/components/layout/scenes/ComparisonScene.tsx @@ -1,6 +1,5 @@ import { useProductContext } from '../../../modules/simulation/products/productContext' import RegularDropDown from '../../ui/inputs/RegularDropDown'; -import { useProductStore } from '../../../store/simulation/useProductStore'; import { useCompareProductDataStore, useLoadingProgress, useSaveVersion } from '../../../store/builder/store'; import useModuleStore from '../../../store/useModuleStore'; import CompareLayOut from '../../ui/compareVersion/CompareLayOut'; @@ -8,10 +7,12 @@ import ComparisonResult from '../../ui/compareVersion/ComparisonResult'; import { useComparisonProduct, useMainProduct } from '../../../store/simulation/useSimulationStore'; import { usePlayButtonStore } from '../../../store/usePlayButtonStore'; import { useEffect, useState } from 'react'; +import { useDuplicateProductStore } from '../../../modules/duplicate/duplicateStores/duplicateProductStore'; +import ProductsDuplicate from '../../../modules/duplicate/duplicateSimulation/duplicateProduct/duplicateProduct'; function ComparisonScene() { - const { isPlaying, setIsPlaying } = usePlayButtonStore(); - const { products } = useProductStore(); + const { isPlaying } = usePlayButtonStore(); + const { products, getProductById } = useDuplicateProductStore(); const { isVersionSaved } = useSaveVersion(); const { activeModule } = useModuleStore(); const { selectedProductStore } = useProductContext(); @@ -28,41 +29,69 @@ function ComparisonScene() { setComparisonProduct(product.productUuid, product.productName); } }; - // useEffect(() => { - // setCompareProductsData([ - // { - // "productUuid": "15193386-ec58-4ec6-8a92-e665a39eebf1", - // "productName": "Product 1", - // "simulationData": { - // "roiPercentage": 273.9428571428571, - // "paybackPeriod": 1.8251981643721318, - // "netProfit": 9588000, - // "productionCapacity": 4508.5, - // "machineIdleTime": 1450, - // "machineActiveTime": 430, - // "throughputData": 180.34 - // } - // }, - // { - // "productUuid": "f614bf50-f61d-41c5-acc0-3783fb4da6b8", - // "productName": "Product 2", - // "simulationData": { - // "roiPercentage": 281.7214285714286, - // "paybackPeriod": 1.7748028701097842, - // "netProfit": 9860250, - // "productionCapacity": 4599.25, - // "machineIdleTime": 1885, - // "machineActiveTime": 646, - // "throughputData": 183.97 - // } - // } - // ]) - // }, []); // ✅ Runs only once on mount + useEffect(() => { + if (compareProductsData.length === 2) return; + // const productData = getProductById(selectedProduct.productUuid); + + // const newData = { + // "productUuid": "15193386-ec58-4ec6-8a92-e665a39eebf2", + // "productName": "Product 2", + // "simulationData": { + // "roiPercentage": 281.7214285714286, + // "paybackPeriod": 1.7748028701097842, + // "netProfit": 9860250, + // "productionCapacity": 4599.25, + // "machineIdleTime": 1885, + // "machineActiveTime": 646, + // "throughputData": 183.97 + // } + // } + + // const existingIndex = compareProductsData.findIndex((item) => + // item.productUuid === productData?.productUuid + // ); + + // if (existingIndex !== -1) { + // const updated = [...compareProductsData]; + // updated[existingIndex] = newData; + // setCompareProductsData(updated); + // } else { + // setCompareProductsData([...compareProductsData, newData]); + // } + + setCompareProductsData([ + { + "productUuid": "15193386-ec58-4ec6-8a92-e665a39eebf1", + "productName": "Product 1", + "simulationData": { + "roiPercentage": 273.9428571428571, + "paybackPeriod": 1.8251981643721318, + "netProfit": 9588000, + "productionCapacity": 4508.5, + "machineIdleTime": 1450, + "machineActiveTime": 430, + "throughputData": 180.34 + } + }, + { + "productUuid": "15193386-ec58-4ec6-8a92-e665a39eebf2", + "productName": "Product 2", + "simulationData": { + "roiPercentage": 281.7214285714286, + "paybackPeriod": 1.7748028701097842, + "netProfit": 9860250, + "productionCapacity": 4599.25, + "machineIdleTime": 1885, + "machineActiveTime": 646, + "throughputData": 183.97 + } + } + ]) + }, [compareProductsData]); // ✅ Runs only once on mount useEffect(() => { if (mainProduct && comparisonProduct && compareProductsData.length > 1) { - // console.log('compareProductsData: ', compareProductsData); const hasMain = compareProductsData.some(val => val.productUuid === mainProduct.productUuid); const hasComparison = compareProductsData.some(val => val.productUuid === comparisonProduct.productUuid); if (hasMain && hasComparison && mainProduct.productUuid !== comparisonProduct.productUuid) { @@ -91,6 +120,8 @@ function ComparisonScene() { } + + {(shouldShowComparisonResult && !loadingProgress) && } )} diff --git a/app/src/components/layout/scenes/MainScene.tsx b/app/src/components/layout/scenes/MainScene.tsx index 48d6adf..f2c62ff 100644 --- a/app/src/components/layout/scenes/MainScene.tsx +++ b/app/src/components/layout/scenes/MainScene.tsx @@ -53,7 +53,7 @@ function MainScene() { const { visualizationSocket } = useSocketStore(); const { selectedZone } = useSelectedZoneStore(); const { setFloatingWidget } = useFloatingWidget(); - const { clearComparisonProduct } = useComparisonProduct(); + const { comparisonProduct, clearComparisonProduct } = useComparisonProduct(); const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem(); const { setName } = useAssetsStore(); const { projectId } = useParams() @@ -120,7 +120,9 @@ function MainScene() { )}
{ - const { comparisonProduct, setComparisonProduct, clearComparisonProduct } = - useComparisonProduct(); - const { products } = useProductStore(); + const { comparisonProduct, setComparisonProduct, clearComparisonProduct } = useComparisonProduct(); + const { products } = useDuplicateProductStore(); const { setLoadingProgress } = useLoadingProgress(); const [width, setWidth] = useState("50vw"); const [isResizing, setIsResizing] = useState(false); @@ -141,7 +141,8 @@ const CompareLayOut = () => { {comparisonProduct && (
- + {/* */} +
)} diff --git a/app/src/modules/builder/aisle/Instances/aisleInstances.tsx b/app/src/modules/builder/aisle/Instances/aisleInstances.tsx index d83028b..d0e44a4 100644 --- a/app/src/modules/builder/aisle/Instances/aisleInstances.tsx +++ b/app/src/modules/builder/aisle/Instances/aisleInstances.tsx @@ -26,8 +26,6 @@ function AisleInstances() { return points; }, [aisles]); - - return ( <> {toggleView && diff --git a/app/src/modules/duplicate/duplicateBuilder/builderDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/builderDuplicate.tsx new file mode 100644 index 0000000..1a40f24 --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/builderDuplicate.tsx @@ -0,0 +1,48 @@ +////////// Three and React Three Fiber Imports ////////// + +import * as THREE from "three"; +import { useRef } from "react"; + +import * as Types from "../../../types/world/worldTypes"; +import Ground from "../../scene/environment/ground"; +import { Bvh } from "@react-three/drei"; +import AssetsGroupDuplicate from "./duplicateAsset/assetsGroupDuplicate"; +import FloorGroupDuplicate from "./duplicateFloor/floorGroupDuplicate"; +import WallsDuplicate from "./duplicateWall/wallsDuplicate"; +import ZoneDuplicate from "./duplicateZone/zoneDuplicate"; +import AislesDuplicate from "./duplicateAisle/aislesDuplicate"; + +export default function BuilderDuplicate({ projectId }: { projectId: string }) { + const plane = useRef(null); // Reference for a plane object for raycaster reference. + const grid = useRef() as any; // Reference for a grid object for raycaster reference. + const lines = useRef([]) as Types.RefLines; // Reference for lines which stores all the lines that are ever drawn. + const floorGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the roofs and the floors. + + ////////// Return ////////// + + return ( + <> + + + + + + + + + + + + + + + + ); +} diff --git a/app/src/modules/duplicate/duplicateBuilder/duplicateAisle/aislesDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/duplicateAisle/aislesDuplicate.tsx new file mode 100644 index 0000000..ac8ba0c --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/duplicateAisle/aislesDuplicate.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react' +import { getAisleApi } from '../../../../services/factoryBuilder/aisle/getAisleApi'; +import AisleInstance from '../../../builder/aisle/Instances/instance/aisleInstance'; + +function AislesDuplicate({ projectId }: { projectId: string }) { + const [aisles, setAisles] = useState([]); + + useEffect(() => { + const fetchAisle = async () => { + if (projectId) { + const aisles = await getAisleApi(projectId); + setAisles(aisles); + } + } + fetchAisle() + }, []) + + return ( + + {aisles.map((aisle) => ( + + ))} + + ) +} + +export default AislesDuplicate \ No newline at end of file diff --git a/app/src/modules/duplicate/duplicateBuilder/duplicateAsset/assetsGroupDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/duplicateAsset/assetsGroupDuplicate.tsx new file mode 100644 index 0000000..6759b2d --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/duplicateAsset/assetsGroupDuplicate.tsx @@ -0,0 +1,253 @@ +import * as THREE from "three" +import { useEffect, useState } from 'react' +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; +import ModelDuplicate from "./modelDuplicate"; +import { useLoadingProgress } from "../../../../store/builder/store"; +import { getFloorAssets } from "../../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi"; +import { FloorItems } from "../../../../types/world/worldTypes"; +import { useDuplicateEventsStore } from "../../duplicateStores/duplicateEventStore"; + +const gltfLoaderWorker = new Worker( + new URL( + "../../../../services/factoryBuilder/webWorkers/gltfLoaderWorker.js", + import.meta.url + ) +); + +function AssetsGroupDuplicate({ projectId }: { projectId: string }) { + const { setLoadingProgress } = useLoadingProgress(); + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + const [assetsDuplicates, setAssetsDuplicates] = useState([]); + const { addEvent } = useDuplicateEventsStore(); + + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); + + dracoLoader.setDecoderPath( + "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/" + ); + loader.setDRACOLoader(dracoLoader); + + useEffect(() => { + let totalAssets = 0; + let loadedAssets = 0; + + const updateLoadingProgress = (progress: number) => { + if (progress < 100) { + setLoadingProgress(progress); + } else if (progress === 100) { + setTimeout(() => { + setLoadingProgress(100); + setTimeout(() => { + setLoadingProgress(0); + }, 1500); + }, 1000); + } + }; + + getFloorAssets(organization, projectId).then((data) => { + if (data.length > 0) { + const uniqueItems = (data as FloorItems).filter((item, index, self) => index === self.findIndex((t) => t.assetId === item.assetId)); + totalAssets = uniqueItems.length; + if (totalAssets === 0) { + updateLoadingProgress(100); + return; + } + gltfLoaderWorker.postMessage({ floorItems: uniqueItems }); + } else { + gltfLoaderWorker.postMessage({ floorItems: [] }); + updateLoadingProgress(100); + } + }); + + gltfLoaderWorker.onmessage = async (event) => { + if (event.data.message === "gltfLoaded" && event.data.modelBlob) { + const blobUrl = URL.createObjectURL(event.data.modelBlob); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(event.data.modelID, gltf); + + loadedAssets++; + const progress = Math.round((loadedAssets / totalAssets) * 100); + updateLoadingProgress(progress); + + if (loadedAssets === totalAssets) { + const assets: Asset[] = []; + getFloorAssets(organization, projectId).then((data: FloorItems) => { + data.map((item) => { + if (item.eventData) { + assets.push({ + modelUuid: item.modelUuid, + modelName: item.modelName, + assetId: item.assetId, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + isLocked: item.isLocked, + isCollidable: false, + isVisible: item.isVisible, + opacity: 1, + eventData: item.eventData + }) + + if (item.eventData.type === "Vehicle") { + const vehicleEvent: VehicleEventSchema = { + modelUuid: item.modelUuid, + modelName: item.modelName, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + state: "idle", + type: "vehicle", + speed: 1, + point: { + uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(), + position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0], + rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0], + action: { + actionUuid: THREE.MathUtils.generateUUID(), + actionName: "Action 1", + actionType: "travel", + unLoadDuration: 5, + loadCapacity: 1, + steeringAngle: 0, + pickUpPoint: null, + unLoadPoint: null, + triggers: [] + } + } + }; + addEvent(vehicleEvent); + } else if (item.eventData.type === "Conveyor") { + const ConveyorEvent: ConveyorEventSchema = { + modelUuid: item.modelUuid, + modelName: item.modelName, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + state: "idle", + type: "transfer", + speed: 1, + points: item.eventData.points?.map((point: any, index: number) => ({ + uuid: point.uuid || THREE.MathUtils.generateUUID(), + position: [point.position[0], point.position[1], point.position[2]], + rotation: [point.rotation[0], point.rotation[1], point.rotation[2]], + action: { + actionUuid: THREE.MathUtils.generateUUID(), + actionName: `Action 1`, + actionType: 'default', + material: 'Default material', + delay: 0, + spawnInterval: 5, + spawnCount: 1, + triggers: [] + } + })) || [], + }; + addEvent(ConveyorEvent); + } else if (item.eventData.type === "StaticMachine") { + const machineEvent: MachineEventSchema = { + modelUuid: item.modelUuid, + modelName: item.modelName, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + state: "idle", + type: "machine", + point: { + uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(), + position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0], + rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0], + action: { + actionUuid: THREE.MathUtils.generateUUID(), + actionName: "Action 1", + actionType: "process", + processTime: 10, + swapMaterial: "Default Material", + triggers: [] + } + } + }; + addEvent(machineEvent); + } else if (item.eventData.type === "ArmBot") { + const roboticArmEvent: RoboticArmEventSchema = { + modelUuid: item.modelUuid, + modelName: item.modelName, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + state: "idle", + type: "roboticArm", + speed: 1, + point: { + uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(), + position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0], + rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0], + actions: [ + { + actionUuid: THREE.MathUtils.generateUUID(), + actionName: "Action 1", + actionType: "pickAndPlace", + process: { + startPoint: null, + endPoint: null + }, + triggers: [] + } + ] + } + }; + addEvent(roboticArmEvent); + } else if (item.eventData.type === 'Storage') { + const storageEvent: StorageEventSchema = { + modelUuid: item.modelUuid, + modelName: item.modelName, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], state: "idle", + type: "storageUnit", + point: { + uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(), + position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0], + rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0], + action: { + actionUuid: THREE.MathUtils.generateUUID(), + actionName: "Action 1", + actionType: "store", + storageCapacity: 10, + triggers: [] + } + } + }; + addEvent(storageEvent); + } + } else { + assets.push({ + modelUuid: item.modelUuid, + modelName: item.modelName, + assetId: item.assetId, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + isLocked: item.isLocked, + isCollidable: false, + isVisible: item.isVisible, + opacity: 1, + }); + } + }); + setAssetsDuplicates(assets); + }); + updateLoadingProgress(100); + } + }); + } + }; + }, []); + + return ( + <> + {assetsDuplicates.map((asset) => + + )} + + ) +} + +export default AssetsGroupDuplicate; \ No newline at end of file diff --git a/app/src/modules/duplicate/duplicateBuilder/duplicateAsset/modelDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/duplicateAsset/modelDuplicate.tsx new file mode 100644 index 0000000..f25458e --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/duplicateAsset/modelDuplicate.tsx @@ -0,0 +1,123 @@ +import * as THREE from 'three'; +import { useEffect, useRef, useState } from 'react'; +import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import { useFrame, useThree } from '@react-three/fiber'; +import { useRenderDistance } from '../../../../store/builder/store'; +import { AssetBoundingBox } from '../../../builder/asset/functions/assetBoundingBox'; +import { retrieveGLTF, storeGLTF } from '../../../../utils/indexDB/idbUtils'; + +function ModelDuplicate({ asset }: { readonly asset: Asset }) { + const { camera } = useThree(); + const { renderDistance } = useRenderDistance(); + const [isRendered, setIsRendered] = useState(false); + const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + const [gltfScene, setGltfScene] = useState(null); + const [boundingBox, setBoundingBox] = useState(null); + const groupRef = useRef(null); + + useEffect(() => { + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); + + dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); + loader.setDRACOLoader(dracoLoader); + const loadModel = async () => { + try { + // Check Cache + const assetId = asset.assetId; + const cachedModel = THREE.Cache.get(assetId); + if (cachedModel) { + setGltfScene(cachedModel.scene.clone()); + calculateBoundingBox(cachedModel.scene); + return; + } + + // Check IndexedDB + const indexedDBModel = await retrieveGLTF(assetId); + if (indexedDBModel) { + const blobUrl = URL.createObjectURL(indexedDBModel); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(assetId, gltf); + setGltfScene(gltf.scene.clone()); + calculateBoundingBox(gltf.scene); + }, + undefined, + (error) => { + echo.error(`[IndexedDB] Error loading ${asset.modelName}:`); + URL.revokeObjectURL(blobUrl); + } + ); + return; + } + + // Fetch from Backend + const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${assetId}`; + const handleBackendLoad = async (gltf: GLTF) => { + try { + const response = await fetch(modelUrl); + const modelBlob = await response.blob(); + await storeGLTF(assetId, modelBlob); + THREE.Cache.add(assetId, gltf); + setGltfScene(gltf.scene.clone()); + calculateBoundingBox(gltf.scene); + } catch (error) { + console.error(`[Backend] Error storing/loading ${asset.modelName}:`, error); + } + }; + loader.load( + modelUrl, + handleBackendLoad, + undefined, + (error) => { + echo.error(`[Backend] Error loading ${asset.modelName}:`); + } + ); + } catch (err) { + console.error("Failed to load model:", asset.assetId, err); + } + }; + + const calculateBoundingBox = (scene: THREE.Object3D) => { + const box = new THREE.Box3().setFromObject(scene); + setBoundingBox(box); + }; + + loadModel(); + + }, []); + + useFrame(() => { + const assetPosition = new THREE.Vector3(...asset.position); + if (!isRendered && assetPosition.distanceTo(camera.position) <= renderDistance) { + setIsRendered(true); + } else if (isRendered && assetPosition.distanceTo(camera.position) > renderDistance) { + setIsRendered(false); + } + }) + + return ( + + {gltfScene && ( + isRendered ? ( + + ) : ( + + ) + )} + + ); +} + +export default ModelDuplicate; \ No newline at end of file diff --git a/app/src/modules/duplicate/duplicateBuilder/duplicateFloor/floorGroupDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/duplicateFloor/floorGroupDuplicate.tsx new file mode 100644 index 0000000..9f062df --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/duplicateFloor/floorGroupDuplicate.tsx @@ -0,0 +1,47 @@ +import { + useRoofVisibility, + useToggleView, + useWallVisibility, +} from "../../../../store/builder/store"; +import { useEffect } from "react"; +import loadFloor from "../../../builder/geomentries/floors/loadFloor"; +import { getLines } from "../../../../services/factoryBuilder/lines/getLinesApi"; +import objectLinesToArray from "../../../builder/geomentries/lines/lineConvertions/objectLinesToArray"; +import { useFrame, useThree } from "@react-three/fiber"; +import hideRoof from "../../../builder/geomentries/roofs/hideRoof"; +import hideWalls from "../../../builder/geomentries/walls/hideWalls"; + +const FloorGroupDuplicate = ({ + floorGroup, + lines, + projectId +}: any) => { + const { scene, camera } = useThree(); + const { toggleView } = useToggleView(); + const { roofVisibility } = useRoofVisibility(); + const { wallVisibility } = useWallVisibility(); + + useEffect(() => { + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + getLines(organization, projectId).then((data) => { + const Lines = objectLinesToArray(data); + + if (Lines) { + lines.current = Lines; + loadFloor(lines, floorGroup); + } + }) + }, []); + + useFrame(() => { + hideRoof(roofVisibility, floorGroup, camera); + hideWalls(wallVisibility, scene, camera); + }); + + return ( + + ); +}; + +export default FloorGroupDuplicate; diff --git a/app/src/modules/duplicate/duplicateBuilder/duplicateWall/wallMeshDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/duplicateWall/wallMeshDuplicate.tsx new file mode 100644 index 0000000..d819d57 --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/duplicateWall/wallMeshDuplicate.tsx @@ -0,0 +1,68 @@ +import * as THREE from "three"; +import * as Types from "../../../../types/world/worldTypes"; +import * as CONSTANTS from "../../../../types/world/worldConstants"; +import { Base } from "@react-three/csg"; +import { MeshDiscardMaterial } from "@react-three/drei"; +import React, { useEffect } from "react"; +import objectLinesToArray from "../../../builder/geomentries/lines/lineConvertions/objectLinesToArray"; +import loadWalls from "../../../builder/geomentries/walls/loadWalls"; +import texturePath from "../../../../assets/textures/floor/wall-tex.png"; +import { getLines } from "../../../../services/factoryBuilder/lines/getLinesApi"; + +const WallsMeshDuplicate = ({ projectId, walls, setWalls, lines }: any) => { + + useEffect(() => { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + + getLines(organization, projectId).then((data) => { + const Lines: Types.Lines = objectLinesToArray(data); + if (Lines) { + lines.current = Lines; + loadWalls(lines, setWalls); + } + }); + }, []); + + const wallTexture = React.useMemo(() => { + const textureLoader = new THREE.TextureLoader(); + const texture = textureLoader.load(texturePath); + texture.wrapS = texture.wrapT = THREE.RepeatWrapping; + texture.repeat.set(0.1, 0.1); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; + }, []); + + return ( + <> + {walls.map((wall: Types.Wall, index: number) => ( + + + + + + + + + ))} + + ); +}; + +export default WallsMeshDuplicate; diff --git a/app/src/modules/duplicate/duplicateBuilder/duplicateWall/wallsDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/duplicateWall/wallsDuplicate.tsx new file mode 100644 index 0000000..bcedb51 --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/duplicateWall/wallsDuplicate.tsx @@ -0,0 +1,29 @@ +import { Geometry } from "@react-three/csg"; +import { + useToggleView, +} from "../../../../store/builder/store"; +import WallsMeshDuplicate from "./wallMeshDuplicate"; +import { useState } from "react"; + +const WallsDuplicate = ({ + lines, + projectId +}: any) => { + const { toggleView } = useToggleView(); + const [walls, setWalls] = useState([]); + + return ( + + + + + + ); +}; + +export default WallsDuplicate; diff --git a/app/src/modules/duplicate/duplicateBuilder/duplicateZone/zoneDuplicate.tsx b/app/src/modules/duplicate/duplicateBuilder/duplicateZone/zoneDuplicate.tsx new file mode 100644 index 0000000..8158438 --- /dev/null +++ b/app/src/modules/duplicate/duplicateBuilder/duplicateZone/zoneDuplicate.tsx @@ -0,0 +1,182 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { Html } from "@react-three/drei"; +import * as THREE from "three"; +import { + useToggleView, + useZonePoints, +} from "../../../../store/builder/store"; +import { getZonesApi } from "../../../../services/factoryBuilder/zones/getZonesApi"; +import * as CONSTANTS from "../../../../types/world/worldConstants"; +import * as turf from "@turf/turf"; +import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore"; + +const ZoneDuplicate = ({ projectId }: { projectId: string }) => { + const [zones, setZones] = useState([]); + const { setZonePoints } = useZonePoints(); + const { selectedZone } = useSelectedZoneStore(); + const { toggleView } = useToggleView(); + + const groupsRef = useRef(); + + const zoneMaterial = useMemo( + () => + new THREE.ShaderMaterial({ + side: THREE.DoubleSide, + vertexShader: ` + varying vec2 vUv; + void main(){ + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + vUv = uv; + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform vec3 uOuterColor; + void main(){ + float alpha = 1.0 - vUv.y; + gl_FragColor = vec4(uOuterColor, alpha); + } + `, + uniforms: { + uOuterColor: { value: new THREE.Color(CONSTANTS.zoneConfig.color) }, + }, + transparent: true, + depthWrite: false, + }), + [] + ); + + useEffect(() => { + const fetchZones = async () => { + const email = localStorage.getItem("email"); + if (!email) return; + + const organization = email.split("@")[1].split(".")[0]; + const data = await getZonesApi(organization, projectId); + + if (data.length > 0) { + const fetchedZones = data.map((zone: any) => ({ + zoneUuid: zone.zoneUuid, + zoneName: zone.zoneName, + points: zone.points, + viewPortCenter: zone.viewPortCenter, + viewPortposition: zone.viewPortposition, + layer: zone.layer, + })); + + setZones(fetchedZones); + + const fetchedPoints = data.flatMap((zone: any) => + zone.points.slice(0, 4).map((point: [number, number, number]) => new THREE.Vector3(...point)) + ); + + setZonePoints(fetchedPoints); + } + }; + + fetchZones(); + }, []); + + return ( + + + {zones.map((zone: any) => ( + + {zone.points + .slice(0, -1) + .map((point: [number, number, number], index: number) => { + const nextPoint = zone.points[index + 1]; + + const point1 = new THREE.Vector3(point[0], point[1], point[2]); + const point2 = new THREE.Vector3( + nextPoint[0], + nextPoint[1], + nextPoint[2] + ); + + const planeWidth = point1.distanceTo(point2); + const planeHeight = CONSTANTS.zoneConfig.height; + + const midpoint = new THREE.Vector3( + (point1.x + point2.x) / 2, + CONSTANTS.zoneConfig.height / 2 + + (zone.layer - 1) * CONSTANTS.zoneConfig.height, + (point1.z + point2.z) / 2 + ); + + const angle = Math.atan2( + point2.z - point1.z, + point2.x - point1.x + ); + + return ( + + + + + ); + })} + {!toggleView && + (() => { + const points3D = zone.points || []; + const coords2D = points3D.map((p: any) => [p[0], p[2]]); + + // Ensure the polygon is closed + if ( + coords2D.length >= 4 && + (coords2D[0][0] !== coords2D[coords2D.length - 1][0] || + coords2D[0][1] !== coords2D[coords2D.length - 1][1]) + ) { + coords2D.push(coords2D[0]); + } + if (coords2D.length < 4) return null; + + const polygon = turf.polygon([coords2D]); + const center2D = turf.center(polygon).geometry.coordinates; + + // Calculate the average Y value + const sumY = points3D.reduce( + (sum: number, p: any) => sum + p[1], + 0 + ); + const avgY = points3D.length > 0 ? sumY / points3D.length : 0; + + const htmlPosition: [number, number, number] = [ + center2D[0], + avgY + (CONSTANTS.zoneConfig.height || 0) + 1.5, + center2D[1], + ]; + + return ( + +
{zone.zoneName}
+ + ); + })()} +
+ ))} +
+
+ ); +}; + +export default ZoneDuplicate; diff --git a/app/src/modules/duplicate/duplicateSetup/controlsDuplicate.tsx b/app/src/modules/duplicate/duplicateSetup/controlsDuplicate.tsx new file mode 100644 index 0000000..18b4259 --- /dev/null +++ b/app/src/modules/duplicate/duplicateSetup/controlsDuplicate.tsx @@ -0,0 +1,37 @@ +import { CameraControls } from "@react-three/drei"; +import { useRef } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import * as CONSTANTS from '../../../types/world/worldConstants'; + +import { useDuplicatedCamData, useToggleView } from "../../../store/builder/store"; + +export default function ControlsDuplicate() { + const controlsRef = useRef(null); + const { toggleView } = useToggleView(); + const state = useThree(); + const { duplicatedCamData } = useDuplicatedCamData(); + + useFrame(() => { + controlsRef.current?.setPosition(...duplicatedCamData.camPosition, true); + controlsRef.current?.setTarget(...duplicatedCamData.camTarget, true); + }) + + return ( + <> + + + + ); +} \ No newline at end of file diff --git a/app/src/modules/duplicate/duplicateSetup/postProcessingDuplicate.tsx b/app/src/modules/duplicate/duplicateSetup/postProcessingDuplicate.tsx new file mode 100644 index 0000000..dfc2e2c --- /dev/null +++ b/app/src/modules/duplicate/duplicateSetup/postProcessingDuplicate.tsx @@ -0,0 +1,18 @@ +import { EffectComposer, N8AO } from "@react-three/postprocessing"; + +export default function PostProcessingDuplicate() { + + return ( + + + + ); +} diff --git a/app/src/modules/duplicate/duplicateSimulation/duplicatePoint/duplicatePoint.tsx b/app/src/modules/duplicate/duplicateSimulation/duplicatePoint/duplicatePoint.tsx new file mode 100644 index 0000000..07e8ad6 --- /dev/null +++ b/app/src/modules/duplicate/duplicateSimulation/duplicatePoint/duplicatePoint.tsx @@ -0,0 +1,142 @@ +import * as THREE from "three"; +import { useEventsStore } from "../../../../store/simulation/useEventsStore"; +import useModuleStore from "../../../../store/useModuleStore"; +import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; + +function PointsDuplicate() { + const { events } = useEventsStore(); + const { activeModule } = useModuleStore(); + const { isPlaying } = usePlayButtonStore(); + + return ( + <> + {activeModule === "simulation" && ( + <> + + {events.map((event, index) => { + const usedEvent = event; + + if (usedEvent.type === "transfer") { + return ( + + {usedEvent.points.map((point, j) => ( + + + + + ))} + + ); + } else if (usedEvent.type === "vehicle") { + const point = usedEvent.point; + return ( + + + + + + + ); + } else if (usedEvent.type === "roboticArm") { + const point = usedEvent.point; + return ( + + + + + + + ); + } else if (usedEvent.type === "machine") { + const point = usedEvent.point; + return ( + + + + + + + ); + } else if (usedEvent.type === "storageUnit") { + const point = usedEvent.point; + return ( + + + + + + + ); + } else { + return null; + } + })} + + + )} + + ); +} + +export default PointsDuplicate; diff --git a/app/src/modules/duplicate/duplicateSimulation/duplicateProduct/duplicateProduct.tsx b/app/src/modules/duplicate/duplicateSimulation/duplicateProduct/duplicateProduct.tsx new file mode 100644 index 0000000..4c1a69d --- /dev/null +++ b/app/src/modules/duplicate/duplicateSimulation/duplicateProduct/duplicateProduct.tsx @@ -0,0 +1,130 @@ +import * as THREE from 'three'; +import { useEffect } from 'react'; +import { upsertProductOrEventApi } from '../../../../services/simulation/products/UpsertProductOrEventApi'; +import { getAllProductsApi } from '../../../../services/simulation/products/getallProductsApi'; +import { usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore'; +import { useSceneContext } from '../../../scene/sceneContext'; +import { useProductContext } from '../../../simulation/products/productContext'; +import { useComparisonProduct } from '../../../../store/simulation/useSimulationStore'; +import { useDuplicateProductStore } from '../../duplicateStores/duplicateProductStore'; + +function ProductsDuplicate({ projectId }: { projectId: string }) { + const { armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, layout } = useSceneContext(); + const { products, getProductById, setProducts } = useDuplicateProductStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct, setSelectedProduct } = selectedProductStore(); + const { addVehicle, clearvehicles } = vehicleStore(); + const { addArmBot, clearArmBots } = armBotStore(); + const { addMachine, clearMachines } = machineStore(); + const { addConveyor, clearConveyors } = conveyorStore(); + const { setCurrentMaterials, clearStorageUnits, updateCurrentLoad, addStorageUnit } = storageUnitStore(); + const { isReset } = useResetButtonStore(); + const { isPlaying } = usePlayButtonStore(); + const { comparisonProduct } = useComparisonProduct(); + + useEffect(() => { + if (layout === 'Comparison Layout' && comparisonProduct) { + setSelectedProduct(comparisonProduct.productUuid, comparisonProduct.productName); + } + }, [comparisonProduct]) + + useEffect(() => { + getAllProductsApi(projectId || '').then((data) => { + if (data.length > 0) { + setProducts(data); + } + }) + }, []) + + useEffect(() => { + if (selectedProduct.productUuid) { + const product = getProductById(selectedProduct.productUuid); + if (product) { + clearvehicles(); + product.eventDatas.forEach(events => { + if (events.type === 'vehicle') { + addVehicle(selectedProduct.productUuid, events); + } + }); + } + } + }, [selectedProduct, products, isReset, isPlaying]); + + useEffect(() => { + if (selectedProduct.productUuid) { + const product = getProductById(selectedProduct.productUuid); + if (product) { + clearArmBots(); + product.eventDatas.forEach(events => { + if (events.type === 'roboticArm') { + addArmBot(selectedProduct.productUuid, events); + } + }); + } + } + }, [selectedProduct, products, isReset, isPlaying]); + + useEffect(() => { + if (selectedProduct.productUuid) { + const product = getProductById(selectedProduct.productUuid); + if (product) { + clearConveyors(); + product.eventDatas.forEach(events => { + if (events.type === 'transfer') { + addConveyor(selectedProduct.productUuid, events); + } + }); + } + } + }, [selectedProduct, products, isReset, isPlaying]); + + useEffect(() => { + if (selectedProduct.productUuid) { + const product = getProductById(selectedProduct.productUuid); + if (product) { + clearMachines(); + product.eventDatas.forEach(events => { + if (events.type === 'machine') { + addMachine(selectedProduct.productUuid, events); + } + }); + } + } + }, [selectedProduct, products, isReset, isPlaying]); + + useEffect(() => { + if (selectedProduct.productUuid) { + const product = getProductById(selectedProduct.productUuid); + if (product) { + clearStorageUnits(); + product.eventDatas.forEach(event => { + if (event.type === 'storageUnit') { + addStorageUnit(selectedProduct.productUuid, event); + + if (event.point.action.actionType === 'retrieve') { + const storageAction = event.point.action; + const materials = Array.from({ length: storageAction.storageCapacity }, () => ({ + materialType: storageAction.materialType || 'Default material', + materialId: THREE.MathUtils.generateUUID() + })); + + setCurrentMaterials(event.modelUuid, materials); + updateCurrentLoad(event.modelUuid, storageAction.storageCapacity); + } else { + setCurrentMaterials(event.modelUuid, []); + updateCurrentLoad(event.modelUuid, 0); + } + } + }); + } + } + }, [selectedProduct, products, isReset, isPlaying]); + + return ( + <> + + + ) +} + +export default ProductsDuplicate \ No newline at end of file diff --git a/app/src/modules/duplicate/duplicateSimulation/duplicateSimulation.tsx b/app/src/modules/duplicate/duplicateSimulation/duplicateSimulation.tsx new file mode 100644 index 0000000..71aa629 --- /dev/null +++ b/app/src/modules/duplicate/duplicateSimulation/duplicateSimulation.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import ProductsDuplicate from './duplicateProduct/duplicateProduct' +import PointsDuplicate from './duplicatePoint/duplicatePoint' +import TriggerDuplicate from './duplicateTrigger/duplicateTrigger' + +function SimulationDuplicate() { + return ( + <> + + + + + + + ) +} + +export default SimulationDuplicate \ No newline at end of file diff --git a/app/src/modules/duplicate/duplicateSimulation/duplicateTrigger/duplicateTrigger.tsx b/app/src/modules/duplicate/duplicateSimulation/duplicateTrigger/duplicateTrigger.tsx new file mode 100644 index 0000000..a6cd8fd --- /dev/null +++ b/app/src/modules/duplicate/duplicateSimulation/duplicateTrigger/duplicateTrigger.tsx @@ -0,0 +1,165 @@ +import { useEffect, useState } from "react"; +import { useThree } from "@react-three/fiber"; +import * as THREE from "three"; +import { QuadraticBezierLine } from "@react-three/drei"; +import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; +import { Arrows } from "../../../simulation/events/arrows/arrows"; +import { useProductContext } from "../../../simulation/products/productContext"; +import { useDuplicateProductStore } from "../../duplicateStores/duplicateProductStore"; + +interface ConnectionLine { + id: string; + startPointUuid: string; + endPointUuid: string; + trigger: TriggerSchema; +} + +function TriggerDuplicate() { + const { scene } = useThree(); + const { selectedProductStore } = useProductContext(); + const { products, getProductById } = useDuplicateProductStore(); + const { selectedProduct } = selectedProductStore(); + const { isPlaying } = usePlayButtonStore(); + + const [connections, setConnections] = useState([]); + + useEffect(() => { + const newConnections: ConnectionLine[] = []; + + const product = getProductById(selectedProduct.productUuid); + if (!product || products.length === 0) return; + + product.eventDatas.forEach(event => { + // Handle Conveyor points + if (event.type === "transfer" && 'points' in event) { + event.points.forEach(point => { + if (point.action?.triggers) { + point.action.triggers.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + } + }); + } + // Handle Vehicle point + else if (event.type === "vehicle" && 'point' in event) { + const point = event.point; + if (point.action?.triggers) { + point.action.triggers.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + } + } + // Handle Robotic Arm points + else if (event.type === "roboticArm" && 'point' in event) { + const point = event.point; + point.actions?.forEach(action => { + action.triggers?.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + }); + } + // Handle Machine point + else if (event.type === "machine" && 'point' in event) { + const point = event.point; + if (point.action?.triggers) { + point.action.triggers.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + } + } + // Handle StorageUnit point + else if (event.type === "storageUnit" && 'point' in event) { + const point = event.point; + if (point.action?.triggers) { + point.action.triggers.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + } + } + }); + + setConnections(newConnections); + }, [products, selectedProduct.productUuid]); + + const getWorldPositionFromScene = (pointUuid: string): THREE.Vector3 | null => { + const pointObj = scene.getObjectByProperty("uuid", pointUuid); + if (!pointObj) return null; + + const worldPosition = new THREE.Vector3(); + pointObj.getWorldPosition(worldPosition); + return worldPosition; + }; + + return ( + + {connections.map((connection) => { + const startPoint = getWorldPositionFromScene(connection.startPointUuid); + const endPoint = getWorldPositionFromScene(connection.endPointUuid); + + if (!startPoint || !endPoint) return null; + + const distance = startPoint.distanceTo(endPoint); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (startPoint.x + endPoint.x) / 2, + Math.max(startPoint.y, endPoint.y) + heightFactor, + (startPoint.z + endPoint.z) / 2 + ); + + return ( + + ); + })} + + + + ); +} + +export default TriggerDuplicate; \ No newline at end of file diff --git a/app/src/modules/duplicate/duplicateStores/duplicateEventStore.ts b/app/src/modules/duplicate/duplicateStores/duplicateEventStore.ts new file mode 100644 index 0000000..81594ea --- /dev/null +++ b/app/src/modules/duplicate/duplicateStores/duplicateEventStore.ts @@ -0,0 +1,357 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; + +type EventsStore = { + events: EventsSchema[]; + + // Event-level actions + addEvent: (event: EventsSchema) => void; + removeEvent: (modelUuid: string) => void; + clearEvents: () => void; + updateEvent: (modelUuid: string, updates: Partial) => EventsSchema | undefined; + + // Point-level actions + addPoint: (modelUuid: string, point: ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema) => void; + removePoint: (modelUuid: string, pointUuid: string) => void; + updatePoint: ( + modelUuid: string, + pointUuid: string, + updates: Partial + ) => EventsSchema | undefined; + + // Action-level actions + addAction: ( + modelUuid: string, + pointUuid: string, + action: ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] + ) => void; + removeAction: (actionUuid: string) => void; + updateAction: ( + actionUuid: string, + updates: Partial + ) => void; + + // Trigger-level actions + addTrigger: (actionUuid: string, trigger: TriggerSchema) => void; + removeTrigger: (triggerUuid: string) => void; + updateTrigger: (triggerUuid: string, updates: Partial) => void; + + // Helper functions + getEventByModelUuid: (modelUuid: string) => EventsSchema | undefined; + getPointByUuid: (modelUuid: string, pointUuid: string) => ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema | undefined; + getActionByUuid: (actionUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']) | undefined; + getTriggerByUuid: (triggerUuid: string) => TriggerSchema | undefined; +}; + +export const useDuplicateEventsStore = create()( + immer((set, get) => ({ + events: [], + + // Event-level actions + addEvent: (event) => { + set((state) => { + if (!state.events.some(e => 'modelUuid' in e && e.modelUuid === event.modelUuid)) { + state.events.push(event); + } + }); + }, + + removeEvent: (modelUuid) => { + set((state) => { + state.events = state.events.filter(e => 'modelUuid' in e && e.modelUuid !== modelUuid); + }); + }, + + clearEvents: () => { + set((state) => { + state.events = []; + }); + }, + + updateEvent: (modelUuid, updates) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event) { + Object.assign(event, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + }); + return updatedEvent; + }, + + // Point-level actions + addPoint: (modelUuid, point) => { + set((state) => { + const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + const existingPoint = (event as ConveyorEventSchema).points.find(p => p.uuid === point.uuid); + if (!existingPoint) { + (event as ConveyorEventSchema).points.push(point as ConveyorPointSchema); + } + } else if (event && 'point' in event) { + if (!(event as any).point || (event as any).point.uuid !== point.uuid) { + (event as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point = point as any; + } + } + }); + }, + + removePoint: (modelUuid, pointUuid) => { + set((state) => { + const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + (event as ConveyorEventSchema).points = (event as ConveyorEventSchema).points.filter(p => p.uuid !== pointUuid); + } + // For single-point events, you might want to handle differently + }); + }, + + updatePoint: (modelUuid, pointUuid, updates) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + const point = (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); + if (point) { + Object.assign(point, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { + Object.assign((event as any).point, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + }); + return updatedEvent; + }, + + // Action-level actions + addAction: (modelUuid, pointUuid, action) => { + set((state) => { + const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + const point = (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); + if (point && (!point.action || point.action.actionUuid !== action.actionUuid)) { + point.action = action as any; + } + } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { + const point = (event as any).point; + if ('action' in point && (!point.action || point.action.actionUuid !== action.actionUuid)) { + point.action = action; + } else if ('actions' in point && !point.actions.some((a: any) => a.actionUuid === action.actionUuid)) { + point.actions.push(action); + } + } + }); + }, + + removeAction: (actionUuid) => { + set((state) => { + for (const event of state.events) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && point.action.actionUuid === actionUuid) { + // Handle removal for single action points + } + } + } else if ('point' in event) { + const point = (event as any).point; + if (event.type === "roboticArm") { + if ('actions' in point) { + point.actions = point.actions.filter((a: any) => a.actionUuid !== actionUuid); + } + } else if ('action' in point && point.action?.actionUuid === actionUuid) { + // Handle single action + } + } + } + }); + }, + + updateAction: (actionUuid, updates) => { + set((state) => { + for (const event of state.events) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && point.action.actionUuid === actionUuid) { + Object.assign(point.action, updates); + return; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action.actionUuid === actionUuid) { + Object.assign(point.action, updates); + return; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) { + Object.assign(action, updates); + return; + } + } + } + } + }); + }, + + // Trigger-level actions + addTrigger: (actionUuid, trigger) => { + set((state) => { + for (const event of state.events) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && point.action.actionUuid === actionUuid) { + if (!point.action.triggers.some(t => t.triggerUuid === trigger.triggerUuid)) { + point.action.triggers.push(trigger); + } + return; + } + } + } else if ('point' in event) { + const point: MachinePointSchema | VehiclePointSchema = (event as any).point; + if ('action' in point && point.action.actionUuid === actionUuid) { + if (!point.action.triggers.some(t => t.triggerUuid === trigger.triggerUuid)) { + point.action.triggers.push(trigger); + } + return; + } else if ('actions' in point) { + const action = (point as RoboticArmPointSchema).actions.find((a) => a.actionUuid === actionUuid); + if (action && !action.triggers.some(t => t.triggerUuid === trigger.triggerUuid)) { + action.triggers.push(trigger); + return; + } + } + } + } + }); + }, + + removeTrigger: (triggerUuid) => { + set((state) => { + for (const event of state.events) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && 'triggers' in point.action) { + point.action.triggers = point.action.triggers.filter(t => t.triggerUuid !== triggerUuid); + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && 'triggers' in point.action) { + point.action.triggers = point.action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + } else if ('actions' in point) { + for (const action of point.actions) { + if ('triggers' in action) { + action.triggers = action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + } + } + } + } + } + }); + }, + + updateTrigger: (triggerUuid, updates) => { + set((state) => { + for (const event of state.events) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && 'triggers' in point.action) { + const trigger = point.action.triggers.find(t => t.triggerUuid === triggerUuid); + if (trigger) { + Object.assign(trigger, updates); + return; + } + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && 'triggers' in point.action) { + const trigger = point.action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) { + Object.assign(trigger, updates); + return; + } + } else if ('actions' in point) { + for (const action of point.actions) { + if ('triggers' in action) { + const trigger = action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) { + Object.assign(trigger, updates); + return; + } + } + } + } + } + } + }); + }, + + // Helper functions + getEventByModelUuid: (modelUuid) => { + return get().events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + }, + + getPointByUuid: (modelUuid, pointUuid) => { + const event = get().events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + return (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); + } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { + return (event as any).point; + } + return undefined; + }, + + getActionByUuid: (actionUuid) => { + const state = get(); + for (const event of state.events) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && point.action.actionUuid === actionUuid) { + return point.action; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action.actionUuid === actionUuid) { + return point.action; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) return action; + } + } + } + return undefined; + }, + + getTriggerByUuid: (triggerUuid) => { + const state = get(); + for (const event of state.events) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && 'triggers' in point.action) { + const trigger = point.action.triggers.find(t => t.triggerUuid === triggerUuid); + if (trigger) return trigger; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && 'triggers' in point.action) { + const trigger = point.action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) return trigger; + } else if ('actions' in point) { + for (const action of point.actions) { + if ('triggers' in action) { + const trigger = action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) return trigger; + } + } + } + } + } + return undefined; + } + })) +); diff --git a/app/src/modules/duplicate/duplicateStores/duplicateProductStore.ts b/app/src/modules/duplicate/duplicateStores/duplicateProductStore.ts new file mode 100644 index 0000000..d3dc8ff --- /dev/null +++ b/app/src/modules/duplicate/duplicateStores/duplicateProductStore.ts @@ -0,0 +1,798 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; + +type ProductsStore = { + products: productsSchema; + + // Product-level actions + addProduct: (productName: string, productUuid: string) => void; + setProducts: (products: productsSchema) => void; + clearProducts: () => void; + removeProduct: (productUuid: string) => void; + updateProduct: (productUuid: string, updates: Partial<{ productName: string; eventDatas: EventsSchema[] }>) => void; + + // Event-level actions + addEvent: (productUuid: string, event: EventsSchema) => void; + removeEvent: (productUuid: string, modelUuid: string) => void; + deleteEvent: (modelUuid: string) => void; + updateEvent: (productUuid: string, modelUuid: string, updates: Partial) => EventsSchema | undefined; + + // Point-level actions + addPoint: (productUuid: string, modelUuid: string, point: ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema) => void; + removePoint: (productUuid: string, modelUuid: string, pointUuid: string) => void; + updatePoint: ( + productUuid: string, + modelUuid: string, + pointUuid: string, + updates: Partial + ) => EventsSchema | undefined; + + // Action-level actions + addAction: ( + productUuid: string, + modelUuid: string, + pointUuid: string, + action: ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] + ) => EventsSchema | undefined; + removeAction: (productUuid: string, actionUuid: string) => EventsSchema | undefined; + updateAction: ( + productUuid: string, + actionUuid: string, + updates: Partial + ) => EventsSchema | undefined; + + // Trigger-level actionss + addTrigger: ( + productUuid: string, + actionUuid: string, + trigger: TriggerSchema + ) => EventsSchema | undefined; + removeTrigger: (productUuid: string, triggerUuid: string) => EventsSchema | undefined; + updateTrigger: ( + productUuid: string, + triggerUuid: string, + updates: Partial + ) => EventsSchema | undefined; + + // Renaming functions + renameProduct: (productUuid: string, newName: string) => void; + renameAction: (productUuid: string, actionUuid: string, newName: string) => EventsSchema | undefined; + renameTrigger: (productUuid: string, triggerUuid: string, newName: string) => EventsSchema | undefined; + + // Helper functions + getProductById: (productUuid: string) => { productName: string; productUuid: string; eventDatas: EventsSchema[] } | undefined; + getEventByModelUuid: (productUuid: string, modelUuid: string) => EventsSchema | undefined; + getEventByActionUuid: (productUuid: string, actionUuid: string) => EventsSchema | undefined; + getEventByTriggerUuid: (productUuid: string, triggerUuid: string) => EventsSchema | undefined; + getEventByPointUuid: (productUuid: string, pointUuid: string) => EventsSchema | undefined; + getPointByUuid: (productUuid: string, modelUuid: string, pointUuid: string) => ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema | undefined; + getActionByUuid: (productUuid: string, actionUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']) | undefined; + getActionByPointUuid: (productUuid: string, pointUuid: string) => (ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']) | undefined; + getModelUuidByPointUuid: (productUuid: string, actionUuid: string) => (string) | undefined; + getModelUuidByActionUuid: (productUuid: string, actionUuid: string) => (string) | undefined; + getPointUuidByActionUuid: (productUuid: string, actionUuid: string) => (string) | undefined; + getTriggerByUuid: (productUuid: string, triggerUuid: string) => TriggerSchema | undefined; + getIsEventInProduct: (productUuid: string, modelUuid: string) => boolean; +}; + +export const useDuplicateProductStore = create()( + immer((set, get) => ({ + products: [], + + // Product-level actions + addProduct: (productName, productUuid) => { + set((state) => { + const existingProduct = state.products.find(p => p.productUuid === productUuid); + if (!existingProduct) { + const newProduct = { + productName, + productUuid: productUuid, + eventDatas: [] + }; + state.products.push(newProduct); + } + }); + }, + + setProducts: (products) => { + set((state) => { + state.products = products; + }); + }, + + clearProducts: () => { + set((state) => { + state.products = []; + }); + }, + + removeProduct: (productUuid) => { + set((state) => { + state.products = state.products.filter(p => p.productUuid !== productUuid); + }); + }, + + updateProduct: (productUuid, updates) => { + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + Object.assign(product, updates); + } + }); + }, + + // Event-level actions + addEvent: (productUuid, event) => { + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + const existingEvent = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === event.modelUuid); + if (!existingEvent) { + product.eventDatas.push(event); + } + } + }); + }, + + removeEvent: (productUuid, modelUuid) => { + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + product.eventDatas = product.eventDatas.filter(e => 'modelUuid' in e && e.modelUuid !== modelUuid); + } + }); + }, + + deleteEvent: (modelUuid) => { + set((state) => { + for (const product of state.products) { + product.eventDatas = product.eventDatas.filter(e => 'modelUuid' in e && e.modelUuid !== modelUuid); + } + }); + }, + + updateEvent: (productUuid, modelUuid, updates) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + const event = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event) { + Object.assign(event, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } + }); + return updatedEvent; + }, + + // Point-level actions + addPoint: (productUuid, modelUuid, point) => { + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + const event = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + const existingPoint = (event as ConveyorEventSchema).points.find(p => p.uuid === point.uuid); + if (!existingPoint) { + (event as ConveyorEventSchema).points.push(point as ConveyorPointSchema); + } + } else if (event && 'point' in event) { + const existingPoint = (event as any).point?.uuid === point.uuid; + if (!existingPoint) { + (event as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point = point as any; + } + } + } + }); + }, + + removePoint: (productUuid, modelUuid, pointUuid) => { + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + const event = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + (event as ConveyorEventSchema).points = (event as ConveyorEventSchema).points.filter(p => p.uuid !== pointUuid); + } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { + // For events with single point, we can't remove it, only reset to empty + } + } + }); + }, + + updatePoint: (productUuid, modelUuid, pointUuid, updates) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + const event = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + const point = (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); + if (point) { + Object.assign(point, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { + Object.assign((event as any).point, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } + }); + return updatedEvent; + }, + + // Action-level actions + addAction: (productUuid, modelUuid, pointUuid, action) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + const event = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + if (event && 'points' in event) { + const point = (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); + if (point && (!point.action || point.action.actionUuid !== action.actionUuid)) { + point.action = action as any; + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { + if ('action' in (event as any).point) { + if (!(event as any).point.action || (event as any).point.action.actionUuid !== action.actionUuid) { + (event as any).point.action = action; + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } else if ('actions' in (event as any).point) { + const existingAction = (event as any).point.actions.find((a: any) => a.actionUuid === action.actionUuid); + if (!existingAction) { + (event as any).point.actions.push(action); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } + } + } + }); + return updatedEvent; + }, + + removeAction: (productUuid, actionUuid) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + for (const event of product.eventDatas) { + if ('points' in event) { + // Handle ConveyorEventSchema + for (const point of (event as ConveyorEventSchema).points) { + } + } else if ('point' in event) { + const point = (event as any).point; + if (event.type === "roboticArm") { + if ('actions' in point) { + const index = point.actions.findIndex((a: any) => a.actionUuid === actionUuid); + if (index !== -1) { + point.actions.splice(index, 1); + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } else if ('action' in point && point.action?.actionUuid === actionUuid) { + point.action = undefined; + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } + } + }); + return updatedEvent; + }, + + updateAction: (productUuid, actionUuid, updates) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && point.action.actionUuid === actionUuid) { + Object.assign(point.action, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action.actionUuid === actionUuid) { + Object.assign(point.action, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) { + Object.assign(action, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } + } + } + }); + return updatedEvent; + }, + + // Trigger-level actions + addTrigger: (productUuid, actionUuid, trigger) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && point.action.actionUuid === actionUuid) { + const existingTrigger = point.action.triggers.find(t => t.triggerUuid === trigger.triggerUuid); + if (!existingTrigger) { + point.action.triggers.push(trigger); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + return; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action.actionUuid === actionUuid) { + const existingTrigger = point.action.triggers.find((t: any) => t.triggerUuid === trigger.triggerUuid); + if (!existingTrigger) { + point.action.triggers.push(trigger); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + return; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) { + const existingTrigger = action.triggers.find((t: any) => t.triggerUuid === trigger.triggerUuid); + if (!existingTrigger) { + action.triggers.push(trigger); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + return; + } + } + } + } + } + }); + return updatedEvent; + }, + + removeTrigger: (productUuid, triggerUuid) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && 'triggers' in point.action) { + const Trigger = point.action.triggers.find(t => t.triggerUuid === triggerUuid); + if (Trigger) { + point.action.triggers = point.action.triggers.filter(t => t.triggerUuid !== triggerUuid); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && 'triggers' in point.action) { + const Trigger = point.action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (Trigger) { + point.action.triggers = point.action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } else if ('actions' in point) { + for (const action of point.actions) { + if ('triggers' in action) { + const Trigger = action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (Trigger) { + action.triggers = action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + updatedEvent = JSON.parse(JSON.stringify(event)); + } + } + } + } + } + } + } + }); + return updatedEvent; + }, + + updateTrigger: (productUuid, triggerUuid, updates) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && 'triggers' in point.action) { + const trigger = point.action.triggers.find(t => t.triggerUuid === triggerUuid); + if (trigger) { + Object.assign(trigger, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && 'triggers' in point.action) { + const trigger = point.action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) { + Object.assign(trigger, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } else if ('actions' in point) { + for (const action of point.actions) { + if ('triggers' in action) { + const trigger = action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) { + Object.assign(trigger, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } + } + } + } + } + }); + return updatedEvent; + }, + + // Renaming functions + renameProduct: (productUuid, newName) => { + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + product.productName = newName; + } + }); + }, + + renameAction: (productUuid, actionUuid, newName) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && point.action.actionUuid === actionUuid) { + point.action.actionName = newName; + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action.actionUuid === actionUuid) { + point.action.actionName = newName; + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) { + action.actionName = newName; + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } + } + } + }); + return updatedEvent; + }, + + renameTrigger: (productUuid, triggerUuid, newName) => { + let updatedEvent: EventsSchema | undefined; + set((state) => { + const product = state.products.find(p => p.productUuid === productUuid); + if (product) { + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action && 'triggers' in point.action) { + const trigger = point.action.triggers.find(t => t.triggerUuid === triggerUuid); + if (trigger) { + trigger.triggerName = newName; + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && 'triggers' in point.action) { + const trigger = point.action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) { + trigger.triggerName = newName; + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } else if ('actions' in point) { + for (const action of point.actions) { + if ('triggers' in action) { + const trigger = action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (trigger) { + trigger.triggerName = newName; + updatedEvent = JSON.parse(JSON.stringify(event)); + return; + } + } + } + } + } + } + } + }); + return updatedEvent; + }, + + // Helper functions + getProductById: (productUuid) => { + return get().products.find(p => p.productUuid === productUuid); + }, + + getEventByModelUuid: (productUuid, modelUuid) => { + const product = get().getProductById(productUuid); + if (!product) return undefined; + return product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); + }, + + getEventByActionUuid: (productUuid, actionUuid) => { + const product = get().getProductById(productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action?.actionUuid === actionUuid) { + return event; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action?.actionUuid === actionUuid) { + return event; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) { + return event; + } + } + } + } + return undefined; + }, + + getEventByTriggerUuid: (productUuid, triggerUuid) => { + const product = get().getProductById(productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action?.triggers?.some(t => t.triggerUuid === triggerUuid)) { + return event; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point) { + if (point.action?.triggers?.some((t: any) => t.triggerUuid === triggerUuid)) { + return event; + } + } else if ('actions' in point) { + for (const action of point.actions) { + if (action.triggers?.some((t: any) => t.triggerUuid === triggerUuid)) { + return event; + } + } + } + } + } + return undefined; + }, + + getEventByPointUuid: (productUuid, pointUuid) => { + const product = get().getProductById(productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + if ((event as ConveyorEventSchema).points.some(p => p.uuid === pointUuid)) { + return event; + } + } else if ('point' in event) { + if ((event as any).point?.uuid === pointUuid) { + return event; + } + } + } + return undefined; + }, + + getPointByUuid: (productUuid, modelUuid, pointUuid) => { + const event = get().getEventByModelUuid(productUuid, modelUuid); + if (!event) return undefined; + + if ('points' in event) { + return (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); + } else if ('point' in event && (event as any).point.uuid === pointUuid) { + return (event as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point; + } + return undefined; + }, + + getActionByUuid: (productUuid, actionUuid) => { + const product = get().products.find(p => p.productUuid === productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action?.actionUuid === actionUuid) { + return point.action; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action?.actionUuid === actionUuid) { + return point.action; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) return action; + } + } + } + return undefined; + }, + + getActionByPointUuid: (productUuid, pointUuid) => { + const product = get().products.find(p => p.productUuid === productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.uuid === pointUuid) { + return point.action; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if (point.uuid === pointUuid) { + return point.action; + } + } + } + return undefined; + }, + + getModelUuidByPointUuid: (productUuid, pointUuid) => { + const product = get().products.find(p => p.productUuid === productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.uuid === pointUuid) { + return event.modelUuid; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if (point.uuid === pointUuid) { + return event.modelUuid; + } + } + } + return undefined; + }, + + getModelUuidByActionUuid: (productUuid, actionUuid) => { + const product = get().products.find(p => p.productUuid === productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action?.actionUuid === actionUuid) { + return event.modelUuid; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action?.actionUuid === actionUuid) { + return event.modelUuid; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) return event.modelUuid; + } + } + } + return undefined; + }, + + getPointUuidByActionUuid: (productUuid, actionUuid) => { + const product = get().products.find(p => p.productUuid === productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + if (point.action?.actionUuid === actionUuid) { + return point.uuid; + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point && point.action?.actionUuid === actionUuid) { + return point.uuid; + } else if ('actions' in point) { + const action = point.actions.find((a: any) => a.actionUuid === actionUuid); + if (action) return point.uuid; + } + } + } + return undefined; + }, + + getTriggerByUuid: (productUuid, triggerUuid) => { + const product = get().products.find(p => p.productUuid === productUuid); + if (!product) return undefined; + + for (const event of product.eventDatas) { + if ('points' in event) { + for (const point of (event as ConveyorEventSchema).points) { + for (const trigger of point.action?.triggers || []) { + if (trigger.triggerUuid === triggerUuid) { + return trigger; + } + } + } + } else if ('point' in event) { + const point = (event as any).point; + if ('action' in point) { + for (const trigger of point.action?.triggers || []) { + if (trigger.triggerUuid === triggerUuid) { + return trigger; + } + } + } else if ('actions' in point) { + for (const action of point.actions) { + for (const trigger of action.triggers || []) { + if (trigger.triggerUuid === triggerUuid) { + return trigger; + } + } + } + } + } + } + return undefined; + }, + + getIsEventInProduct: (productUuid, modelUuid) => { + const product = get().getProductById(productUuid); + if (!product) return false; + return product.eventDatas.some(e => 'modelUuid' in e && e.modelUuid === modelUuid); + } + })) +); diff --git a/app/src/modules/duplicate/sceneDuplicate.tsx b/app/src/modules/duplicate/sceneDuplicate.tsx new file mode 100644 index 0000000..3c501a5 --- /dev/null +++ b/app/src/modules/duplicate/sceneDuplicate.tsx @@ -0,0 +1,50 @@ +import { Canvas } from "@react-three/fiber"; + +import BuilderDuplicate from "./duplicateBuilder/builderDuplicate"; +import { Environment } from "@react-three/drei"; +import Shadows from "../scene/environment/shadow"; +import Sun from "../scene/environment/sky"; + +import background from "../../assets/textures/hdr/mudroadpuresky2k.hdr"; +import ControlsDuplicate from "./duplicateSetup/controlsDuplicate"; +import PostProcessingDuplicate from "./duplicateSetup/postProcessingDuplicate"; +import { Color } from "three"; +import SimulationDuplicate from "./duplicateSimulation/duplicateSimulation"; + +export default function DuplicateScene() { + const projectId = "684bcd620a64bc2a815a88d6"; + + return ( + { + e.preventDefault(); + }} + onCreated={(e) => { + e.scene.background = new Color(0x19191d); + }} + gl={{ powerPreference: "high-performance", antialias: true, preserveDrawingBuffer: true }} + > + + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/app/src/modules/scene/controls/controls.tsx b/app/src/modules/scene/controls/controls.tsx index c8fe33a..c4f2288 100644 --- a/app/src/modules/scene/controls/controls.tsx +++ b/app/src/modules/scene/controls/controls.tsx @@ -4,7 +4,7 @@ import { useThree } from "@react-three/fiber"; import * as THREE from "three"; import * as CONSTANTS from '../../../types/world/worldConstants'; -import { useSocketStore, useToggleView, useResetCamera } from "../../../store/builder/store"; +import { useSocketStore, useToggleView, useResetCamera, useDuplicatedCamData } from "../../../store/builder/store"; import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi"; import updateCamPosition from "../camera/updateCameraPosition"; import CamMode from "../camera/camMode"; @@ -22,6 +22,7 @@ export default function Controls() { const { socket } = useSocketStore(); const state = useThree(); const { projectId } = useParams(); + const { setDuplicatedCamData } = useDuplicatedCamData(); const { userId, organization, email } = getUserData(); useEffect(() => { @@ -103,10 +104,23 @@ export default function Controls() { } }; + const camUpdate = () => { + if (!controlsRef.current) return; + const position = controlsRef.current.getPosition(new THREE.Vector3(0, 0, 0)); + const target = controlsRef.current.getTarget(new THREE.Vector3(0, 0, 0)); + const rotation = state.camera.rotation; + setDuplicatedCamData( + [position.x, position.y, position.z], + [target.x, target.y, target.z], + [rotation.x, rotation.y, rotation.z] + ) + }; + const controls = controlsRef.current; if (controls) { controls.addEventListener("sleep", handleRest); controls.addEventListener("control", startInterval); + controls.addEventListener("control", camUpdate); controls.addEventListener("controlend", stopInterval); } @@ -114,6 +128,7 @@ export default function Controls() { if (controls) { controls.removeEventListener("sleep", handleRest); controls.removeEventListener("control", startInterval); + controls.removeEventListener("control", camUpdate); controls.removeEventListener("controlend", stopInterval); } stopInterval(); diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index e49ab1f..96b3c89 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -29,6 +29,7 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co const { projectSocket } = useSocketStore(); const { activeModule } = useModuleStore(); const { loadingProgress } = useLoadingProgress(); + const handleUpdatingProject = async () => { if (!projectId && loadingProgress > 1) return; try { @@ -36,18 +37,37 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co let projectUuid = projects.Projects.find( (val: any) => val.projectUuid === projectId || val._id === projectId ); - const canvas = - document.getElementById("sceneCanvas")?.children[0]?.children[0]; - const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png"); - const updateProjects = { - projectId: projectUuid, - organization, - userId, - projectName: projectUuid.projectName, - thumbnail: screenshotDataUrl, - }; - if (projectSocket) { - projectSocket.emit("v1:project:update", updateProjects); + + if (activeModule === "builder" && loadingProgress !== 1) { + const canvas = + document.getElementById("sceneCanvas")?.children[0]?.children[0]; + const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png"); + setTimeout(() => { + const updateProjects = { + projectId: projectUuid, + organization, + userId, + projectName: projectUuid.projectName, + thumbnail: screenshotDataUrl, + }; + if (projectSocket) { + projectSocket.emit("v1:project:update", updateProjects); + } + }, 8000); + } else { + const canvas = + document.getElementById("sceneCanvas")?.children[0]?.children[0]; + const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png"); + const updateProjects = { + projectId: projectUuid, + organization, + userId, + projectName: projectUuid.projectName, + thumbnail: screenshotDataUrl, + }; + if (projectSocket) { + projectSocket.emit("v1:project:update", updateProjects); + } } } catch (error) { } }; diff --git a/app/src/modules/simulation/events/arrows/arrows.tsx b/app/src/modules/simulation/events/arrows/arrows.tsx index 59e56e2..eca413f 100644 --- a/app/src/modules/simulation/events/arrows/arrows.tsx +++ b/app/src/modules/simulation/events/arrows/arrows.tsx @@ -10,7 +10,7 @@ interface ConnectionLine { trigger: TriggerSchema; } -export function Arrows({ connections }: { readonly connections: ConnectionLine[] }) { +export function Arrows({ connections, color = "#42a5f5" }: { readonly connections: ConnectionLine[], color?: string }) { const [hoveredLineKey, setHoveredLineKey] = useState(null); const groupRef = useRef(null); const { scene } = useThree(); @@ -61,7 +61,7 @@ export function Arrows({ connections }: { readonly connections: ConnectionLine[] onPointerOut={() => setHoveredLineKey(null)} > setHoveredLineKey(null)} > diff --git a/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx b/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx index c63612a..80e5ae9 100644 --- a/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx +++ b/app/src/modules/simulation/events/triggerConnections/triggerConnector.tsx @@ -477,7 +477,7 @@ function TriggerConnector() { start={startPoint.toArray()} end={endPoint.toArray()} mid={midPoint.toArray()} - color={toolMode === '3D-Delete' && hoveredLineKey === connection.id ? "red" : "#42a5f5"} + color={toolMode === '3D-Delete' && hoveredLineKey === connection.id ? "red" : "#6f42c1"} lineWidth={4} dashed={toolMode === '3D-Delete' && hoveredLineKey === connection.id ? false : true} dashSize={0.75} @@ -496,7 +496,7 @@ function TriggerConnector() { ); })} - + {currentLine && ( <> diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index 66de117..653f2fa 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -34,6 +34,7 @@ import { setFloorItemApi } from "../services/factoryBuilder/assest/floorAsset/se import { useAssetsStore } from "../store/builder/useAssetStore"; import ComparisonSceneProvider from "../components/layout/scenes/ComparisonSceneProvider"; import MainSceneProvider from "../components/layout/scenes/MainSceneProvider"; +import { SceneProvider } from "../modules/scene/sceneContext"; import { getUserData } from "../functions/getUserData"; const Project: React.FC = () => { @@ -116,7 +117,9 @@ const Project: React.FC = () => { return (
- + + + {selectedUser && } {isLogListVisible && ( diff --git a/app/src/store/builder/store.ts b/app/src/store/builder/store.ts index 890a0e8..4cb8947 100644 --- a/app/src/store/builder/store.ts +++ b/app/src/store/builder/store.ts @@ -741,4 +741,36 @@ export const useCompareProductDataStore = create<{ }>((set) => ({ compareProductsData: [], setCompareProductsData: (x) => set({ compareProductsData: x }), -})); \ No newline at end of file +})); + +export const useDuplicatedCamData = create<{ + duplicatedCamData: { + camPosition: [number, number, number]; + camTarget: [number, number, number]; + camRotation: [number, number, number]; + }; + setDuplicatedCamData: ( + camPosition: [number, number, number], + camTarget: [number, number, number], + camRotation: [number, number, number], + ) => + void; +}>((set) => ({ + duplicatedCamData: { + camPosition: [0, 0, 0], + camTarget: [0, 0, 0], + camRotation: [0, 0, 0], + }, + setDuplicatedCamData: ( + camPosition: [number, number, number], + camTarget: [number, number, number], + camRotation: [number, number, number], + ) => set({ + duplicatedCamData: { + camPosition: camPosition, + camTarget: camTarget, + camRotation: camRotation, + } + }) +}) +) diff --git a/app/src/styles/base/global.scss b/app/src/styles/base/global.scss index 3b12635..56dc3cc 100644 --- a/app/src/styles/base/global.scss +++ b/app/src/styles/base/global.scss @@ -28,6 +28,9 @@ section, border: none; } } +.scene-container.half-view{ + width: 50vw !important; +} .tooltip { position: absolute; diff --git a/app/src/styles/layout/compareLayout.scss b/app/src/styles/layout/compareLayout.scss index 1e6545e..15c0a6b 100644 --- a/app/src/styles/layout/compareLayout.scss +++ b/app/src/styles/layout/compareLayout.scss @@ -2,607 +2,613 @@ @use "../abstracts/mixins" as *; .initial-selectLayout-wrapper { - position: fixed; - top: 100px; - right: 40px; - z-index: 10; + position: fixed; + top: 100px; + right: 40px; + z-index: 10; - .regularDropdown-container { - background: var(--background-color); - } + .regularDropdown-container { + background: var(--background-color); + } } .selectLayout-wrapper { - position: fixed; - left: 40px; - top: 100px; - z-index: 10; + position: fixed; + left: 40px; + top: 100px; + z-index: 10; - .regularDropdown-container { - background: var(--background-color); - } + .regularDropdown-container { + background: var(--background-color); + } } .compareLayOut-wrapper { - position: fixed; - top: 0; - right: 0; - z-index: 2; - height: 100vh; + position: fixed; + top: 0; + right: 0; + z-index: 2; + height: 100vh; + background: var(--background-color); + backdrop-filter: blur(20px); + display: flex; + justify-content: center; + align-items: center; + animation: slideInFromRight 0.4s ease-out forwards; + user-select: none; + border-left: 2px solid var(--border-color); + pointer-events: none; + + .resizer { + width: 32px; + height: 32px; + @include flex-center; + padding: 6px; + position: absolute; + top: 50%; + left: 0; + transform: translate(-50%, -50%); background: var(--background-color); backdrop-filter: blur(20px); + box-shadow: $box-shadow-heavy; + border-radius: 50%; + cursor: ew-resize; + transition: transform 0.1s ease; + z-index: 100; + // pointer-events: all; + opacity: 0; + } + + .chooseLayout-container { + width: 100%; + height: 100%; display: flex; justify-content: center; align-items: center; - animation: slideInFromRight 0.4s ease-out forwards; - user-select: none; - border-left: 2px solid var(--border-color); + position: relative; + overflow: hidden; - .resizer { - width: 32px; - height: 32px; - @include flex-center; - padding: 6px; - position: absolute; - top: 50%; - left: 0; - transform: translate(-50%, -50%); + .compare-layout-canvas-container { + position: absolute; + height: 100vh; + width: 50vw; + top: 0; + right: 0; + } + + .chooseLayout-wrapper { + background: var(--background-color); + backdrop-filter: blur(20px); + padding: 20px; + border-radius: 8px; + box-shadow: $box-shadow-medium; + max-width: 80%; + text-align: center; + position: relative; + pointer-events: all; + + .icon { + width: 100%; + margin-bottom: 15px; + text-align: center; + + svg { + width: 100%; + } + } + + .value { + margin-bottom: 15px; + font-size: var(--font-size-small); + font-weight: 500; + color: var(--text-primary); + } + + button { + display: block; + margin: 0 auto; + padding: 8px 16px; + background: var(--background-color-button); + color: var(--icon-default-color-active); + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + } + } + + .displayLayouts-container { + max-width: 170px; + height: auto; background: var(--background-color); backdrop-filter: blur(20px); - box-shadow: $box-shadow-heavy; - border-radius: 50%; - cursor: ew-resize; - transition: transform 0.1s ease; - z-index: 100; - } - - .chooseLayout-container { - width: 100%; - height: 100%; + padding: 6px; + border-radius: 8px; + box-shadow: $box-shadow-medium; + position: absolute; + right: 0; + top: 100%; + transform: translate(50%, -10px); display: flex; - justify-content: center; - align-items: center; - position: relative; - overflow: hidden; + flex-direction: column; + gap: 6px; - .compare-layout-canvas-container { - position: absolute; - height: 100vh; - width: 100vw; - top: 0; - right: 0; + .header { + text-align: left; + padding-top: 6px; + padding-left: 6px; } - .chooseLayout-wrapper { - background: var(--background-color); - backdrop-filter: blur(20px); - padding: 20px; - border-radius: 8px; - box-shadow: $box-shadow-medium; - max-width: 80%; - text-align: center; - position: relative; + .search-wrapper { + padding: 6px 0; - .icon { - width: 100%; - margin-bottom: 15px; - text-align: center; - - svg { - width: 100%; - } - } - - .value { - margin-bottom: 15px; - font-size: var(--font-size-small); - font-weight: 500; - color: var(--text-primary); - } - - button { - display: block; - margin: 0 auto; - padding: 8px 16px; - background: var(--background-color-button); - color: var(--icon-default-color-active); - border: none; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - transform: translateY(-1px); - } - } - - .displayLayouts-container { - max-width: 170px; - height: auto; - background: var(--background-color); - backdrop-filter: blur(20px); - padding: 6px; - border-radius: 8px; - box-shadow: $box-shadow-medium; - position: absolute; - right: 0; - top: 100%; - transform: translate(50%, -10px); - display: flex; - flex-direction: column; - gap: 6px; - - .header { - text-align: left; - padding-top: 6px; - padding-left: 6px; - } - - .search-wrapper { - padding: 6px 0; - - .search-container { - padding: 0; - border-radius: 6px; - } - } - - .layouts-container { - .layout { - padding: 6px 0; - } - - .layout-wrapper { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; - padding: 0 10px; - background: none; - width: 100%; - - &:hover { - background-color: var(--highlight-text-color) !important; - border-radius: 4px; - - .layout { - color: var(--text-button-color) !important; - } - - svg { - path { - fill: var(--text-button-color) !important; - } - } - } - } - } - } + .search-container { + padding: 0; + border-radius: 6px; + } } + + .layouts-container { + .layout { + padding: 6px 0; + } + + .layout-wrapper { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + padding: 0 10px; + background: none; + width: 100%; + + &:hover { + background-color: var(--highlight-text-color) !important; + border-radius: 4px; + + .layout { + color: var(--text-button-color) !important; + } + + svg { + path { + fill: var(--text-button-color) !important; + } + } + } + } + } + } } + } } .compare-result-container { - display: flex; - flex-direction: column; - gap: 6px; - position: fixed; - bottom: 40px; - width: 100%; - min-height: 200px; - z-index: 10; - background: var(--background-color-secondary); - backdrop-filter: blur(20px); - padding: 18px 8px; + display: flex; + flex-direction: column; + gap: 6px; + position: fixed; + bottom: 40px; + width: 100%; + min-height: 200px; + z-index: 10; + background: var(--background-color-secondary); + backdrop-filter: blur(20px); + padding: 18px 8px; - .header { - width: fit-content; - background-color: var(--background-color-solid); - color: var(--background-color-accent); - padding: 6px 10px; - border-radius: 6px; + .header { + width: fit-content; + background-color: var(--background-color-solid); + color: var(--background-color-accent); + padding: 6px 10px; + border-radius: 6px; + } + + .compare-result-wrapper { + display: flex; + gap: 12px; + + h4 { + font-weight: 600; } - .compare-result-wrapper { + .comparisionCard { + position: relative; + flex: 1; + width: auto; + max-height: 200px; + background: var(--background-color); + outline: 1px solid var(--border-color); + outline-offset: -1px; + border-radius: 12px; + padding: 8px 12px; + overflow: hidden; + } + + .performanceResult-wrapper { + min-width: 328px; + flex: 0; + position: relative; + padding-right: 65px; + + .header { display: flex; gap: 12px; + align-items: center; + } - h4 { - font-weight: 600; - } + .metrics-container { + display: flex; + gap: 12px; + height: 100%; - .comparisionCard { - position: relative; - flex: 1; - width: auto; - max-height: 200px; - background: var(--background-color); - outline: 1px solid var(--border-color); - outline-offset: -1px; - border-radius: 12px; - padding: 8px 12px; - overflow: hidden; - } + .metrics-left { + display: flex; + flex-direction: column; + justify-content: space-around; + height: 100%; - .performanceResult-wrapper { - min-width: 328px; - flex: 0; - position: relative; - padding-right: 65px; + .metric { + .metric-label { + display: flex; + align-items: center; + gap: 6px; - .header { + span { display: flex; - gap: 12px; - align-items: center; + } } - .metrics-container { - display: flex; - gap: 12px; - height: 100%; - - .metrics-left { - display: flex; - flex-direction: column; - justify-content: space-around; - height: 100%; - - .metric { - .metric-label { - display: flex; - align-items: center; - gap: 6px; - - span { - display: flex; - } - } - - .metric-value { - padding-top: 6px; - font-size: var(--font-size-xlarge); - color: var(--background-color-accent); - font-weight: 600; - } - } - - .label { - padding-bottom: 68px; - } - } - - .metrics-right { - height: fit-content; - display: grid; - grid-template-columns: repeat(2, 1fr); - grid-template-rows: repeat(2, 1fr); - - gap: 2px; - overflow: visible; - - margin: auto 0; - - .metric-wrapper { - position: relative; - width: 64px; - height: 50px; - overflow: visible; // allow content like labels to overflow - - &:nth-child(1) { - .metric-label { - top: -57%; - left: 220%; - } - - &::after { - content: ""; - position: absolute; - - top: -100%; - left: 50%; - width: 100%; // Required for visible shape - height: 40px; - background-color: #b7b7c6; - - // Custom polygon shape (adjust if needed) - clip-path: polygon(96% 52%, - 96% 54%, - 45% 53%, - 3% 100%, - 0 100%, - 42% 52%); - - z-index: 0; // Behind any inner content - } - } - - // Optional: content above the shape - >* { - position: relative; - z-index: 1; - } - - &:nth-child(2) { - grid-column-start: 1; - grid-row-start: 2; - - .metric-label { - white-space: normal; - width: 50px; - left: 230%; - } - } - - &:nth-child(3) { - grid-row: span 2 / span 2; - grid-column-start: 2; - grid-row-start: 1; - margin-top: 40%; - left: -16px; - position: relative; - } - } - - .metric-label { - position: absolute; - top: 0px; - left: 0%; - white-space: nowrap; - - transform: translate(-50%, -50%); - - font-size: 10px; - z-index: 1; - } - - .metric { - width: 100%; - height: 100%; - position: relative; - display: flex; - justify-content: center; - align-items: center; - - &::after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - - background: var(--background-color, wheat); - clip-path: polygon(25% 0%, - 75% 0%, - 100% 50%, - 75% 100%, - 25% 100%, - 0% 50%); - filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.25)); - - z-index: 0; - } - - // Content stays above the shape - >* { - position: relative; - z-index: 1; - } - } - } + .metric-value { + padding-top: 6px; + font-size: var(--font-size-xlarge); + color: var(--background-color-accent); + font-weight: 600; } + } - .simulation-tag { - background: var(--background-color-button); + .label { + padding-bottom: 68px; + } + } - color: var(--icon-default-color-active); + .metrics-right { + height: fit-content; + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + + gap: 2px; + overflow: visible; + + margin: auto 0; + + .metric-wrapper { + position: relative; + width: 64px; + height: 50px; + overflow: visible; // allow content like labels to overflow + + &:nth-child(1) { + .metric-label { + top: -57%; + left: 220%; + } + + &::after { + content: ""; position: absolute; - bottom: 0; - right: 0; - padding: 10px 5px; - border-radius: 12px 0 0 0; + + top: -100%; + left: 50%; + width: 100%; // Required for visible shape + height: 40px; + background-color: #b7b7c6; + + // Custom polygon shape (adjust if needed) + clip-path: polygon( + 96% 52%, + 96% 54%, + 45% 53%, + 3% 100%, + 0 100%, + 42% 52% + ); + + z-index: 0; // Behind any inner content + } } + + // Optional: content above the shape + > * { + position: relative; + z-index: 1; + } + + &:nth-child(2) { + grid-column-start: 1; + grid-row-start: 2; + + .metric-label { + white-space: normal; + width: 50px; + left: 230%; + } + } + + &:nth-child(3) { + grid-row: span 2 / span 2; + grid-column-start: 2; + grid-row-start: 1; + margin-top: 40%; + left: -16px; + position: relative; + } + } + + .metric-label { + position: absolute; + top: 0px; + left: 0%; + white-space: nowrap; + + transform: translate(-50%, -50%); + + font-size: 10px; + z-index: 1; + } + + .metric { + width: 100%; + height: 100%; + position: relative; + display: flex; + justify-content: center; + align-items: center; + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: var(--background-color, wheat); + clip-path: polygon( + 25% 0%, + 75% 0%, + 100% 50%, + 75% 100%, + 25% 100%, + 0% 50% + ); + filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.25)); + + z-index: 0; + } + + // Content stays above the shape + > * { + position: relative; + z-index: 1; + } + } } + } + + .simulation-tag { + background: var(--background-color-button); + + color: var(--icon-default-color-active); + position: absolute; + bottom: 0; + right: 0; + padding: 10px 5px; + border-radius: 12px 0 0 0; + } } + } } @keyframes slideInFromRight { - from { - transform: translateX(100%); - opacity: 0; - } + from { + transform: translateX(100%); + opacity: 0; + } - to { - transform: translateX(0); - opacity: 1; - } + to { + transform: translateX(0); + opacity: 1; + } } .energy-usage { - position: relative; + position: relative; - .energy-usage-wrapper { - - - .value { - padding-top: 25px; - font-size: var(--font-size-xxxlarge); - color: var(--background-color-accent); - } + .energy-usage-wrapper { + .value { + padding-top: 25px; + font-size: var(--font-size-xxxlarge); + color: var(--background-color-accent); } + } - .simulation-details { - position: absolute; - bottom: 12px; - right: 12px; + .simulation-details { + position: absolute; + bottom: 12px; + right: 12px; - .simulation-wrapper { - display: flex; - align-items: center; - gap: 6px; + .simulation-wrapper { + display: flex; + align-items: center; + gap: 6px; - .icon { - width: 20px; - height: 20px; - border-radius: 50%; - background-color: var(--background-color-accent); - } - } + .icon { + width: 20px; + height: 20px; + border-radius: 50%; + background-color: var(--background-color-accent); + } } + } - .chart { - width: 90%; - position: absolute; - top: 10px; - left: 0; - } + .chart { + width: 90%; + position: absolute; + top: 10px; + left: 0; + } } .throughPutCard-container { - .layers-wrapper { - padding: 20px 10px; - height: 100%; - width: 100%; - display: flex; - justify-content: space-between; - position: relative; + .layers-wrapper { + padding: 20px 10px; + height: 100%; + width: 100%; + display: flex; + justify-content: space-between; + position: relative; - .layer-wrapper { - display: flex; - flex-direction: column; + .layer-wrapper { + display: flex; + flex-direction: column; - &:last-child { - justify-content: end; - } - } - - .chart { - width: 60%; - height: 70%; - position: absolute; - } + &:last-child { + justify-content: end; + } } .chart { - width: 80%; - height: 90%; - position: absolute; - bottom: 0; - left: 0; + width: 60%; + height: 70%; + position: absolute; } + } + + .chart { + width: 80%; + height: 90%; + position: absolute; + bottom: 0; + left: 0; + } } .cycle-time-container { - position: relative; + position: relative; - .cycle-main { + .cycle-main { + display: flex; + justify-content: space-between; + height: 100%; + + .layers-wrapper { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + + .layers { display: flex; - justify-content: space-between; - height: 100%; + flex-direction: column; + gap: 4px; - .layers-wrapper { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-between; - - .layers { - display: flex; - flex-direction: column; - gap: 4px; - - .layer-name { - color: var(--background-color-accent); - } - - .layer-time { - font-size: var(--font-size-large); - } - - .layer-profit { - color: #14ca44; - text-align: end; - - span { - color: #14ca44; - } - } - } + .layer-name { + color: var(--background-color-accent); } - } - .chart { - position: absolute; - bottom: 0; - left: 10px; - width: 60%; - height: 80%; + .layer-time { + font-size: var(--font-size-large); + } + + .layer-profit { + color: #14ca44; + text-align: end; + + span { + color: #14ca44; + } + } + } } + } + + .chart { + position: absolute; + bottom: 0; + left: 10px; + width: 60%; + height: 80%; + } } .overallDowntime-container { - .totalDownTime-wrapper { + .totalDownTime-wrapper { + display: flex; + + .totalDownTime { + width: 70%; + background: var(--background-color-secondary); + backdrop-filter: blur(20px); + border-radius: 12px; + + display: flex; + justify-content: space-between; + align-items: center; + // gap: 20px; + padding: 8px 10px; + margin: 44px 0; + + .totalDownTime-right { display: flex; + flex-direction: column; + gap: 6px; + } - .totalDownTime { - width: 70%; - background: var(--background-color-secondary); - backdrop-filter: blur(20px); - border-radius: 12px; + .totalDownTime-left { + display: flex; + align-items: center; + gap: 6px; - display: flex; - justify-content: space-between; - align-items: center; - // gap: 20px; - padding: 8px 10px; - margin: 44px 0; - - .totalDownTime-right { - display: flex; - flex-direction: column; - gap: 6px; - } - - .totalDownTime-left { - display: flex; - align-items: center; - gap: 6px; - - .value { - font-size: var(--font-size-xlarge); - color: var(--background-color-button); - } - } - } - - .chart { - width: 30%; - position: relative; + .value { + font-size: var(--font-size-xlarge); + color: var(--background-color-button); } + } } + + .chart { + width: 30%; + position: relative; + } + } } .overallScrapRate { - .overallScrapRate-wrapper { - display: flex; + .overallScrapRate-wrapper { + display: flex; - .overallScrapRate-value { - width: 50%; - display: flex; - flex-direction: column; - gap: 6px; - margin: 40px 0; + .overallScrapRate-value { + width: 50%; + display: flex; + flex-direction: column; + gap: 6px; + margin: 40px 0; - .overallScrapRate-key { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - .chart { - width: 50%; - position: relative; - } + .overallScrapRate-key { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } -} \ No newline at end of file + + .chart { + width: 50%; + position: relative; + } + } +} diff --git a/app/src/utils/shortcutkeys/handleShortcutKeys.ts b/app/src/utils/shortcutkeys/handleShortcutKeys.ts index cbfae82..000cfa0 100644 --- a/app/src/utils/shortcutkeys/handleShortcutKeys.ts +++ b/app/src/utils/shortcutkeys/handleShortcutKeys.ts @@ -242,6 +242,7 @@ const KeyPressListener: React.FC = () => { hidePlayer, selectedFloorItem, isRenameMode, + comparisonProduct ]); return null;