changed the heatmap limit to use the grain height

This commit is contained in:
csawatzky 2026-04-30 09:42:02 -06:00
parent b1c676987e
commit 874be1d8b7
3 changed files with 489 additions and 525 deletions

View file

@ -79,9 +79,9 @@ export default function BaseMesh(props: BaseMeshProps) {
}; };
return ( return (
<group position={position} rotation={rotation} renderOrder={renderOrder}> <group position={position} rotation={rotation}>
{/* Main surface */} {/* Main surface */}
<mesh geometry={geometry} onClick={onClick}> <mesh geometry={geometry} onClick={onClick} renderOrder={renderOrder}>
{buildMaterial()} {buildMaterial()}
</mesh> </mesh>

View file

@ -1,3 +1,4 @@
import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls"; import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls";
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { Bin } from "models"; import { Bin } from "models";
@ -10,51 +11,54 @@ import { BuildCableData, CableData } from "../Data/BuildCableData";
import { BuildNodeData, NodeData } from "../Data/BuildNodeData"; import { BuildNodeData, NodeData } from "../Data/BuildNodeData";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import GrainCableFill from "../Systems/Inventory/GrainCableFill"; import GrainCableFill from "../Systems/Inventory/GrainCableFill";
import TempHeatMap from "../Systems/Heatmap/TempHeatMap"; //grain heat map import TempHeatMap from "../Systems/Heatmap/TempHeatMap";
import NodePointCloud from "../Systems/Heatmap/NodePointCloud"; //hot spots import NodePointCloud from "../Systems/Heatmap/NodePointCloud";
interface Props { interface Props {
/**
* The bin to generate a 3D model of
*/
bin: Bin bin: Bin
/**
* The scale to apply to the bin dimensions
* ie 100 would make the bin 1:100 scale
* @default 100
*/
scale: number scale: number
/**
* optional: the percent of the bin to fill using a level top, this will be used with manual and lidar controls
*/
fillPercent?: number fillPercent?: number
nodeClick?: (node: NodeData, cable: CableData) => void nodeClick?: (node: NodeData, cable: CableData) => void
/**
* toggles the grain in the bin
*/
showGrain?: boolean showGrain?: boolean
/**
* toggles the heatmap overlay
*/
showHeatmap?: boolean showHeatmap?: boolean
/**
* toggles the hotspots in the bin
*/
showHotspots?: boolean showHotspots?: boolean
} }
export default function Bin3dView(props: Props){ 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 const { bin, scale = 100, fillPercent, nodeClick, showHeatmap, showGrain, showHotspots } = props
// 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, showGrain, showHotspots} = props
const binCenter = useMemo(() => new Vector3(0, 0, 0), []); const binCenter = useMemo(() => new Vector3(0, 0, 0), []);
const cableData = useMemo(() => BuildCableData(bin), [bin]); const cableData = useMemo(() => BuildCableData(bin), [bin]);
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]); const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
const isCableInventory =
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_CABLE;
// For cable inventory, TempHeatMap derives the wavy surface directly from
// the top nodes in nodeData — no scalar needed.
//
// For flat fill percent, we convert the fill percent to a Y coordinate
// using the same volume math as GrainFillFlat and pass it as flatMaxY.
const flatMaxY = useMemo(() => {
if (isCableInventory || fillPercent === undefined) return undefined;
const radius = bin.diameter() / 2;
const sidewallH = bin.sidewallHeight();
const hopperH = bin.hopperHeight() ?? 0;
const cylVolume = Math.PI * radius * radius * sidewallH;
const hopperVol = hopperH > 0 ? (1 / 3) * Math.PI * radius * radius * hopperH : 0;
const filled = (cylVolume + hopperVol) * fillPercent;
const cylinderFillH = hopperH > 0 && filled <= hopperVol
? 0
: Math.max(0, (filled - hopperVol) / (Math.PI * radius * radius));
return -sidewallH / 2 + cylinderFillH;
}, [isCableInventory, fillPercent, bin]);
const grainInventory = () => { const grainInventory = () => {
if(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC || if (isCableInventory) {
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_CABLE){
return ( return (
<GrainCableFill <GrainCableFill
diameter={bin.diameter()} diameter={bin.diameter()}
@ -65,20 +69,22 @@ export default function Bin3dView(props: Props){
grainOpacity={0.3} grainOpacity={0.3}
/> />
) )
}else if (fillPercent){ } else if (fillPercent) {
<GrainFillFlat return (
diameter={bin.diameter()} <GrainFillFlat
sidewallHeight={bin.sidewallHeight()} diameter={bin.diameter()}
hopperHeight={bin.hopperHeight()} sidewallHeight={bin.sidewallHeight()}
fillPercent={fillPercent} hopperHeight={bin.hopperHeight()}
grainOpacity={0.3} fillPercent={fillPercent}
/> grainOpacity={0.3}
/>
)
} }
} }
return ( return (
<Canvas> <Canvas>
<group scale={[1/scale, 1/scale, 1/scale]}> <group scale={[1 / scale, 1 / scale, 1 / scale]}>
<BinShell <BinShell
binBodyColour="#fff" binBodyColour="#fff"
radialSegments={20} radialSegments={20}
@ -92,29 +98,36 @@ export default function Bin3dView(props: Props){
renderOrder={4} renderOrder={4}
/> />
{/* grain - cylinder*/}
{showGrain && grainInventory()} {showGrain && grainInventory()}
{/* cables */}
<BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}
onNodeClick={(node, cable) => {
// console.log(node)
// console.log(cable)
if(nodeClick){
nodeClick(node, cable)
}
}}
renderOrder={1}/>
{showHotspots && <NodePointCloud bin={bin} nodes={nodeData} /> }
{showHeatmap && <TempHeatMap bin={bin} nodes={nodeData}/>}
</group>
{/* lighting */} <BinCables
<ambientLight intensity={0.2} color={"white"}/> cableData={cableData}
<directionalLight intensity={0.2} color={"white"} position={[0,0,5]}/> nodeData={nodeData}
<directionalLight intensity={0.2} color={"white"} position={[0,0,-5]}/> bin={bin}
<directionalLight intensity={0.2} color={"white"} position={[5,0,0]}/> binCenter={binCenter}
<directionalLight intensity={0.2} color={"white"} position={[-5,0,0]}/> onNodeClick={(node, cable) => {
<OrbitCameraControls clampVerticalRotation /> if (nodeClick) nodeClick(node, cable)
</Canvas> }}
renderOrder={1}
/>
{showHotspots && <NodePointCloud bin={bin} nodes={nodeData} />}
{showHeatmap && (
<TempHeatMap
bin={bin}
nodes={nodeData}
flatMaxY={flatMaxY}
/>
)}
</group>
<ambientLight intensity={0.2} color={"white"} />
<directionalLight intensity={0.2} color={"white"} position={[0, 0, 5]} />
<directionalLight intensity={0.2} color={"white"} position={[0, 0, -5]} />
<directionalLight intensity={0.2} color={"white"} position={[5, 0, 0]} />
<directionalLight intensity={0.2} color={"white"} position={[-5, 0, 0]} />
<OrbitCameraControls clampVerticalRotation />
</Canvas>
) )
} }

View file

@ -7,6 +7,11 @@ import React from "react";
interface Props { interface Props {
bin: Bin; bin: Bin;
nodes: NodeData[]; nodes: NodeData[];
/**
* For flat fill percent inventory a scalar Y ceiling for the heatmap top.
* Used when there are no cable top nodes to drive a surface.
*/
flatMaxY?: number;
} }
const RADIAL_RINGS = 20; const RADIAL_RINGS = 20;
@ -21,471 +26,417 @@ const RED_OPACITY = 0.3;
const GREEN_OPACITY = 0.3; const GREEN_OPACITY = 0.3;
const YELLOW_OPACITY = 0.8; const YELLOW_OPACITY = 0.8;
// New tuning knobs
const ANGLE_JITTER = 0.2; const ANGLE_JITTER = 0.2;
const RADIAL_JITTER = 0.05; const RADIAL_JITTER = 0.05;
const LAYER_TWIST = 0.22; const LAYER_TWIST = 0.22;
function tempToHeat(temp:number, upper:number):number { // IDW power for the horizontal surface interpolation —
// matches GrainCableFill's default of 2
const SURFACE_IDW_POWER = 2;
function tempToHeat(temp: number, upper: number): number {
if (temp <= upper) return 0; if (temp <= upper) return 0;
const delta = temp - upper; const delta = temp - upper;
if (delta >= RED_DELTA) return 2; 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; return delta / YELLOW_DELTA;
} }
function heatToRGB(heat:number): number[] { 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];
const GREEN = [0,0.5,0.02]; if (heat <= 0) return GREEN;
const YELLOW = [0.7,0.86,0.0];
const RED = [0.8,0.0,0.01];
if (heat <= 0)
return GREEN;
if (heat <= 1) { if (heat <= 1) {
const t = Math.pow(heat, 0.75);
const t = Math.pow(heat,0.75); return [
GREEN[0] + (YELLOW[0] - GREEN[0]) * t,
return [ GREEN[1] + (YELLOW[1] - GREEN[1]) * t,
GREEN[0] + (YELLOW[0]-GREEN[0])*t, GREEN[2] + (YELLOW[2] - GREEN[2]) * t,
GREEN[1] + (YELLOW[1]-GREEN[1])*t, ];
GREEN[2] + (YELLOW[2]-GREEN[2])*t,
];
} }
const t = Math.pow(heat-1,0.75); const t = Math.pow(heat - 1, 0.75);
return [ return [
YELLOW[0] + (RED[0]-YELLOW[0])*t, YELLOW[0] + (RED[0] - YELLOW[0]) * t,
YELLOW[1] + (RED[1]-YELLOW[1])*t, YELLOW[1] + (RED[1] - YELLOW[1]) * t,
YELLOW[2] + (RED[2]-YELLOW[2])*t, YELLOW[2] + (RED[2] - YELLOW[2]) * t,
]; ];
} }
interface TempAnchor { interface TempAnchor {
x:number; x: number; y: number; z: number;
y:number; celcius: number;
z:number; }
celcius:number;
} interface SurfaceAnchor {
x: number; z: number; y: number;
function idwTemp( }
px:number,
py:number, // 3D IDW for temperature interpolation
pz:number, function idwTemp(
anchors:TempAnchor[], px: number, py: number, pz: number,
power:number anchors: TempAnchor[],
):number { power: number
): number {
let totalWeight=0; let totalWeight = 0;
let weightedSum=0; let weightedSum = 0;
for (const a of anchors){ for (const a of anchors) {
const dx = px - a.x;
const dx=px-a.x; const dy = py - a.y;
const dy=py-a.y; const dz = pz - a.z;
const dz=pz-a.z; const distSq = dx * dx + dy * dy + dz * dz;
if (distSq < 0.001) return a.celcius;
const distSq=dx*dx+dy*dy+dz*dz; const weight = 1 / Math.pow(distSq, power / 2);
totalWeight += weight;
if (distSq < 0.001) weightedSum += a.celcius * weight;
return a.celcius; }
const weight = return totalWeight === 0 ? 0 : weightedSum / totalWeight;
1 / Math.pow(distSq, power/2); }
totalWeight += weight; // 2D IDW for surface height interpolation — matches GrainCableFill exactly
weightedSum += a.celcius * weight; function idwSurfaceY(
} x: number, z: number,
anchors: SurfaceAnchor[],
return totalWeight===0 power: number
? 0 ): number {
: weightedSum/totalWeight; let totalWeight = 0;
} let weightedY = 0;
// deterministic pseudo-random based on indices for (const a of anchors) {
function hashNoise(a:number,b:number,c:number){ const dx = x - a.x;
const x = Math.sin( const dz = z - a.z;
a*127.1 + b*311.7 + c*74.7 const distSq = dx * dx + dz * dz;
) * 43758.5453; if (distSq < 0.001) return a.y;
const weight = 1 / Math.pow(distSq, power / 2);
return (x - Math.floor(x))*2 -1; totalWeight += weight;
} weightedY += a.y * weight;
export default function TempHeatMapGPT({bin,nodes}:Props){ }
const binRadius=bin.diameter()/2; return totalWeight === 0 ? 0 : weightedY / totalWeight;
const sidewallHeight=bin.sidewallHeight(); }
const hopperHeight=bin.hopperHeight() ?? 0;
const upperThreshold=bin.upperTempThreshold(); function hashNoise(a: number, b: number, c: number) {
const x = Math.sin(a * 127.1 + b * 311.7 + c * 74.7) * 43758.5453;
const sidewallBaseY=-sidewallHeight/2; return (x - Math.floor(x)) * 2 - 1;
const hopperTipY=sidewallBaseY-hopperHeight; }
const maxRadiusAtY=(y:number)=>{ export default function TempHeatMap({ bin, nodes, flatMaxY }: Props) {
const binRadius = bin.diameter() / 2;
if(y>=sidewallBaseY) const sidewallHeight = bin.sidewallHeight();
return binRadius; const hopperHeight = bin.hopperHeight() ?? 0;
const upperThreshold = bin.upperTempThreshold();
if(hopperHeight<=0 || y<=hopperTipY)
return 0; const sidewallBaseY = -sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight;
return binRadius*
((y-hopperTipY)/hopperHeight); const maxRadiusAtY = (y: number) => {
}; if (y >= sidewallBaseY) return binRadius;
const anchors=useMemo( if (hopperHeight <= 0 || y <= hopperTipY) return 0;
()=>nodes return binRadius * ((y - hopperTipY) / hopperHeight);
.filter(n=>n.inGrain && !n.excluded) };
.map(n=>({
x:n.position.x, // Temperature anchors — all in-grain nodes
y:n.position.y, const tempAnchors = useMemo<TempAnchor[]>(
z:n.position.z, () =>
celcius:n.celcius, nodes
})), .filter(n => n.inGrain && !n.excluded)
[nodes] .map(n => ({
); x: n.position.x,
y: n.position.y,
const maxGrainY=useMemo(()=>{ z: n.position.z,
let maxY=sidewallBaseY; celcius: n.celcius,
})),
for(const a of anchors) [nodes]
if(a.y>maxY) );
maxY=a.y;
// Surface anchors — top nodes only, Y offset by half spacing
return maxY; // Matches GrainCableFill's anchor computation exactly.
},[anchors,sidewallBaseY]); const surfaceAnchors = useMemo<SurfaceAnchor[]>(
() =>
const geometry=useMemo(()=>{ nodes
.filter(n => n.topNode && n.inGrain && !n.excluded)
if(!anchors.length) .map(n => ({
return null; x: n.position.x,
z: n.position.z,
const pointsPerLayer= y: n.position.y + n.nodeSpacing * 0.5,
1 + RADIAL_RINGS*THETA_SEGMENTS; })),
[nodes]
const totalVerts= );
HEIGHT_STEPS*pointsPerLayer +1;
// Wall clamp Y — average of surface anchor heights.
const positions= // Prevents the surface from piling up at the bin wall where there are no cables.
new Float32Array(totalVerts*3); // Matches GrainCableFill's wallY computation.
const wallY = useMemo(() => {
const colors= if (surfaceAnchors.length === 0) return flatMaxY ?? sidewallBaseY;
new Float32Array(totalVerts*3); return surfaceAnchors.reduce((sum, a) => sum + a.y, 0) / surfaceAnchors.length;
}, [surfaceAnchors, flatMaxY, sidewallBaseY]);
const heats=new Float32Array(totalVerts);
// For each (x, z) position, returns the Y ceiling of the grain surface.
const rawBottomY= // Uses cable surface IDW when top nodes exist, falls back to flatMaxY.
hopperHeight>0 const getSurfaceY = (x: number, z: number): number => {
? hopperTipY if (surfaceAnchors.length > 0) {
: sidewallBaseY; // Outermost ring clamps to wallY — matches GrainCableFill
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
const grainBottomY= return Math.max(sidewallBaseY, Math.min(sidewallHeight / 2, raw));
hopperHeight>0 }
? hopperTipY + return flatMaxY ?? sidewallBaseY;
(maxGrainY-hopperTipY)/HEIGHT_STEPS };
: sidewallBaseY;
// Global max Y — highest point of the surface (used for grid bottom calc)
const grainHeight= const maxGrainY = useMemo(() => {
maxGrainY-grainBottomY; if (surfaceAnchors.length > 0) {
return Math.max(...surfaceAnchors.map(a => a.y), wallY);
if(grainHeight<=0) }
return null; return flatMaxY ?? sidewallBaseY;
}, [surfaceAnchors, wallY, flatMaxY, sidewallBaseY]);
for(let hStep=0; hStep<HEIGHT_STEPS; hStep++){
const geometry = useMemo(() => {
const t=hStep/(HEIGHT_STEPS-1); if (!tempAnchors.length) return null;
const y=grainBottomY+t*grainHeight;
const pointsPerLayer = 1 + RADIAL_RINGS * THETA_SEGMENTS;
const allowedRadius= const totalVerts = HEIGHT_STEPS * pointsPerLayer + 1;
maxRadiusAtY(y)*0.97;
const positions = new Float32Array(totalVerts * 3);
const layerBase= const colors = new Float32Array(totalVerts * 3);
hStep*pointsPerLayer; const heats = new Float32Array(totalVerts);
const centerTemp= const rawBottomY = hopperHeight > 0 ? hopperTipY : sidewallBaseY;
idwTemp( const grainBottomY = hopperHeight > 0
0,y,0, ? hopperTipY + (maxGrainY - hopperTipY) / HEIGHT_STEPS
anchors, : sidewallBaseY;
IDW_POWER const grainHeight = maxGrainY - grainBottomY;
);
if (grainHeight <= 0) return null;
const centerHeat=
tempToHeat(centerTemp,upperThreshold); // Pre-compute the (x, z) position for each (ring, seg) slot so we
// can look up the surface Y ceiling per column
const [cr,cg,cb]= const colX: number[] = new Array(RADIAL_RINGS * THETA_SEGMENTS);
heatToRGB(centerHeat); const colZ: number[] = new Array(RADIAL_RINGS * THETA_SEGMENTS);
const colSurfaceY: number[] = new Array(RADIAL_RINGS * THETA_SEGMENTS);
positions[layerBase*3]=0;
positions[layerBase*3+1]=y; // Use the outermost layer's radius for surface Y lookup (no jitter/twist)
positions[layerBase*3+2]=0; // so the surface shape matches GrainCableFill cleanly
const topLayerAllowedRadius = maxRadiusAtY(maxGrainY) * 0.97;
colors[layerBase*3]=cr;
colors[layerBase*3+1]=cg; for (let ring = 0; ring < RADIAL_RINGS; ring++) {
colors[layerBase*3+2]=cb; const u = (ring + 1) / RADIAL_RINGS;
const rFrac = 1 - Math.pow(1 - u, 2.2);
for(let ring=0; ring<RADIAL_RINGS; ring++){ const r = rFrac * topLayerAllowedRadius;
//biases density towards the walls of the bin for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
const u = (ring+1)/RADIAL_RINGS; const angle = (seg / THETA_SEGMENTS) * Math.PI * 2;
const rFrac = 1 - Math.pow(1-u, 2.2); const x = Math.cos(angle) * r;
const z = Math.sin(angle) * r;
for(let seg=0; seg<THETA_SEGMENTS; seg++){ const ci = ring * THETA_SEGMENTS + seg;
colX[ci] = x;
const noiseA= colZ[ci] = z;
hashNoise(hStep,ring,seg); // Outermost ring uses wallY, inner rings use full IDW surface
colSurfaceY[ci] = ring === RADIAL_RINGS - 1
const noiseR= ? wallY
hashNoise(seg,hStep,ring+11); : getSurfaceY(x, z);
}
const baseRadius= }
rFrac*allowedRadius; // Center column surface Y
const centerSurfaceY = getSurfaceY(0, 0);
const jitterRadius=
baseRadius + for (let hStep = 0; hStep < HEIGHT_STEPS; hStep++) {
noiseR* const t = hStep / (HEIGHT_STEPS - 1);
RADIAL_JITTER*
allowedRadius; const layerBase = hStep * pointsPerLayer;
const allowedRadius = maxRadiusAtY(grainBottomY + t * grainHeight) * 0.97;
const r=Math.max(
0, // Center vertex — Y is lerped from bottom to its column surface ceiling
Math.min( const centerY = grainBottomY + t * (centerSurfaceY - grainBottomY);
jitterRadius,
allowedRadius const centerTemp = idwTemp(0, centerY, 0, tempAnchors, IDW_POWER);
) const centerHeat = tempToHeat(centerTemp, upperThreshold);
); const [cr, cg, cb] = heatToRGB(centerHeat);
const layerPhase= positions[layerBase * 3] = 0;
t*LAYER_TWIST; positions[layerBase * 3 + 1] = centerY;
positions[layerBase * 3 + 2] = 0;
const angle= colors[layerBase * 3] = cr;
(seg/THETA_SEGMENTS) colors[layerBase * 3 + 1] = cg;
*Math.PI*2 colors[layerBase * 3 + 2] = cb;
+ layerPhase heats[layerBase] = centerHeat;
+ noiseA*ANGLE_JITTER;
for (let ring = 0; ring < RADIAL_RINGS; ring++) {
const x=Math.cos(angle)*r; const u = (ring + 1) / RADIAL_RINGS;
const z=Math.sin(angle)*r; const rFrac = 1 - Math.pow(1 - u, 2.2);
const temp=idwTemp( for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
x,y,z, const noiseA = hashNoise(hStep, ring, seg);
anchors, const noiseR = hashNoise(seg, hStep, ring + 11);
IDW_POWER
); const baseRadius = rFrac * allowedRadius;
const jitterRadius = baseRadius + noiseR * RADIAL_JITTER * allowedRadius;
const heat= const r = Math.max(0, Math.min(jitterRadius, allowedRadius));
tempToHeat(temp,upperThreshold);
const layerPhase = t * LAYER_TWIST;
const [vr,vg,vb]= const angle =
heatToRGB(heat); (seg / THETA_SEGMENTS) * Math.PI * 2 +
layerPhase +
const vi= noiseA * ANGLE_JITTER;
layerBase+
1+ const x = Math.cos(angle) * r;
ring*THETA_SEGMENTS+ const z = Math.sin(angle) * r;
seg;
// Per-column surface Y ceiling — this is what gives the wavy top
heats[vi]=heat; const ci = ring * THETA_SEGMENTS + seg;
const surfaceY = colSurfaceY[ci];
positions[vi*3]=x;
positions[vi*3+1]=y; // Lerp this vertex Y from grainBottomY up to its column surface ceiling
positions[vi*3+2]=z; const y = grainBottomY + t * (surfaceY - grainBottomY);
colors[vi*3]=vr; const temp = idwTemp(x, y, z, tempAnchors, IDW_POWER);
colors[vi*3+1]=vg; const heat = tempToHeat(temp, upperThreshold);
colors[vi*3+2]=vb; const [vr, vg, vb] = heatToRGB(heat);
}
} const vi = layerBase + 1 + ring * THETA_SEGMENTS + seg;
}
heats[vi] = heat;
const idx=( positions[vi * 3] = x;
hStep:number, positions[vi * 3 + 1] = y;
ring:number, positions[vi * 3 + 2] = z;
seg:number colors[vi * 3] = vr;
)=>{ colors[vi * 3 + 1] = vg;
colors[vi * 3 + 2] = vb;
if(ring<0) }
return hStep*pointsPerLayer; }
}
const s=
((seg%THETA_SEGMENTS) const idx = (hStep: number, ring: number, seg: number) => {
+THETA_SEGMENTS) if (ring < 0) return hStep * pointsPerLayer;
%THETA_SEGMENTS; const s = ((seg % THETA_SEGMENTS) + THETA_SEGMENTS) % THETA_SEGMENTS;
return hStep * pointsPerLayer + 1 + ring * THETA_SEGMENTS + s;
return hStep*pointsPerLayer+ };
1+
ring*THETA_SEGMENTS+ const green: number[] = [];
s; const yellow: number[] = [];
}; const red: number[] = [];
const green:number[]=[]; function pushTri(a: number, b: number, c: number) {
const yellow:number[]=[]; const avg = (heats[a] + heats[b] + heats[c]) / 3;
const red:number[]=[]; if (avg < 1) green.push(a, b, c);
else if (avg < 2) yellow.push(a, b, c);
function pushTri(a:number,b:number,c:number){ else red.push(a, b, c);
}
const avg=(
heats[a]+heats[b]+heats[c] for (let h = 0; h < HEIGHT_STEPS - 1; h++) {
)/3; for (let ring = 0; ring < RADIAL_RINGS - 1; ring++) {
for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
if(avg<1) const next = (seg + 1) % THETA_SEGMENTS;
green.push(a,b,c); const a = idx(h, ring, seg);
else if(avg<2) const b = idx(h, ring, next);
yellow.push(a,b,c); const e = idx(h + 1, ring, seg);
else const f = idx(h + 1, ring, next);
red.push(a,b,c); pushTri(a, e, b);
} pushTri(e, f, b);
}
for(let h=0; h<HEIGHT_STEPS-1; h++){ }
for(let ring=0; ring<RADIAL_RINGS-1; ring++){ // center fill
for(let seg=0; seg<THETA_SEGMENTS; seg++){ for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
const next = (seg + 1) % THETA_SEGMENTS;
const next=(seg+1)%THETA_SEGMENTS; const center0 = idx(h, -1, 0);
const center1 = idx(h + 1, -1, 0);
const a=idx(h,ring,seg); const a = idx(h, 0, seg);
const b=idx(h,ring,next); const b = idx(h, 0, next);
const e=idx(h+1,ring,seg); const c = idx(h + 1, 0, seg);
const f=idx(h+1,ring,next); const d = idx(h + 1, 0, next);
pushTri(center0, c, a);
// indices.push(a,e,b); pushTri(center0, center1, c);
// indices.push(e,f,b); pushTri(a, c, b);
pushTri(a,e,b); pushTri(b, c, d);
pushTri(e,f,b); }
} }
}
// Hopper tip
//center fill const tipVertexIndex = HEIGHT_STEPS * pointsPerLayer;
for (let seg=0; seg<THETA_SEGMENTS; seg++) { if (hopperHeight > 0) {
const tipTemp = idwTemp(0, rawBottomY, 0, tempAnchors, IDW_POWER);
const next=(seg+1)%THETA_SEGMENTS; const tipHeat = tempToHeat(tipTemp, upperThreshold);
const [tr, tg, tb] = heatToRGB(tipHeat);
const center0 = idx(h,-1,0);
const center1 = idx(h+1,-1,0); positions[tipVertexIndex * 3] = 0;
positions[tipVertexIndex * 3 + 1] = rawBottomY;
const a = idx(h,0,seg); positions[tipVertexIndex * 3 + 2] = 0;
const b = idx(h,0,next); colors[tipVertexIndex * 3] = tr;
colors[tipVertexIndex * 3 + 1] = tg;
const c = idx(h+1,0,seg); colors[tipVertexIndex * 3 + 2] = tb;
const d = idx(h+1,0,next); heats[tipVertexIndex] = tipHeat;
pushTri(center0,c,a); const outerRing = RADIAL_RINGS - 1;
pushTri(center0,center1,c); for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
pushTri(a,c,b); const next = (seg + 1) % THETA_SEGMENTS;
pushTri(b,c,d); pushTri(tipVertexIndex, idx(0, outerRing, next), idx(0, outerRing, seg));
} }
} }
const tipVertexIndex=
HEIGHT_STEPS*pointsPerLayer; function makeGeo(indices: number[]) {
const g = new THREE.BufferGeometry();
if(hopperHeight>0){ g.setAttribute("position", new THREE.BufferAttribute(positions, 3));
g.setAttribute("color", new THREE.BufferAttribute(colors, 3));
const tipTemp=idwTemp( g.setIndex(indices);
0, return g;
rawBottomY, }
0,
anchors, return {
IDW_POWER green: makeGeo(green),
); yellow: makeGeo(yellow),
red: makeGeo(red),
const tipHeat= };
tempToHeat( }, [
tipTemp, tempAnchors,
upperThreshold surfaceAnchors,
); wallY,
maxGrainY,
const [tr,tg,tb]= hopperTipY,
heatToRGB(tipHeat); sidewallBaseY,
binRadius,
positions[tipVertexIndex*3]=0; upperThreshold,
positions[tipVertexIndex*3+1]=rawBottomY; hopperHeight,
positions[tipVertexIndex*3+2]=0; flatMaxY,
]);
colors[tipVertexIndex*3]=tr;
colors[tipVertexIndex*3+1]=tg; if (!geometry) return null;
colors[tipVertexIndex*3+2]=tb;
const outerRing=RADIAL_RINGS-1; return (
<React.Fragment>
for(let seg=0; seg<THETA_SEGMENTS; seg++){ <mesh geometry={geometry.green} renderOrder={1}>
const next=(seg+1)%THETA_SEGMENTS; <meshBasicMaterial
vertexColors
const a=idx(0,outerRing,seg); transparent
const b=idx(0,outerRing,next); opacity={GREEN_OPACITY}
pushTri(tipVertexIndex,b,a) side={THREE.DoubleSide}
} depthWrite={false}
} />
</mesh>
function makeGeo(indices:number[]){
const g=new THREE.BufferGeometry(); <mesh geometry={geometry.yellow} renderOrder={2}>
g.setAttribute( <meshBasicMaterial
'position', vertexColors
new THREE.BufferAttribute(positions,3) transparent
); opacity={YELLOW_OPACITY}
g.setAttribute( side={THREE.DoubleSide}
'color', depthWrite={false}
new THREE.BufferAttribute(colors,3) />
); </mesh>
g.setIndex(indices);
return g; <mesh geometry={geometry.red} renderOrder={3}>
} <meshBasicMaterial
vertexColors
return { transparent
green:makeGeo(green), opacity={RED_OPACITY}
yellow:makeGeo(yellow), side={THREE.DoubleSide}
red:makeGeo(red) depthWrite={false}
}; />
</mesh>
},[ </React.Fragment>
anchors, );
maxGrainY,
hopperTipY,
sidewallBaseY,
binRadius,
upperThreshold,
hopperHeight,
]);
if(!geometry)
return null;
return (
<React.Fragment>
<mesh geometry={geometry.green} renderOrder={1}>
<meshBasicMaterial
vertexColors
transparent
opacity={GREEN_OPACITY}
side={THREE.DoubleSide}
depthWrite={false}
/>
</mesh>
<mesh geometry={geometry.yellow} renderOrder={2}>
<meshBasicMaterial
vertexColors
transparent
opacity={YELLOW_OPACITY}
side={THREE.DoubleSide}
depthWrite={false}
/>
</mesh>
<mesh geometry={geometry.red} renderOrder={3}>
<meshBasicMaterial
vertexColors
transparent
opacity={RED_OPACITY}
side={THREE.DoubleSide}
depthWrite={false}
/>
</mesh>
</React.Fragment>
);
} }