diff --git a/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx b/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx index c774943..dd89d6c 100644 --- a/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx +++ b/app/src/components/heatMapGenerator/baked/bakedHeatMap.tsx @@ -3,126 +3,223 @@ 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 { Html } from "@react-three/drei"; +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, setIsPlaying } = 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); + 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"); + // exportHeatmapAsPNG(); + } + }, [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..678e774 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,154 @@ 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/heatMap.tsx b/app/src/components/heatMapGenerator/heatMap.tsx index bda354a..199e32c 100644 --- a/app/src/components/heatMapGenerator/heatMap.tsx +++ b/app/src/components/heatMapGenerator/heatMap.tsx @@ -14,7 +14,7 @@ function HeatMap() { <> - + {/* */} ); } diff --git a/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx b/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx index 760dcde..1bb118d 100644 --- a/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx +++ b/app/src/components/heatMapGenerator/realTime/realTimeHeatMap.tsx @@ -48,7 +48,7 @@ const RealTimeHeatMap = () => { u_radius: { value: RADIUS }, u_opacity: { value: OPACITY }, u_debugMode: { value: debugModeMap[debugMode] }, - u_growthRate: { value: GROWTH_TIME_MULTIPLIER }, + u_growthRate: { value: GROWTH_TIME_MULTIPLIER }, }); useEffect(() => { @@ -59,10 +59,34 @@ const RealTimeHeatMap = () => { } }, [isReset, isPlaying]); - useEffect(() => { - addMonitoringVehicle("26770368-55e8-4d40-87f7-8eacb48dc236"); - addMonitoringHuman("264a51e7-d8b9-4093-95ac-fa7e2dc49cfa"); - }, []); + // useEffect(() => { + // addMonitoringVehicle("26770368-55e8-4d40-87f7-8eacb48dc236"); + // addMonitoringHuman("264a51e7-d8b9-4093-95ac-fa7e2dc49cfa"); + // }, []); + + // useEffect(() => { + // const selectedProductData = getProductById(selectedProduct.productUuid); + // const newEvents: EventsSchema[] = []; + + // if (selectedProductData) { + // determineExecutionMachineSequences([selectedProductData]).then((sequences) => { + // sequences.forEach((sequence) => { + // sequence.forEach((event) => { + // if (event.type === "human") { + // if (hasHuman(event.modelUuid)) { + // newEvents.push(event); + // } + // } else if (event.type === "vehicle") { + // if (hasVehicle(event.modelUuid)) { + // newEvents.push(event); + // } + // } + // }); + // }); + // setEvents(newEvents); + // }); + // } + // }, [selectedProduct, products, monitoringHuman, monitoringVehicle]); useEffect(() => { const selectedProductData = getProductById(selectedProduct.productUuid); @@ -72,14 +96,8 @@ const RealTimeHeatMap = () => { determineExecutionMachineSequences([selectedProductData]).then((sequences) => { sequences.forEach((sequence) => { sequence.forEach((event) => { - if (event.type === "human") { - if (hasHuman(event.modelUuid)) { - newEvents.push(event); - } - } else if (event.type === "vehicle") { - if (hasVehicle(event.modelUuid)) { - newEvents.push(event); - } + if (event.type === "human" || event.type === "vehicle") { + newEvents.push(event); } }); }); diff --git a/app/src/modules/simulation/simulator/SimulationHandler.tsx b/app/src/modules/simulation/simulator/SimulationHandler.tsx index 3cd7f8d..f109707 100644 --- a/app/src/modules/simulation/simulator/SimulationHandler.tsx +++ b/app/src/modules/simulation/simulator/SimulationHandler.tsx @@ -1,22 +1,22 @@ -import React, { 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 React, { 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 { exportHeatmapAsPNG } from "../../../components/heatMapGenerator/functions/exportHeatmapAsPNG"; +import { WebGLRenderer } from "three"; +import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore"; +import { useThree } from "@react-three/fiber"; +import * as CONSTANTS from "../../../types/world/worldConstants"; +import { heatMapImageApi } from "../../../services/simulation/products/heatMapImageApi"; +import { generateHeatmapOutput } from "./functions/generateHeatmapOutput"; interface SimulationUsageRecord { activeTime: number; isActive: boolean; idleTime: number; - type: - | "roboticArm" - | "vehicle" - | "transfer" - | "storageUnit" - | "crane" - | "human" - | "machine"; + type: "roboticArm" | "vehicle" | "transfer" | "storageUnit" | "crane" | "human" | "machine"; } // Product → holds multiple usage records @@ -45,15 +45,16 @@ const SimulationHandler = () => { const { getProductById, selectedProduct } = productStore(); const { machines, getMachineById } = machineStore(); const { getHumanById } = humanStore(); - const { getCraneById, } = craneStore(); + const { getCraneById } = craneStore(); const { getStorageUnitById } = storageUnitStore(); const { isPlaying, setIsPlaying } = usePlayButtonStore(); const { simulationData, addData } = useSimulationManager(); const { projectId } = useParams(); const { selectedVersion } = versionStore(); - - - + const { bakedPoints } = useHeatMapStore(); + const height = CONSTANTS.gridConfig.size; + const width = CONSTANTS.gridConfig.size; + const { gl } = useThree(); useEffect(() => { let checkTimer: ReturnType; @@ -65,39 +66,39 @@ const SimulationHandler = () => { if (currentProduct) { const executionSequences = await determineExecutionMachineSequences([currentProduct]); if (executionSequences?.length > 0) { - executionSequences.forEach(sequence => { - sequence.forEach(entity => { - if (entity.type === 'roboticArm') { + executionSequences.forEach((sequence) => { + sequence.forEach((entity) => { + if (entity.type === "roboticArm") { const roboticArm = getArmBotById(entity.modelUuid); if (roboticArm?.isActive) { hasActiveEntity = true; } } - if (entity.type === 'vehicle') { + if (entity.type === "vehicle") { const vehicle = getVehicleById(entity.modelUuid); if (vehicle?.isActive) { hasActiveEntity = true; } } - if (entity.type === 'machine') { + if (entity.type === "machine") { const machine = getMachineById(entity.modelUuid); if (machine?.isActive) { hasActiveEntity = true; } } - if (entity.type === 'human') { + if (entity.type === "human") { const human = getHumanById(entity.modelUuid); if (human?.isActive) { hasActiveEntity = true; } } - if (entity.type === 'crane') { + if (entity.type === "crane") { const crane = getCraneById(entity.modelUuid); if (crane?.isActive) { hasActiveEntity = true; } } - if (entity.type === 'storageUnit') { + if (entity.type === "storageUnit") { const storageUnit = getStorageUnitById(entity.modelUuid); if (storageUnit?.isActive) { hasActiveEntity = true; @@ -114,6 +115,7 @@ const SimulationHandler = () => { } if (materials.length === 0 && materialHistory.length >= 0 && !hasActiveEntity) { + let bakedResult = generateHeatmapOutput({ bakedPoints, gl, width, height, outputType: "url" }); if (executionSequences?.length > 0) { executionSequences.forEach((sequence) => { @@ -134,24 +136,14 @@ const SimulationHandler = () => { const obj = getter(entity.modelUuid); if (!obj) return; // skip if not found - addData( - 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", - } - ); + addData(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", + }); + + heatMapImageApi(projectId || "", selectedVersion?.versionId || "", selectedProduct?.productUuid, bakedResult); }); }); } @@ -172,6 +164,6 @@ const SimulationHandler = () => { }, [materials, materialHistory, selectedVersion, selectedProduct?.productUuid, isPlaying, armBots, vehicles, machines]); return null; -} +}; export default SimulationHandler; 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..4f07dd5 --- /dev/null +++ b/app/src/modules/simulation/simulator/functions/generateHeatmapOutput.ts @@ -0,0 +1,90 @@ +import { WebGLRenderer } from "three"; +import { exportHeatmapAsPNG } from "../../../../components/heatMapGenerator/functions/exportHeatmapAsPNG"; + +// Type for a single baked point +interface BakedPoint { + type: string; + points: { x: number; y: number }; +} + +// Heatmap item returned by exportHeatmapAsPNG +interface ExportedHeatmap { + type: string; + image: Blob | null; +} + +// Output types +interface HeatmapUrlResult { + type: string; + url: string; +} + +interface HeatmapFileResult { + type: string; + file: File; +} + +type OutputType = "url" | "file" | "blob"; + +interface GenerateHeatmapOutputParams { + bakedPoints: BakedPoint[]; + gl: WebGLRenderer; + width: number; + height: number; + outputType?: OutputType; + download?: boolean; // <-- NEW PARAM +} + +/** + * Generates heatmap output as either a File or a URL. + * If `download` is true, automatically triggers file download. + */ +export function generateHeatmapOutput({ bakedPoints, gl, width, height, outputType = "file", download = false }: GenerateHeatmapOutputParams): (HeatmapUrlResult | HeatmapFileResult)[] { + const bakedResult: ExportedHeatmap[] = exportHeatmapAsPNG({ bakedPoints, gl, width, height }); + + return bakedResult + .map((item) => { + if (!item.image) return null; + + const fileName = `${item.type}-heatmap.png`; + + if (outputType === "file") { + const file = new File([item.image], fileName, { type: "image/png" }); + + // If download flag is true, trigger download + if (download) { + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + + return { type: item.type, file } as HeatmapFileResult; + } else if (outputType === "url") { + const url = URL.createObjectURL(item.image); + + // If download flag is true, trigger download + if (download) { + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + // We don't revoke here immediately, or the download may fail + setTimeout(() => URL.revokeObjectURL(url), 1000); + } + + return { type: item.type, url } as HeatmapUrlResult; + } else if (outputType === "blob") { + return bakedResult; + } + + throw new Error("Invalid outputType. Use 'url' or 'file'."); + }) + .filter((result): result is HeatmapUrlResult | HeatmapFileResult => result !== null); +} diff --git a/app/src/services/simulation/products/heatMapImageApi.ts b/app/src/services/simulation/products/heatMapImageApi.ts new file mode 100644 index 0000000..ac69da5 --- /dev/null +++ b/app/src/services/simulation/products/heatMapImageApi.ts @@ -0,0 +1,36 @@ +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); + console.log('productUuid: ', productUuid); + console.log('versionId: ', versionId); + console.log('projectId: ', projectId); + 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(); + console.log('result: ', result); + return result; + } catch { + echo.error("Failed to delete event data"); + } +};