diff --git a/app/src/components/HeatMap/HeatMap.tsx b/app/src/components/HeatMap/HeatMap.tsx new file mode 100644 index 0000000..e2c7c74 --- /dev/null +++ b/app/src/components/HeatMap/HeatMap.tsx @@ -0,0 +1,203 @@ +import * as THREE from "three"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useProductContext } from "../../modules/simulation/products/productContext"; +import { useSceneContext } from "../../modules/scene/sceneContext"; +import * as CONSTANTS from "../../types/world/worldConstants"; +import { determineExecutionMachineSequences } from "../../modules/simulation/simulator/functions/determineExecutionMachineSequences"; + +const MAX_POINTS = 100; + +const HeatMap = () => { + const isUsingBBOX = false; + const height = CONSTANTS.gridConfig.size; + const width = CONSTANTS.gridConfig.size; + const radius = 0.005; + const opacity = 0.8; const debugMode: "solid" | "grayscale" | "normal" = "normal"; + const debugModeMap = { solid: 0, grayscale: 1, normal: 2, } as const; + const { productStore } = useSceneContext(); + const { getProductById, products } = productStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); + const { scene } = useThree(); + const [events, setEvents] = useState([]); + const [points, setPoints] = useState<{ x: number; y: number; value: number; }[]>([]); + const materialRef = useRef(null); + const uniformsRef = useRef({ + u_points: { value: null as THREE.DataTexture | null }, + u_count: { value: 0 }, + u_radius: { value: radius }, + u_opacity: { value: opacity }, + u_debugMode: { value: debugModeMap[debugMode] }, + }); + + useEffect(() => { + const selectedProductData = getProductById(selectedProduct.productUuid); + const events: EventsSchema[] = []; + + if (selectedProductData) { + determineExecutionMachineSequences([selectedProductData]).then( + (sequences) => { + sequences.forEach((sequence) => { + sequence.forEach((event) => { + if (event.type === 'human' || event.type === 'vehicle') { + events.push(event); + } + }) + }); + setEvents(events); + } + ); + } + }, [selectedProduct, products]) + + useFrame(() => { + if (!materialRef.current || !scene) return; + + const heatmapPoints: { x: number; y: number; value: number }[] = []; + + events.forEach((event) => { + const model = scene.getObjectByProperty('uuid', event.modelUuid) as THREE.Object3D; + if (!model) return; + + if (isUsingBBOX) { + const box = new THREE.Box3().setFromObject(model); + + const { min, max } = box; + + const corners = [ + new THREE.Vector3(min.x, 0, min.z), + new THREE.Vector3(min.x, 0, max.z), + new THREE.Vector3(max.x, 0, min.z), + new THREE.Vector3(max.x, 0, max.z), + ]; + + corners.forEach((corner) => { + heatmapPoints.push({ + x: corner.x, + y: corner.z, + value: 1.0, + }); + }); + } else { + heatmapPoints.push({ + x: model.position.x, + y: model.position.z, + value: 1.0, + }); + } + }); + + setPoints(heatmapPoints); + }); + + const pointTexture = useMemo(() => { + if (points.length === 0) return null; + const data = new Float32Array(MAX_POINTS * 4); + + points.forEach((p, i) => { + const index = i * 4; + data[index] = (p.x + width / 2) / width; + data[index + 1] = (p.y + height / 2) / height; + data[index + 2] = p.value; + data[index + 3] = 0.0; + }); + + const texture = new THREE.DataTexture(data, MAX_POINTS, 1, THREE.RGBAFormat, THREE.FloatType); + texture.needsUpdate = true; + return texture; + }, [points, width, height]); + + useEffect(() => { + uniformsRef.current.u_radius.value = radius; + uniformsRef.current.u_opacity.value = opacity; + uniformsRef.current.u_debugMode.value = debugModeMap[debugMode]; + }, [radius, opacity, debugMode, debugModeMap]); + + useEffect(() => { + uniformsRef.current.u_points.value = pointTexture; + uniformsRef.current.u_count.value = points.length; + }, [pointTexture, points.length]); + + return ( + + + = u_count) break; + float fi = float(i) + 0.5; + float u = fi / float(MAX_POINTS); + + vec4 point = texture2D(u_points, vec2(u, 0.5)); + vec2 pos = point.rg; + float value = point.b; + + float d = distance(vUv, pos); + intensity += value * gauss(d, u_radius); + } + + // Debug mode 1: grayscale + if (u_debugMode == 1) { + gl_FragColor = vec4(vec3(intensity), 1.0); + return; + } + + // Debug mode 2: heatmap color + vec3 color = vec3(0.0); + if (intensity > 0.0) { + if (intensity < 0.5) { + color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), intensity * 2.0); + } else { + color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (intensity - 0.5) * 2.0); + } + } + + gl_FragColor = vec4(color, intensity * u_opacity); + } + `} + /> + + ); +}; + +export default HeatMap; diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index 4490aeb..80467e4 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -14,6 +14,7 @@ import Trigger from './triggers/trigger'; import useModuleStore from '../../store/useModuleStore'; import SimulationAnalysis from './analysis/simulationAnalysis'; import { useSceneContext } from '../scene/sceneContext'; +import HeatMap from '../../components/HeatMap/HeatMap'; function Simulation() { const { activeModule } = useModuleStore(); @@ -62,6 +63,8 @@ function Simulation() { + + }