diff --git a/app/package-lock.json b/app/package-lock.json index 04f7c7c..f45a024 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -27,6 +27,7 @@ "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@use-gesture/react": "^10.3.1", + "buffer": "^6.0.3", "chart.js": "^4.4.8", "chartjs-plugin-annotation": "^3.1.0", "clsx": "^2.1.1", @@ -8407,6 +8408,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" diff --git a/app/package.json b/app/package.json index b370228..b7aa83f 100644 --- a/app/package.json +++ b/app/package.json @@ -22,6 +22,7 @@ "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@use-gesture/react": "^10.3.1", + "buffer": "^6.0.3", "chart.js": "^4.4.8", "chartjs-plugin-annotation": "^3.1.0", "clsx": "^2.1.1", diff --git a/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx b/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx index c774943..f980fde 100644 --- a/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx +++ b/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx @@ -3,126 +3,240 @@ import { useEffect, useMemo, useRef, useCallback } from "react"; import { useThree } from "@react-three/fiber"; import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore"; import * as CONSTANTS from "../../../types/world/worldConstants"; -import { exportHeatmapAsPNG } from "../functions/exportHeatmapAsPNG"; +import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore"; +import { useSceneContext } from "../../../modules/scene/sceneContext"; -// Constants const RADIUS = 0.0025; const OPACITY = 0.8; const GROWTH_RATE = 20.0; -// 🔹 React Component const BakedHeatMap = () => { + const { materialStore } = useSceneContext(); const { bakedPoints } = useHeatMapStore(); const materialRef = useRef(null); const meshRef = useRef(null); const { gl } = useThree(); - + const { isPlaying } = usePlayButtonStore(); const height = CONSTANTS.gridConfig.size; const width = CONSTANTS.gridConfig.size; + const { materialHistory, materials } = materialStore(); - const pointTexture = useMemo(() => { - if (bakedPoints.length === 0) return null; - const data = new Float32Array(bakedPoints.length * 4); - bakedPoints.forEach((p, i) => { - const index = i * 4; - data[index] = (p.points.x + width / 2) / width; - data[index + 1] = (p.points.y + height / 2) / height; - data[index + 2] = 0.3; - data[index + 3] = 0.0; - }); - const texture = new THREE.DataTexture(data, bakedPoints.length, 1, THREE.RGBAFormat, THREE.FloatType); - texture.needsUpdate = true; - return texture; - }, [bakedPoints, width, height]); + const createPointTexture = useCallback( + (filteredPoints: typeof bakedPoints) => { + if (filteredPoints.length === 0) return null; + + const data = new Float32Array(filteredPoints.length * 4); + filteredPoints.forEach((p, i) => { + const index = i * 4; + data[index] = (p.points.x + width / 2) / width; + data[index + 1] = (p.points.y + height / 2) / height; + data[index + 2] = 0.3; + data[index + 3] = 0.0; + }); + + const texture = new THREE.DataTexture( + data, + filteredPoints.length, + 1, + THREE.RGBAFormat, + THREE.FloatType + ); + texture.needsUpdate = true; + return texture; + }, + [width, height] + ); const uniformsRef = useRef({ - u_points: { value: pointTexture }, - u_count: { value: bakedPoints.length }, + u_points: { value: null as THREE.DataTexture | null }, + u_count: { value: 0 }, u_radius: { value: RADIUS }, u_opacity: { value: OPACITY }, u_growthRate: { value: GROWTH_RATE }, }); + const renderHeatmapToImage = useCallback( + (type: string) => { + if (!meshRef.current) return null; + + const filteredPoints = bakedPoints.filter((p) => p.type === type); + if (filteredPoints.length === 0) return null; + + const pointTexture = createPointTexture(filteredPoints); + if (!pointTexture) return null; + + uniformsRef.current.u_points.value = pointTexture; + uniformsRef.current.u_count.value = filteredPoints.length; + + const exportCamera = new THREE.OrthographicCamera( + width / -2, + width / 2, + height / 2, + height / -2, + 0.1, + 10 + ); + exportCamera.position.set(0, 1, 0); + exportCamera.lookAt(0, 0, 0); + + const renderTarget = new THREE.WebGLRenderTarget(1024, 1024, { + format: THREE.RGBAFormat, + type: THREE.UnsignedByteType, + }); + + const tempScene = new THREE.Scene(); + tempScene.add(meshRef.current); + + gl.setRenderTarget(renderTarget); + gl.render(tempScene, exportCamera); + gl.setRenderTarget(null); + + const pixels = new Uint8Array(1024 * 1024 * 4); + gl.readRenderTargetPixels(renderTarget, 0, 0, 1024, 1024, pixels); + + const canvas = document.createElement("canvas"); + canvas.width = 1024; + canvas.height = 1024; + const ctx = canvas.getContext("2d"); + + if (ctx) { + const imageData = ctx.createImageData(1024, 1024); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + return canvas.toDataURL("image/png"); + } + + return null; + }, + [gl, width, height, bakedPoints, createPointTexture] + ); + + // const downloadImage = (base64: string, filename: string) => { + // const link = document.createElement("a"); + // link.href = base64; + // link.download = filename; + // document.body.appendChild(link); + // link.click(); + // document.body.removeChild(link); + // }; + + const exportHeatmapAsPNG = useCallback(() => { + const types = ["human", "vehicle"]; + + const result = types.map((type) => { + const image = renderHeatmapToImage(type); + // console.log("image: ", type, image); + if (image) { + // downloadImage(image, `${type}-heatmap.png`); + } + return { type, image }; + }); + + // console.log("Exported Heatmaps:", result); + return result; + }, [renderHeatmapToImage]); + + useEffect(() => { + if (materials.length === 0 && materialHistory.length >= 0) { + // console.log("SimulationCompleted"); + const getImage = exportHeatmapAsPNG(); + // console.log("getImage: ", getImage); + } + }, [isPlaying, materials, materialHistory]); + + const pointTexture = useMemo( + () => createPointTexture(bakedPoints), + [bakedPoints, createPointTexture] + ); + useEffect(() => { uniformsRef.current.u_points.value = pointTexture; uniformsRef.current.u_count.value = bakedPoints.length; }, [pointTexture, bakedPoints.length]); - useEffect(() => { - if (meshRef.current) { - exportHeatmapAsPNG({ - bakedPoints, - gl, - width: width, - height: height, - mesh: meshRef.current, - }); - } - }, []); - return ( - <> - // - // - // + + + = u_count) break; - // float fi = float(i) + 0.5; - // float u = fi / float(u_count); + for (int i = 0; i < 10000; i++) { + if (i >= u_count) break; + float fi = float(i) + 0.5; + float u = fi / float(u_count); - // vec4 point = texture2D(u_points, vec2(u, 0.5)); - // vec2 pos = point.rg; - // float strength = point.b; + vec4 point = texture2D(u_points, vec2(u, 0.5)); + vec2 pos = point.rg; + float strength = point.b; - // float d = distance(vUv, pos); - // intensity += strength * gauss(d, u_radius); - // } + float d = distance(vUv, pos); + intensity += strength * gauss(d, u_radius); + } - // float normalized = clamp(intensity / max(u_growthRate, 0.0001), 0.0, 1.0); + float normalized = clamp(intensity / max(u_growthRate, 0.0001), 0.0, 1.0); - // vec3 color = vec3(0.0); - // if (normalized < 0.33) { - // color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), normalized / 0.33); - // } else if (normalized < 0.66) { - // color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (normalized - 0.33) / 0.33); - // } else { - // color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (normalized - 0.66) / 0.34); - // } + vec3 color = vec3(0.0); + if (normalized < 0.33) { + color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), normalized / 0.33); + } else if (normalized < 0.66) { + color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (normalized - 0.33) / 0.33); + } else { + color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (normalized - 0.66) / 0.34); + } - // gl_FragColor = vec4(color, normalized * u_opacity); - // } - // `} - // /> - // + gl_FragColor = vec4(color, normalized * u_opacity); + } + `} + /> + + + {/* + + */} + ); }; diff --git a/app/src/components/heatMapGenerator/functions/exportHeatmapAsPNG.ts b/app/src/components/heatMapGenerator/functions/exportHeatmapAsPNG.ts index 1084f10..166c38a 100644 --- a/app/src/components/heatMapGenerator/functions/exportHeatmapAsPNG.ts +++ b/app/src/components/heatMapGenerator/functions/exportHeatmapAsPNG.ts @@ -1,11 +1,34 @@ -import * as THREE from "three"; +import { AdditiveBlending, DataTexture, DoubleSide, FloatType, Mesh, OrthographicCamera, PlaneGeometry, RGBAFormat, Scene, ShaderMaterial, UnsignedByteType, WebGLRenderer, WebGLRenderTarget } from "three"; const RADIUS = 0.0025; const OPACITY = 0.8; const GROWTH_RATE = 20.0; -export function exportHeatmapAsPNG({ bakedPoints, gl, width, height, mesh }: { bakedPoints: any[]; gl: THREE.WebGLRenderer; width: number; height: number; mesh: THREE.Mesh }) { - const createPointTexture = (filteredPoints: typeof bakedPoints) => { +interface BakedPoint { + type: string; + points: { x: number; y: number }; +} + +interface ExportResult { + type: string; + image: Blob | null; // Now returns a Blob +} + +interface ExportHeatmapParams { + bakedPoints: BakedPoint[]; + gl: WebGLRenderer; + width: number; + height: number; +} + +/** + * Export heatmaps as PNG images for each type ("human", "vehicle") + */ +export function exportHeatmapAsPNG({ bakedPoints, gl, width, height }: ExportHeatmapParams): ExportResult[] { + if (!bakedPoints || bakedPoints.length === 0) return []; + + // Create a DataTexture from filtered points + const createPointTexture = (filteredPoints: BakedPoint[]): DataTexture | null => { if (filteredPoints.length === 0) return null; const data = new Float32Array(filteredPoints.length * 4); @@ -13,82 +36,155 @@ export function exportHeatmapAsPNG({ bakedPoints, gl, width, height, mesh }: { b const index = i * 4; data[index] = (p.points.x + width / 2) / width; data[index + 1] = (p.points.y + height / 2) / height; - data[index + 2] = 0.3; + data[index + 2] = 0.3; // intensity data[index + 3] = 0.0; }); - const texture = new THREE.DataTexture(data, filteredPoints.length, 1, THREE.RGBAFormat, THREE.FloatType); + const texture = new DataTexture(data, filteredPoints.length, 1, RGBAFormat, FloatType); texture.needsUpdate = true; return texture; }; - const downloadImage = (base64: string, filename: string) => { - const link = document.createElement("a"); - link.href = base64; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + // Create heatmap rendering shader material + const createHeatmapMaterial = (pointsTexture: DataTexture, count: number): ShaderMaterial => { + return new ShaderMaterial({ + transparent: true, + depthWrite: false, + blending: AdditiveBlending, + side: DoubleSide, + uniforms: { + u_points: { value: pointsTexture }, + u_count: { value: count }, + u_radius: { value: RADIUS }, + u_opacity: { value: OPACITY }, + u_growthRate: { value: GROWTH_RATE }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D u_points; + precision highp float; + uniform int u_count; + uniform float u_radius; + uniform float u_opacity; + uniform float u_growthRate; + varying vec2 vUv; + + float gauss(float dist, float radius) { + return exp(-pow(dist / radius, 2.0)); + } + + void main() { + float intensity = 0.0; + + for (int i = 0; i < 10000; i++) { + if (i >= u_count) break; + float fi = float(i) + 0.5; + float u = fi / float(u_count); + + vec4 point = texture2D(u_points, vec2(u, 0.5)); + vec2 pos = point.rg; + float strength = point.b; + + float d = distance(vUv, pos); + intensity += strength * gauss(d, u_radius); + } + + float normalized = clamp(intensity / max(u_growthRate, 0.0001), 0.0, 1.0); + + vec3 color = vec3(0.0); + if (normalized < 0.33) { + color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), normalized / 0.33); + } else if (normalized < 0.66) { + color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (normalized - 0.33) / 0.33); + } else { + color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (normalized - 0.66) / 0.34); + } + + gl_FragColor = vec4(color, normalized * u_opacity); + } + `, + }); }; - const renderHeatmapToImage = (type: string) => { + /** + * Renders the heatmap for a given type and returns it as a Blob + */ + const renderHeatmapToImage = (type: string): Blob | null => { const filteredPoints = bakedPoints.filter((p) => p.type === type); if (filteredPoints.length === 0) return null; - const pointTexture = createPointTexture(filteredPoints); - if (!pointTexture) return null; + const pointsTexture = createPointTexture(filteredPoints); + if (!pointsTexture) return null; - const uniforms = { - u_points: { value: pointTexture }, - u_count: { value: filteredPoints.length }, - u_radius: { value: RADIUS }, - u_opacity: { value: OPACITY }, - u_growthRate: { value: GROWTH_RATE }, - }; + const material = createHeatmapMaterial(pointsTexture, filteredPoints.length); - const exportCamera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0.1, 10); + const mesh = new Mesh(new PlaneGeometry(width, height), material); + mesh.rotation.x = Math.PI / 2; + mesh.position.set(0, 0.025, 0); + + const exportCamera = new OrthographicCamera( + width / -2, + width / 2, + height / 2, + height / -2, + 0.1, + 10 + ); exportCamera.position.set(0, 1, 0); exportCamera.lookAt(0, 0, 0); - const renderTarget = new THREE.WebGLRenderTarget(1024, 1024, { - format: THREE.RGBAFormat, - type: THREE.UnsignedByteType, + const renderTarget = new WebGLRenderTarget(1024, 1024, { + format: RGBAFormat, + type: UnsignedByteType, }); - const tempScene = new THREE.Scene(); + const tempScene = new Scene(); tempScene.add(mesh); + // Render heatmap gl.setRenderTarget(renderTarget); gl.render(tempScene, exportCamera); gl.setRenderTarget(null); + // Read pixels const pixels = new Uint8Array(1024 * 1024 * 4); gl.readRenderTargetPixels(renderTarget, 0, 0, 1024, 1024, pixels); + // Convert to Blob const canvas = document.createElement("canvas"); canvas.width = 1024; canvas.height = 1024; const ctx = canvas.getContext("2d"); - if (ctx) { - const imageData = ctx.createImageData(1024, 1024); - imageData.data.set(pixels); - ctx.putImageData(imageData, 0, 0); - return canvas.toDataURL("image/png"); + if (!ctx) return null; + + const imageData = ctx.createImageData(1024, 1024); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + + // Convert canvas to Blob + const dataURL = canvas.toDataURL("image/png"); + const byteString = atob(dataURL.split(",")[1]); + const arrayBuffer = new ArrayBuffer(byteString.length); + const uint8Array = new Uint8Array(arrayBuffer); + + for (let i = 0; i < byteString.length; i++) { + uint8Array[i] = byteString.charCodeAt(i); } - return null; + return new Blob([arrayBuffer], { type: "image/png" }); }; const types = ["human", "vehicle"]; - const result = types.map((type) => { - const image = renderHeatmapToImage(type); - if (image) { - downloadImage(image, `${type}-heatmap.png`); - } - return { type, image }; - }); - - console.log("Exported Heatmaps:", result); - return result; + + return types.map((type) => ({ + type, + image: renderHeatmapToImage(type), + })); } diff --git a/app/src/components/heatMapGenerator/heatmapPreview.tsx b/app/src/components/heatMapGenerator/heatmapPreview.tsx new file mode 100644 index 0000000..4aec0f4 --- /dev/null +++ b/app/src/components/heatMapGenerator/heatmapPreview.tsx @@ -0,0 +1,83 @@ +import { useState, useEffect } from "react"; +import { TextureLoader, Texture, DoubleSide } from "three"; +import * as CONSTANTS from "../../types/world/worldConstants"; + +interface HeatmapPreviewProps { + image: string | Blob | null; + type: string; +} + +const HeatmapPreview: React.FC = ({ image, type }) => { + const [texture, setTexture] = useState(null); + + const width = CONSTANTS.gridConfig.size; + const height = CONSTANTS.gridConfig.size; + + useEffect(() => { + if (!image) { + setTexture(null); + return; + } + + const loader = new TextureLoader(); + loader.crossOrigin = "anonymous"; + + // Handle Blob input + if (image instanceof Blob) { + const blobUrl = URL.createObjectURL(image); + loader.load( + blobUrl, + (tex) => { + setTexture(tex); + URL.revokeObjectURL(blobUrl); + }, + undefined, + () => setTexture(null) + ); + return; + } + + // Handle string URL input + if (typeof image === "string") { + const fixedImage = image.includes(":undefined") + ? image.replace(":undefined", ":8400") + : image; + + // Only load if it ends with a valid image extension + const isValidImage = /\.(png|jpg|jpeg|webp|gif)$/i.test(fixedImage.trim()); + if (!isValidImage) { + console.warn("⚠️ Skipping invalid heatmap URL:", fixedImage); + setTexture(null); + return; + } + + loader.load( + fixedImage, + (tex) => setTexture(tex), + undefined, + () => setTexture(null) + ); + } + }, [image, type]); + + if (!texture) return null; + + return ( + <> + {type === "human" && ( + + + + + )} + {type === "vehicle" && ( + + + + + )} + + ); +}; + +export default HeatmapPreview; diff --git a/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx b/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx index ef8bcb7..c002181 100644 --- a/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx +++ b/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx @@ -4,14 +4,19 @@ import { useFrame, useThree } from "@react-three/fiber"; import { useSceneContext } from "../../../modules/scene/sceneContext"; import * as CONSTANTS from "../../../types/world/worldConstants"; import { determineExecutionMachineSequences } from "../../../modules/simulation/simulator/functions/determineExecutionMachineSequences"; -import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from "../../../store/ui/usePlayButtonStore"; +import { + useAnimationPlaySpeed, + usePauseButtonStore, + usePlayButtonStore, + useResetButtonStore, +} from "../../../store/ui/usePlayButtonStore"; import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore"; const DECAY_RATE = 0.01; const GROWTH_TIME_MULTIPLIER = 20; const RADIUS = 0.005; const OPACITY = 0.8; -const UPDATE_INTERVAL = 1; +const UPDATE_INTERVAL = 0.1; interface HeatPoint { x: number; @@ -28,7 +33,16 @@ const RealTimeHeatMap = () => { const debugModeMap = { solid: 0, grayscale: 1, normal: 2 } as const; const { productStore } = useSceneContext(); const { getProductById, products, selectedProduct } = productStore(); - const { hasHuman, hasVehicle, monitoringHuman, monitoringVehicle, addMonitoringHuman, addMonitoringVehicle, addBakedPoint } = useHeatMapStore(); + const { + bakedPoints, + hasHuman, + hasVehicle, + monitoringHuman, + monitoringVehicle, + addMonitoringHuman, + addMonitoringVehicle, + addBakedPoint, + } = useHeatMapStore(); const { isPlaying } = usePlayButtonStore(); const { isReset } = useResetButtonStore(); const { isPaused } = usePauseButtonStore(); @@ -51,15 +65,19 @@ const RealTimeHeatMap = () => { u_growthRate: { value: GROWTH_TIME_MULTIPLIER }, }); - useEffect(() => { - if (isReset || !isPlaying) { - setPoints([]); - lastFrameTime.current = null; - lastUpdateTime.current = 0; - } - }, [isReset, isPlaying]); + // useEffect(() => { + // if (isReset || !isPlaying) { + // setPoints([]); + // lastFrameTime.current = null; + // lastUpdateTime.current = 0; + // } + // }, [isReset, isPlaying]); // Added human or vehicle + // useEffect(() => { + // addMonitoringVehicle("26770368-55e8-4d40-87f7-8eacb48dc236"); + // addMonitoringHuman("264a51e7-d8b9-4093-95ac-fa7e2dc49cfa"); + // }, []); // useEffect(() => { // const selectedProductData = getProductById(selectedProduct.productUuid); @@ -148,7 +166,9 @@ const RealTimeHeatMap = () => { updatedPoints.push({ x: pos.x, y: pos.z, strength: 0.3, lastUpdated: now }); }); - updatedPoints = updatedPoints.map((p) => ({ ...p, strength: Math.max(0, p.strength - DECAY_RATE * scaledDelta) })).filter((p) => p.strength > 0.01); + updatedPoints = updatedPoints + .map((p) => ({ ...p, strength: Math.max(0, p.strength - DECAY_RATE * scaledDelta) })) + .filter((p) => p.strength > 0.01); setPoints(updatedPoints); }); @@ -164,7 +184,13 @@ const RealTimeHeatMap = () => { data[index + 3] = 0.0; }); - const texture = new THREE.DataTexture(data, points.length, 1, THREE.RGBAFormat, THREE.FloatType); + const texture = new THREE.DataTexture( + data, + points.length, + 1, + THREE.RGBAFormat, + THREE.FloatType + ); texture.needsUpdate = true; return texture; }, [points, width, height]); diff --git a/app/src/components/layout/scenes/ComparisonScene.tsx b/app/src/components/layout/scenes/ComparisonScene.tsx index 8a476fe..c9e4c6d 100644 --- a/app/src/components/layout/scenes/ComparisonScene.tsx +++ b/app/src/components/layout/scenes/ComparisonScene.tsx @@ -66,9 +66,6 @@ function ComparisonScene() { const [shouldShowComparisonResult, setShouldShowComparisonResult] = useState(false); const { addSimulationRecord } = useSimulationManager(); const { createNewWindow } = useCreateNewWindow(); - useEffect(() => { - console.log("comparisonScene: ", comparisonScene); - }, [comparisonScene]); const handleSelectVersion = (option: string) => { const version = versionHistory.find((version) => version.versionName === option); @@ -215,6 +212,7 @@ function ComparisonScene() { )} + {createNewWindow && } {shouldShowComparisonResult && !loadingProgress && } diff --git a/app/src/components/layout/scenes/MainScene.tsx b/app/src/components/layout/scenes/MainScene.tsx index a9c7e7d..8f94d64 100644 --- a/app/src/components/layout/scenes/MainScene.tsx +++ b/app/src/components/layout/scenes/MainScene.tsx @@ -65,9 +65,6 @@ function MainScene() { const { organization, userId } = getUserData(); const { createNewWindow } = useCreateNewWindow(); - useEffect(() => { - console.log("createNewWindow: ", createNewWindow); - }, [createNewWindow]); useEffect(() => { return () => { resetStates(); diff --git a/app/src/components/layout/scenes/functions/calculateSimulationData.ts b/app/src/components/layout/scenes/functions/calculateSimulationData.ts index 96c1e63..e823ff9 100644 --- a/app/src/components/layout/scenes/functions/calculateSimulationData.ts +++ b/app/src/components/layout/scenes/functions/calculateSimulationData.ts @@ -55,7 +55,9 @@ export const calculateSimulationData = (assets: AssetData[]) => { storageUnit: 10, }; - const energyUsage = assets.filter((a) => a.type === "human").reduce((sum, a) => sum + a.activeTime * 1, 0); + const energyUsage = assets + .filter((a) => a.type === "human") + .reduce((sum, a) => sum + a.activeTime * 1, 0); assets.forEach((asset) => { totalActiveTime += asset.activeTime; @@ -67,10 +69,13 @@ export const calculateSimulationData = (assets: AssetData[]) => { simulationCost += asset.activeTime * (costWeight[asset.type] || 10); }); - const machineActiveTime = assets.filter((a) => a.type === "human").reduce((acc, a) => acc + a.activeTime, 0); - console.log('assets: ', assets); + const machineActiveTime = assets + .filter((a) => a.type === "human") + .reduce((acc, a) => acc + a.activeTime, 0); - const machineIdleTime = assets.filter((a) => a.type === "machine").reduce((acc, a) => acc + a.idleTime, 0); + const machineIdleTime = assets + .filter((a) => a.type === "machine") + .reduce((acc, a) => acc + a.idleTime, 0); const simulationTime = totalActiveTime + totalIdleTime; diff --git a/app/src/components/templates/CreateNewWindow.tsx b/app/src/components/templates/CreateNewWindow.tsx index 0697adc..c874688 100644 --- a/app/src/components/templates/CreateNewWindow.tsx +++ b/app/src/components/templates/CreateNewWindow.tsx @@ -54,12 +54,10 @@ export const RenderInNewWindow: React.FC = ({ const finalLeft = center && availWidth ? Math.max(0, screenLeft + (availWidth - width) / 2) : left ?? 100; - console.log("finalLeft: ", finalLeft); const finalTop = center && availHeight ? Math.max(0, screenTop + (availHeight - height) / 2) : top ?? 100; - console.log("finalTop: ", finalTop); const baseFeatures = [ `width=${Math.floor(width)}`, @@ -164,7 +162,7 @@ export const RenderInNewWindow: React.FC = ({ const actualWidth = newWin.innerWidth; const actualHeight = newWin.innerHeight; - console.log("Window dimensions:", actualWidth, actualHeight); + // console.log("Window dimensions:", actualWidth, actualHeight); // Trigger resize event for Three.js newWin.dispatchEvent(new Event("resize")); diff --git a/app/src/components/ui/compareVersion/Button.tsx b/app/src/components/ui/compareVersion/Button.tsx index 7729adf..fde4b42 100644 --- a/app/src/components/ui/compareVersion/Button.tsx +++ b/app/src/components/ui/compareVersion/Button.tsx @@ -1,6 +1,7 @@ -import React from "react"; +import React, { useState } from "react"; import { useCreateNewWindow, + useHeatmapTypeStore, useIsComparing, useLimitDistance, useRenderDistance, @@ -16,6 +17,7 @@ const Button = () => { const { projectId } = useParams(); const { setRenderDistance } = useRenderDistance(); const { setLimitDistance } = useLimitDistance(); + const { selectedTypes, toggleType } = useHeatmapTypeStore(); const handleExit = () => { setIsComparing(false); @@ -48,6 +50,24 @@ const Button = () => { {isComparing && !createNewWindow && ( )} + + + + ); }; diff --git a/app/src/components/ui/compareVersion/CompareLayOut.tsx b/app/src/components/ui/compareVersion/CompareLayOut.tsx index 60e6ca3..42e86ff 100644 --- a/app/src/components/ui/compareVersion/CompareLayOut.tsx +++ b/app/src/components/ui/compareVersion/CompareLayOut.tsx @@ -18,6 +18,7 @@ import useRestStates from "../../../hooks/useResetStates"; import { getVersionHistoryApi } from "../../../services/factoryBuilder/versionControl/getVersionHistoryApi"; import { validateSimulationDataApi } from "../../../services/simulation/comparison/validateSimulationDataApi"; +import { useSimulationManager } from "../../../store/rough/useSimulationManagerStore"; const CompareLayOut = () => { const { clearComparisonState, comparisonScene, setComparisonState } = useSimulationState(); @@ -43,6 +44,11 @@ const CompareLayOut = () => { const { resetStates } = useRestStates(); const { createNewWindow } = useCreateNewWindow(); const { limitDistance, setLimitDistance } = useLimitDistance(); + const { simulationRecords } = useSimulationManager(); + + useEffect(() => { + // console.log("simulationRecords: ", simulationRecords); + }, [simulationRecords]); useEffect(() => { return () => { resetStates(); @@ -189,7 +195,7 @@ const CompareLayOut = () => { } }); }; - console.log("limitDistance: ", limitDistance); + useEffect(() => { setLimitDistance(false); }, [limitDistance]); diff --git a/app/src/components/ui/compareVersion/ComparisonResult.tsx b/app/src/components/ui/compareVersion/ComparisonResult.tsx index 5fd585a..b3f063f 100644 --- a/app/src/components/ui/compareVersion/ComparisonResult.tsx +++ b/app/src/components/ui/compareVersion/ComparisonResult.tsx @@ -29,7 +29,6 @@ const ComparisonResult = () => { ); useEffect(() => { - console.log("compareProductsData: ", compareProductsData); if (compareProductsData.length > 0 && comparisonScene && mainScene) { setComparedProducts([compareProductsData[0], compareProductsData[1]]); } else { diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index c2c8f28..6a5288d 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -11,11 +11,19 @@ import Collaboration from "../collaboration/collaboration"; import useModuleStore from "../../store/ui/useModuleStore"; import { useParams } from "react-router-dom"; import { getUserData } from "../../functions/getUserData"; -import { useCreateNewWindow, useLoadingProgress } from "../../store/builder/store"; +import { + useCreateNewWindow, + useHeatmapTypeStore, + useIsComparing, + useLoadingProgress, +} from "../../store/builder/store"; import { useSocketStore } from "../../store/socket/useSocketStore"; import { Color, SRGBColorSpace } from "three"; import { compressImage } from "../../utils/compressImage"; import { ALPHA_ORG } from "../../pages/Dashboard"; +import HeatmapPreview from "../../components/heatMapGenerator/heatmapPreview"; +import { useSimulationManager } from "../../store/rough/useSimulationManagerStore"; +import { useSimulationState } from "../../store/simulation/useSimulationStore"; export default function Scene({ layout, @@ -32,7 +40,7 @@ export default function Scene({ ], [] ); - const { assetStore, layoutType } = useSceneContext(); + const { assetStore, layoutType, versionStore } = useSceneContext(); const { assets } = assetStore(); const { userId, organization } = getUserData(); const { projectId } = useParams(); @@ -40,7 +48,11 @@ export default function Scene({ const { activeModule } = useModuleStore(); const { loadingProgress } = useLoadingProgress(); const { setWindowRendered } = useCreateNewWindow(); - + const { simulationRecords } = useSimulationManager(); + const { isComparing } = useIsComparing(); + const { mainScene, comparisonScene } = useSimulationState(); + const { selectedVersion } = versionStore(); + const { selectedTypes } = useHeatmapTypeStore(); useEffect(() => { if (!projectId || loadingProgress !== 0) return; const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName("canvas")[0]; @@ -62,7 +74,83 @@ export default function Scene({ }); // eslint-disable-next-line }, [activeModule, assets, loadingProgress, projectId, layoutType]); + // Prepare heatmaps per scene + const mainSceneHeatmaps = useMemo(() => { + const heatmaps: Array<{ image: string | Blob; type: string }> = []; + simulationRecords.forEach((project) => + project.versions.forEach((version) => + version.products.forEach((product) => + product.simulateData.forEach((simulateDataItem) => { + const isMainScene = + product.productId === mainScene.product.productUuid && + version.versionId === mainScene.version.versionUuid && + selectedVersion?.versionId; + if (!isMainScene) return; + + product.heatMaps?.forEach((heatMap) => { + if (heatMap.type !== simulateDataItem.type) return; + + // Filter by selected types + if ( + (heatMap.type === "vehicle" && !selectedTypes.vehicle) || + (heatMap.type === "human" && !selectedTypes.human) + ) + return; + + const img = heatMap.image; + if (typeof img === "string") { + if (/\.(png|jpg)$/i.test(img)) { + heatmaps.push({ image: img, type: heatMap.type }); + } + } else if (img instanceof Blob) { + heatmaps.push({ image: img, type: heatMap.type }); + } + }); + }) + ) + ) + ); + return heatmaps; + }, [simulationRecords, mainScene, selectedVersion, selectedTypes]); + + const comparisonSceneHeatmaps = useMemo(() => { + const heatmaps: Array<{ image: string | Blob; type: string }> = []; + simulationRecords.forEach((project) => + project.versions.forEach((version) => + version.products.forEach((product) => + product.simulateData.forEach((simulateDataItem) => { + const isComparisonScene = + product.productId === comparisonScene?.product.productUuid && + version.versionId === comparisonScene?.version.versionUuid && + selectedVersion?.versionId; + if (!isComparisonScene) return; + + product.heatMaps?.forEach((heatMap) => { + if (heatMap.type !== simulateDataItem.type) return; + + // Filter by selected types + if ( + (heatMap.type === "vehicle" && !selectedTypes.vehicle) || + (heatMap.type === "human" && !selectedTypes.human) + ) + return; + + const img = heatMap.image; + if (typeof img === "string") { + if (/\.(png|jpg)$/i.test(img)) { + heatmaps.push({ image: img, type: heatMap.type }); + } + } else if (img instanceof Blob) { + heatmaps.push({ image: img, type: heatMap.type }); + } + }); + }) + ) + ) + ); + return heatmaps; + }, [simulationRecords, comparisonScene, selectedVersion, selectedTypes]); return ( + {isComparing && (selectedTypes.vehicle || selectedTypes.human) && ( + <> + {layout === "Main Layout" && + mainSceneHeatmaps.map((heatMap, idx) => ( + + ))} + {layout === "Comparison Layout" && + comparisonSceneHeatmaps.map((heatMap, idx) => ( + + ))} + + )} + diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index fdf34e4..20abfe8 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -1,26 +1,28 @@ -import { useEffect } from 'react'; -import Vehicles from './vehicle/vehicles'; -import Points from './events/points/points'; -import Conveyor from './conveyor/conveyor'; -import RoboticArm from './roboticArm/roboticArm'; -import Materials from './materials/materials'; -import Machine from './machine/machine'; -import StorageUnit from './storageUnit/storageUnit'; -import Human from './human/human'; -import Crane from './crane/crane'; -import Simulator from './simulator/simulator'; -import Products from './products/products'; -import Trigger from './triggers/trigger'; -import useModuleStore from '../../store/ui/useModuleStore'; -import SimulationAnalysis from './analysis/simulationAnalysis'; -import { useSceneContext } from '../scene/sceneContext'; -import HeatMap from '../../components/heatMapGenerator/heatMap'; +import { useEffect } from "react"; +import Vehicles from "./vehicle/vehicles"; +import Points from "./events/points/points"; +import Conveyor from "./conveyor/conveyor"; +import RoboticArm from "./roboticArm/roboticArm"; +import Materials from "./materials/materials"; +import Machine from "./machine/machine"; +import StorageUnit from "./storageUnit/storageUnit"; +import Human from "./human/human"; +import Crane from "./crane/crane"; +import Simulator from "./simulator/simulator"; +import Products from "./products/products"; +import Trigger from "./triggers/trigger"; +import useModuleStore from "../../store/ui/useModuleStore"; +import SimulationAnalysis from "./analysis/simulationAnalysis"; +import { useSceneContext } from "../scene/sceneContext"; +import HeatMap from "../../components/heatMapGenerator/heatMap"; +import { useIsComparing } from "../../store/builder/store"; function Simulation() { const { activeModule } = useModuleStore(); const { eventStore, productStore } = useSceneContext(); const { events } = eventStore(); const { products } = productStore(); + const { isComparing } = useIsComparing(); useEffect(() => { // console.log('events: ', events); @@ -60,7 +62,7 @@ function Simulation() { - + {!isComparing && } )} diff --git a/app/src/modules/simulation/simulator/functions/generateHeatmapOutput.ts b/app/src/modules/simulation/simulator/functions/generateHeatmapOutput.ts new file mode 100644 index 0000000..f3078ff --- /dev/null +++ b/app/src/modules/simulation/simulator/functions/generateHeatmapOutput.ts @@ -0,0 +1,97 @@ +import { WebGLRenderer } from "three"; +import { Buffer } from "buffer"; +import { exportHeatmapAsPNG } from "../../../../components/heatMapGenerator/functions/exportHeatmapAsPNG"; + +// Types +interface BakedPoint { + type: string; + points: { x: number; y: number }; +} + +interface ExportedHeatmap { + type: string; + image: Blob | null; +} + +interface HeatmapFileResult { + type: string; + file: HeatmapBackendFile; +} + +interface HeatmapBackendFile { + fieldname: string; + originalname: string; + encoding: string; + mimetype: string; + buffer: Buffer; + size: number; +} + +interface GenerateHeatmapOutputParams { + bakedPoints: BakedPoint[]; + gl: WebGLRenderer; + width: number; + height: number; + outputType?: "url" | "file" | "blob"; + download?: boolean; +} + +export async function generateHeatmapOutput({ + bakedPoints, + gl, + width, + height, + outputType = "file", + download = false, +}: GenerateHeatmapOutputParams): Promise { + const bakedResult: ExportedHeatmap[] = exportHeatmapAsPNG({ bakedPoints, gl, width, height }); + + const fileResults = await Promise.all( + bakedResult.map(async (item) => { + console.log("item: ", item); + + let imageBlob: Blob; + + if (!item.image) { + // Create a fully transparent 1x1 PNG image + const transparentPNG = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; + const response = await fetch(transparentPNG); + imageBlob = await response.blob(); + } else { + imageBlob = item.image; + } + + const fileName = `${item.type}-heatmap.png`; + + // Convert Blob -> ArrayBuffer -> Buffer + const arrayBuffer = await imageBlob.arrayBuffer(); + const nodeBuffer = Buffer.from(arrayBuffer); + + const backendFile: HeatmapBackendFile = { + fieldname: "file", + originalname: fileName, + encoding: "7bit", + mimetype: "image/png", + buffer: nodeBuffer, + size: nodeBuffer.length, + }; + + if (download) { + const url = URL.createObjectURL(imageBlob); + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } + + return { type: item.type, file: backendFile }; + }) + ); + + console.log('fileResults: ', fileResults); + return fileResults; +} diff --git a/app/src/modules/simulation/simulator/simulationHandler.tsx b/app/src/modules/simulation/simulator/simulationHandler.tsx index 5aff421..d419998 100644 --- a/app/src/modules/simulation/simulator/simulationHandler.tsx +++ b/app/src/modules/simulation/simulator/simulationHandler.tsx @@ -1,13 +1,28 @@ -import React, { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useSceneContext } from "../../scene/sceneContext"; import { determineExecutionMachineSequences } from "./functions/determineExecutionMachineSequences"; import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore"; import { useSimulationManager } from "../../../store/rough/useSimulationManagerStore"; import { useParams } from "react-router-dom"; import { saveSimulationDataApi } from "../../../services/simulation/comparison/saveSimulationDataApi"; - +import { generateHeatmapOutput } from "./functions/generateHeatmapOutput"; +import { heatMapImageApi } from "../../../services/simulation/products/heatMapImageApi"; +import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore"; +import * as CONSTANTS from "../../../types/world/worldConstants"; +import { useThree } from "@react-three/fiber"; const SimulationHandler = () => { - const { materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, productStore, craneStore, humanStore, versionStore } = useSceneContext(); + const { + materialStore, + armBotStore, + machineStore, + conveyorStore, + vehicleStore, + storageUnitStore, + productStore, + craneStore, + humanStore, + versionStore, + } = useSceneContext(); const { armBots, getArmBotById } = armBotStore(); const { vehicles, getVehicleById } = vehicleStore(); const { getConveyorById } = conveyorStore(); @@ -21,7 +36,10 @@ const SimulationHandler = () => { const { resetProductRecords, addSimulationRecord } = useSimulationManager(); const { selectedVersion } = versionStore(); const { projectId } = useParams(); - + const { bakedPoints } = useHeatMapStore(); + const height = CONSTANTS.gridConfig.size; + const width = CONSTANTS.gridConfig.size; + const { gl } = useThree(); useEffect(() => { if (!projectId) return; @@ -48,15 +66,37 @@ const SimulationHandler = () => { const obj = getter?.(entity.modelUuid); if (!obj) return; - addSimulationRecord(projectId, selectedVersion?.versionId || "", selectedProduct?.productUuid, { - activeTime: obj.activeTime ?? 0, - isActive: obj.isActive ?? false, - idleTime: obj.idleTime ?? 0, - type: entity.type as "roboticArm" | "vehicle" | "machine" | "human" | "crane" | "storageUnit" | "transfer", - assetId: entity.modelUuid, - }); + addSimulationRecord( + projectId, + selectedVersion?.versionId || "", + selectedProduct?.productUuid, + { + activeTime: obj.activeTime ?? 0, + isActive: obj.isActive ?? false, + idleTime: obj.idleTime ?? 0, + type: entity.type as + | "roboticArm" + | "vehicle" + | "machine" + | "human" + | "crane" + | "storageUnit" + | "transfer", + assetId: entity.modelUuid, + } + ); }; + async function handleSaveSimulationData(data: any) { + try { + await saveSimulationDataApi(data); + console.log("Simulation data saved successfully"); + setIsPlaying(false); + } catch (err) { + console.error("Failed to save simulation data:", err); + } + } + function checkActiveMachines() { const currentProduct = getProductById(selectedProduct.productUuid); if (!currentProduct) return; @@ -74,30 +114,71 @@ const SimulationHandler = () => { // --- Save simulation data if idle --- const noMaterials = materials.length === 0; - const hasHistory = materialHistory.length >= 0; + const hasHistory = materialHistory.length > 0; if (noMaterials && hasHistory && !hasActiveEntity) { if (!selectedVersion) return; + console.log("SIMULATION COMPLETED"); - resetProductRecords(projectId, selectedVersion.versionId, selectedProduct.productUuid); + resetProductRecords( + projectId, + selectedVersion.versionId, + selectedProduct.productUuid + ); - executionSequences?.forEach((sequence) => sequence.forEach((entity) => recordEntity(entity))); + executionSequences?.forEach((sequence) => + sequence.forEach((entity) => recordEntity(entity)) + ); const data = { projectId, versionId: selectedVersion.versionId, productUuid: selectedProduct.productUuid, - simulateData: useSimulationManager.getState().getProductById(projectId, selectedVersion.versionId, selectedProduct.productUuid)?.simulateData, + simulateData: useSimulationManager + .getState() + .getProductById( + projectId, + selectedVersion.versionId, + selectedProduct.productUuid + )?.simulateData, }; - saveSimulationDataApi(data) - .then(() => { - echo.log("Simulation data saved successfully"); - setIsPlaying(false); - }) - .catch((err) => { - console.error("Failed to save simulation data:", err); + handleSaveSimulationData(data); + if (executionSequences?.length > 0) { + executionSequences.forEach((sequence) => { + sequence.forEach((entity) => { + const typeToGetter: Record any> = { + roboticArm: getArmBotById, + vehicle: getVehicleById, + machine: getMachineById, + human: getHumanById, + crane: getCraneById, + storageUnit: getStorageUnitById, + transfer: getConveyorById, + }; + + const getter = typeToGetter[entity.type]; + if (!getter) return; // skip unknown entity types + + const obj = getter(entity.modelUuid); + if (!obj) return; // skip if not found + }); }); + generateHeatmapOutput({ + bakedPoints, + gl, + width, + height, + download: true, + }).then((bakedResult) => { + heatMapImageApi( + projectId || "", + selectedVersion?.versionId || "", + selectedProduct?.productUuid, + bakedResult + ).then((img) => console.log("getImg: ", img)); + }); + } } }); } @@ -110,7 +191,16 @@ const SimulationHandler = () => { if (checkTimer) clearTimeout(checkTimer); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [materials, materialHistory, selectedVersion, selectedProduct?.productUuid, isPlaying, armBots, vehicles, machines]); + }, [ + materials, + materialHistory, + selectedVersion, + selectedProduct?.productUuid, + isPlaying, + armBots, + vehicles, + machines, + ]); return null; }; diff --git a/app/src/modules/simulation/simulator/simulator.tsx b/app/src/modules/simulation/simulator/simulator.tsx index 185f8b1..ba1b134 100644 --- a/app/src/modules/simulation/simulator/simulator.tsx +++ b/app/src/modules/simulation/simulator/simulator.tsx @@ -20,7 +20,6 @@ function Simulator() { const { selectedVersion } = versionStore(); const { setIsComparing } = useIsComparing(); const { addSimulationRecords } = useSimulationManager(); - useEffect(() => { if (!isPlaying || isReset || !selectedProduct.productUuid) return; @@ -37,20 +36,40 @@ function Simulator() { useEffect(() => { if (!projectId || !selectedVersion || !selectedProduct?.productUuid) return; - getSimulationDataApi(projectId, selectedVersion.versionId, selectedProduct?.productUuid).then((getData) => { - getProductApi(selectedProduct.productUuid, projectId, selectedVersion.versionId).then((product) => { - if (!product) return; - const getSimulate = getData?.data; - if (getData?.message !== "Simulated data not found" && getSimulate && getSimulate.productTimestamp === product?.timestamp) { - addSimulationRecords(projectId, selectedVersion?.versionId || "", selectedProduct?.productUuid || "", getSimulate.data); - // echo.warn("Simulation data is up to date"); - return; - } else { - setIsComparing(false); - // echo.warn("Please run the simulation before comparing."); + getSimulationDataApi( + projectId, + selectedVersion.versionId, + selectedProduct?.productUuid + ).then((getData) => { + getProductApi(selectedProduct.productUuid, projectId, selectedVersion.versionId).then( + (product) => { + if (!product) return; + const getSimulate = getData?.data; + + if (!getSimulate) return; + const getHeatmaps = getSimulate.heatmaps; + + if ( + getData?.message !== "Simulated data not found" && + getSimulate && + getSimulate.productTimestamp === product?.timestamp + ) { + addSimulationRecords( + projectId, + selectedVersion?.versionId || "", + selectedProduct?.productUuid || "", + getSimulate.data, + getHeatmaps + ); + + return echo.warn("Simulation data is up to date"); + } else { + setIsComparing(false); + echo.warn("Please run the simulation before comparing."); + } } - }); + ); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedProduct, projectId]); diff --git a/app/src/services/simulation/products/heatMapImageApi.ts b/app/src/services/simulation/products/heatMapImageApi.ts new file mode 100644 index 0000000..c3e9191 --- /dev/null +++ b/app/src/services/simulation/products/heatMapImageApi.ts @@ -0,0 +1,37 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const heatMapImageApi = async ( + projectId: string, + versionId: string, + productUuid: string, + heatmaps: any +) => { + console.log('heatmaps: ', heatmaps); + try { + const response = await fetch(`${url_Backend_dwinzo}/api/V1/SimulatedImage`, { + method: "PATCH", + headers: { + Authorization: "Bearer ", + "Content-Type": "application/json", + token: localStorage.getItem("token") || "", + refresh_token: localStorage.getItem("refreshToken") || "", + }, + body: JSON.stringify({ projectId, versionId, productUuid, heatmaps }), + }); + + const newAccessToken = response.headers.get("x-access-token"); + if (newAccessToken) { + localStorage.setItem("token", newAccessToken); + } + + if (!response.ok) { + echo.error("Failed to delete event data"); + } + + const result = await response.json(); + + return result; + } catch { + echo.error("Failed to delete event data"); + } +}; diff --git a/app/src/store/builder/store.ts b/app/src/store/builder/store.ts index bda5a2b..bf159af 100644 --- a/app/src/store/builder/store.ts +++ b/app/src/store/builder/store.ts @@ -478,3 +478,23 @@ export const useCreateNewWindow = create((set: any) => ({ windowRendered: "", setWindowRendered: (x: any) => set({ windowRendered: x }), })); +interface HeatmapTypeState { + selectedTypes: { + vehicle: boolean; + human: boolean; + }; + toggleType: (type: "vehicle" | "human") => void; +} +export const useHeatmapTypeStore = create((set) => ({ + selectedTypes: { + vehicle: false, + human: false, + }, + toggleType: (type) => + set((state) => ({ + selectedTypes: { + ...state.selectedTypes, + [type]: !state.selectedTypes[type], + }, + })), +})); diff --git a/app/src/store/rough/useSimulationManagerStore.ts b/app/src/store/rough/useSimulationManagerStore.ts index 80dbb97..5ce4a4c 100644 --- a/app/src/store/rough/useSimulationManagerStore.ts +++ b/app/src/store/rough/useSimulationManagerStore.ts @@ -12,6 +12,7 @@ interface SimulationUsageRecord { interface ProductSimulation { productId: string; simulateData: SimulationUsageRecord[]; + heatMaps?: { type: string; image: string | Blob | null }[]; } // Version → holds multiple products @@ -29,12 +30,35 @@ interface ProjectSimulation { interface SimulationManagerStore { simulationRecords: ProjectSimulation[]; setSimulationRecords: (simulateData: ProjectSimulation[]) => void; - addSimulationRecord: (projectId: string | undefined, versionId: string, productId: string, record: SimulationUsageRecord) => void; - addSimulationRecords: (projectId: string | undefined, versionId: string, productId: string, records: SimulationUsageRecord[]) => void; - resetProductRecords: (projectId: string | undefined, versionId: string | null, productId: string) => void; + + addSimulationRecord: ( + projectId: string | undefined, + versionId: string, + productId: string, + record: SimulationUsageRecord + ) => void; + addSimulationRecords: ( + projectId: string | undefined, + versionId: string, + productId: string, + records: SimulationUsageRecord[], + heatMaps?: { type: string; image: string | Blob | null }[] + ) => void; + resetProductRecords: ( + projectId: string | undefined, + versionId: string | null, + productId: string + ) => void; getProjectById: (projectId: string | undefined) => ProjectSimulation | undefined; - getVersionById: (projectId: string | undefined, versionId: string) => VersionSimulation | undefined; - getProductById: (projectId: string | undefined, versionId: string, productId: string) => ProductSimulation | undefined; + getVersionById: ( + projectId: string | undefined, + versionId: string + ) => VersionSimulation | undefined; + getProductById: ( + projectId: string | undefined, + versionId: string, + productId: string + ) => ProductSimulation | undefined; } export const useSimulationManager = create((set, get) => ({ @@ -55,12 +79,16 @@ export const useSimulationManager = create((set, get) => products: version.products.map((product) => { if (product.productId !== productId) return product; - const exists = product.simulateData.some((r) => r.assetId === record.assetId); + const exists = product.simulateData.some( + (r) => r.assetId === record.assetId + ); return { ...product, simulateData: exists - ? product.simulateData.map((r) => (r.assetId === record.assetId ? record : r)) // update + ? product.simulateData.map((r) => + r.assetId === record.assetId ? record : r + ) // update : [...product.simulateData, record], // insert }; }), @@ -98,53 +126,133 @@ export const useSimulationManager = create((set, get) => return { simulationRecords: projects }; }), - addSimulationRecords: (projectId, versionId, productId, records) => + addSimulationRecords: ( + projectId: string | undefined, + versionId: string, + productId: string, + records: SimulationUsageRecord[], + heatMaps?: { type: string; image: string | Blob | null }[] + ) => set((state) => { - const projects = state.simulationRecords.map((project) => { - if (project.projectId !== projectId) return project; + const projects = [...state.simulationRecords]; - return { - ...project, - versions: project.versions.map((version) => { - if (version.versionId !== versionId) return version; + // --- Helper functions --- + const findOrCreateProject = (id: string | undefined): ProjectSimulation => { + let project = projects.find((p) => p.projectId === id); + if (!project) { + project = { projectId: id, versions: [] }; + projects.push(project); + } + return project; + }; - return { - ...version, - products: version.products.map((product) => (product.productId === productId ? { ...product, simulateData: [...product.simulateData, ...records] } : product)), - }; - }), - }; + const findOrCreateVersion = ( + project: ProjectSimulation, + id: string + ): VersionSimulation => { + let version = project.versions.find((v) => v.versionId === id); + if (!version) { + version = { versionId: id, products: [] }; + project.versions.push(version); + } + return version; + }; + + const findOrCreateProduct = ( + version: VersionSimulation, + id: string + ): ProductSimulation => { + let product = version.products.find((p) => p.productId === id); + if (!product) { + product = { productId: id, simulateData: [] }; + version.products.push(product); + } + return product; + }; + + // --- Get or create project/version/product hierarchy --- + const project = findOrCreateProject(projectId); + const version = findOrCreateVersion(project, versionId); + const product = findOrCreateProduct(version, productId); + + // --- Merge or add simulation records --- + records.forEach((record) => { + const existingIndex = product.simulateData.findIndex( + (r) => r.assetId === record.assetId + ); + + if (existingIndex !== -1) { + // Update existing record + product.simulateData[existingIndex] = { + ...product.simulateData[existingIndex], + ...record, + }; + } else { + // Add new record + product.simulateData.push(record); + } }); - // same creation logic for new project/version/product - if (!state.simulationRecords.find((p) => p.projectId === projectId)) { - projects.push({ - projectId, - versions: [ - { - versionId, - products: [{ productId, simulateData: [...records] }], - }, - ], - }); - } else { - const project = projects.find((p) => p.projectId === projectId)!; - if (!project.versions.find((v) => v.versionId === versionId)) { - project.versions.push({ - versionId, - products: [{ productId, simulateData: [...records] }], - }); - } else { - const version = project.versions.find((v) => v.versionId === versionId)!; - if (!version.products.find((p) => p.productId === productId)) { - version.products.push({ productId, simulateData: [...records] }); - } - } - } + // --- Update or set product-level heatmaps --- + product.heatMaps = heatMaps ?? product.heatMaps; return { simulationRecords: projects }; }), + // addSimulationRecords: (projectId, versionId, productId, records) => + // set((state) => { + // const projects = state.simulationRecords.map((project) => { + // if (project.projectId !== projectId) return project; + + // return { + // ...project, + // versions: project.versions.map((version) => { + // if (version.versionId !== versionId) return version; + + // return { + // ...version, + // products: version.products.map((product) => + // product.productId === productId + // ? { + // ...product, + // simulateData: [...product.simulateData, ...records], + // } + // : product + // ), + // }; + // }), + // }; + // }); + + // // same creation logic for new project/version/product + // if (!state.simulationRecords.find((p) => p.projectId === projectId)) { + // projects.push({ + // projectId, + // versions: [ + // { + // versionId, + // products: [{ productId, simulateData: [...records] }], + // }, + // ], + // }); + // } else { + // const project = projects.find((p) => p.projectId === projectId)!; + // if (!project.versions.find((v) => v.versionId === versionId)) { + // project.versions.push({ + // versionId, + // products: [{ productId, simulateData: [...records] }], + // }); + // } else { + // const version = project.versions.find((v) => v.versionId === versionId)!; + // if (!version.products.find((p) => p.productId === productId)) { + // version.products.push({ productId, simulateData: [...records] }); + // } + // } + // } + + // return { simulationRecords: projects }; + // }), + resetProductRecords: (projectId, versionId, productId) => set((state) => { const projects = state.simulationRecords.map((project) => { @@ -157,7 +265,11 @@ export const useSimulationManager = create((set, get) => return { ...version, - products: version.products.map((product) => (product.productId === productId ? { ...product, simulateData: [] } : product)), + products: version.products.map((product) => + product.productId === productId + ? { ...product, simulateData: [] } + : product + ), }; }), }; @@ -173,12 +285,16 @@ export const useSimulationManager = create((set, get) => }, getVersionById: (projectId: string | undefined, versionId: string) => { - const project = get().simulationRecords.find((p: ProjectSimulation) => p.projectId === projectId); + const project = get().simulationRecords.find( + (p: ProjectSimulation) => p.projectId === projectId + ); return project?.versions.find((v: VersionSimulation) => v.versionId === versionId); }, getProductById: (projectId: string | undefined, versionId: string, productId: string) => { - const project = get().simulationRecords.find((p: ProjectSimulation) => p.projectId === projectId); + const project = get().simulationRecords.find( + (p: ProjectSimulation) => p.projectId === projectId + ); const version = project?.versions.find((v: VersionSimulation) => v.versionId === versionId); return version?.products.find((p: ProductSimulation) => p.productId === productId); }, diff --git a/app/src/store/simulation/useHeatMapStore.ts b/app/src/store/simulation/useHeatMapStore.ts index c6231bd..b470a40 100644 --- a/app/src/store/simulation/useHeatMapStore.ts +++ b/app/src/store/simulation/useHeatMapStore.ts @@ -11,6 +11,7 @@ interface BakedPointImage { points: BakedPoint; } + interface HeatMapState { monitoringVehicle: string[]; monitoringHuman: string[];