trying to fix overlaping temp clouds
This commit is contained in:
parent
f4d1167e4d
commit
6f6a062106
2 changed files with 143 additions and 98 deletions
|
|
@ -44,6 +44,12 @@ export function BuildNodeData(cables: CableData[]): NodeData[] {
|
|||
const humidity = grainCable.relativeHumidity[i] || undefined;
|
||||
const moisture = grainCable.moisture[i] || undefined;
|
||||
let t = celcius
|
||||
if(i===0){
|
||||
t = 30
|
||||
}
|
||||
if (i===1){
|
||||
t = 0
|
||||
}
|
||||
|
||||
nodeData.push({
|
||||
cableIndex: cableIndex,
|
||||
|
|
|
|||
|
|
@ -28,18 +28,27 @@ export default function NodePointCloud(props: Props) {
|
|||
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;
|
||||
|
||||
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;
|
||||
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);
|
||||
|
||||
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 theta = (tStep / thetaSteps) * Math.PI * 2;
|
||||
|
||||
const x =
|
||||
node.position.x +
|
||||
r * Math.sin(phi) * Math.cos(theta);
|
||||
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);
|
||||
|
||||
const y =
|
||||
node.position.y +
|
||||
r * Math.cos(phi);
|
||||
|
||||
const z =
|
||||
node.position.z +
|
||||
r * Math.sin(phi) * Math.sin(theta);
|
||||
|
||||
// cylindrical bin bounds
|
||||
// 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,7 +195,7 @@ 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;
|
||||
|
|
@ -162,12 +205,8 @@ export default function NodePointCloud(props: Props) {
|
|||
|
||||
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)");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue