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

@ -15,31 +15,40 @@ export default function NodePointCloud(props: Props) {
const BASE_RADIUS_FACTOR = 0.15;
const INTENSITY_DENSITY_MULT = 1.25;
const INTENSITY_DENSITY_BASE = 0.3;
const RADIAL_BASE = 6;
const THETA_BASE = 10;
const PHI_BASE = 10;
const MIN_EDGE_INTENSITY = 0.2;
const BASE_BRIGHTNESS = 0.2;
const INTENSITY_BRIGHTNESS_MULT = 0.5;
const OPACITY = 0.7;
const POINT_SIZE = 1;
const getHeatIntensity = (
temp: number,
lower: number,
upper: number,
fade: number
) => {
if (temp >= lower && temp <= upper) return 0;
// IDW power — how sharply nearer nodes dominate the field
const IDW_POWER = 2;
const distance =
temp < lower ? lower - temp : temp - upper;
// Minimum net signed magnitude to emit a point.
// 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(() => {
let maxY = -Infinity;
nodes.forEach((node) => {
if (!node.inGrain || node.excluded) return;
if (node.position.y > maxY) maxY = node.position.y;
});
return maxY;
}, [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: number[] = [];
const colors: number[] = [];
const baseRadius = bin.diameter() * BASE_RADIUS_FACTOR;
signedNodes.forEach((node) => {
const radialSteps = Math.floor(RADIAL_BASE * node.densityMultiplier);
const thetaSteps = Math.floor(THETA_BASE * node.densityMultiplier);
const phiSteps = Math.floor(PHI_BASE * node.densityMultiplier);
nodes.forEach((node) => {
if (!node.inGrain || node.excluded) return;
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++) {
// sqrt for even density
const r = Math.sqrt(rStep / radialSteps) * radius;
const r = Math.sqrt(rStep / radialSteps) * node.cloudRadius;
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++) {
const theta =
(tStep / thetaSteps) * Math.PI * 2;
const x =
node.position.x +
r * Math.sin(phi) * Math.cos(theta);
const y =
node.position.y +
r * Math.cos(phi);
const z =
node.position.z +
r * Math.sin(phi) * Math.sin(theta);
// cylindrical bin bounds
const theta = (tStep / thetaSteps) * Math.PI * 2;
const x = node.x + r * Math.sin(phi) * Math.cos(theta);
const y = node.y + r * Math.cos(phi);
const z = node.z + r * Math.sin(phi) * Math.sin(theta);
// Bin boundary checks
if (y < hopperTipY || y > maxGrainY) continue;
const distXZ = Math.sqrt(x * x + z * z);
if (distXZ > radiusLimit) continue;
if (y > maxGrainY || y < minY) continue;
if (distXZ > maxRadiusAtY(y)) 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);
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
const spatialFalloff = Math.pow(falloff, 2);
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);
if (netField > 0) {
colors.push(brightness, 0, 0); // hot → red
} else {
colors.push(0, 0, brightness);
colors.push(0, 0, brightness); // cold → blue
}
}
}
@ -152,30 +195,26 @@ export default function NodePointCloud(props: Props) {
positions: new Float32Array(positions),
colors: new Float32Array(colors),
};
}, [nodes, bin, maxGrainY]);
}, [signedNodes, maxGrainY, hopperTipY]);
const circleTexture = useMemo(() => {
const size = 64;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d")!;
const gradient = ctx.createRadialGradient(
size / 2,
size / 2,
0,
size / 2,
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(1, "rgba(255,255,255,0)");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
const texture = new CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;