Refactor BakedHeatMap and useHeatMapStore to support typed baked points; enhance heatmap export functionality
This commit is contained in:
@@ -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={{
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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),
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user