Refactor BakedHeatMap and useHeatMapStore to support typed baked points; enhance heatmap export functionality

This commit is contained in:
2025-09-06 13:01:45 +05:30
parent eaa487e561
commit a7b584e799
4 changed files with 157 additions and 100 deletions

View File

@@ -13,33 +13,39 @@ const BakedHeatMap = () => {
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, scene, camera, size } = useThree(); const { gl } = useThree();
const height = CONSTANTS.gridConfig.size; const height = CONSTANTS.gridConfig.size;
const width = CONSTANTS.gridConfig.size; const width = CONSTANTS.gridConfig.size;
const pointTexture = useMemo(() => { /**
if (bakedPoints.length === 0) return null; * Helper: Create a DataTexture from filtered baked points
*/
const createPointTexture = useCallback(
(filteredPoints: typeof bakedPoints) => {
if (filteredPoints.length === 0) return null;
const data = new Float32Array(bakedPoints.length * 4); const data = new Float32Array(filteredPoints.length * 4);
bakedPoints.forEach((p, i) => { filteredPoints.forEach((p, i) => {
const index = i * 4; const index = i * 4;
data[index] = (p.x + width / 2) / width; data[index] = (p.points.x + width / 2) / width;
data[index + 1] = (p.y + height / 2) / height; data[index + 1] = (p.points.y + height / 2) / height;
data[index + 2] = 0.3; data[index + 2] = 0.3; // heat strength
data[index + 3] = 0.0; data[index + 3] = 0.0; // unused
}); });
const texture = new THREE.DataTexture( const texture = new THREE.DataTexture(
data, data,
bakedPoints.length, filteredPoints.length,
1, 1,
THREE.RGBAFormat, THREE.RGBAFormat,
THREE.FloatType 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: null as THREE.DataTexture | null }, u_points: { value: null as THREE.DataTexture | null },
@@ -49,18 +55,25 @@ const BakedHeatMap = () => {
u_growthRate: { value: GROWTH_RATE }, u_growthRate: { value: GROWTH_RATE },
}); });
useEffect(() => {
uniformsRef.current.u_points.value = pointTexture;
uniformsRef.current.u_count.value = bakedPoints.length;
}, [pointTexture, bakedPoints.length]);
/** /**
* Export the heatmap as PNG * Render the heatmap to a base64 PNG for a specific type (human/vehicle)
*/ */
const exportHeatmapAsPNG = useCallback(() => { const renderHeatmapToImage = useCallback(
if (!meshRef.current) return; (type: string) => {
if (!meshRef.current) return null;
// Create a temporary orthographic camera // Filter points by type first
const filteredPoints = bakedPoints.filter((p) => p.type === type);
if (filteredPoints.length === 0) return null;
// Create texture for these points
const pointTexture = createPointTexture(filteredPoints);
if (!pointTexture) return null;
uniformsRef.current.u_points.value = pointTexture;
uniformsRef.current.u_count.value = filteredPoints.length;
// Temporary top-down orthographic camera
const exportCamera = new THREE.OrthographicCamera( const exportCamera = new THREE.OrthographicCamera(
width / -2, width / -2,
width / 2, width / 2,
@@ -69,19 +82,20 @@ const BakedHeatMap = () => {
0.1, 0.1,
10 10
); );
exportCamera.position.set(0, 1, 0); // top-down view exportCamera.position.set(0, 1, 0);
exportCamera.lookAt(0, 0, 0); exportCamera.lookAt(0, 0, 0);
// Create an offscreen render target // Offscreen render target
const renderTarget = new THREE.WebGLRenderTarget(1024, 1024, { const renderTarget = new THREE.WebGLRenderTarget(1024, 1024, {
format: THREE.RGBAFormat, format: THREE.RGBAFormat,
type: THREE.UnsignedByteType, type: THREE.UnsignedByteType,
}); });
// Render only the heatmap mesh // Temporary scene with only this mesh
const tempScene = new THREE.Scene(); const tempScene = new THREE.Scene();
tempScene.add(meshRef.current); tempScene.add(meshRef.current);
// Render to texture
gl.setRenderTarget(renderTarget); gl.setRenderTarget(renderTarget);
gl.render(tempScene, exportCamera); gl.render(tempScene, exportCamera);
gl.setRenderTarget(null); gl.setRenderTarget(null);
@@ -90,7 +104,7 @@ const BakedHeatMap = () => {
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 pixel data to image // Convert pixels to base64 PNG
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = 1024; canvas.width = 1024;
canvas.height = 1024; canvas.height = 1024;
@@ -100,14 +114,38 @@ const BakedHeatMap = () => {
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");
// Create a download link
const link = document.createElement("a");
link.href = canvas.toDataURL("image/png");
link.download = "heatmap.png";
link.click();
} }
}, [gl, width, height]); return null;
},
[gl, width, height, bakedPoints, createPointTexture]
);
/**
* Export both human and vehicle heatmaps and log them
*/
const exportHeatmapAsPNG = useCallback(() => {
const types = ["human", "vehicle"];
const result = types.map((type) => {
const image = renderHeatmapToImage(type);
return {
type,
image,
};
});
console.log("Exported Heatmaps:", result);
return result;
}, [renderHeatmapToImage]);
// Create default texture for initial rendering
const pointTexture = useMemo(() => createPointTexture(bakedPoints), [bakedPoints, createPointTexture]);
useEffect(() => {
uniformsRef.current.u_points.value = pointTexture;
uniformsRef.current.u_count.value = bakedPoints.length;
}, [pointTexture, bakedPoints.length]);
return ( return (
<> <>
@@ -173,7 +211,7 @@ const BakedHeatMap = () => {
/> />
</mesh> </mesh>
{/* Button to trigger export */} {/* Button to export */}
<Html> <Html>
<button <button
style={{ style={{

View File

@@ -7,7 +7,7 @@ function HeatMap() {
const { bakedPoints, setBakedPoints } = useHeatMapStore(); const { bakedPoints, setBakedPoints } = useHeatMapStore();
useEffect(() => { useEffect(() => {
// console.log("bakedPoints: ", bakedPoints); console.log("bakedPoints: ", bakedPoints);
}, [bakedPoints]); }, [bakedPoints]);
return ( return (

View File

@@ -126,7 +126,10 @@ const RealTimeHeatMap = () => {
})() })()
: model.position; : model.position;
addBakedPoint({ x: pos.x, y: pos.z }); addBakedPoint({
type: event.type,
points: { x: pos.x, y: pos.z },
});
updatedPoints.push({ x: pos.x, y: pos.z, strength: 0.3, lastUpdated: now }); updatedPoints.push({ x: pos.x, y: pos.z, strength: 0.3, lastUpdated: now });
}); });

View File

@@ -6,27 +6,37 @@ interface BakedPoint {
y: number; y: number;
} }
interface BakedPointImage {
type: string;
points: BakedPoint;
}
interface HeatMapState { interface HeatMapState {
monitoringVehicle: string[]; monitoringVehicle: string[];
monitoringHuman: string[]; monitoringHuman: string[];
bakedPoints: BakedPoint[]; bakedPoints: BakedPointImage[];
// Vehicle monitoring
setMonitoringVehicle: (vehiclesUuid: string[]) => void; setMonitoringVehicle: (vehiclesUuid: string[]) => void;
addMonitoringVehicle: (vehicleUuid: string) => void; addMonitoringVehicle: (vehicleUuid: string) => void;
removeMonitoringVehicle: (vehicleUuid: string) => void; removeMonitoringVehicle: (vehicleUuid: string) => void;
clearMonitoringVehicle: () => void; clearMonitoringVehicle: () => void;
// Human monitoring
setMonitoringHuman: (humansUuid: string[]) => void; setMonitoringHuman: (humansUuid: string[]) => void;
addMonitoringHuman: (humanUuid: string) => void; addMonitoringHuman: (humanUuid: string) => void;
removeMonitoringHuman: (humanUuid: string) => void; removeMonitoringHuman: (humanUuid: string) => void;
clearMonitoringHuman: () => void; clearMonitoringHuman: () => void;
// Clear both monitors
clearAllMonitors: () => void; clearAllMonitors: () => void;
setBakedPoints: (points: BakedPoint[]) => void; // Baked points
addBakedPoint: (point: BakedPoint) => void; setBakedPoints: (points: BakedPointImage[]) => void;
addBakedPoint: (point: BakedPointImage) => void;
clearBakedPoints: () => void; clearBakedPoints: () => void;
// Utility checkers
hasVehicle: (vehicleUuid: string) => boolean; hasVehicle: (vehicleUuid: string) => boolean;
hasHuman: (humanUuid: string) => boolean; hasHuman: (humanUuid: string) => boolean;
} }
@@ -37,6 +47,7 @@ export const useHeatMapStore = create<HeatMapState>()(
monitoringHuman: [], monitoringHuman: [],
bakedPoints: [], bakedPoints: [],
// Vehicles
setMonitoringVehicle: (vehiclesUuid) => setMonitoringVehicle: (vehiclesUuid) =>
set((state) => { set((state) => {
state.monitoringVehicle = vehiclesUuid; state.monitoringVehicle = vehiclesUuid;
@@ -59,6 +70,7 @@ export const useHeatMapStore = create<HeatMapState>()(
state.monitoringVehicle = []; state.monitoringVehicle = [];
}), }),
// Humans
setMonitoringHuman: (humansUuid) => setMonitoringHuman: (humansUuid) =>
set((state) => { set((state) => {
state.monitoringHuman = humansUuid; state.monitoringHuman = humansUuid;
@@ -81,17 +93,20 @@ export const useHeatMapStore = create<HeatMapState>()(
state.monitoringHuman = []; state.monitoringHuman = [];
}), }),
// Clear all
clearAllMonitors: () => clearAllMonitors: () =>
set((state) => { set((state) => {
state.monitoringVehicle = []; state.monitoringVehicle = [];
state.monitoringHuman = []; state.monitoringHuman = [];
}), }),
setBakedPoints: (points: BakedPoint[]) => // Baked Points
setBakedPoints: (points) =>
set((state) => { set((state) => {
state.bakedPoints = points; state.bakedPoints = points;
}), }),
addBakedPoint: (point: BakedPoint) =>
addBakedPoint: (point) =>
set((state) => { set((state) => {
state.bakedPoints.push(point); state.bakedPoints.push(point);
}), }),
@@ -101,6 +116,7 @@ export const useHeatMapStore = create<HeatMapState>()(
state.bakedPoints = []; state.bakedPoints = [];
}), }),
// Utility checkers
hasVehicle: (vehicleUuid) => get().monitoringVehicle.includes(vehicleUuid), hasVehicle: (vehicleUuid) => get().monitoringVehicle.includes(vehicleUuid),
hasHuman: (humanUuid) => get().monitoringHuman.includes(humanUuid), hasHuman: (humanUuid) => get().monitoringHuman.includes(humanUuid),
})) }))