trying to fix overlaping temp clouds

This commit is contained in:
csawatzky 2026-04-24 11:43:56 -06:00
parent f4d1167e4d
commit 6f6a062106
2 changed files with 143 additions and 98 deletions

View file

@ -44,6 +44,12 @@ export function BuildNodeData(cables: CableData[]): NodeData[] {
const humidity = grainCable.relativeHumidity[i] || undefined; const humidity = grainCable.relativeHumidity[i] || undefined;
const moisture = grainCable.moisture[i] || undefined; const moisture = grainCable.moisture[i] || undefined;
let t = celcius let t = celcius
if(i===0){
t = 30
}
if (i===1){
t = 0
}
nodeData.push({ nodeData.push({
cableIndex: cableIndex, cableIndex: cableIndex,

View file

@ -28,18 +28,27 @@ export default function NodePointCloud(props: Props) {
const OPACITY = 0.7; const OPACITY = 0.7;
const POINT_SIZE = 1; const POINT_SIZE = 1;
const getHeatIntensity = ( // IDW power — how sharply nearer nodes dominate the field
temp: number, const IDW_POWER = 2;
lower: number,
upper: number,
fade: number
) => {
if (temp >= lower && temp <= upper) return 0;
const distance = // Minimum net signed magnitude to emit a point.
temp < lower ? lower - temp : temp - upper; // Keeps near-zero cancellation zones empty rather than noisy.
const EMIT_THRESHOLD = 0.05;
return Math.min(1, distance / fade); // -----------------------------
// BIN GEOMETRY
// -----------------------------
const binRadius = bin.diameter() / 2;
const sidewallHeight = bin.sidewallHeight();
const hopperHeight = bin.hopperHeight() ?? 0;
const sidewallBaseY = -sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight;
const maxRadiusAtY = (y: number): number => {
if (y >= sidewallBaseY) return binRadius;
if (hopperHeight <= 0 || y <= hopperTipY) return 0;
const t = (y - hopperTipY) / hopperHeight;
return binRadius * t;
}; };
// ----------------------------- // -----------------------------
@ -47,101 +56,135 @@ export default function NodePointCloud(props: Props) {
// ----------------------------- // -----------------------------
const maxGrainY = useMemo(() => { const maxGrainY = useMemo(() => {
let maxY = -Infinity; let maxY = -Infinity;
nodes.forEach((node) => { nodes.forEach((node) => {
if (!node.inGrain || node.excluded) return; if (!node.inGrain || node.excluded) return;
if (node.position.y > maxY) maxY = node.position.y; if (node.position.y > maxY) maxY = node.position.y;
}); });
return maxY; return maxY;
}, [nodes]); }, [nodes]);
// ----------------------------- // -----------------------------
// BUILD POINT CLOUD FROM NODES // PRE-COMPUTE SIGNED NODE CONTRIBUTIONS
// Only nodes outside threshold contribute to the field.
// Hot nodes carry a positive signed intensity, cold nodes negative.
// -----------------------------
const signedNodes = useMemo(() => {
return nodes
.filter(n => n.inGrain && !n.excluded)
.map(n => {
const lower = bin.lowerTempThreshold();
const upper = bin.upperTempThreshold();
if (n.celcius >= lower && n.celcius <= upper) return null;
const distance = n.celcius < lower
? lower - n.celcius
: n.celcius - upper;
const intensity = Math.min(1, distance / colourFade);
// positive = hot, negative = cold
const signedIntensity = n.celcius > upper ? intensity : -intensity;
return {
x: n.position.x,
y: n.position.y,
z: n.position.z,
signedIntensity,
cloudRadius: bin.diameter() * BASE_RADIUS_FACTOR * (0.5 + intensity),
densityMultiplier: INTENSITY_DENSITY_BASE + intensity * INTENSITY_DENSITY_MULT,
};
})
.filter(Boolean) as {
x: number; y: number; z: number;
signedIntensity: number;
cloudRadius: number;
densityMultiplier: number;
}[];
}, [nodes, bin]);
/**
* Evaluates the net signed field at (px, py, pz) by summing
* IDW-weighted signed intensities from all contributing nodes.
*
* Returns a value in [-1, 1]:
* > 0 net hot
* < 0 net cold
* ~0 cancelled out point will not be emitted
*/
const evaluateField = (px: number, py: number, pz: number): number => {
let totalWeight = 0;
let weightedSum = 0;
for (const node of signedNodes) {
const dx = px - node.x;
const dy = py - node.y;
const dz = pz - node.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 += node.signedIntensity * weight;
}
if (totalWeight === 0) return 0;
return weightedSum / totalWeight;
};
// -----------------------------
// BUILD POINT CLOUD
// ----------------------------- // -----------------------------
const { positions, colors } = useMemo(() => { const { positions, colors } = useMemo(() => {
const positions: number[] = []; const positions: number[] = [];
const colors: number[] = []; const colors: number[] = [];
const baseRadius = bin.diameter() * BASE_RADIUS_FACTOR; signedNodes.forEach((node) => {
const radialSteps = Math.floor(RADIAL_BASE * node.densityMultiplier);
nodes.forEach((node) => { const thetaSteps = Math.floor(THETA_BASE * node.densityMultiplier);
if (!node.inGrain || node.excluded) return; const phiSteps = Math.floor(PHI_BASE * node.densityMultiplier);
const intensity = getHeatIntensity(
node.celcius,
bin.lowerTempThreshold(),
bin.upperTempThreshold(),
colourFade
);
// skip normal nodes
if (intensity === 0) return;
const isHot = node.celcius > bin.upperTempThreshold();
// expand cloud based on severity
const radius = baseRadius * (0.5 + intensity);
//these control the number of points along that axis
const densityMultiplier = INTENSITY_DENSITY_BASE + intensity * INTENSITY_DENSITY_MULT;
const radialSteps = Math.floor(RADIAL_BASE * densityMultiplier);//points along the radius to the edge
const thetaSteps = Math.floor(THETA_BASE * densityMultiplier);//points along the azimuth (horizontal angle)
const phiSteps = Math.floor(PHI_BASE * densityMultiplier);//points along the vertical angle
const radiusLimit = bin.diameter() / 2;
const minY = -bin.sidewallHeight() / 2;
for (let rStep = 0; rStep < radialSteps; rStep++) { for (let rStep = 0; rStep < radialSteps; rStep++) {
// sqrt for even density const r = Math.sqrt(rStep / radialSteps) * node.cloudRadius;
const r = Math.sqrt(rStep / radialSteps) * radius;
for (let pStep = 0; pStep < phiSteps; pStep++) { for (let pStep = 0; pStep < phiSteps; pStep++) {
// avoid poles clustering by not hitting exact 0/PI const phi = ((pStep + 0.5) / phiSteps) * Math.PI;
const phi =
((pStep + 0.5) / phiSteps) * Math.PI;
for (let tStep = 0; tStep < thetaSteps; tStep++) { for (let tStep = 0; tStep < thetaSteps; tStep++) {
const theta = const theta = (tStep / thetaSteps) * Math.PI * 2;
(tStep / thetaSteps) * Math.PI * 2;
const x = const x = node.x + r * Math.sin(phi) * Math.cos(theta);
node.position.x + const y = node.y + r * Math.cos(phi);
r * Math.sin(phi) * Math.cos(theta); const z = node.z + r * Math.sin(phi) * Math.sin(theta);
const y = // Bin boundary checks
node.position.y + if (y < hopperTipY || y > maxGrainY) continue;
r * Math.cos(phi);
const z =
node.position.z +
r * Math.sin(phi) * Math.sin(theta);
// cylindrical bin bounds
const distXZ = Math.sqrt(x * x + z * z); const distXZ = Math.sqrt(x * x + z * z);
if (distXZ > radiusLimit) continue; if (distXZ > maxRadiusAtY(y)) continue;
if (y > maxGrainY || y < minY) continue;
// Evaluate the full signed field at this point.
// A point in the overlap of a hot and cold cloud will have
// a net value near zero and will be skipped rather than
// rendered pink.
const netField = evaluateField(x, y, z);
if (Math.abs(netField) < EMIT_THRESHOLD) continue;
positions.push(x, y, z); positions.push(x, y, z);
const falloff = 1 - r / radius; // Brightness driven by net magnitude, not per-node intensity
const netMagnitude = Math.abs(netField);
const falloff = 1 - r / node.cloudRadius;
const spatialFalloff = Math.pow(Math.max(0, falloff), 2);
const adjustedFalloff = MIN_EDGE_INTENSITY + (1 - MIN_EDGE_INTENSITY) * spatialFalloff;
const intensityCurve = netMagnitude * netMagnitude;
const brightness = adjustedFalloff * (BASE_BRIGHTNESS + INTENSITY_BRIGHTNESS_MULT * intensityCurve);
// shape of the curve if (netField > 0) {
const spatialFalloff = Math.pow(falloff, 2); colors.push(brightness, 0, 0); // hot → red
const adjustedFalloff =
MIN_EDGE_INTENSITY + (1 - MIN_EDGE_INTENSITY) * spatialFalloff;
const intensityCurve = intensity * intensity; // quadratic easing
const baseBrightness =
BASE_BRIGHTNESS + INTENSITY_BRIGHTNESS_MULT * intensityCurve;
const brightness = adjustedFalloff * baseBrightness;
if (isHot) {
colors.push(brightness, 0, 0);
} else { } else {
colors.push(0, 0, brightness); colors.push(0, 0, brightness); // cold → blue
} }
} }
} }
@ -152,7 +195,7 @@ export default function NodePointCloud(props: Props) {
positions: new Float32Array(positions), positions: new Float32Array(positions),
colors: new Float32Array(colors), colors: new Float32Array(colors),
}; };
}, [nodes, bin, maxGrainY]); }, [signedNodes, maxGrainY, hopperTipY]);
const circleTexture = useMemo(() => { const circleTexture = useMemo(() => {
const size = 64; const size = 64;
@ -162,12 +205,8 @@ export default function NodePointCloud(props: Props) {
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
const gradient = ctx.createRadialGradient( const gradient = ctx.createRadialGradient(
size / 2, size / 2, size / 2, 0,
size / 2, size / 2, size / 2, size / 2
0,
size / 2,
size / 2,
size / 2
); );
gradient.addColorStop(0, "rgba(255,255,255,1)"); gradient.addColorStop(0, "rgba(255,255,255,1)");