moved from a general heatmap to a more spatial hot cold point around nodes that the density and brightness are controlled by how far past the threshold the nodes temp is
This commit is contained in:
parent
6c87ed03ed
commit
6cc40e159a
9 changed files with 493 additions and 26 deletions
|
|
@ -14,6 +14,7 @@ export interface BaseMeshProps {
|
||||||
materialProps?: Partial<MaterialParameters>;
|
materialProps?: Partial<MaterialParameters>;
|
||||||
materialOverride?: React.ReactNode;
|
materialOverride?: React.ReactNode;
|
||||||
side?: Side
|
side?: Side
|
||||||
|
depthWrite?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BaseMesh(props: BaseMeshProps) {
|
export default function BaseMesh(props: BaseMeshProps) {
|
||||||
|
|
@ -30,7 +31,8 @@ export default function BaseMesh(props: BaseMeshProps) {
|
||||||
materialType,
|
materialType,
|
||||||
materialProps,
|
materialProps,
|
||||||
materialOverride,
|
materialOverride,
|
||||||
side
|
side,
|
||||||
|
depthWrite = true
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isTransparent = opacity < 1;
|
const isTransparent = opacity < 1;
|
||||||
|
|
@ -43,7 +45,7 @@ export default function BaseMesh(props: BaseMeshProps) {
|
||||||
color={colour}
|
color={colour}
|
||||||
transparent={isTransparent}
|
transparent={isTransparent}
|
||||||
opacity={opacity}
|
opacity={opacity}
|
||||||
depthWrite={!isTransparent}
|
depthWrite={depthWrite}
|
||||||
side={side}
|
side={side}
|
||||||
{...materialProps}
|
{...materialProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,28 @@ export function BuildNodeData(cables: CableData[]): NodeData[] {
|
||||||
|
|
||||||
|
|
||||||
grainCable.celcius.forEach((celcius,i) => {
|
grainCable.celcius.forEach((celcius,i) => {
|
||||||
|
|
||||||
const nodeY = bottomY + (i * spacing);
|
const nodeY = bottomY + (i * spacing);
|
||||||
|
|
||||||
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
|
||||||
|
//this is for testing to force a single node to a specific temp
|
||||||
|
// if (i === 0) {
|
||||||
|
// t = 30
|
||||||
|
// }
|
||||||
|
// if (i === 1) {
|
||||||
|
// t = -30
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
nodeData.push({
|
nodeData.push({
|
||||||
cableIndex: cableIndex,
|
cableIndex: cableIndex,
|
||||||
nodeIndex: i,
|
nodeIndex: i,
|
||||||
radius: nodeRadius,
|
radius: nodeRadius,
|
||||||
position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z),
|
position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z),
|
||||||
celcius: celcius,
|
celcius: t,
|
||||||
humidity: humidity,
|
humidity: humidity,
|
||||||
moisture: moisture,
|
moisture: moisture,
|
||||||
topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index
|
topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import { Vector3 } from "three";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { BuildCableData } from "../Data/BuildCableData";
|
import { BuildCableData } from "../Data/BuildCableData";
|
||||||
import { BuildNodeData } from "../Data/BuildNodeData";
|
import { BuildNodeData } from "../Data/BuildNodeData";
|
||||||
|
import Heatmap from "../Systems/Heatmap/HeatMapAlpha";
|
||||||
|
import NodePointCloud from "../Systems/Heatmap/NodePointCloud";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
|
|
@ -43,7 +45,7 @@ export default function Bin3dView(props: Props){
|
||||||
radialSegments={20}
|
radialSegments={20}
|
||||||
binMetalness={0.5}
|
binMetalness={0.5}
|
||||||
binRoughness={0.7}
|
binRoughness={0.7}
|
||||||
binOpacity={0.5}
|
binOpacity={0.2}
|
||||||
diameter={bin.diameter()}
|
diameter={bin.diameter()}
|
||||||
sidewallHeight={bin.sidewallHeight()}
|
sidewallHeight={bin.sidewallHeight()}
|
||||||
roofHeight={bin.roofHeight()}
|
roofHeight={bin.roofHeight()}
|
||||||
|
|
@ -62,6 +64,7 @@ export default function Bin3dView(props: Props){
|
||||||
)}
|
)}
|
||||||
{/* cables */}
|
{/* cables */}
|
||||||
<BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}/>
|
<BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}/>
|
||||||
|
<NodePointCloud bin={bin} nodes={nodeData} />
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* lighting */}
|
{/* lighting */}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { pond } from "protobuf-ts/pond"
|
||||||
import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring"
|
import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring"
|
||||||
// import { Color } from "three";
|
// import { Color } from "three";
|
||||||
import { NodeData } from "../../Data/BuildNodeData"
|
import { NodeData } from "../../Data/BuildNodeData"
|
||||||
import { tempToColour } from "bin/3dView/utils/tempToColour"
|
import { TempToColour } from "bin/3dView/utils/tempToColour"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: NodeData
|
node: NodeData
|
||||||
|
|
@ -193,12 +193,26 @@ export default function CableNode(props: Props){
|
||||||
return totalHeight / 2 - (index * ROW_HEIGHT) - ROW_HEIGHT / 2;
|
return totalHeight / 2 - (index * ROW_HEIGHT) - ROW_HEIGHT / 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nodeColour = () => {
|
||||||
|
if (node.excluded){
|
||||||
|
return "grey"
|
||||||
|
}
|
||||||
|
if (!node.inGrain){
|
||||||
|
return "white"
|
||||||
|
}
|
||||||
|
let c = TempToColour(node, lowerThreshold, upperThreshold, "node")
|
||||||
|
if (c !== null){
|
||||||
|
return c.getStyle()
|
||||||
|
}
|
||||||
|
return "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{(!node.inGrain || !showLabel) &&
|
{(!node.inGrain || !showLabel) &&
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Sphere geometry={geometry} position={node.position} colour={tempToColour(node, lowerThreshold, upperThreshold, "node")?.getStyle()}/>
|
<Sphere geometry={geometry} position={node.position} colour={nodeColour()}/>
|
||||||
{node.topNode && (
|
{node.topNode && (
|
||||||
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
|
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
238
src/bin/3dView/Systems/Heatmap/HeatMapAlpha.tsx
Normal file
238
src/bin/3dView/Systems/Heatmap/HeatMapAlpha.tsx
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
import { NodeData } from "bin/3dView/Data/BuildNodeData";
|
||||||
|
import { colourFade, TempToColour } from "bin/3dView/utils/tempToColour";
|
||||||
|
import { Bin } from "models";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { AdditiveBlending, Vector3 } from "three";
|
||||||
|
|
||||||
|
interface Props{
|
||||||
|
bin: Bin
|
||||||
|
nodes: NodeData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this is a work in progress, the heatmap generated may not be accurate so avoid using this component for now
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function Heatmap(props: Props){
|
||||||
|
const {bin, nodes} = props
|
||||||
|
//the steps that control how many 'layers' the heatmap will generate for each direction
|
||||||
|
const radialSteps = 18; // number of points across the diameter of the pin
|
||||||
|
const heightSteps = 20; // number of points up the side of the bin
|
||||||
|
const angleSteps = 40; // number of points around the circumfrence of the bin
|
||||||
|
|
||||||
|
const getHeatIntensity = (
|
||||||
|
temp: number,
|
||||||
|
lower: number,
|
||||||
|
upper: number,
|
||||||
|
fade: number
|
||||||
|
) => {
|
||||||
|
if (temp >= lower && temp <= upper) return 0;
|
||||||
|
|
||||||
|
const distance =
|
||||||
|
temp < lower ? lower - temp : temp - upper;
|
||||||
|
|
||||||
|
// clamp 0 → 1
|
||||||
|
return Math.min(1, distance / fade);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTemperatureAtPoint = (
|
||||||
|
point: Vector3,
|
||||||
|
nodes: NodeData[]
|
||||||
|
) => {
|
||||||
|
let totalWeight = 0;
|
||||||
|
let weightedTemp = 0;
|
||||||
|
|
||||||
|
//not sure if we should use a hard coded value or use a percentage of the bins diameter
|
||||||
|
//const maxDistance = 450; //tweak this (cm), it is the max distance that a node can influence the heatmap points
|
||||||
|
const maxDistance = bin.diameter() * 0.25;
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (!node.inGrain || node.excluded) return;
|
||||||
|
|
||||||
|
const distance = point.distanceTo(node.position);
|
||||||
|
|
||||||
|
const intensity = getHeatIntensity(
|
||||||
|
node.celcius,
|
||||||
|
bin.lowerTempThreshold(),
|
||||||
|
bin.upperTempThreshold(),
|
||||||
|
colourFade
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeMaxDistance = maxDistance * (0.5 + intensity);
|
||||||
|
// tweakable: 0.5–1.5 range
|
||||||
|
|
||||||
|
if (distance > nodeMaxDistance) return;
|
||||||
|
|
||||||
|
const weight = 1 / (distance * distance + 1);
|
||||||
|
|
||||||
|
totalWeight += weight;
|
||||||
|
weightedTemp += node.celcius * weight;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalWeight === 0) return null;
|
||||||
|
|
||||||
|
return weightedTemp / totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
//this gets the highest top node to prevent points from being rendered above it
|
||||||
|
//we could in the future us the multple top nodes to clamp within a cloumn around that cable which would give us a more realistic grain area
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const samplePoints = useMemo(() => {
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
const radius = bin.diameter() / 2;
|
||||||
|
const height = bin.sidewallHeight();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for (let yStep = 0; yStep < heightSteps; yStep++) {
|
||||||
|
const y = -height / 2 + (yStep / heightSteps) * height;
|
||||||
|
if (y > maxGrainY) continue;
|
||||||
|
|
||||||
|
for (let rStep = 0; rStep < radialSteps; rStep++) {
|
||||||
|
// const r = (rStep / radialSteps) * radius;
|
||||||
|
const r = Math.sqrt(rStep / radialSteps) * radius;
|
||||||
|
|
||||||
|
for (let aStep = 0; aStep < angleSteps; aStep++) {
|
||||||
|
const angle = (aStep / angleSteps) * Math.PI * 2;
|
||||||
|
|
||||||
|
const x = Math.cos(angle) * r;
|
||||||
|
const z = Math.sin(angle) * r;
|
||||||
|
|
||||||
|
const position = new Vector3(x, y, z);
|
||||||
|
|
||||||
|
const temp = getTemperatureAtPoint(position, nodes);
|
||||||
|
|
||||||
|
points.push({
|
||||||
|
position,
|
||||||
|
temp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
}, [bin, nodes]);
|
||||||
|
|
||||||
|
const { positions, colors, alphas } = useMemo(() => {
|
||||||
|
const positions: number[] = [];
|
||||||
|
const colors: number[] = [];
|
||||||
|
const alphas: number[] = [];
|
||||||
|
|
||||||
|
samplePoints.forEach((p) => {
|
||||||
|
if (p.temp === null) return;
|
||||||
|
const intensity = getHeatIntensity(
|
||||||
|
p.temp,
|
||||||
|
bin.lowerTempThreshold(),
|
||||||
|
bin.upperTempThreshold(),
|
||||||
|
colourFade
|
||||||
|
);
|
||||||
|
const alpha = Math.pow(intensity, 2); // try 2 → 3 for stronger fade
|
||||||
|
|
||||||
|
const virtualNode: NodeData = {
|
||||||
|
cableIndex: -1,
|
||||||
|
nodeIndex: -1,
|
||||||
|
radius: 0,
|
||||||
|
position: p.position,
|
||||||
|
celcius: p.temp,
|
||||||
|
inGrain: true,
|
||||||
|
excluded: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const color = TempToColour(
|
||||||
|
virtualNode,
|
||||||
|
bin.lowerTempThreshold(),
|
||||||
|
bin.upperTempThreshold(),
|
||||||
|
"heatmap"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!color) return;
|
||||||
|
|
||||||
|
// position
|
||||||
|
positions.push(p.position.x, p.position.y, p.position.z);
|
||||||
|
|
||||||
|
// color (normalized 0–1)
|
||||||
|
colors.push(color.r, color.g, color.b);
|
||||||
|
alphas.push(alpha)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
positions: new Float32Array(positions),
|
||||||
|
colors: new Float32Array(colors),
|
||||||
|
alphas: new Float32Array(alphas)
|
||||||
|
};
|
||||||
|
}, [samplePoints, bin]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<points>
|
||||||
|
<bufferGeometry>
|
||||||
|
<bufferAttribute
|
||||||
|
attach="attributes-position"
|
||||||
|
array={positions}
|
||||||
|
count={positions.length / 3}
|
||||||
|
itemSize={3}
|
||||||
|
/>
|
||||||
|
<bufferAttribute
|
||||||
|
attach="attributes-color"
|
||||||
|
array={colors}
|
||||||
|
count={colors.length / 3}
|
||||||
|
itemSize={3}
|
||||||
|
/>
|
||||||
|
<bufferAttribute
|
||||||
|
attach="attributes-alpha"
|
||||||
|
array={alphas}
|
||||||
|
itemSize={1}
|
||||||
|
/>
|
||||||
|
</bufferGeometry>
|
||||||
|
|
||||||
|
<shaderMaterial
|
||||||
|
transparent
|
||||||
|
depthWrite={false}
|
||||||
|
blending={AdditiveBlending}
|
||||||
|
vertexColors
|
||||||
|
uniforms={{ pointSize: { value: 0.7 } }}
|
||||||
|
vertexShader={`
|
||||||
|
attribute float alpha;
|
||||||
|
varying float vAlpha;
|
||||||
|
varying vec3 vColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vAlpha = alpha;
|
||||||
|
vColor = color;
|
||||||
|
|
||||||
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||||
|
gl_PointSize = 10.0; // tweak this instead of size prop
|
||||||
|
gl_Position = projectionMatrix * mvPosition;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
fragmentShader={`
|
||||||
|
varying float vAlpha;
|
||||||
|
varying vec3 vColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float dist = length(gl_PointCoord - vec2(0.5));
|
||||||
|
float falloff = smoothstep(0.5, 0.0, dist);
|
||||||
|
|
||||||
|
gl_FragColor = vec4(vColor, vAlpha * falloff);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</points>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { NodeData } from "bin/3dView/Data/BuildNodeData";
|
|
||||||
import { Bin } from "models";
|
|
||||||
|
|
||||||
interface Props{
|
|
||||||
bin: Bin
|
|
||||||
nodes: NodeData
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Heatmap(props: Props){
|
|
||||||
const {bin, nodes} = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
215
src/bin/3dView/Systems/Heatmap/NodePointCloud.tsx
Normal file
215
src/bin/3dView/Systems/Heatmap/NodePointCloud.tsx
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { NodeData } from "bin/3dView/Data/BuildNodeData";
|
||||||
|
import { colourFade } from "bin/3dView/utils/tempToColour";
|
||||||
|
import { Bin } from "models";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { AdditiveBlending, CanvasTexture } from "three";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bin: Bin;
|
||||||
|
nodes: NodeData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodePointCloud(props: Props) {
|
||||||
|
const { bin, nodes } = props;
|
||||||
|
// 🎛️ TUNING KNOBS
|
||||||
|
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;
|
||||||
|
|
||||||
|
const distance =
|
||||||
|
temp < lower ? lower - temp : temp - upper;
|
||||||
|
|
||||||
|
return Math.min(1, distance / fade);
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// GRAIN LIMIT
|
||||||
|
// -----------------------------
|
||||||
|
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
|
||||||
|
// -----------------------------
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (let rStep = 0; rStep < radialSteps; rStep++) {
|
||||||
|
// sqrt for even density
|
||||||
|
const r = Math.sqrt(rStep / radialSteps) * radius;
|
||||||
|
|
||||||
|
for (let pStep = 0; pStep < phiSteps; pStep++) {
|
||||||
|
// avoid poles clustering by not hitting exact 0/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 distXZ = Math.sqrt(x * x + z * z);
|
||||||
|
if (distXZ > radiusLimit) continue;
|
||||||
|
if (y > maxGrainY || y < minY) continue;
|
||||||
|
|
||||||
|
positions.push(x, y, z);
|
||||||
|
|
||||||
|
const falloff = 1 - r / radius;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
} else {
|
||||||
|
colors.push(0, 0, brightness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
positions: new Float32Array(positions),
|
||||||
|
colors: new Float32Array(colors),
|
||||||
|
};
|
||||||
|
}, [nodes, bin, maxGrainY]);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// RENDER
|
||||||
|
// -----------------------------
|
||||||
|
return (
|
||||||
|
<points>
|
||||||
|
<bufferGeometry>
|
||||||
|
<bufferAttribute
|
||||||
|
attach="attributes-position"
|
||||||
|
array={positions}
|
||||||
|
count={positions.length / 3}
|
||||||
|
itemSize={3}
|
||||||
|
/>
|
||||||
|
<bufferAttribute
|
||||||
|
attach="attributes-color"
|
||||||
|
array={colors}
|
||||||
|
count={colors.length / 3}
|
||||||
|
itemSize={3}
|
||||||
|
/>
|
||||||
|
</bufferGeometry>
|
||||||
|
|
||||||
|
<pointsMaterial
|
||||||
|
size={POINT_SIZE}
|
||||||
|
map={circleTexture}
|
||||||
|
vertexColors
|
||||||
|
transparent
|
||||||
|
opacity={OPACITY}
|
||||||
|
depthWrite={false}
|
||||||
|
blending={AdditiveBlending}
|
||||||
|
/>
|
||||||
|
</points>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ const GREEN = new Color("#52c41a");
|
||||||
const BLUE = new Color("#3399ff");
|
const BLUE = new Color("#3399ff");
|
||||||
const RED = new Color("#ff4d4f");
|
const RED = new Color("#ff4d4f");
|
||||||
|
|
||||||
const colourFade = 20;
|
export const colourFade = 20;
|
||||||
const minimumLightness = 0.3;
|
const minimumLightness = 0.3;
|
||||||
const lightnessRange = 0.2;
|
const lightnessRange = 0.2;
|
||||||
const minimumSaturation = 0.7;
|
const minimumSaturation = 0.7;
|
||||||
|
|
@ -30,7 +30,7 @@ const getVisualIntensity = (distance: number, mode: ColourMode) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function tempToColour(
|
export function TempToColour(
|
||||||
node: NodeData,
|
node: NodeData,
|
||||||
lower: number,
|
lower: number,
|
||||||
upper: number,
|
upper: number,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue