changed the heatmap limit to use the grain height
This commit is contained in:
parent
b1c676987e
commit
874be1d8b7
3 changed files with 489 additions and 525 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,53 +11,56 @@ 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()}
|
||||||
nodes={nodeData}
|
nodes={nodeData}
|
||||||
sidewallHeight={bin.sidewallHeight()}
|
sidewallHeight={bin.sidewallHeight()}
|
||||||
|
|
@ -65,25 +69,27 @@ 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}
|
||||||
binMetalness={0.5}
|
binMetalness={0.5}
|
||||||
binRoughness={0.7}
|
binRoughness={0.7}
|
||||||
binOpacity={0.2}
|
binOpacity={0.2}
|
||||||
diameter={bin.diameter()}
|
diameter={bin.diameter()}
|
||||||
sidewallHeight={bin.sidewallHeight()}
|
sidewallHeight={bin.sidewallHeight()}
|
||||||
|
|
@ -91,30 +97,37 @@ export default function Bin3dView(props: Props){
|
||||||
hopperHeight={bin.hopperHeight()}
|
hopperHeight={bin.hopperHeight()}
|
||||||
renderOrder={4}
|
renderOrder={4}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* grain - cylinder*/}
|
|
||||||
{showGrain && grainInventory()}
|
{showGrain && grainInventory()}
|
||||||
{/* cables */}
|
|
||||||
<BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}
|
<BinCables
|
||||||
|
cableData={cableData}
|
||||||
|
nodeData={nodeData}
|
||||||
|
bin={bin}
|
||||||
|
binCenter={binCenter}
|
||||||
onNodeClick={(node, cable) => {
|
onNodeClick={(node, cable) => {
|
||||||
// console.log(node)
|
if (nodeClick) nodeClick(node, cable)
|
||||||
// console.log(cable)
|
|
||||||
if(nodeClick){
|
|
||||||
nodeClick(node, cable)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
renderOrder={1}/>
|
renderOrder={1}
|
||||||
{showHotspots && <NodePointCloud bin={bin} nodes={nodeData} /> }
|
/>
|
||||||
{showHeatmap && <TempHeatMap bin={bin} nodes={nodeData}/>}
|
|
||||||
</group>
|
{showHotspots && <NodePointCloud bin={bin} nodes={nodeData} />}
|
||||||
|
|
||||||
{/* lighting */}
|
{showHeatmap && (
|
||||||
<ambientLight intensity={0.2} color={"white"}/>
|
<TempHeatMap
|
||||||
<directionalLight intensity={0.2} color={"white"} position={[0,0,5]}/>
|
bin={bin}
|
||||||
<directionalLight intensity={0.2} color={"white"} position={[0,0,-5]}/>
|
nodes={nodeData}
|
||||||
<directionalLight intensity={0.2} color={"white"} position={[5,0,0]}/>
|
flatMaxY={flatMaxY}
|
||||||
<directionalLight intensity={0.2} color={"white"} position={[-5,0,0]}/>
|
/>
|
||||||
<OrbitCameraControls clampVerticalRotation />
|
)}
|
||||||
</Canvas>
|
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,489 +3,440 @@ import * as THREE from "three";
|
||||||
import { Bin } from "models";
|
import { Bin } from "models";
|
||||||
import { NodeData } from "../../Data/BuildNodeData";
|
import { NodeData } from "../../Data/BuildNodeData";
|
||||||
import React from "react";
|
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;
|
||||||
const THETA_SEGMENTS = 40;
|
const THETA_SEGMENTS = 40;
|
||||||
const HEIGHT_STEPS = 28;
|
const HEIGHT_STEPS = 28;
|
||||||
|
|
||||||
const YELLOW_DELTA = 5;
|
const YELLOW_DELTA = 5;
|
||||||
const RED_DELTA = 10;
|
const RED_DELTA = 10;
|
||||||
|
|
||||||
const IDW_POWER = 4;
|
const IDW_POWER = 4;
|
||||||
const RED_OPACITY = 0.3;
|
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 GREEN = [0,0.5,0.02];
|
const YELLOW = [0.7, 0.86, 0.0];
|
||||||
const YELLOW = [0.7,0.86,0.0];
|
const RED = [0.8, 0.0, 0.01];
|
||||||
const RED = [0.8,0.0,0.01];
|
|
||||||
|
if (heat <= 0) return GREEN;
|
||||||
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 =
|
|
||||||
1 / Math.pow(distSq, power/2);
|
|
||||||
|
|
||||||
totalWeight += weight;
|
|
||||||
weightedSum += a.celcius * weight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalWeight===0
|
|
||||||
? 0
|
|
||||||
: weightedSum/totalWeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// deterministic pseudo-random based on indices
|
return totalWeight === 0 ? 0 : weightedSum / totalWeight;
|
||||||
function hashNoise(a:number,b:number,c:number){
|
}
|
||||||
const x = Math.sin(
|
|
||||||
a*127.1 + b*311.7 + c*74.7
|
|
||||||
) * 43758.5453;
|
|
||||||
|
|
||||||
return (x - Math.floor(x))*2 -1;
|
// 2D IDW for surface height interpolation — matches GrainCableFill exactly
|
||||||
}
|
function idwSurfaceY(
|
||||||
export default function TempHeatMapGPT({bin,nodes}:Props){
|
x: number, z: number,
|
||||||
|
anchors: SurfaceAnchor[],
|
||||||
const binRadius=bin.diameter()/2;
|
power: number
|
||||||
const sidewallHeight=bin.sidewallHeight();
|
): number {
|
||||||
const hopperHeight=bin.hopperHeight() ?? 0;
|
let totalWeight = 0;
|
||||||
const upperThreshold=bin.upperTempThreshold();
|
let weightedY = 0;
|
||||||
|
|
||||||
const sidewallBaseY=-sidewallHeight/2;
|
for (const a of anchors) {
|
||||||
const hopperTipY=sidewallBaseY-hopperHeight;
|
const dx = x - a.x;
|
||||||
|
const dz = z - a.z;
|
||||||
const maxRadiusAtY=(y:number)=>{
|
const distSq = dx * dx + dz * dz;
|
||||||
|
if (distSq < 0.001) return a.y;
|
||||||
if(y>=sidewallBaseY)
|
const weight = 1 / Math.pow(distSq, power / 2);
|
||||||
return binRadius;
|
totalWeight += weight;
|
||||||
|
weightedY += a.y * weight;
|
||||||
if(hopperHeight<=0 || y<=hopperTipY)
|
}
|
||||||
return 0;
|
|
||||||
|
return totalWeight === 0 ? 0 : weightedY / totalWeight;
|
||||||
return binRadius*
|
}
|
||||||
((y-hopperTipY)/hopperHeight);
|
|
||||||
|
function hashNoise(a: number, b: number, c: number) {
|
||||||
|
const x = Math.sin(a * 127.1 + b * 311.7 + c * 74.7) * 43758.5453;
|
||||||
|
return (x - Math.floor(x)) * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TempHeatMap({ bin, nodes, flatMaxY }: 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);
|
||||||
};
|
};
|
||||||
const anchors=useMemo(
|
|
||||||
()=>nodes
|
// Temperature anchors — all in-grain nodes
|
||||||
.filter(n=>n.inGrain && !n.excluded)
|
const tempAnchors = useMemo<TempAnchor[]>(
|
||||||
.map(n=>({
|
() =>
|
||||||
x:n.position.x,
|
nodes
|
||||||
y:n.position.y,
|
.filter(n => n.inGrain && !n.excluded)
|
||||||
z:n.position.z,
|
.map(n => ({
|
||||||
celcius:n.celcius,
|
x: n.position.x,
|
||||||
})),
|
y: n.position.y,
|
||||||
|
z: n.position.z,
|
||||||
|
celcius: n.celcius,
|
||||||
|
})),
|
||||||
[nodes]
|
[nodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const maxGrainY=useMemo(()=>{
|
// Surface anchors — top nodes only, Y offset by half spacing
|
||||||
let maxY=sidewallBaseY;
|
// Matches GrainCableFill's anchor computation exactly.
|
||||||
|
const surfaceAnchors = useMemo<SurfaceAnchor[]>(
|
||||||
for(const a of anchors)
|
() =>
|
||||||
if(a.y>maxY)
|
nodes
|
||||||
maxY=a.y;
|
.filter(n => n.topNode && n.inGrain && !n.excluded)
|
||||||
|
.map(n => ({
|
||||||
return maxY;
|
x: n.position.x,
|
||||||
},[anchors,sidewallBaseY]);
|
z: n.position.z,
|
||||||
|
y: n.position.y + n.nodeSpacing * 0.5,
|
||||||
const geometry=useMemo(()=>{
|
})),
|
||||||
|
[nodes]
|
||||||
if(!anchors.length)
|
);
|
||||||
return null;
|
|
||||||
|
// Wall clamp Y — average of surface anchor heights.
|
||||||
const pointsPerLayer=
|
// Prevents the surface from piling up at the bin wall where there are no cables.
|
||||||
1 + RADIAL_RINGS*THETA_SEGMENTS;
|
// Matches GrainCableFill's wallY computation.
|
||||||
|
const wallY = useMemo(() => {
|
||||||
const totalVerts=
|
if (surfaceAnchors.length === 0) return flatMaxY ?? sidewallBaseY;
|
||||||
HEIGHT_STEPS*pointsPerLayer +1;
|
return surfaceAnchors.reduce((sum, a) => sum + a.y, 0) / surfaceAnchors.length;
|
||||||
|
}, [surfaceAnchors, flatMaxY, sidewallBaseY]);
|
||||||
const positions=
|
|
||||||
new Float32Array(totalVerts*3);
|
// For each (x, z) position, returns the Y ceiling of the grain surface.
|
||||||
|
// Uses cable surface IDW when top nodes exist, falls back to flatMaxY.
|
||||||
const colors=
|
const getSurfaceY = (x: number, z: number): number => {
|
||||||
new Float32Array(totalVerts*3);
|
if (surfaceAnchors.length > 0) {
|
||||||
|
// Outermost ring clamps to wallY — matches GrainCableFill
|
||||||
const heats=new Float32Array(totalVerts);
|
const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER);
|
||||||
|
return Math.max(sidewallBaseY, Math.min(sidewallHeight / 2, raw));
|
||||||
const rawBottomY=
|
}
|
||||||
hopperHeight>0
|
return flatMaxY ?? sidewallBaseY;
|
||||||
? hopperTipY
|
};
|
||||||
: sidewallBaseY;
|
|
||||||
|
// Global max Y — highest point of the surface (used for grid bottom calc)
|
||||||
const grainBottomY=
|
const maxGrainY = useMemo(() => {
|
||||||
hopperHeight>0
|
if (surfaceAnchors.length > 0) {
|
||||||
? hopperTipY +
|
return Math.max(...surfaceAnchors.map(a => a.y), wallY);
|
||||||
(maxGrainY-hopperTipY)/HEIGHT_STEPS
|
}
|
||||||
|
return flatMaxY ?? sidewallBaseY;
|
||||||
|
}, [surfaceAnchors, wallY, flatMaxY, sidewallBaseY]);
|
||||||
|
|
||||||
|
const geometry = useMemo(() => {
|
||||||
|
if (!tempAnchors.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;
|
: sidewallBaseY;
|
||||||
|
const grainHeight = maxGrainY - grainBottomY;
|
||||||
const grainHeight=
|
|
||||||
maxGrainY-grainBottomY;
|
|
||||||
|
|
||||||
if(grainHeight<=0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
for(let hStep=0; hStep<HEIGHT_STEPS; hStep++){
|
|
||||||
|
|
||||||
const t=hStep/(HEIGHT_STEPS-1);
|
|
||||||
const y=grainBottomY+t*grainHeight;
|
|
||||||
|
|
||||||
const allowedRadius=
|
|
||||||
maxRadiusAtY(y)*0.97;
|
|
||||||
|
|
||||||
const layerBase=
|
|
||||||
hStep*pointsPerLayer;
|
|
||||||
|
|
||||||
const centerTemp=
|
|
||||||
idwTemp(
|
|
||||||
0,y,0,
|
|
||||||
anchors,
|
|
||||||
IDW_POWER
|
|
||||||
);
|
|
||||||
|
|
||||||
const centerHeat=
|
|
||||||
tempToHeat(centerTemp,upperThreshold);
|
|
||||||
|
|
||||||
const [cr,cg,cb]=
|
|
||||||
heatToRGB(centerHeat);
|
|
||||||
|
|
||||||
positions[layerBase*3]=0;
|
|
||||||
positions[layerBase*3+1]=y;
|
|
||||||
positions[layerBase*3+2]=0;
|
|
||||||
|
|
||||||
colors[layerBase*3]=cr;
|
|
||||||
colors[layerBase*3+1]=cg;
|
|
||||||
colors[layerBase*3+2]=cb;
|
|
||||||
|
|
||||||
for(let ring=0; ring<RADIAL_RINGS; ring++){
|
|
||||||
|
|
||||||
//biases density towards the walls of the bin
|
|
||||||
const u = (ring+1)/RADIAL_RINGS;
|
|
||||||
const rFrac = 1 - Math.pow(1-u, 2.2);
|
|
||||||
|
|
||||||
for(let seg=0; seg<THETA_SEGMENTS; seg++){
|
|
||||||
|
|
||||||
const noiseA=
|
|
||||||
hashNoise(hStep,ring,seg);
|
|
||||||
|
|
||||||
const noiseR=
|
|
||||||
hashNoise(seg,hStep,ring+11);
|
|
||||||
|
|
||||||
const baseRadius=
|
|
||||||
rFrac*allowedRadius;
|
|
||||||
|
|
||||||
const jitterRadius=
|
|
||||||
baseRadius +
|
|
||||||
noiseR*
|
|
||||||
RADIAL_JITTER*
|
|
||||||
allowedRadius;
|
|
||||||
|
|
||||||
const r=Math.max(
|
|
||||||
0,
|
|
||||||
Math.min(
|
|
||||||
jitterRadius,
|
|
||||||
allowedRadius
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const layerPhase=
|
|
||||||
t*LAYER_TWIST;
|
|
||||||
|
|
||||||
const angle=
|
|
||||||
(seg/THETA_SEGMENTS)
|
|
||||||
*Math.PI*2
|
|
||||||
+ layerPhase
|
|
||||||
+ noiseA*ANGLE_JITTER;
|
|
||||||
|
|
||||||
const x=Math.cos(angle)*r;
|
|
||||||
const z=Math.sin(angle)*r;
|
|
||||||
|
|
||||||
const temp=idwTemp(
|
|
||||||
x,y,z,
|
|
||||||
anchors,
|
|
||||||
IDW_POWER
|
|
||||||
);
|
|
||||||
|
|
||||||
const heat=
|
|
||||||
tempToHeat(temp,upperThreshold);
|
|
||||||
|
|
||||||
const [vr,vg,vb]=
|
|
||||||
heatToRGB(heat);
|
|
||||||
|
|
||||||
const vi=
|
|
||||||
layerBase+
|
|
||||||
1+
|
|
||||||
ring*THETA_SEGMENTS+
|
|
||||||
seg;
|
|
||||||
|
|
||||||
heats[vi]=heat;
|
|
||||||
|
|
||||||
positions[vi*3]=x;
|
|
||||||
positions[vi*3+1]=y;
|
|
||||||
positions[vi*3+2]=z;
|
|
||||||
|
|
||||||
colors[vi*3]=vr;
|
|
||||||
colors[vi*3+1]=vg;
|
|
||||||
colors[vi*3+2]=vb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const idx=(
|
|
||||||
hStep:number,
|
|
||||||
ring:number,
|
|
||||||
seg:number
|
|
||||||
)=>{
|
|
||||||
|
|
||||||
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<HEIGHT_STEPS-1; h++){
|
|
||||||
|
|
||||||
for(let ring=0; ring<RADIAL_RINGS-1; ring++){
|
|
||||||
for(let seg=0; seg<THETA_SEGMENTS; seg++){
|
|
||||||
|
|
||||||
const next=(seg+1)%THETA_SEGMENTS;
|
if (grainHeight <= 0) return null;
|
||||||
|
|
||||||
const a=idx(h,ring,seg);
|
// Pre-compute the (x, z) position for each (ring, seg) slot so we
|
||||||
const b=idx(h,ring,next);
|
// can look up the surface Y ceiling per column
|
||||||
const e=idx(h+1,ring,seg);
|
const colX: number[] = new Array(RADIAL_RINGS * THETA_SEGMENTS);
|
||||||
const f=idx(h+1,ring,next);
|
const colZ: number[] = new Array(RADIAL_RINGS * THETA_SEGMENTS);
|
||||||
|
const colSurfaceY: number[] = new Array(RADIAL_RINGS * THETA_SEGMENTS);
|
||||||
|
|
||||||
// indices.push(a,e,b);
|
// Use the outermost layer's radius for surface Y lookup (no jitter/twist)
|
||||||
// indices.push(e,f,b);
|
// so the surface shape matches GrainCableFill cleanly
|
||||||
pushTri(a,e,b);
|
const topLayerAllowedRadius = maxRadiusAtY(maxGrainY) * 0.97;
|
||||||
pushTri(e,f,b);
|
|
||||||
}
|
for (let ring = 0; ring < RADIAL_RINGS; ring++) {
|
||||||
}
|
const u = (ring + 1) / RADIAL_RINGS;
|
||||||
|
const rFrac = 1 - Math.pow(1 - u, 2.2);
|
||||||
//center fill
|
const r = rFrac * topLayerAllowedRadius;
|
||||||
for (let seg=0; seg<THETA_SEGMENTS; seg++) {
|
|
||||||
|
for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
|
||||||
const next=(seg+1)%THETA_SEGMENTS;
|
const angle = (seg / THETA_SEGMENTS) * Math.PI * 2;
|
||||||
|
const x = Math.cos(angle) * r;
|
||||||
const center0 = idx(h,-1,0);
|
const z = Math.sin(angle) * r;
|
||||||
const center1 = idx(h+1,-1,0);
|
const ci = ring * THETA_SEGMENTS + seg;
|
||||||
|
colX[ci] = x;
|
||||||
const a = idx(h,0,seg);
|
colZ[ci] = z;
|
||||||
const b = idx(h,0,next);
|
// Outermost ring uses wallY, inner rings use full IDW surface
|
||||||
|
colSurfaceY[ci] = ring === RADIAL_RINGS - 1
|
||||||
const c = idx(h+1,0,seg);
|
? wallY
|
||||||
const d = idx(h+1,0,next);
|
: getSurfaceY(x, z);
|
||||||
|
}
|
||||||
pushTri(center0,c,a);
|
}
|
||||||
pushTri(center0,center1,c);
|
// Center column surface Y
|
||||||
pushTri(a,c,b);
|
const centerSurfaceY = getSurfaceY(0, 0);
|
||||||
pushTri(b,c,d);
|
|
||||||
}
|
for (let hStep = 0; hStep < HEIGHT_STEPS; hStep++) {
|
||||||
}
|
const t = hStep / (HEIGHT_STEPS - 1);
|
||||||
const tipVertexIndex=
|
|
||||||
HEIGHT_STEPS*pointsPerLayer;
|
const layerBase = hStep * pointsPerLayer;
|
||||||
|
const allowedRadius = maxRadiusAtY(grainBottomY + t * grainHeight) * 0.97;
|
||||||
if(hopperHeight>0){
|
|
||||||
|
// Center vertex — Y is lerped from bottom to its column surface ceiling
|
||||||
const tipTemp=idwTemp(
|
const centerY = grainBottomY + t * (centerSurfaceY - grainBottomY);
|
||||||
0,
|
|
||||||
rawBottomY,
|
const centerTemp = idwTemp(0, centerY, 0, tempAnchors, IDW_POWER);
|
||||||
0,
|
const centerHeat = tempToHeat(centerTemp, upperThreshold);
|
||||||
anchors,
|
const [cr, cg, cb] = heatToRGB(centerHeat);
|
||||||
IDW_POWER
|
|
||||||
);
|
positions[layerBase * 3] = 0;
|
||||||
|
positions[layerBase * 3 + 1] = centerY;
|
||||||
const tipHeat=
|
positions[layerBase * 3 + 2] = 0;
|
||||||
tempToHeat(
|
colors[layerBase * 3] = cr;
|
||||||
tipTemp,
|
colors[layerBase * 3 + 1] = cg;
|
||||||
upperThreshold
|
colors[layerBase * 3 + 2] = cb;
|
||||||
);
|
heats[layerBase] = centerHeat;
|
||||||
|
|
||||||
const [tr,tg,tb]=
|
for (let ring = 0; ring < RADIAL_RINGS; ring++) {
|
||||||
heatToRGB(tipHeat);
|
const u = (ring + 1) / RADIAL_RINGS;
|
||||||
|
const rFrac = 1 - Math.pow(1 - u, 2.2);
|
||||||
positions[tipVertexIndex*3]=0;
|
|
||||||
positions[tipVertexIndex*3+1]=rawBottomY;
|
for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
|
||||||
positions[tipVertexIndex*3+2]=0;
|
const noiseA = hashNoise(hStep, ring, seg);
|
||||||
|
const noiseR = hashNoise(seg, hStep, ring + 11);
|
||||||
colors[tipVertexIndex*3]=tr;
|
|
||||||
colors[tipVertexIndex*3+1]=tg;
|
const baseRadius = rFrac * allowedRadius;
|
||||||
colors[tipVertexIndex*3+2]=tb;
|
const jitterRadius = baseRadius + noiseR * RADIAL_JITTER * allowedRadius;
|
||||||
const outerRing=RADIAL_RINGS-1;
|
const r = Math.max(0, Math.min(jitterRadius, allowedRadius));
|
||||||
|
|
||||||
for(let seg=0; seg<THETA_SEGMENTS; seg++){
|
const layerPhase = t * LAYER_TWIST;
|
||||||
const next=(seg+1)%THETA_SEGMENTS;
|
const angle =
|
||||||
|
(seg / THETA_SEGMENTS) * Math.PI * 2 +
|
||||||
const a=idx(0,outerRing,seg);
|
layerPhase +
|
||||||
const b=idx(0,outerRing,next);
|
noiseA * ANGLE_JITTER;
|
||||||
pushTri(tipVertexIndex,b,a)
|
|
||||||
}
|
const x = Math.cos(angle) * r;
|
||||||
}
|
const z = Math.sin(angle) * r;
|
||||||
|
|
||||||
function makeGeo(indices:number[]){
|
// Per-column surface Y ceiling — this is what gives the wavy top
|
||||||
const g=new THREE.BufferGeometry();
|
const ci = ring * THETA_SEGMENTS + seg;
|
||||||
g.setAttribute(
|
const surfaceY = colSurfaceY[ci];
|
||||||
'position',
|
|
||||||
new THREE.BufferAttribute(positions,3)
|
// Lerp this vertex Y from grainBottomY up to its column surface ceiling
|
||||||
|
const y = grainBottomY + t * (surfaceY - grainBottomY);
|
||||||
|
|
||||||
|
const temp = idwTemp(x, y, z, tempAnchors, IDW_POWER);
|
||||||
|
const heat = tempToHeat(temp, upperThreshold);
|
||||||
|
const [vr, vg, vb] = heatToRGB(heat);
|
||||||
|
|
||||||
|
const vi = layerBase + 1 + ring * THETA_SEGMENTS + seg;
|
||||||
|
|
||||||
|
heats[vi] = heat;
|
||||||
|
positions[vi * 3] = x;
|
||||||
|
positions[vi * 3 + 1] = y;
|
||||||
|
positions[vi * 3 + 2] = z;
|
||||||
|
colors[vi * 3] = vr;
|
||||||
|
colors[vi * 3 + 1] = vg;
|
||||||
|
colors[vi * 3 + 2] = vb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = (hStep: number, ring: number, seg: number) => {
|
||||||
|
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 < HEIGHT_STEPS - 1; h++) {
|
||||||
|
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(h, ring, seg);
|
||||||
|
const b = idx(h, ring, next);
|
||||||
|
const e = idx(h + 1, ring, seg);
|
||||||
|
const f = idx(h + 1, ring, next);
|
||||||
|
pushTri(a, e, b);
|
||||||
|
pushTri(e, f, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// center fill
|
||||||
|
for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
|
||||||
|
const next = (seg + 1) % THETA_SEGMENTS;
|
||||||
|
const center0 = idx(h, -1, 0);
|
||||||
|
const center1 = idx(h + 1, -1, 0);
|
||||||
|
const a = idx(h, 0, seg);
|
||||||
|
const b = idx(h, 0, next);
|
||||||
|
const c = idx(h + 1, 0, seg);
|
||||||
|
const d = idx(h + 1, 0, next);
|
||||||
|
pushTri(center0, c, a);
|
||||||
|
pushTri(center0, center1, c);
|
||||||
|
pushTri(a, c, b);
|
||||||
|
pushTri(b, c, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hopper tip
|
||||||
|
const tipVertexIndex = HEIGHT_STEPS * pointsPerLayer;
|
||||||
|
if (hopperHeight > 0) {
|
||||||
|
const tipTemp = idwTemp(0, rawBottomY, 0, tempAnchors, 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;
|
||||||
|
heats[tipVertexIndex] = tipHeat;
|
||||||
|
|
||||||
|
const outerRing = RADIAL_RINGS - 1;
|
||||||
|
for (let seg = 0; seg < THETA_SEGMENTS; seg++) {
|
||||||
|
const next = (seg + 1) % THETA_SEGMENTS;
|
||||||
|
pushTri(tipVertexIndex, idx(0, outerRing, next), idx(0, outerRing, seg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeGeo(indices: number[]) {
|
||||||
|
const g = new THREE.BufferGeometry();
|
||||||
|
g.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
||||||
|
g.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
||||||
|
g.setIndex(indices);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
green: makeGeo(green),
|
||||||
|
yellow: makeGeo(yellow),
|
||||||
|
red: makeGeo(red),
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
tempAnchors,
|
||||||
|
surfaceAnchors,
|
||||||
|
wallY,
|
||||||
|
maxGrainY,
|
||||||
|
hopperTipY,
|
||||||
|
sidewallBaseY,
|
||||||
|
binRadius,
|
||||||
|
upperThreshold,
|
||||||
|
hopperHeight,
|
||||||
|
flatMaxY,
|
||||||
|
]);
|
||||||
|
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
g.setAttribute(
|
}
|
||||||
'color',
|
|
||||||
new THREE.BufferAttribute(colors,3)
|
|
||||||
);
|
|
||||||
g.setIndex(indices);
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
green:makeGeo(green),
|
|
||||||
yellow:makeGeo(yellow),
|
|
||||||
red:makeGeo(red)
|
|
||||||
};
|
|
||||||
|
|
||||||
},[
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue