diff --git a/src/bin/3dView/Data/BuildNodeData.ts b/src/bin/3dView/Data/BuildNodeData.ts index 9d7de98..2aed727 100644 --- a/src/bin/3dView/Data/BuildNodeData.ts +++ b/src/bin/3dView/Data/BuildNodeData.ts @@ -46,9 +46,9 @@ export function BuildNodeData(cables: CableData[]): NodeData[] { let t = celcius // for testing if(i===0){ - t = 30 + t = 40 } - if (i===1){ + if (i===5){ t = 0 } diff --git a/src/bin/3dView/Scene/Bin3dView.tsx b/src/bin/3dView/Scene/Bin3dView.tsx index a704007..a9124c7 100644 --- a/src/bin/3dView/Scene/Bin3dView.tsx +++ b/src/bin/3dView/Scene/Bin3dView.tsx @@ -8,11 +8,10 @@ import { Vector3 } from "three"; import { useMemo } from "react"; import { BuildCableData, CableData } from "../Data/BuildCableData"; import { BuildNodeData, NodeData } from "../Data/BuildNodeData"; -import Heatmap from "../Systems/Heatmap/HeatMapAlpha"; import { pond } from "protobuf-ts/pond"; import GrainCableFill from "../Systems/Inventory/GrainCableFill"; -import TempHeatMap from "../Systems/Heatmap/TempHeatMap"; -// import NodePointCloud from "../Systems/Heatmap/NodePointCloud"; +import TempHeatMap from "../Systems/Heatmap/TempHeatMap"; //grain heat map +import NodePointCloud from "../Systems/Heatmap/NodePointCloud"; //hot spots interface Props { /** @@ -31,16 +30,24 @@ interface Props { fillPercent?: number nodeClick?: (node: NodeData, cable: CableData) => void /** - * When true, renders the heatmap instead of the grain fill. + * toggles the grain in the bin + */ + showGrain?: boolean + /** + * toggles the heatmap overlay */ showHeatmap?: boolean + /** + * toggles the hotspots in the bin + */ + showHotspots?: boolean } export default function Bin3dView(props: Props){ //this function will generate a 3D model of a bin based on its settings using multiple meshes, cylinder for the body, cone for the roof // and either a cone for the hopper or circle for flat bottom, it is possible to also use lathe geometry for this as well, // it might even work better because we can control the smoothness easier with it being only one mesh rather than 3 - const {bin, scale = 100, fillPercent, nodeClick, showHeatmap = false} = props + const {bin, scale = 100, fillPercent, nodeClick, showHeatmap, showGrain, showHotspots} = props const binCenter = useMemo(() => new Vector3(0, 0, 0), []); const cableData = useMemo(() => BuildCableData(bin), [bin]); const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]); @@ -86,7 +93,7 @@ export default function Bin3dView(props: Props){ /> {/* grain - cylinder*/} - {/* {!showHeatmap && grainInventory()} */} + {showGrain && grainInventory()} {/* cables */} { @@ -97,8 +104,8 @@ export default function Bin3dView(props: Props){ } }} renderOrder={1}/> - {/* */} - + {showHotspots && } + {showHeatmap && } {/* lighting */} diff --git a/src/bin/3dView/Systems/Cables/CableNode.tsx b/src/bin/3dView/Systems/Cables/CableNode.tsx index 782b40f..080422a 100644 --- a/src/bin/3dView/Systems/Cables/CableNode.tsx +++ b/src/bin/3dView/Systems/Cables/CableNode.tsx @@ -72,18 +72,16 @@ export default function CableNode(props: Props) { color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour() }); - if (node.humidity) { - r.push({ - text: `${node.humidity.toFixed(2)}%`, - color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour() - }); - } - if (node.moisture) { r.push({ text: `${node.moisture.toFixed(2)}% EMC`, color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour() }); + } else if (node.humidity) { + r.push({ + text: `${node.humidity.toFixed(2)}%`, + color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour() + }); } return r; @@ -151,7 +149,7 @@ export default function CableNode(props: Props) { 1 makes only strong deviations pop; <1 makes small deviations pop more. - */ - deviationPower?: number - // (reverted) extra perf knobs removed -} - -/** - * this is a work in progress, the heatmap generated may not be accurate so avoid using this component for now - * @param props - * @returns - */ -export default function Heatmap(props: Props){ - const { - bin, - nodes, - opacity = 0.65, // kept for backward compatibility - pointOpacity, - pointSize = 5, - ySlices = 22, - radialRings = 16, - thetaSegments = 28, - wallInsetFactor = 0.99, - samplesPerCell = 1, - jitter = 0.75, - alphaHash = true, - greenOpacityFactor = 0.18, - deviationPower = 0.6, - } = props; - - useThree(); // keep fiber context available if needed later - - const sidewallHeight = bin.sidewallHeight(); - const hopperHeight = bin.hopperHeight() ?? 0; - const sidewallBaseY = -sidewallHeight / 2; - const hopperTipY = sidewallBaseY - hopperHeight; - - const inGrainNodes = useMemo( - () => nodes.filter((n) => n.inGrain && !n.excluded), - [nodes], - ); - - const maxGrainY = useMemo(() => { - let maxY = -Infinity; - for (const n of inGrainNodes) maxY = Math.max(maxY, n.position.y); - return Number.isFinite(maxY) ? maxY : sidewallBaseY; - }, [inGrainNodes, sidewallBaseY]); - - const topNodes = useMemo( - () => nodes.filter((n) => n.topNode && n.inGrain && !n.excluded), - [nodes], - ); - - const anchors = useMemo( - () => - topNodes.map((n) => ({ - x: n.position.x, - z: n.position.z, - y: n.position.y + n.nodeSpacing * 0.5, - })), - [topNodes], - ); - - const wallY = useMemo(() => { - if (anchors.length === 0) return -sidewallHeight / 2; - return anchors.reduce((sum, a) => sum + a.y, 0) / anchors.length; - }, [anchors, sidewallHeight]); - - const idwHeight = ( - x: number, - z: number, - inputAnchors: { x: number; z: number; y: number }[], - power = 2, - ): number => { - let totalWeight = 0; - let weightedY = 0; - - for (const a of inputAnchors) { - const dx = x - a.x; - const dz = z - a.z; - const distSq = dx * dx + dz * dz; - if (distSq < 0.001) return a.y; - const w = 1 / Math.pow(distSq, power / 2); - totalWeight += w; - weightedY += a.y * w; - } - - return weightedY / totalWeight; - }; - - const maxRadiusAtY = (y: number, maxR: number): number => { - if (y >= sidewallBaseY) return maxR; - if (hopperHeight <= 0 || y <= hopperTipY) return 0; - const t = (y - hopperTipY) / hopperHeight; // 0..1 - return maxR * t; - }; - - const grainSurfaceY = (x: number, z: number, rNorm: number): number => { - // If we don't have top nodes, use a flat surface at maxGrainY. - if (anchors.length === 0) return maxGrainY; - - const rawY = idwHeight(x, z, anchors); - - // Match `GrainCableFill` outer-wall taper so switching isn't jarring. - const edgeStart = 0.8; - const blendT = Math.max(0, (rNorm - edgeStart) / (1 - edgeStart)); - const s = blendT * blendT * (3 - 2 * blendT); - const y = rawY * (1 - s) + wallY * s; - return Math.max(-sidewallHeight / 2, Math.min(sidewallHeight / 2, y)); - }; - - const evaluateTemp = (px: number, py: number, pz: number): number | null => { - if (inGrainNodes.length === 0) return null; - - // Inverse-distance weighted interpolation. - // Keep power modest so the field stays smooth. - const IDW_POWER = 2; - let totalWeight = 0; - let weightedSum = 0; - - for (const n of inGrainNodes) { - const dx = px - n.position.x; - const dy = py - n.position.y; - const dz = pz - n.position.z; - const distSq = dx * dx + dy * dy + dz * dz; - const weight = distSq < 0.001 ? 1e6 : 1 / Math.pow(distSq, IDW_POWER / 2); - totalWeight += weight; - weightedSum += n.celcius * weight; - } - - if (totalWeight === 0) return null; - return weightedSum / totalWeight; - }; - - const tempToHeatColor = (temp: number): Color => { - // Match your 2D/point visuals: green in-threshold, fade to red/blue as distance grows. - const lower = bin.lowerTempThreshold(); - const upper = bin.upperTempThreshold(); - - const GREEN = new Color("#52c41a"); - const BLUE = new Color("#3399ff"); - const RED = new Color("#ff4d4f"); - - if (temp >= lower && temp <= upper) return GREEN; - - const distance = temp < lower ? lower - temp : temp - upper; - const intensity = Math.min(1, distance / colourFade); // 0..1 - - // Similar HSL shaping as `TempToColour`, but always returns a color. - const minimumLightness = 0.3; - const lightnessRange = 0.2; - const minimumSaturation = 0.7; - const saturationRange = 0.8; - - const hsl = { h: 0, s: 1, l: 1 }; - (temp < lower ? BLUE : RED).getHSL(hsl); - - const c = new Color(); - c.setHSL( - hsl.h, - saturationRange * intensity + minimumSaturation, - lightnessRange * intensity + minimumLightness, - ); - return c; - }; - - const tempToDeviation = (temp: number): number => { - const lower = bin.lowerTempThreshold(); - const upper = bin.upperTempThreshold(); - if (temp >= lower && temp <= upper) return 0; - const distance = temp < lower ? lower - temp : temp - upper; - return Math.min(1, distance / colourFade); - }; - - const { positions, colors, deviations } = useMemo(() => { - const binR = bin.diameter() / 2; - // Important: points are rendered as *sprites*, so even if the center is inside the wall, - // the visible circle can extend outside. Shrink the sampling radius by ~half pointSize - // so the rendered splats stay within the bin. - const maxR = Math.max(0, binR * wallInsetFactor - pointSize * 0.55); - - const y0 = hopperHeight > 0 ? hopperTipY : sidewallBaseY; - const y1 = Math.max(y0, maxGrainY); - - const pos: number[] = []; - const col: number[] = []; - const dev: number[] = []; - const tmpColor = new Color(); - - const safeYSlices = Math.max(6, Math.floor(ySlices)); - const safeRings = Math.max(4, Math.floor(radialRings)); - const safeTheta = Math.max(12, Math.floor(thetaSegments)); - const safeSamples = Math.max(1, Math.floor(samplesPerCell)); - const j = Math.min(1, Math.max(0, jitter)); - // Deterministic "random" so the cloud doesn't shimmer every render. - const rand01 = (seed: number) => { - // xorshift32 - let x = seed | 0; - x ^= x << 13; - x ^= x >>> 17; - x ^= x << 5; - // convert to [0,1) - return ((x >>> 0) % 1000000) / 1000000; - }; - - for (let yi = 0; yi < safeYSlices; yi++) { - const ty = safeYSlices === 1 ? 0 : yi / (safeYSlices - 1); - const y = y0 + (y1 - y0) * ty; - - const rAtY = maxRadiusAtY(y, maxR); - if (rAtY <= 0.001) continue; - - for (let ring = 0; ring < safeRings; ring++) { - for (let seg = 0; seg < safeTheta; seg++) { - // Cell bounds in polar space - const ring0 = ring / safeRings; - const ring1 = (ring + 1) / safeRings; - const r0 = Math.sqrt(ring0) * rAtY; - const r1 = Math.sqrt(ring1) * rAtY; - - const theta0 = (seg / safeTheta) * Math.PI * 2; - const theta1 = ((seg + 1) / safeTheta) * Math.PI * 2; - - for (let s = 0; s < safeSamples; s++) { - const seed = yi * 73856093 + ring * 19349663 + seg * 83492791 + s * 2654435761; - const u = rand01(seed); - const v = rand01(seed ^ 0x9e3779b9); - - // Jitter inside the cell - const rr = r0 + (r1 - r0) * (j === 0 ? 0.5 : (0.5 + (u - 0.5) * j)); - const tt = theta0 + (theta1 - theta0) * (j === 0 ? 0.5 : (0.5 + (v - 0.5) * j)); - - const x = Math.cos(tt) * rr; - const z = Math.sin(tt) * rr; - - const rNorm = rAtY <= 0 ? 0 : rr / rAtY; - const surfaceY = grainSurfaceY(x, z, rNorm); - if (y > surfaceY) continue; - - const temp = evaluateTemp(x, y, z); - const d0 = temp == null ? 0 : tempToDeviation(temp); - - pos.push(x, y, z); - const c = temp == null ? tmpColor.set("#52c41a") : tempToHeatColor(temp); - col.push(c.r, c.g, c.b); - dev.push(d0); - } - } - } - } - - return { - positions: new Float32Array(pos), - colors: new Float32Array(col), - deviations: new Float32Array(dev), - }; - }, [ - bin, - wallInsetFactor, - hopperHeight, - hopperTipY, - sidewallBaseY, - maxGrainY, - ySlices, - radialRings, - thetaSegments, - anchors, - wallY, - inGrainNodes, - samplesPerCell, - jitter, - deviationPower, - ]); - - const alphaHashedMaterial = useMemo(() => { - return new ShaderMaterial({ - transparent: !alphaHash, - depthTest: true, - depthWrite: alphaHash, - uniforms: { - uOpacity: { value: pointOpacity ?? opacity }, - uSize: { value: pointSize }, - uMaxRadius: { value: (bin.diameter() / 2) * wallInsetFactor }, - uSidewallBaseY: { value: -bin.sidewallHeight() / 2 }, - uHopperHeight: { value: bin.hopperHeight() ?? 0 }, - uAlphaHash: { value: alphaHash ? 1 : 0 }, - uGreenOpacityFactor: { value: Math.min(1, Math.max(0, greenOpacityFactor)) }, - uDeviationPower: { value: Math.max(0.05, deviationPower) }, - }, - vertexShader: ` - uniform float uSize; - varying vec3 vWorldPos; - varying vec3 vColor; - varying float vDev; - attribute vec3 color; - attribute float deviation; - void main() { - vColor = color; - vDev = deviation; - vec4 world = modelMatrix * vec4(position, 1.0); - vWorldPos = world.xyz; - vec4 mvPosition = viewMatrix * world; - float attn = 300.0 / max(1.0, -mvPosition.z); - gl_PointSize = uSize * attn; - gl_Position = projectionMatrix * mvPosition; - } - `, - fragmentShader: ` - precision highp float; - uniform float uOpacity; - uniform float uMaxRadius; - uniform float uSidewallBaseY; - uniform float uHopperHeight; - uniform float uAlphaHash; - uniform float uGreenOpacityFactor; - uniform float uDeviationPower; - varying vec3 vColor; - varying float vDev; - varying vec3 vWorldPos; - // interleaved gradient noise - float ign(vec2 p) { - return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); - } - void main() { - // Hard clip pixels to bin radius at this Y (prevents splats outside wall). - float y = vWorldPos.y; - float sidewallBaseY = uSidewallBaseY; - float hopperHeight = uHopperHeight; - float hopperTipY = sidewallBaseY - hopperHeight; - float maxR; - if (y >= sidewallBaseY) { - maxR = uMaxRadius; - } else if (hopperHeight <= 0.0 || y <= hopperTipY) { - maxR = 0.0; - } else { - float t = (y - hopperTipY) / hopperHeight; - maxR = uMaxRadius * t; - } - float r = length(vWorldPos.xz); - if (r > maxR) discard; - - vec2 p = gl_PointCoord - vec2(0.5); - float d = length(p) * 2.0; - float mask = smoothstep(1.0, 0.0, d); - - float dev = clamp(vDev, 0.0, 1.0); - float devCurve = pow(dev, uDeviationPower); - // 0 => green/in-threshold, 1 => strong deviation - float localOpacityFactor = mix(uGreenOpacityFactor, 1.0, devCurve); - float a = clamp(mask * uOpacity * localOpacityFactor, 0.0, 1.0); - - if (uAlphaHash > 0.5) { - float n = ign(gl_FragCoord.xy); - if (n > a) discard; - gl_FragColor = vec4(vColor, 1.0); - } else { - gl_FragColor = vec4(vColor, a); - } - } - `, - }); - }, [alphaHash, bin, deviationPower, greenOpacityFactor, opacity, pointOpacity, pointSize, wallInsetFactor]); - - // Fallback: normal points (no OIT) - return ( - - - - - - - - ); -} \ No newline at end of file diff --git a/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx b/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx index cbb9d0d..a2ab743 100644 --- a/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx +++ b/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx @@ -2,405 +2,490 @@ import { useMemo } from "react"; import * as THREE from "three"; import { Bin } from "models"; import { NodeData } from "../../Data/BuildNodeData"; - +import React from "react"; + interface Props { bin: Bin; nodes: NodeData[]; } - -// ----------------------------------------------------------------------- -// 🎛️ TUNING KNOBS -// ----------------------------------------------------------------------- - -// Grid resolution — more = smoother but heavier -const RADIAL_RINGS = 12; // rings of sample points from center outward -const THETA_SEGMENTS = 24; // points around each ring -const HEIGHT_STEPS = 20; // vertical layers - -// Colour thresholds — degrees °C above the bin's upper threshold -const YELLOW_DELTA = 5; // at this far above threshold → full yellow -const RED_DELTA = 10; // at this far above threshold → full red - -// IDW power — higher = sharper transitions between nodes (2 is standard) -const IDW_POWER = 2; - -// Mesh appearance -const OPACITY = 0.55; - -// ----------------------------------------------------------------------- -// COLOUR HELPERS -// ----------------------------------------------------------------------- - -// Returns 0 (green) → 1 (yellow) → 2 (red) based on how far above -// the upper threshold the interpolated temperature is. -// Everything at or below upper threshold = 0. -function tempToHeat(temp: number, upper: number): number { + +const RADIAL_RINGS = 20; +const THETA_SEGMENTS = 40; +const HEIGHT_STEPS = 28; + +const YELLOW_DELTA = 5; +const RED_DELTA = 10; + +const IDW_POWER = 4; +const RED_OPACITY = 0.3; +const GREEN_OPACITY = 0.3; +const YELLOW_OPACITY = 0.8; + +// New tuning knobs +const ANGLE_JITTER = 0.2; +const RADIAL_JITTER = 0.05; +const LAYER_TWIST = 0.22; + +function tempToHeat(temp:number, upper:number):number { if (temp <= upper) return 0; + const delta = temp - upper; + if (delta >= RED_DELTA) return 2; - if (delta >= YELLOW_DELTA) return 1 + (delta - YELLOW_DELTA) / (RED_DELTA - YELLOW_DELTA); + + if (delta >= YELLOW_DELTA) { + return 1 + + (delta - YELLOW_DELTA) / + (RED_DELTA - YELLOW_DELTA); + } + return delta / YELLOW_DELTA; } - -// Maps heat value [0–2] to RGB. -// 0 = green (#52c41a) -// 1 = yellow (#fadb14) -// 2 = red (#ff4d4f) -function heatToRGB(heat: number): [number, number, number] { - if (heat <= 0) return [0.322, 0.761, 0.102]; // green - + +function heatToRGB(heat:number): number[] { + + const GREEN = [0,0.5,0.02]; + const YELLOW = [0.7,0.86,0.0]; + const RED = [0.8,0.0,0.01]; + + if (heat <= 0) + return GREEN; + if (heat <= 1) { - // green → yellow - const t = heat; - return [ - 0.322 + (0.980 - 0.322) * t, // R - 0.761 + (0.859 - 0.761) * t, // G - 0.102 + (0.078 - 0.102) * t, // B - ]; + + const t = Math.pow(heat,0.75); + + return [ + GREEN[0] + (YELLOW[0]-GREEN[0])*t, + GREEN[1] + (YELLOW[1]-GREEN[1])*t, + GREEN[2] + (YELLOW[2]-GREEN[2])*t, + ]; } - - // yellow → red - const t = heat - 1; + + const t = Math.pow(heat-1,0.75); + return [ - 0.980 + (1.000 - 0.980) * t, // R - 0.859 + (0.302 - 0.859) * t, // G - 0.078 + (0.310 - 0.078) * t, // B + YELLOW[0] + (RED[0]-YELLOW[0])*t, + YELLOW[1] + (RED[1]-YELLOW[1])*t, + YELLOW[2] + (RED[2]-YELLOW[2])*t, ]; -} - -// ----------------------------------------------------------------------- -// IDW TEMPERATURE INTERPOLATION -// ----------------------------------------------------------------------- - + } + interface TempAnchor { - x: number; y: number; z: number; - celcius: number; -} - -function idwTemp( - px: number, py: number, pz: number, - anchors: TempAnchor[], - power: number -): number { - let totalWeight = 0; - let weightedSum = 0; - - for (const a of anchors) { - const dx = px - a.x; - const dy = py - a.y; - const dz = pz - a.z; - const distSq = dx * dx + dy * dy + dz * dz; - - if (distSq < 0.001) return a.celcius; // exactly on a node - - const weight = 1 / Math.pow(distSq, power / 2); - totalWeight += weight; - weightedSum += a.celcius * weight; + x:number; + y:number; + z:number; + celcius:number; + } + + function idwTemp( + px:number, + py:number, + pz:number, + anchors:TempAnchor[], + power:number + ):number { + + let totalWeight=0; + let weightedSum=0; + + for (const a of anchors){ + + const dx=px-a.x; + const dy=py-a.y; + const dz=pz-a.z; + + const distSq=dx*dx+dy*dy+dz*dz; + + if (distSq < 0.001) + return a.celcius; + + const weight = + 1 / Math.pow(distSq, power/2); + + totalWeight += weight; + weightedSum += a.celcius * weight; } + + return totalWeight===0 + ? 0 + : weightedSum/totalWeight; + } - return totalWeight === 0 ? 0 : weightedSum / totalWeight; -} + // deterministic pseudo-random based on indices + function hashNoise(a:number,b:number,c:number){ + const x = Math.sin( + a*127.1 + b*311.7 + c*74.7 + ) * 43758.5453; -// ----------------------------------------------------------------------- -// COMPONENT -// ----------------------------------------------------------------------- - -export default function TempHeatMap(props: Props) { - const { bin, nodes } = props; - - const binRadius = bin.diameter() / 2; - const sidewallHeight = bin.sidewallHeight(); - const hopperHeight = bin.hopperHeight() ?? 0; - const upperThreshold = bin.upperTempThreshold(); - const sidewallBaseY = -sidewallHeight / 2; - const hopperTipY = sidewallBaseY - hopperHeight; - - // Taper radius inside the hopper cone - const maxRadiusAtY = (y: number): number => { - if (y >= sidewallBaseY) return binRadius; - if (hopperHeight <= 0 || y <= hopperTipY) return 0; - return binRadius * ((y - hopperTipY) / hopperHeight); + return (x - Math.floor(x))*2 -1; + } + export default function TempHeatMapGPT({bin,nodes}:Props){ + + const binRadius=bin.diameter()/2; + const sidewallHeight=bin.sidewallHeight(); + const hopperHeight=bin.hopperHeight() ?? 0; + const upperThreshold=bin.upperTempThreshold(); + + const sidewallBaseY=-sidewallHeight/2; + const hopperTipY=sidewallBaseY-hopperHeight; + + const maxRadiusAtY=(y:number)=>{ + + if(y>=sidewallBaseY) + return binRadius; + + if(hopperHeight<=0 || y<=hopperTipY) + return 0; + + return binRadius* + ((y-hopperTipY)/hopperHeight); }; - - // Only use in-grain, non-excluded nodes as temperature anchors - const anchors = useMemo(() => - nodes - .filter(n => n.inGrain && !n.excluded) - .map(n => ({ - x: n.position.x, - y: n.position.y, - z: n.position.z, - celcius: n.celcius, - })), + const anchors=useMemo( + ()=>nodes + .filter(n=>n.inGrain && !n.excluded) + .map(n=>({ + x:n.position.x, + y:n.position.y, + z:n.position.z, + celcius:n.celcius, + })), [nodes] - ); - - // Top of grain — highest in-grain node Y - const maxGrainY = useMemo(() => { - let maxY = sidewallBaseY; - for (const a of anchors) { - if (a.y > maxY) maxY = a.y; - } - return maxY; - }, [anchors, sidewallBaseY]); - - // ----------------------------------------------------------------------- - // BUILD GEOMETRY - // ----------------------------------------------------------------------- - const geometry = useMemo(() => { - if (anchors.length === 0) return null; - - // ------------------------------------------------------------------- - // 1. Sample the cylindrical grid - // ------------------------------------------------------------------- - // Layout: center column + RADIAL_RINGS rings, each with THETA_SEGMENTS - // vertices, stacked HEIGHT_STEPS times vertically. - // - // Vertex index scheme: - // layer * pointsPerLayer + ringOffset - // where ringOffset: 0 = center, 1..N = ring vertices - - const pointsPerLayer = 1 + RADIAL_RINGS * THETA_SEGMENTS; - // +1 for the optional hopper tip vertex (unused for flat-bottom bins) - const totalVerts = HEIGHT_STEPS * pointsPerLayer + 1; - - const positions = new Float32Array(totalVerts * 3); - const colors = new Float32Array(totalVerts * 3); - - // Grain bottom Y — bottom of the grain, either hopper tip or sidewall base - // For hopper bins, starting exactly at hopperTipY causes the entire - // bottom layer to collapse to radius=0 (degenerate triangles that - // disappear when viewed from below). Instead start one HEIGHT_STEPS - // increment above the tip so the bottom layer always has a visible - // radius, then add a separate tip vertex that fans down to a point. - const rawBottomY = hopperHeight > 0 ? hopperTipY : sidewallBaseY; - const grainBottomY = hopperHeight > 0 - ? hopperTipY + (maxGrainY - hopperTipY) / HEIGHT_STEPS + ); + + const maxGrainY=useMemo(()=>{ + let maxY=sidewallBaseY; + + for(const a of anchors) + if(a.y>maxY) + maxY=a.y; + + return maxY; + },[anchors,sidewallBaseY]); + + const geometry=useMemo(()=>{ + + if(!anchors.length) + return null; + + const pointsPerLayer= + 1 + RADIAL_RINGS*THETA_SEGMENTS; + + const totalVerts= + HEIGHT_STEPS*pointsPerLayer +1; + + const positions= + new Float32Array(totalVerts*3); + + const colors= + new Float32Array(totalVerts*3); + + const heats=new Float32Array(totalVerts); + + const rawBottomY= + hopperHeight>0 + ? hopperTipY + : sidewallBaseY; + + const grainBottomY= + hopperHeight>0 + ? hopperTipY + + (maxGrainY-hopperTipY)/HEIGHT_STEPS : sidewallBaseY; - const grainHeight = maxGrainY - grainBottomY; + + const grainHeight= + maxGrainY-grainBottomY; + + if(grainHeight<=0) + return null; + + for(let hStep=0; hStep{ + + if(ring<0) + return hStep*pointsPerLayer; + + const s= + ((seg%THETA_SEGMENTS) + +THETA_SEGMENTS) + %THETA_SEGMENTS; + + return hStep*pointsPerLayer+ + 1+ + ring*THETA_SEGMENTS+ + s; + }; + + const green:number[]=[]; + const yellow:number[]=[]; + const red:number[]=[]; + + function pushTri(a:number,b:number,c:number){ + + const avg=( + heats[a]+heats[b]+heats[c] + )/3; + + if(avg<1) + green.push(a,b,c); + else if(avg<2) + yellow.push(a,b,c); + else + red.push(a,b,c); + } + + for(let h=0; h { - // ring -1 = center vertex - if (ring < 0) return hStep * pointsPerLayer; - const s = ((seg % THETA_SEGMENTS) + THETA_SEGMENTS) % THETA_SEGMENTS; - return hStep * pointsPerLayer + 1 + ring * THETA_SEGMENTS + s; - }; - - // Side walls — connect each layer to the next - for (let h = 0; h < HEIGHT_STEPS - 1; h++) { - // Center → first ring quads (actually triangles since one side is a point) - for (let seg = 0; seg < THETA_SEGMENTS; seg++) { - const next = (seg + 1) % THETA_SEGMENTS; - // tri: center(h), ring0(h,seg), ring0(h,next) - indices.push(idx(h, -1, 0), idx(h, 0, seg), idx(h, 0, next)); - // tri: center(h+1), ring0(h+1,next), ring0(h+1,seg) - indices.push(idx(h + 1, -1, 0), idx(h + 1, 0, next), idx(h + 1, 0, seg)); - // quad connecting the two center fans - indices.push( - idx(h, -1, 0), idx(h + 1, -1, 0), idx(h, 0, seg), - ); - indices.push( - idx(h + 1, -1, 0), idx(h + 1, 0, seg), idx(h, 0, seg), - ); - } - - // Ring-to-ring quads - for (let ring = 0; ring < RADIAL_RINGS - 1; ring++) { - for (let seg = 0; seg < THETA_SEGMENTS; seg++) { - const next = (seg + 1) % THETA_SEGMENTS; - - // quad between ring and ring+1 at layer h - const a = idx(h, ring, seg); - const b = idx(h, ring, next); - const c = idx(h, ring + 1, seg); - const d = idx(h, ring + 1, next); - - // quad between ring and ring+1 at layer h+1 - const e = idx(h + 1, ring, seg); - const f = idx(h + 1, ring, next); - const g = idx(h + 1, ring + 1, seg); - const hh = idx(h + 1, ring + 1, next); - - // side face (h → h+1 for this quad) - indices.push(a, e, b); - indices.push(e, f, b); - - // inner ring cap face at layer h - indices.push(a, b, c); - indices.push(b, d, c); - - // inner ring cap face at layer h+1 - indices.push(e, g, f); - indices.push(f, g, hh); - } - } - - // Outermost ring side faces - const outerRing = RADIAL_RINGS - 1; - for (let seg = 0; seg < THETA_SEGMENTS; seg++) { - const next = (seg + 1) % THETA_SEGMENTS; - const a = idx(h, outerRing, seg); - const b = idx(h, outerRing, next); - const c = idx(h + 1, outerRing, seg); - const d = idx(h + 1, outerRing, next); - indices.push(a, c, b); - indices.push(b, c, d); - } - } - - // Bottom cap — fan from center to outermost ring - const hBottom = 0; - for (let seg = 0; seg < THETA_SEGMENTS; seg++) { - const next = (seg + 1) % THETA_SEGMENTS; - indices.push( - idx(hBottom, -1, 0), - idx(hBottom, RADIAL_RINGS - 1, next), - idx(hBottom, RADIAL_RINGS - 1, seg), - ); - } - - // Top cap - const hTop = HEIGHT_STEPS - 1; - for (let seg = 0; seg < THETA_SEGMENTS; seg++) { - const next = (seg + 1) % THETA_SEGMENTS; - indices.push( - idx(hTop, -1, 0), - idx(hTop, RADIAL_RINGS - 1, seg), - idx(hTop, RADIAL_RINGS - 1, next), - ); - } - - // ------------------------------------------------------------------- - // 2b. Hopper tip vertex + fan (only for hopper bins) - // ------------------------------------------------------------------- - // The tip vertex sits at the very last slot in the buffer. - const tipVertexIndex = HEIGHT_STEPS * pointsPerLayer; - - if (hopperHeight > 0) { - const tipTemp = idwTemp(0, rawBottomY, 0, anchors, IDW_POWER); - const tipHeat = tempToHeat(tipTemp, upperThreshold); - const [tr, tg, tb] = heatToRGB(tipHeat); - - positions[tipVertexIndex * 3 + 0] = 0; - positions[tipVertexIndex * 3 + 1] = rawBottomY; // hopperTipY - positions[tipVertexIndex * 3 + 2] = 0; - colors[tipVertexIndex * 3 + 0] = tr; - colors[tipVertexIndex * 3 + 1] = tg; - colors[tipVertexIndex * 3 + 2] = tb; - - // Fan from bottom layer's outermost ring down to the tip point. - // This fills the gap between grainBottomY and hopperTipY. - const hBottom = 0; - const outerRing = RADIAL_RINGS - 1; - for (let seg = 0; seg < THETA_SEGMENTS; seg++) { - const next = (seg + 1) % THETA_SEGMENTS; - const a = idx(hBottom, outerRing, seg); - const b = idx(hBottom, outerRing, next); - // Wind so the face is visible from below (tip → b → a) - indices.push(tipVertexIndex, b, a); - } - - // Also fan the bottom layer rings down to the tip for the - // interior of the hopper cone - for (let ring = 0; ring < RADIAL_RINGS - 1; ring++) { - for (let seg = 0; seg < THETA_SEGMENTS; seg++) { - const next = (seg + 1) % THETA_SEGMENTS; - const a = idx(hBottom, ring, seg); - const b = idx(hBottom, ring, next); - indices.push(tipVertexIndex, b, a); - } - } - // Center to tip - indices.push(tipVertexIndex, idx(hBottom, -1, 0), idx(hBottom, 0, 0)); - } - - // ------------------------------------------------------------------- - // 3. Assemble BufferGeometry - // ------------------------------------------------------------------- - const geo = new THREE.BufferGeometry(); - geo.setAttribute("position", new THREE.BufferAttribute(positions, 3)); - geo.setAttribute("color", new THREE.BufferAttribute(colors, 3)); - geo.setIndex(indices); - return geo; - }, [anchors, maxGrainY, hopperTipY, sidewallBaseY, binRadius, upperThreshold, hopperHeight]); - - if (!geometry) return null; - - // meshBasicMaterial is used intentionally here instead of meshStandardMaterial: - // - No lighting/normal calculations means face winding direction does not affect - // visibility, so the mesh looks identical from all camera angles including - // below and inside the volume. - // - vertexColors drives all colour — lighting would wash out the green/yellow/red - // gradient anyway depending on light angle. - return ( - - - + // indices.push(a,e,b); + // indices.push(e,f,b); + pushTri(a,e,b); + pushTri(e,f,b); + } + } + + //center fill + for (let seg=0; seg0){ + + const tipTemp=idwTemp( + 0, + rawBottomY, + 0, + anchors, + IDW_POWER + ); + + const tipHeat= + tempToHeat( + tipTemp, + upperThreshold + ); + + const [tr,tg,tb]= + heatToRGB(tipHeat); + + positions[tipVertexIndex*3]=0; + positions[tipVertexIndex*3+1]=rawBottomY; + positions[tipVertexIndex*3+2]=0; + + colors[tipVertexIndex*3]=tr; + colors[tipVertexIndex*3+1]=tg; + colors[tipVertexIndex*3+2]=tb; + const outerRing=RADIAL_RINGS-1; + + for(let seg=0; seg + + + + + + + + + + + + + ); + +} \ No newline at end of file diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 78e6c38..7531ab0 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -22,6 +22,9 @@ import { AccordionDetails, Typography, TextField, + RadioGroup, + Checkbox, + FormControlLabel, } from "@mui/material"; import BinActions from "bin/BinActions"; import BinHistory from "bin/BinHistory"; @@ -212,6 +215,11 @@ export default function Bin(props: Props) { const [binPresets, setBinPresets] = useState([]); const [missedReadings, setMissedReadings] = useState(0); + //3d bin variables/toggle + const [showGrain, setShowGrain] = useState(false) + const [showHotspots, setShowHotspots] = useState(false) + const [showHeatmap, setShowHeatmap] = useState(false) + const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); }; @@ -816,7 +824,43 @@ export default function Bin(props: Props) { setFillPercent(+e.target.value) }} /> - + { + setShowGrain(checked); + }} + /> + } + label={"Inventory Toggle"} + /> + { + setShowHotspots(checked); + }} + /> + } + label={"Hot Spots"} + /> + { + setShowHeatmap(checked); + }} + /> + } + label={"Heatmap"} + /> +