backend intgration and file export function
This commit is contained in:
@@ -3,126 +3,223 @@ import { useEffect, useMemo, useRef, useCallback } from "react";
|
|||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore";
|
import { useHeatMapStore } from "../../../store/simulation/useHeatMapStore";
|
||||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
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 RADIUS = 0.0025;
|
||||||
const OPACITY = 0.8;
|
const OPACITY = 0.8;
|
||||||
const GROWTH_RATE = 20.0;
|
const GROWTH_RATE = 20.0;
|
||||||
|
|
||||||
// 🔹 React Component
|
|
||||||
const BakedHeatMap = () => {
|
const BakedHeatMap = () => {
|
||||||
|
const { materialStore } = useSceneContext();
|
||||||
const { bakedPoints } = useHeatMapStore();
|
const { bakedPoints } = useHeatMapStore();
|
||||||
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
const { gl } = useThree();
|
const { gl } = useThree();
|
||||||
|
const { isPlaying, setIsPlaying } = usePlayButtonStore();
|
||||||
const height = CONSTANTS.gridConfig.size;
|
const height = CONSTANTS.gridConfig.size;
|
||||||
const width = CONSTANTS.gridConfig.size;
|
const width = CONSTANTS.gridConfig.size;
|
||||||
|
const { materialHistory, materials } = materialStore();
|
||||||
|
|
||||||
const pointTexture = useMemo(() => {
|
const createPointTexture = useCallback(
|
||||||
if (bakedPoints.length === 0) return null;
|
(filteredPoints: typeof bakedPoints) => {
|
||||||
const data = new Float32Array(bakedPoints.length * 4);
|
if (filteredPoints.length === 0) return null;
|
||||||
bakedPoints.forEach((p, i) => {
|
|
||||||
|
const data = new Float32Array(filteredPoints.length * 4);
|
||||||
|
filteredPoints.forEach((p, i) => {
|
||||||
const index = i * 4;
|
const index = i * 4;
|
||||||
data[index] = (p.points.x + width / 2) / width;
|
data[index] = (p.points.x + width / 2) / width;
|
||||||
data[index + 1] = (p.points.y + height / 2) / height;
|
data[index + 1] = (p.points.y + height / 2) / height;
|
||||||
data[index + 2] = 0.3;
|
data[index + 2] = 0.3;
|
||||||
data[index + 3] = 0.0;
|
data[index + 3] = 0.0;
|
||||||
});
|
});
|
||||||
const texture = new THREE.DataTexture(data, bakedPoints.length, 1, THREE.RGBAFormat, THREE.FloatType);
|
|
||||||
|
const texture = new THREE.DataTexture(data, filteredPoints.length, 1, THREE.RGBAFormat, THREE.FloatType);
|
||||||
texture.needsUpdate = true;
|
texture.needsUpdate = true;
|
||||||
return texture;
|
return texture;
|
||||||
}, [bakedPoints, width, height]);
|
},
|
||||||
|
[width, height]
|
||||||
|
);
|
||||||
|
|
||||||
const uniformsRef = useRef({
|
const uniformsRef = useRef({
|
||||||
u_points: { value: pointTexture },
|
u_points: { value: null as THREE.DataTexture | null },
|
||||||
u_count: { value: bakedPoints.length },
|
u_count: { value: 0 },
|
||||||
u_radius: { value: RADIUS },
|
u_radius: { value: RADIUS },
|
||||||
u_opacity: { value: OPACITY },
|
u_opacity: { value: OPACITY },
|
||||||
u_growthRate: { value: GROWTH_RATE },
|
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(() => {
|
useEffect(() => {
|
||||||
uniformsRef.current.u_points.value = pointTexture;
|
uniformsRef.current.u_points.value = pointTexture;
|
||||||
uniformsRef.current.u_count.value = bakedPoints.length;
|
uniformsRef.current.u_count.value = bakedPoints.length;
|
||||||
}, [pointTexture, bakedPoints.length]);
|
}, [pointTexture, bakedPoints.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (meshRef.current) {
|
|
||||||
exportHeatmapAsPNG({
|
|
||||||
bakedPoints,
|
|
||||||
gl,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
mesh: meshRef.current,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<></>
|
<>
|
||||||
// <mesh ref={meshRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0.025, 0]}>
|
<mesh ref={meshRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0.025, 0]}>
|
||||||
// <planeGeometry args={[width, height]} />
|
<planeGeometry args={[width, height]} />
|
||||||
// <shaderMaterial
|
<shaderMaterial
|
||||||
// ref={materialRef}
|
ref={materialRef}
|
||||||
// transparent
|
transparent
|
||||||
// depthWrite={false}
|
depthWrite={false}
|
||||||
// blending={THREE.AdditiveBlending}
|
blending={THREE.AdditiveBlending}
|
||||||
// uniforms={uniformsRef.current}
|
uniforms={uniformsRef.current}
|
||||||
// side={THREE.DoubleSide}
|
side={THREE.DoubleSide}
|
||||||
// vertexShader={`
|
vertexShader={`
|
||||||
// varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
// void main() {
|
void main() {
|
||||||
// vUv = uv;
|
vUv = uv;
|
||||||
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
// }
|
}
|
||||||
// `}
|
`}
|
||||||
// fragmentShader={`
|
fragmentShader={`
|
||||||
// uniform sampler2D u_points;
|
uniform sampler2D u_points;
|
||||||
// precision highp float;
|
precision highp float;
|
||||||
// uniform int u_count;
|
uniform int u_count;
|
||||||
// uniform float u_radius;
|
uniform float u_radius;
|
||||||
// uniform float u_opacity;
|
uniform float u_opacity;
|
||||||
// uniform float u_growthRate;
|
uniform float u_growthRate;
|
||||||
// varying vec2 vUv;
|
varying vec2 vUv;
|
||||||
|
|
||||||
// float gauss(float dist, float radius) {
|
float gauss(float dist, float radius) {
|
||||||
// return exp(-pow(dist / radius, 2.0));
|
return exp(-pow(dist / radius, 2.0));
|
||||||
// }
|
}
|
||||||
|
|
||||||
// void main() {
|
void main() {
|
||||||
// float intensity = 0.0;
|
float intensity = 0.0;
|
||||||
|
|
||||||
// for (int i = 0; i < 10000; i++) {
|
for (int i = 0; i < 10000; i++) {
|
||||||
// if (i >= u_count) break;
|
if (i >= u_count) break;
|
||||||
// float fi = float(i) + 0.5;
|
float fi = float(i) + 0.5;
|
||||||
// float u = fi / float(u_count);
|
float u = fi / float(u_count);
|
||||||
|
|
||||||
// vec4 point = texture2D(u_points, vec2(u, 0.5));
|
vec4 point = texture2D(u_points, vec2(u, 0.5));
|
||||||
// vec2 pos = point.rg;
|
vec2 pos = point.rg;
|
||||||
// float strength = point.b;
|
float strength = point.b;
|
||||||
|
|
||||||
// float d = distance(vUv, pos);
|
float d = distance(vUv, pos);
|
||||||
// intensity += strength * gauss(d, u_radius);
|
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);
|
vec3 color = vec3(0.0);
|
||||||
// if (normalized < 0.33) {
|
if (normalized < 0.33) {
|
||||||
// color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), 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) {
|
} 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);
|
color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (normalized - 0.33) / 0.33);
|
||||||
// } else {
|
} else {
|
||||||
// color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (normalized - 0.66) / 0.34);
|
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);
|
||||||
// }
|
}
|
||||||
// `}
|
`}
|
||||||
// />
|
/>
|
||||||
// </mesh>
|
</mesh>
|
||||||
|
|
||||||
|
{/* <Html>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "10px",
|
||||||
|
left: "10px",
|
||||||
|
padding: "8px 12px",
|
||||||
|
background: "#333",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
onClick={exportHeatmapAsPNG}
|
||||||
|
>
|
||||||
|
Export Heatmap
|
||||||
|
</button>
|
||||||
|
</Html> */}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 RADIUS = 0.0025;
|
||||||
const OPACITY = 0.8;
|
const OPACITY = 0.8;
|
||||||
const GROWTH_RATE = 20.0;
|
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 }) {
|
interface BakedPoint {
|
||||||
const createPointTexture = (filteredPoints: typeof bakedPoints) => {
|
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;
|
if (filteredPoints.length === 0) return null;
|
||||||
|
|
||||||
const data = new Float32Array(filteredPoints.length * 4);
|
const data = new Float32Array(filteredPoints.length * 4);
|
||||||
@@ -13,82 +36,154 @@ export function exportHeatmapAsPNG({ bakedPoints, gl, width, height, mesh }: { b
|
|||||||
const index = i * 4;
|
const index = i * 4;
|
||||||
data[index] = (p.points.x + width / 2) / width;
|
data[index] = (p.points.x + width / 2) / width;
|
||||||
data[index + 1] = (p.points.y + height / 2) / height;
|
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;
|
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;
|
texture.needsUpdate = true;
|
||||||
return texture;
|
return texture;
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadImage = (base64: string, filename: string) => {
|
// Create heatmap rendering shader material
|
||||||
const link = document.createElement("a");
|
const createHeatmapMaterial = (pointsTexture: DataTexture, count: number): ShaderMaterial => {
|
||||||
link.href = base64;
|
return new ShaderMaterial({
|
||||||
link.download = filename;
|
transparent: true,
|
||||||
document.body.appendChild(link);
|
depthWrite: false,
|
||||||
link.click();
|
blending: AdditiveBlending,
|
||||||
document.body.removeChild(link);
|
side: DoubleSide,
|
||||||
};
|
uniforms: {
|
||||||
|
u_points: { value: pointsTexture },
|
||||||
const renderHeatmapToImage = (type: string) => {
|
u_count: { value: count },
|
||||||
const filteredPoints = bakedPoints.filter((p) => p.type === type);
|
|
||||||
if (filteredPoints.length === 0) return null;
|
|
||||||
|
|
||||||
const pointTexture = createPointTexture(filteredPoints);
|
|
||||||
if (!pointTexture) return null;
|
|
||||||
|
|
||||||
const uniforms = {
|
|
||||||
u_points: { value: pointTexture },
|
|
||||||
u_count: { value: filteredPoints.length },
|
|
||||||
u_radius: { value: RADIUS },
|
u_radius: { value: RADIUS },
|
||||||
u_opacity: { value: OPACITY },
|
u_opacity: { value: OPACITY },
|
||||||
u_growthRate: { value: GROWTH_RATE },
|
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 exportCamera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0.1, 10);
|
/**
|
||||||
|
* 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 pointsTexture = createPointTexture(filteredPoints);
|
||||||
|
if (!pointsTexture) return null;
|
||||||
|
|
||||||
|
const material = createHeatmapMaterial(pointsTexture, filteredPoints.length);
|
||||||
|
|
||||||
|
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.position.set(0, 1, 0);
|
||||||
exportCamera.lookAt(0, 0, 0);
|
exportCamera.lookAt(0, 0, 0);
|
||||||
|
|
||||||
const renderTarget = new THREE.WebGLRenderTarget(1024, 1024, {
|
const renderTarget = new WebGLRenderTarget(1024, 1024, {
|
||||||
format: THREE.RGBAFormat,
|
format: RGBAFormat,
|
||||||
type: THREE.UnsignedByteType,
|
type: UnsignedByteType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tempScene = new THREE.Scene();
|
const tempScene = new Scene();
|
||||||
tempScene.add(mesh);
|
tempScene.add(mesh);
|
||||||
|
|
||||||
|
// Render heatmap
|
||||||
gl.setRenderTarget(renderTarget);
|
gl.setRenderTarget(renderTarget);
|
||||||
gl.render(tempScene, exportCamera);
|
gl.render(tempScene, exportCamera);
|
||||||
gl.setRenderTarget(null);
|
gl.setRenderTarget(null);
|
||||||
|
|
||||||
|
// Read pixels
|
||||||
const pixels = new Uint8Array(1024 * 1024 * 4);
|
const pixels = new Uint8Array(1024 * 1024 * 4);
|
||||||
gl.readRenderTargetPixels(renderTarget, 0, 0, 1024, 1024, pixels);
|
gl.readRenderTargetPixels(renderTarget, 0, 0, 1024, 1024, pixels);
|
||||||
|
|
||||||
|
// Convert to Blob
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = 1024;
|
canvas.width = 1024;
|
||||||
canvas.height = 1024;
|
canvas.height = 1024;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
if (ctx) {
|
if (!ctx) return null;
|
||||||
|
|
||||||
const imageData = ctx.createImageData(1024, 1024);
|
const imageData = ctx.createImageData(1024, 1024);
|
||||||
imageData.data.set(pixels);
|
imageData.data.set(pixels);
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
return canvas.toDataURL("image/png");
|
|
||||||
|
// 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 types = ["human", "vehicle"];
|
||||||
const result = types.map((type) => {
|
return types.map((type) => ({
|
||||||
const image = renderHeatmapToImage(type);
|
type,
|
||||||
if (image) {
|
image: renderHeatmapToImage(type),
|
||||||
downloadImage(image, `${type}-heatmap.png`);
|
}));
|
||||||
}
|
|
||||||
return { type, image };
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Exported Heatmaps:", result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function HeatMap() {
|
|||||||
<>
|
<>
|
||||||
<RealTimeHeatMap />
|
<RealTimeHeatMap />
|
||||||
|
|
||||||
<BakedHeatMap />
|
{/* <BakedHeatMap /> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,34 @@ const RealTimeHeatMap = () => {
|
|||||||
}
|
}
|
||||||
}, [isReset, isPlaying]);
|
}, [isReset, isPlaying]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
addMonitoringVehicle("26770368-55e8-4d40-87f7-8eacb48dc236");
|
// addMonitoringVehicle("26770368-55e8-4d40-87f7-8eacb48dc236");
|
||||||
addMonitoringHuman("264a51e7-d8b9-4093-95ac-fa7e2dc49cfa");
|
// 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(() => {
|
useEffect(() => {
|
||||||
const selectedProductData = getProductById(selectedProduct.productUuid);
|
const selectedProductData = getProductById(selectedProduct.productUuid);
|
||||||
@@ -72,15 +96,9 @@ const RealTimeHeatMap = () => {
|
|||||||
determineExecutionMachineSequences([selectedProductData]).then((sequences) => {
|
determineExecutionMachineSequences([selectedProductData]).then((sequences) => {
|
||||||
sequences.forEach((sequence) => {
|
sequences.forEach((sequence) => {
|
||||||
sequence.forEach((event) => {
|
sequence.forEach((event) => {
|
||||||
if (event.type === "human") {
|
if (event.type === "human" || event.type === "vehicle") {
|
||||||
if (hasHuman(event.modelUuid)) {
|
|
||||||
newEvents.push(event);
|
newEvents.push(event);
|
||||||
}
|
}
|
||||||
} else if (event.type === "vehicle") {
|
|
||||||
if (hasVehicle(event.modelUuid)) {
|
|
||||||
newEvents.push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
setEvents(newEvents);
|
setEvents(newEvents);
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from "react";
|
||||||
import { useSceneContext } from '../../scene/sceneContext';
|
import { useSceneContext } from "../../scene/sceneContext";
|
||||||
import { determineExecutionMachineSequences } from './functions/determineExecutionMachineSequences';
|
import { determineExecutionMachineSequences } from "./functions/determineExecutionMachineSequences";
|
||||||
import { usePlayButtonStore } from '../../../store/ui/usePlayButtonStore';
|
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
|
||||||
import { useSimulationManager } from '../../../store/rough/useSimulationManagerStore';
|
import { useSimulationManager } from "../../../store/rough/useSimulationManagerStore";
|
||||||
import { useParams } from 'react-router-dom';
|
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 {
|
interface SimulationUsageRecord {
|
||||||
activeTime: number;
|
activeTime: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
idleTime: number;
|
idleTime: number;
|
||||||
type:
|
type: "roboticArm" | "vehicle" | "transfer" | "storageUnit" | "crane" | "human" | "machine";
|
||||||
| "roboticArm"
|
|
||||||
| "vehicle"
|
|
||||||
| "transfer"
|
|
||||||
| "storageUnit"
|
|
||||||
| "crane"
|
|
||||||
| "human"
|
|
||||||
| "machine";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Product → holds multiple usage records
|
// Product → holds multiple usage records
|
||||||
@@ -45,15 +45,16 @@ const SimulationHandler = () => {
|
|||||||
const { getProductById, selectedProduct } = productStore();
|
const { getProductById, selectedProduct } = productStore();
|
||||||
const { machines, getMachineById } = machineStore();
|
const { machines, getMachineById } = machineStore();
|
||||||
const { getHumanById } = humanStore();
|
const { getHumanById } = humanStore();
|
||||||
const { getCraneById, } = craneStore();
|
const { getCraneById } = craneStore();
|
||||||
const { getStorageUnitById } = storageUnitStore();
|
const { getStorageUnitById } = storageUnitStore();
|
||||||
const { isPlaying, setIsPlaying } = usePlayButtonStore();
|
const { isPlaying, setIsPlaying } = usePlayButtonStore();
|
||||||
const { simulationData, addData } = useSimulationManager();
|
const { simulationData, addData } = useSimulationManager();
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { selectedVersion } = versionStore();
|
const { selectedVersion } = versionStore();
|
||||||
|
const { bakedPoints } = useHeatMapStore();
|
||||||
|
const height = CONSTANTS.gridConfig.size;
|
||||||
|
const width = CONSTANTS.gridConfig.size;
|
||||||
|
const { gl } = useThree();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let checkTimer: ReturnType<typeof setTimeout>;
|
let checkTimer: ReturnType<typeof setTimeout>;
|
||||||
@@ -65,39 +66,39 @@ const SimulationHandler = () => {
|
|||||||
if (currentProduct) {
|
if (currentProduct) {
|
||||||
const executionSequences = await determineExecutionMachineSequences([currentProduct]);
|
const executionSequences = await determineExecutionMachineSequences([currentProduct]);
|
||||||
if (executionSequences?.length > 0) {
|
if (executionSequences?.length > 0) {
|
||||||
executionSequences.forEach(sequence => {
|
executionSequences.forEach((sequence) => {
|
||||||
sequence.forEach(entity => {
|
sequence.forEach((entity) => {
|
||||||
if (entity.type === 'roboticArm') {
|
if (entity.type === "roboticArm") {
|
||||||
const roboticArm = getArmBotById(entity.modelUuid);
|
const roboticArm = getArmBotById(entity.modelUuid);
|
||||||
if (roboticArm?.isActive) {
|
if (roboticArm?.isActive) {
|
||||||
hasActiveEntity = true;
|
hasActiveEntity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entity.type === 'vehicle') {
|
if (entity.type === "vehicle") {
|
||||||
const vehicle = getVehicleById(entity.modelUuid);
|
const vehicle = getVehicleById(entity.modelUuid);
|
||||||
if (vehicle?.isActive) {
|
if (vehicle?.isActive) {
|
||||||
hasActiveEntity = true;
|
hasActiveEntity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entity.type === 'machine') {
|
if (entity.type === "machine") {
|
||||||
const machine = getMachineById(entity.modelUuid);
|
const machine = getMachineById(entity.modelUuid);
|
||||||
if (machine?.isActive) {
|
if (machine?.isActive) {
|
||||||
hasActiveEntity = true;
|
hasActiveEntity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entity.type === 'human') {
|
if (entity.type === "human") {
|
||||||
const human = getHumanById(entity.modelUuid);
|
const human = getHumanById(entity.modelUuid);
|
||||||
if (human?.isActive) {
|
if (human?.isActive) {
|
||||||
hasActiveEntity = true;
|
hasActiveEntity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entity.type === 'crane') {
|
if (entity.type === "crane") {
|
||||||
const crane = getCraneById(entity.modelUuid);
|
const crane = getCraneById(entity.modelUuid);
|
||||||
if (crane?.isActive) {
|
if (crane?.isActive) {
|
||||||
hasActiveEntity = true;
|
hasActiveEntity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entity.type === 'storageUnit') {
|
if (entity.type === "storageUnit") {
|
||||||
const storageUnit = getStorageUnitById(entity.modelUuid);
|
const storageUnit = getStorageUnitById(entity.modelUuid);
|
||||||
if (storageUnit?.isActive) {
|
if (storageUnit?.isActive) {
|
||||||
hasActiveEntity = true;
|
hasActiveEntity = true;
|
||||||
@@ -114,6 +115,7 @@ const SimulationHandler = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (materials.length === 0 && materialHistory.length >= 0 && !hasActiveEntity) {
|
if (materials.length === 0 && materialHistory.length >= 0 && !hasActiveEntity) {
|
||||||
|
let bakedResult = generateHeatmapOutput({ bakedPoints, gl, width, height, outputType: "url" });
|
||||||
|
|
||||||
if (executionSequences?.length > 0) {
|
if (executionSequences?.length > 0) {
|
||||||
executionSequences.forEach((sequence) => {
|
executionSequences.forEach((sequence) => {
|
||||||
@@ -134,24 +136,14 @@ const SimulationHandler = () => {
|
|||||||
const obj = getter(entity.modelUuid);
|
const obj = getter(entity.modelUuid);
|
||||||
if (!obj) return; // skip if not found
|
if (!obj) return; // skip if not found
|
||||||
|
|
||||||
addData(
|
addData(projectId, selectedVersion?.versionId || "", selectedProduct?.productUuid, {
|
||||||
projectId,
|
|
||||||
selectedVersion?.versionId || "",
|
|
||||||
selectedProduct?.productUuid,
|
|
||||||
{
|
|
||||||
activeTime: obj.activeTime ?? 0,
|
activeTime: obj.activeTime ?? 0,
|
||||||
isActive: obj.isActive ?? false,
|
isActive: obj.isActive ?? false,
|
||||||
idleTime: obj.idleTime ?? 0,
|
idleTime: obj.idleTime ?? 0,
|
||||||
type: entity.type as
|
type: entity.type as "roboticArm" | "vehicle" | "machine" | "human" | "crane" | "storageUnit" | "transfer",
|
||||||
| "roboticArm"
|
});
|
||||||
| "vehicle"
|
|
||||||
| "machine"
|
heatMapImageApi(projectId || "", selectedVersion?.versionId || "", selectedProduct?.productUuid, bakedResult);
|
||||||
| "human"
|
|
||||||
| "crane"
|
|
||||||
| "storageUnit"
|
|
||||||
| "transfer",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -172,6 +164,6 @@ const SimulationHandler = () => {
|
|||||||
}, [materials, materialHistory, selectedVersion, selectedProduct?.productUuid, isPlaying, armBots, vehicles, machines]);
|
}, [materials, materialHistory, selectedVersion, selectedProduct?.productUuid, isPlaying, armBots, vehicles, machines]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SimulationHandler;
|
export default SimulationHandler;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
36
app/src/services/simulation/products/heatMapImageApi.ts
Normal file
36
app/src/services/simulation/products/heatMapImageApi.ts
Normal file
@@ -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 <access_token>",
|
||||||
|
"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");
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user