Merge remote-tracking branch 'origin/feature/layout-comparison-version' into main-dev

This commit is contained in:
2025-10-17 15:20:46 +05:30
19 changed files with 180 additions and 514 deletions

View File

@@ -1,243 +0,0 @@
import * as THREE from "three";
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 { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
import { useSceneContext } from "../../../modules/scene/sceneContext";
const RADIUS = 0.0025;
const OPACITY = 0.8;
const GROWTH_RATE = 20.0;
const BakedHeatMap = () => {
const { materialStore } = useSceneContext();
const { bakedPoints } = useHeatMapStore();
const materialRef = useRef<THREE.ShaderMaterial>(null);
const meshRef = useRef<THREE.Mesh>(null);
const { gl } = useThree();
const { isPlaying } = usePlayButtonStore();
const height = CONSTANTS.gridConfig.size;
const width = CONSTANTS.gridConfig.size;
const { materialHistory, materials } = materialStore();
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: 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]);
return (
<>
<mesh ref={meshRef} rotation={[Math.PI / 2, 0, 0]} position={[0, 0.025, 0]}>
<planeGeometry args={[width, height]} />
<shaderMaterial
ref={materialRef}
transparent
depthWrite={false}
blending={THREE.AdditiveBlending}
uniforms={uniformsRef.current}
side={THREE.DoubleSide}
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);
}
`}
/>
</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> */}
</>
);
};
export default BakedHeatMap;

View File

@@ -1,14 +1,11 @@
import RealTimeHeatMap from "./realTime/realTimeHeatMap";
import BakedHeatMap from "./baked/bakedHeatMap";
import { useIsComparing } from "../../store/builder/store";
import HeatMapRenderer from "./heatMapRenderer";
function HeatMap() {
return (
<>
<RealTimeHeatMap />
const { isComparing } = useIsComparing();
<BakedHeatMap />
</>
);
return <>{isComparing ? <HeatMapRenderer /> : <RealTimeHeatMap />}</>;
}
export default HeatMap;

View File

@@ -0,0 +1,68 @@
import React, { useMemo } from "react";
import { useSceneContext } from "../../modules/scene/sceneContext";
import { useSimulationManager } from "../../store/rough/useSimulationManagerStore";
import { useSimulationState } from "../../store/simulation/useSimulationStore";
import HeatmapPreview from "./heatmapPreview";
const HeatMapRenderer = () => {
const { versionStore, layout } = useSceneContext();
const { selectedVersion } = versionStore();
const { simulationRecords } = useSimulationManager();
const { mainScene, comparisonScene } = useSimulationState();
const { mainSceneHeatmaps, comparisonSceneHeatmaps } = useMemo(() => {
const getHeatmaps = (scene: any) => {
const heatmaps: Array<{ image: string | Blob; type: string }> = [];
if (!scene) return heatmaps;
simulationRecords.forEach((project) =>
project.versions.forEach((version) =>
version.products.forEach((product) =>
product.simulateData.forEach((simulateDataItem) => {
const isTargetScene =
product.productId === scene.product.productUuid &&
version.versionId === scene.version.versionUuid &&
selectedVersion?.versionId;
if (!isTargetScene) return;
product.heatMaps?.forEach((heatMap) => {
if (heatMap.type !== simulateDataItem.type) 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;
};
return {
mainSceneHeatmaps: getHeatmaps(mainScene),
comparisonSceneHeatmaps: getHeatmaps(comparisonScene),
};
}, [simulationRecords, mainScene, comparisonScene, selectedVersion]);
return (
<>
{layout === "Main Layout" &&
mainSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview key={`main-${idx}`} image={heatMap.image} type={heatMap.type} />
))}
{layout === "Comparison Layout" &&
comparisonSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview key={`comp-${idx}`} image={heatMap.image} type={heatMap.type} />
))}
</>
);
};
export default HeatMapRenderer;

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { TextureLoader, Texture, DoubleSide } from "three";
import * as CONSTANTS from "../../types/world/worldConstants";
import { useHeatmapTypeStore } from "../../store/builder/store";
interface HeatmapPreviewProps {
image: string | Blob | null;
@@ -9,6 +10,7 @@ interface HeatmapPreviewProps {
const HeatmapPreview: React.FC<HeatmapPreviewProps> = ({ image, type }) => {
const [texture, setTexture] = useState<Texture | null>(null);
const { selectedTypes } = useHeatmapTypeStore();
const width = CONSTANTS.gridConfig.size;
const height = CONSTANTS.gridConfig.size;
@@ -64,16 +66,16 @@ const HeatmapPreview: React.FC<HeatmapPreviewProps> = ({ image, type }) => {
return (
<>
{type === "human" && (
{type === "human" && selectedTypes.human && (
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, 0.035, 0]}>
<planeGeometry args={[width, height]} />
<meshStandardMaterial map={texture} transparent side={DoubleSide} />
<meshStandardMaterial map={texture.clone()} transparent side={DoubleSide} />
</mesh>
)}
{type === "vehicle" && (
{type === "vehicle" && selectedTypes.vehicle && (
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, 0.025, 0]}>
<planeGeometry args={[width, height]} />
<meshStandardMaterial map={texture} transparent side={DoubleSide} />
<meshStandardMaterial map={texture.clone()} transparent side={DoubleSide} />
</mesh>
)}
</>

View File

@@ -33,16 +33,8 @@ const RealTimeHeatMap = () => {
const debugModeMap = { solid: 0, grayscale: 1, normal: 2 } as const;
const { productStore } = useSceneContext();
const { getProductById, products, selectedProduct } = productStore();
const {
bakedPoints,
hasHuman,
hasVehicle,
monitoringHuman,
monitoringVehicle,
addMonitoringHuman,
addMonitoringVehicle,
addBakedPoint,
} = useHeatMapStore();
const { monitoringHuman, monitoringVehicle, clearBakedPoints, addBakedPoint } =
useHeatMapStore();
const { isPlaying } = usePlayButtonStore();
const { isReset } = useResetButtonStore();
const { isPaused } = usePauseButtonStore();
@@ -65,13 +57,14 @@ 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) {
clearBakedPoints();
setPoints([]);
lastFrameTime.current = null;
lastUpdateTime.current = 0;
}
}, [isReset, isPlaying]);
// Added human or vehicle
// useEffect(() => {

View File

@@ -3,6 +3,8 @@ import {
useLoadingProgress,
useIsComparing,
useCreateNewWindow,
useLimitDistance,
useRenderDistance,
} from "../../../store/builder/store";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
import { usePlayButtonStore } from "../../../store/ui/usePlayButtonStore";
@@ -17,7 +19,8 @@ import { useParams } from "react-router-dom";
import { validateSimulationDataApi } from "../../../services/simulation/comparison/validateSimulationDataApi";
import { calculateSimulationData } from "./functions/calculateSimulationData";
import NewWindowScene from "../../ui/compareVersion/NewWindowScene";
import Button from "../../ui/compareVersion/Button";
import ComparisonToolbar from "../../ui/compareVersion/ComparisonToolbar";
import { findEnvironment } from "../../../services/factoryBuilder/environment/findEnvironment";
type AssetData = {
activeTime: number;
idleTime: number;
@@ -56,7 +59,7 @@ function ComparisonScene() {
const { productStore, versionStore } = useSceneContext();
const { versionHistory, selectedVersion, setSelectedVersion } = versionStore();
const { products, selectedProduct } = productStore();
const { isComparing } = useIsComparing();
const { isComparing, setIsComparing } = useIsComparing();
const { activeModule } = useModuleStore();
const { mainScene, comparisonScene, setComparisonState } = useSimulationState();
const { loadingProgress } = useLoadingProgress();
@@ -65,8 +68,10 @@ function ComparisonScene() {
const { setCompareProductsData } = useCompareProductDataStore();
const [shouldShowComparisonResult, setShouldShowComparisonResult] = useState(false);
const { addSimulationRecord } = useSimulationManager();
const { createNewWindow } = useCreateNewWindow();
const { createNewWindow, setCreateNewWindow } = useCreateNewWindow();
const { clearComparisonState } = useSimulationState();
const { setRenderDistance } = useRenderDistance();
const { setLimitDistance } = useLimitDistance();
const handleSelectVersion = (option: string) => {
const version = versionHistory.find((version) => version.versionName === option);
if (version) {
@@ -178,6 +183,17 @@ function ComparisonScene() {
simulationRecords,
]);
const handleExit = () => {
setIsComparing(false);
setCreateNewWindow(false);
clearComparisonState();
if (!projectId) return;
findEnvironment(projectId).then((data) => {
if (!data) return;
setRenderDistance(data.renderDistance);
setLimitDistance(data.limitDistance);
});
};
return (
<>
{isComparing && activeModule === "simulation" && selectedProduct && (
@@ -208,9 +224,10 @@ function ComparisonScene() {
zIndex: 10,
}}
>
<Button />
<ComparisonToolbar />
</div>
)}
<CompareLayOut />
{createNewWindow && <NewWindowScene />}

View File

@@ -0,0 +1,29 @@
import { findEnvironment } from "../../../../services/factoryBuilder/environment/findEnvironment";
import {
useCreateNewWindow,
useIsComparing,
useLimitDistance,
useRenderDistance,
} from "../../../../store/builder/store";
import { useSimulationState } from "../../../../store/simulation/useSimulationStore";
export function handleExit(projectId?: string) {
const setIsComparing = useIsComparing.getState().setIsComparing;
const setCreateNewWindow = useCreateNewWindow.getState().setCreateNewWindow;
const setRenderDistance = useRenderDistance.getState().setRenderDistance;
const setLimitDistance = useLimitDistance.getState().setLimitDistance;
const clearComparisonState = useSimulationState.getState().clearComparisonState;
// Reset UI comparison state
setIsComparing(false);
setCreateNewWindow(false);
clearComparisonState();
if (!projectId) return;
// 🔹 Call async part separately (no async/await in this function)
findEnvironment(projectId).then((data) => {
if (!data) return;
setRenderDistance(data.renderDistance);
setLimitDistance(data.limitDistance);
});
}

View File

@@ -217,16 +217,6 @@ export const RenderInNewWindow: React.FC<NewWindowProps> = ({
};
}, []);
useEffect(() => {
const handleResize = () => {
childWindowRef.current?.dispatchEvent(new Event("resize"));
};
childWindowRef.current?.addEventListener("resize", handleResize);
return () => {
childWindowRef.current?.removeEventListener("resize", handleResize);
};
}, []);
useEffect(() => {
const w = childWindowRef.current;
if (w && !w.closed) {

View File

@@ -19,6 +19,7 @@ import useRestStates from "../../../hooks/useResetStates";
import { getVersionHistoryApi } from "../../../services/builder/versionControl/getVersionHistoryApi";
import { validateSimulationDataApi } from "../../../services/simulation/comparison/validateSimulationDataApi";
import { useSimulationManager } from "../../../store/rough/useSimulationManagerStore";
import { handleExit } from "../../layout/scenes/functions/handleExit";
const CompareLayOut = () => {
const { clearComparisonState, comparisonScene, setComparisonState } = useSimulationState();
@@ -46,9 +47,7 @@ const CompareLayOut = () => {
const { limitDistance, setLimitDistance } = useLimitDistance();
const { simulationRecords } = useSimulationManager();
useEffect(() => {
// console.log("simulationRecords: ", simulationRecords);
}, [simulationRecords]);
useEffect(() => {}, [simulationRecords]);
useEffect(() => {
return () => {
resetStates();
@@ -225,7 +224,7 @@ const CompareLayOut = () => {
</Suspense>
</div>
)}
<button onClick={() => handleExit(projectId)}>Exit</button>
{width !== "0px" &&
!selectedVersion?.versionId && ( // Show only if no layout selected
<div className="chooseLayout-wrapper">

View File

@@ -1,35 +1,16 @@
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import {
useCreateNewWindow,
useHeatmapTypeStore,
useIsComparing,
useLimitDistance,
useRenderDistance,
} from "../../../store/builder/store";
import { useSimulationState } from "../../../store/simulation/useSimulationStore";
import { useParams } from "react-router-dom";
import { findEnvironment } from "../../../services/builder/environment/findEnvironment";
import { handleExit } from "../../layout/scenes/functions/handleExit";
const Button = () => {
const { isComparing, setIsComparing } = useIsComparing();
const ComparisonToolbar = () => {
const { isComparing } = useIsComparing();
const { createNewWindow, setCreateNewWindow } = useCreateNewWindow();
const { clearComparisonState } = useSimulationState();
const { projectId } = useParams();
const { setRenderDistance } = useRenderDistance();
const { setLimitDistance } = useLimitDistance();
const { selectedTypes, toggleType } = useHeatmapTypeStore();
const handleExit = () => {
setIsComparing(false);
setCreateNewWindow(false);
clearComparisonState();
if (!projectId) return;
findEnvironment(projectId).then((data) => {
if (!data) return;
setRenderDistance(data.renderDistance);
setLimitDistance(data.limitDistance);
});
};
const { projectId } = useParams();
const handleOpenInNewWindow = () => {
setCreateNewWindow(true);
@@ -46,7 +27,7 @@ const Button = () => {
gap: "10px",
}}
>
{isComparing && <button onClick={handleExit}>Exit</button>}
{isComparing && <button onClick={() => handleExit(projectId)}>Exit</button>}
{isComparing && !createNewWindow && (
<button onClick={handleOpenInNewWindow}>Open in New Window</button>
)}
@@ -57,7 +38,7 @@ const Button = () => {
checked={selectedTypes.vehicle}
onChange={() => toggleType("vehicle")}
/>
Vehicle
<p>Vehicle</p>
</label>
<label>
@@ -66,10 +47,10 @@ const Button = () => {
checked={selectedTypes.human}
onChange={() => toggleType("human")}
/>
Human
<p>Human</p>
</label>
</div>
);
};
export default Button;
export default ComparisonToolbar;

View File

@@ -1,10 +1,10 @@
import { Suspense } from "react";
import { useCreateNewWindow, useLoadingProgress } from "../../../store/builder/store";
import { RenderInNewWindow } from "../../templates/CreateNewWindow";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { useCreateNewWindow, useLoadingProgress } from "../../../store/builder/store";
import Scene from "../../../modules/scene/scene";
import ComparisonResult from "./ComparisonResult";
import Button from "./Button";
import ComparisonToolbar from "./ComparisonToolbar";
import LoadingPage from "../../templates/LoadingPage";
const NewWindowScene = () => {
@@ -22,7 +22,6 @@ const NewWindowScene = () => {
title="3D Viewer"
onClose={() => setCreateNewWindow(false)}
>
{/* Wait a tick to access child window */}
<Scene layout="Comparison Layout" />
{loadingProgress > 0 && (
<LoadingPage
@@ -39,7 +38,7 @@ const NewWindowScene = () => {
zIndex: 1000,
}}
>
<Button />
<ComparisonToolbar />
</div>
{!loadingProgress && <ComparisonResult />}
</RenderInNewWindow>

View File

@@ -1,29 +1,23 @@
import { useEffect, useMemo } from "react";
import { Canvas } from "@react-three/fiber";
import { useParams } from "react-router-dom";
import { KeyboardControls } from "@react-three/drei";
import { useSceneContext } from "./sceneContext";
import useModuleStore from "../../store/ui/useModuleStore";
import { useCreateNewWindow, useLoadingProgress } from "../../store/builder/store";
import { useSocketStore } from "../../store/socket/useSocketStore";
import Builder from "../builder/builder";
import Visualization from "../visualization/visualization";
import Setup from "./setup/setup";
import Simulation from "../simulation/simulation";
import Collaboration from "../collaboration/collaboration";
import useModuleStore from "../../store/ui/useModuleStore";
import { useParams } from "react-router-dom";
import { getUserData } from "../../functions/getUserData";
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,
@@ -40,7 +34,7 @@ export default function Scene({
],
[]
);
const { assetStore, layoutType, versionStore } = useSceneContext();
const { assetStore, layoutType } = useSceneContext();
const { assets } = assetStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
@@ -48,11 +42,7 @@ 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];
@@ -74,83 +64,7 @@ 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 (
<KeyboardControls map={map}>
<Canvas
@@ -180,27 +94,6 @@ export default function Scene({
<Setup />
<Collaboration />
<Builder />
{isComparing && (selectedTypes.vehicle || selectedTypes.human) && (
<>
{layout === "Main Layout" &&
mainSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview
key={`main-${idx}`}
image={heatMap.image}
type={heatMap.type}
/>
))}
{layout === "Comparison Layout" &&
comparisonSceneHeatmaps.map((heatMap, idx) => (
<HeatmapPreview
key={`comp-${idx}`}
image={heatMap.image}
type={heatMap.type}
/>
))}
</>
)}
<Simulation />
<Visualization />
</Canvas>

View File

@@ -15,14 +15,12 @@ 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);
@@ -62,7 +60,7 @@ function Simulation() {
<SimulationAnalysis />
{!isComparing && <HeatMap />}
<HeatMap />
</>
)}
</>

View File

@@ -1,6 +1,6 @@
import { WebGLRenderer } from "three";
import { Buffer } from "buffer";
import { exportHeatmapAsPNG } from "../../../../components/heatMapGenerator/functions/exportHeatmapAsPNG";
import { exportHeatmapAsPNG } from "./exportHeatmapAsPNG";
// Types
interface BakedPoint {
@@ -32,6 +32,7 @@ interface GenerateHeatmapOutputParams {
gl: WebGLRenderer;
width: number;
height: number;
version: string;
outputType?: "url" | "file" | "blob";
download?: boolean;
}
@@ -40,6 +41,7 @@ export async function generateHeatmapOutput({
bakedPoints,
gl,
width,
version,
height,
outputType = "file",
download = false,
@@ -48,7 +50,6 @@ export async function generateHeatmapOutput({
const fileResults = await Promise.all(
bakedResult.map(async (item) => {
console.log("item: ", item);
let imageBlob: Blob;
@@ -62,7 +63,7 @@ export async function generateHeatmapOutput({
imageBlob = item.image;
}
const fileName = `${item.type}-heatmap.png`;
const fileName = `${item.type}-${version}-heatmap.png`;
// Convert Blob -> ArrayBuffer -> Buffer
const arrayBuffer = await imageBlob.arrayBuffer();

View File

@@ -90,7 +90,7 @@ const SimulationHandler = () => {
async function handleSaveSimulationData(data: any) {
try {
await saveSimulationDataApi(data);
console.log("Simulation data saved successfully");
// console.log("Simulation data saved successfully");
setIsPlaying(false);
} catch (err) {
console.error("Failed to save simulation data:", err);
@@ -169,8 +169,10 @@ const SimulationHandler = () => {
gl,
width,
height,
version: selectedVersion.versionId,
download: true,
}).then((bakedResult) => {
console.log("bakedResult: ", bakedResult);
heatMapImageApi(
projectId || "",
selectedVersion?.versionId || "",

View File

@@ -20,6 +20,7 @@ function Simulator() {
const { selectedVersion } = versionStore();
const { setIsComparing } = useIsComparing();
const { addSimulationRecords } = useSimulationManager();
useEffect(() => {
if (!isPlaying || isReset || !selectedProduct.productUuid) return;
@@ -74,11 +75,7 @@ function Simulator() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedProduct, projectId]);
return (
<>
<SimulationHandler />
</>
);
return <SimulationHandler />;
}
export default Simulator;

View File

@@ -487,8 +487,8 @@ interface HeatmapTypeState {
}
export const useHeatmapTypeStore = create<HeatmapTypeState>((set) => ({
selectedTypes: {
vehicle: false,
human: false,
vehicle: true,
human: true,
},
toggleType: (type) =>
set((state) => ({

View File

@@ -7,12 +7,12 @@ interface SimulationUsageRecord {
type: "roboticArm" | "vehicle" | "transfer" | "storageUnit" | "crane" | "human" | "machine";
assetId: string;
}
type image = string | Blob | null;
// Product → holds multiple usage records
interface ProductSimulation {
productId: string;
simulateData: SimulationUsageRecord[];
heatMaps?: { type: string; image: string | Blob | null }[];
heatMaps?: { type: string; image: image }[];
}
// Version → holds multiple products
@@ -29,7 +29,6 @@ interface ProjectSimulation {
interface SimulationManagerStore {
simulationRecords: ProjectSimulation[];
setSimulationRecords: (simulateData: ProjectSimulation[]) => void;
addSimulationRecord: (
projectId: string | undefined,
@@ -42,7 +41,7 @@ interface SimulationManagerStore {
versionId: string,
productId: string,
records: SimulationUsageRecord[],
heatMaps?: { type: string; image: string | Blob | null }[]
heatMaps?: { type: string; image: image }[]
) => void;
resetProductRecords: (
projectId: string | undefined,
@@ -126,12 +125,12 @@ export const useSimulationManager = create<SimulationManagerStore>((set, get) =>
return { simulationRecords: projects };
}),
addSimulationRecords: (
addSimulationRecords: (
projectId: string | undefined,
versionId: string,
productId: string,
records: SimulationUsageRecord[],
heatMaps?: { type: string; image: string | Blob | null }[]
heatMaps?: { type: string; image: image }[]
) =>
set((state) => {
const projects = [...state.simulationRecords];
@@ -199,60 +198,6 @@ export const useSimulationManager = create<SimulationManagerStore>((set, get) =>
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) => {
@@ -277,9 +222,7 @@ export const useSimulationManager = create<SimulationManagerStore>((set, get) =>
return { simulationRecords: projects };
}),
setSimulationRecords: (simulateData) => {
set({ simulationRecords: simulateData });
},
getProjectById: (projectId: string | undefined) => {
return get().simulationRecords.find((p: ProjectSimulation) => p.projectId === projectId);
},