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 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,
|
||||||
|
|
|
||||||
|
|
@ -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)");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue