did some refactoring so the node and cable data is computed in the data layer so that it is accessible by anything in the bin to make way for a heatmap

This commit is contained in:
csawatzky 2026-04-16 16:27:08 -06:00
parent ff64f8594c
commit 6c87ed03ed
13 changed files with 572 additions and 363 deletions

View file

@ -1,201 +0,0 @@
import GrainCable, { CableData } from "./GrainCable";
import { avg } from "utils";
import { Vector3 } from "three";
import { useMemo } from "react";
import { Bin } from "models";
import React from "react";
interface CableOrbit {
orbit: number;
radius: number; // in cm
expectedCount: number;
assignedCount?: number;
}
interface Props {
bin: Bin
binCenter: Vector3
}
export default function BinCables(props: Props){
const {bin, binCenter} = props
const getLayoutFromDiameter = (diameterCm: number): CableOrbit[] => {
const diameterFt = diameterCm / 30.48;
let summaries: { Orbit: number; DistanceFromCenter: number; NumberOfCables: number }[] = [];
if (diameterFt < 24) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
} else if (diameterFt < 36) {
summaries.push({ Orbit: 1, DistanceFromCenter: 7, NumberOfCables: 3 });
} else if (diameterFt < 42) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
} else if (diameterFt < 48) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 14, NumberOfCables: 4 });
} else if (diameterFt < 54) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 16, NumberOfCables: 5 });
} else if (diameterFt < 60) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
} else if (diameterFt < 72) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 20, NumberOfCables: 7 });
} else if (diameterFt < 78) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 27, NumberOfCables: 9 });
} else if (diameterFt < 90) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 29, NumberOfCables: 10 });
} else if (diameterFt < 105) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
summaries.push({ Orbit: 2, DistanceFromCenter: 36, NumberOfCables: 12 });
} else {
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
summaries.push({ Orbit: 2, DistanceFromCenter: 26.5, NumberOfCables: 9 });
summaries.push({ Orbit: 3, DistanceFromCenter: 44, NumberOfCables: 14 });
}
return summaries.map(s => ({
orbit: s.Orbit,
radius: s.DistanceFromCenter * 30.48, // ft → cm
expectedCount: s.NumberOfCables
}));
};
const distributeCables = (cableCount: number, layout: CableOrbit[]) => {
const totalExpected = layout.reduce((sum, o) => sum + o.expectedCount, 0);
let assigned = layout.map(o => ({
...o,
assignedCount: Math.round((o.expectedCount / totalExpected) * cableCount)
}));
// normalize rounding
let currentTotal = assigned.reduce((sum, o) => sum + (o.assignedCount ?? 0), 0);
while (currentTotal < cableCount) {
assigned[assigned.length - 1].assignedCount!++;
currentTotal++;
}
while (currentTotal > cableCount) {
assigned[assigned.length - 1].assignedCount!--;
currentTotal--;
}
return assigned;
};
const getTopY = (radiusFromCenter: number) => {
const binRadius = bin.diameter() / 2;
const distanceFromEdge = binRadius - radiusFromCenter;
const angleRad = bin.roofAngle() * (Math.PI / 180);
const roofRise = Math.tan(angleRad) * distanceFromEdge;
return bin.sidewallHeight() / 2 + roofRise;
};
const getBottomY = (radiusFromCenter: number) => {
const binRadius = bin.diameter() / 2;
const distanceFromCenter = binRadius - radiusFromCenter;
const angleRad = bin.hopperAngle() * (Math.PI / 180);
const hopperDrop = Math.tan(angleRad) * distanceFromCenter;
const clearance = 60 //this is in cm
return -bin.sidewallHeight() / 2 - hopperDrop + clearance;
};
const buildCablePositions = (bin: Bin): CableData[] => {
const cables = [...(bin.status.grainCables ?? [])];
if (cables.length === 0) return [];
// optional: sort cables (better visual consistency later)
//cables.sort((a, b) => b.celcius.length - a.celcius.length);
cables.sort((a, b) => {
//if the cables have the same number of nodes
if (b.celcius.length === a.celcius.length) {
//sort by temp only last and any type that has humidity first
if ((avg(a.relativeHumidity) === 0) && (avg(b.relativeHumidity) !== 0)) {
return 1;
} else if ((avg(b.relativeHumidity) === 0) && (avg(a.relativeHumidity) !== 0)) {
return -1;
}
else {
return a.key > b.key ? 1 : -1;
}
}
//otherwise sort by the longest cable first
return b.celcius.length - a.celcius.length;
});
const layout = getLayoutFromDiameter(bin.diameter());
const distributed = distributeCables(cables.length, layout);
const cableRadius = 5;
const result: CableData[] = [];
let cableIndex = 0;
distributed.forEach(orbit => {
const count = orbit.assignedCount ?? 0;
const topY = getTopY(orbit.radius)
const bottomY = getBottomY(orbit.radius)
if (orbit.orbit === 0 && count > 0) {
const cable = cables[cableIndex++];
result.push({
radius: cableRadius,
topPosition: new Vector3(0, topY, 0),
bottomPosition: new Vector3(0, bottomY, 0),
grainCable: cable
// optionally attach cable data later
});
return;
}
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const x = Math.cos(angle) * orbit.radius;
const z = Math.sin(angle) * orbit.radius;
const cable = cables[cableIndex++];
result.push({
radius: cableRadius,
topPosition: new Vector3(x,topY,z),
bottomPosition: new Vector3(x,bottomY,z),
grainCable: cable
});
}
});
return result;
};
const cables3D = useMemo(() => {
return buildCablePositions(bin);
}, [bin]);
return (
<React.Fragment>
{cables3D.map((cable, i) => (
<GrainCable key={"cable " + i} cable={cable} bin={bin} binCenter={binCenter}/>
))}
</React.Fragment>
)
}

View file

@ -1,91 +0,0 @@
import Cylinder, { CylinderGeometry } from "3dModels/Shapes/3D/Cylinder"
import { pond } from "protobuf-ts/pond"
import React from "react"
import { Vector3 } from "three"
import CableNode, { NodeData } from "./CableNode"
import { Bin } from "models"
export interface CableData {
radius: number
topPosition: Vector3
bottomPosition: Vector3
grainCable: pond.GrainCable
radialSegments?: number
heightSegments?: number
}
interface Props {
cable: CableData
bin: Bin
binCenter: Vector3
}
export default function GrainCable(props: Props) {
const {cable, bin, binCenter} = props
const calculateHeight = () => {
return cable.topPosition.distanceTo(cable.bottomPosition)
}
const calculateCenter = () => {
return cable.bottomPosition.clone()
.add(
cable.topPosition.clone()
.sub(cable.bottomPosition)
.multiplyScalar(0.5)
);
};
const buildCableNodes = () => {
const nodeRadius = 20
const grainCable = cable.grainCable
const nodeCount = grainCable.celcius.length;
if (nodeCount === 0) return [];
const bottomY = cable.bottomPosition.y;
const topY = cable.topPosition.y;
const height = topY - bottomY;
const spacing = height / nodeCount;
let nodeData: NodeData[] = []
grainCable.celcius.forEach((celcius,i) => {
const nodeY = bottomY + (i * spacing);
const humidity = grainCable.relativeHumidity[i] || undefined;
const moisture = grainCable.moisture[i] || undefined;
nodeData.push({
radius: nodeRadius,
position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z),
celcius: celcius,
humidity: humidity,
moisture: moisture,
topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index
excluded: grainCable.excludedNodes.includes(i),
inGrain: i+1 <= grainCable.topNode
})
})
return nodeData
}
const geometry = React.useMemo(() => ({
radiusTop: cable.radius,
radiusBottom: cable.radius,
height: calculateHeight(),
radialSegments: cable.radialSegments ?? 10,
heightSegments: cable.heightSegments ?? 2
} as CylinderGeometry), [cable]);
return (
<React.Fragment>
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
{buildCableNodes().map((node, i) => (
<CableNode key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
))}
</React.Fragment>
)
}

View file

@ -0,0 +1,194 @@
import { Bin } from "models";
import { pond } from "protobuf-ts/pond";
import { Vector3 } from "three";
import { avg } from "utils";
export interface CableData {
cableIndex: number
radius: number
topPosition: Vector3
bottomPosition: Vector3
grainCable: pond.GrainCable
radialSegments?: number
heightSegments?: number
}
export interface CableOrbit {
orbit: number;
radius: number; // in cm
expectedCount: number;
assignedCount?: number;
}
function getLayoutFromDiameter(diameterCm: number): CableOrbit[] {
const diameterFt = diameterCm / 30.48;
let summaries: { Orbit: number; DistanceFromCenter: number; NumberOfCables: number }[] = [];
if (diameterFt < 24) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
} else if (diameterFt < 36) {
summaries.push({ Orbit: 1, DistanceFromCenter: 7, NumberOfCables: 3 });
} else if (diameterFt < 42) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
} else if (diameterFt < 48) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 14, NumberOfCables: 4 });
} else if (diameterFt < 54) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 16, NumberOfCables: 5 });
} else if (diameterFt < 60) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
} else if (diameterFt < 72) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 20, NumberOfCables: 7 });
} else if (diameterFt < 78) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 27, NumberOfCables: 9 });
} else if (diameterFt < 90) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 29, NumberOfCables: 10 });
} else if (diameterFt < 105) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
summaries.push({ Orbit: 2, DistanceFromCenter: 36, NumberOfCables: 12 });
} else {
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
summaries.push({ Orbit: 2, DistanceFromCenter: 26.5, NumberOfCables: 9 });
summaries.push({ Orbit: 3, DistanceFromCenter: 44, NumberOfCables: 14 });
}
return summaries.map(s => ({
orbit: s.Orbit,
radius: s.DistanceFromCenter * 30.48, // ft → cm
expectedCount: s.NumberOfCables
}));
};
function distributeCables(cableCount: number, layout: CableOrbit[]) {
const totalExpected = layout.reduce((sum, o) => sum + o.expectedCount, 0);
let assigned = layout.map(o => ({
...o,
assignedCount: Math.round((o.expectedCount / totalExpected) * cableCount)
}));
// normalize rounding
let currentTotal = assigned.reduce((sum, o) => sum + (o.assignedCount ?? 0), 0);
while (currentTotal < cableCount) {
assigned[assigned.length - 1].assignedCount!++;
currentTotal++;
}
while (currentTotal > cableCount) {
assigned[assigned.length - 1].assignedCount!--;
currentTotal--;
}
return assigned;
};
function getTopY(radiusFromCenter: number, bin: Bin) {
const binRadius = bin.diameter() / 2;
const distanceFromEdge = binRadius - radiusFromCenter;
const angleRad = bin.roofAngle() * (Math.PI / 180);
const roofRise = Math.tan(angleRad) * distanceFromEdge;
return bin.sidewallHeight() / 2 + roofRise;
};
function getBottomY(radiusFromCenter: number, bin: Bin) {
const binRadius = bin.diameter() / 2;
const distanceFromCenter = binRadius - radiusFromCenter;
const angleRad = bin.hopperAngle() * (Math.PI / 180);
const hopperDrop = Math.tan(angleRad) * distanceFromCenter;
const clearance = 60 //this is in cm
return -bin.sidewallHeight() / 2 - hopperDrop + clearance;
};
export function BuildCableData(bin: Bin): CableData[] {
const cables = [...(bin.status.grainCables ?? [])];
if (cables.length === 0) return [];
// optional: sort cables (better visual consistency later)
//cables.sort((a, b) => b.celcius.length - a.celcius.length);
cables.sort((a, b) => {
//if the cables have the same number of nodes
if (b.celcius.length === a.celcius.length) {
//sort by temp only last and any type that has humidity first
if ((avg(a.relativeHumidity) === 0) && (avg(b.relativeHumidity) !== 0)) {
return 1;
} else if ((avg(b.relativeHumidity) === 0) && (avg(a.relativeHumidity) !== 0)) {
return -1;
}
else {
return a.key > b.key ? 1 : -1;
}
}
//otherwise sort by the longest cable first
return b.celcius.length - a.celcius.length;
});
const layout = getLayoutFromDiameter(bin.diameter());
const distributed = distributeCables(cables.length, layout);
const cableRadius = 5;
const result: CableData[] = [];
let cableIndex = 0;
distributed.forEach(orbit => {
const count = orbit.assignedCount ?? 0;
const topY = getTopY(orbit.radius, bin)
const bottomY = getBottomY(orbit.radius, bin)
if (orbit.orbit === 0 && count > 0) {
const cable = cables[cableIndex];
if (!cable) return
result.push({
cableIndex: cableIndex,
radius: cableRadius,
topPosition: new Vector3(0, topY, 0),
bottomPosition: new Vector3(0, bottomY, 0),
grainCable: cable
});
cableIndex++
return;
}
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const x = Math.cos(angle) * orbit.radius;
const z = Math.sin(angle) * orbit.radius;
const cable = cables[cableIndex];
result.push({
cableIndex: cableIndex,
radius: cableRadius,
topPosition: new Vector3(x,topY,z),
bottomPosition: new Vector3(x,bottomY,z),
grainCable: cable
});
cableIndex++
}
});
return result;
};

View file

@ -0,0 +1,60 @@
import { Vector3 } from "three"
import { CableData } from "./BuildCableData"
export interface NodeData {
cableIndex: number
nodeIndex: number
radius: number
position: Vector3
celcius: number
humidity?: number
moisture?: number
topNode?: boolean
excluded?: boolean
inGrain?: boolean
}
export function BuildNodeData(cables: CableData[]): NodeData[] {
const nodeRadius = 20
let nodeData: NodeData[] = []
cables.forEach((cable, cableIndex) => {
const grainCable = cable.grainCable
const nodeCount = grainCable.celcius.length;
if (nodeCount === 0) return;
const bottomY = cable.bottomPosition.y;
const topY = cable.topPosition.y;
const height = topY - bottomY;
const spacing = height / nodeCount;
grainCable.celcius.forEach((celcius,i) => {
const nodeY = bottomY + (i * spacing);
const humidity = grainCable.relativeHumidity[i] || undefined;
const moisture = grainCable.moisture[i] || undefined;
nodeData.push({
cableIndex: cableIndex,
nodeIndex: i,
radius: nodeRadius,
position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z),
celcius: celcius,
humidity: humidity,
moisture: moisture,
topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index
excluded: grainCable.excludedNodes?.includes(i) ?? false,
inGrain: i+1 <= grainCable.topNode
// optional: (may want to add this to the node data later)
// normalizedHeight: (nodeY - bottomY) / height
})
})
})
return nodeData
}

View file

@ -1,11 +1,13 @@
import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls";
import { Canvas } from "@react-three/fiber";
import { Bin } from "models";
import BinShell from "./BinParts/BinShell";
import GrainFillFlat from "./BinInventory/GrainFillFlat";
import BinCables from "./BinCables/BinCables";
import BinShell from "../BinParts/BinShell";
import GrainFillFlat from "../Systems/Inventory/GrainFillFlat";
import BinCables from "../Systems/Cables/BinCables";
import { Vector3 } from "three";
import { useMemo } from "react";
import { BuildCableData } from "../Data/BuildCableData";
import { BuildNodeData } from "../Data/BuildNodeData";
interface Props {
/**
@ -30,8 +32,8 @@ export default function Bin3dView(props: Props){
// it might even work better because we can control the smoothness easier with it being only one mesh rather than 3
const {bin, scale = 100, fillPercent} = props
const binCenter = useMemo(() => new Vector3(0, 0, 0), []);
console.log(bin)
const cableData = useMemo(() => BuildCableData(bin), [bin]);
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
return (
<Canvas>
@ -46,6 +48,7 @@ export default function Bin3dView(props: Props){
sidewallHeight={bin.sidewallHeight()}
roofHeight={bin.roofHeight()}
hopperHeight={bin.hopperHeight()}
/>
{/* grain - cylinder*/}
@ -58,7 +61,7 @@ export default function Bin3dView(props: Props){
/>
)}
{/* cables */}
<BinCables bin={bin} binCenter={binCenter}/>
<BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}/>
</group>
{/* lighting */}

View file

@ -0,0 +1,28 @@
import GrainCable from "./GrainCable";
import { Vector3 } from "three";
import { Bin } from "models";
import React from "react";
import { CableData } from "../../Data/BuildCableData";
import { NodeData } from "../../Data/BuildNodeData";
interface Props {
cableData: CableData[]
nodeData: NodeData[]
bin: Bin
binCenter: Vector3
}
export default function BinCables(props: Props){
const {bin, binCenter, cableData, nodeData} = props
return (
<React.Fragment>
{cableData.map((cable, i) => {
const cableNodes = nodeData.filter(n => n.cableIndex === cable.cableIndex);
return (
<GrainCable key={"cable " + i} cable={cable} nodes={cableNodes} bin={bin} binCenter={binCenter}/>
)}
)}
</React.Fragment>
)
}

View file

@ -1,24 +1,16 @@
import Sphere, { SphereGeometry } from "3dModels/Shapes/3D/Sphere"
import { useFrame, useThree } from "@react-three/fiber"
import { Billboard, Text } from "@react-three/drei"
import React, { useMemo, useRef, useState } from "react"
import React, { useMemo, useState } from "react"
import { Euler, Group, Vector3 } from "three"
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"
import { quack } from "protobuf-ts/quack"
import { useGlobalState } from "providers"
import { pond } from "protobuf-ts/pond"
import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring"
export interface NodeData {
radius: number
position: Vector3
celcius: number
humidity?: number
moisture?: number
topNode?: boolean
excluded?: boolean
inGrain?: boolean
}
// import { Color } from "three";
import { NodeData } from "../../Data/BuildNodeData"
import { tempToColour } from "bin/3dView/utils/tempToColour"
interface Props {
node: NodeData
@ -36,6 +28,13 @@ export default function CableNode(props: Props){
const ROW_HEIGHT = 35;
const PADDING = 20;
//node colour varables for adjusting lighness and saturation for the thresholds
// const colourFade = 20 //this number is the temp diff above or below the thresholds that the node will reach its maximum red or blue intensity
// const minimumLightness = 0.3 //the minimum brightness of the colour, to low would approach black
// const lightnessRange = 0.2 //how much can the node brighten, to high will allow the node to turn white
// const minimumSaturation = 0.7
// const saturationRange = 0.8
useFrame(() => {
//use the cameras distance to the center of the bin
const distance = camera.position.distanceTo(binCenter)
@ -64,22 +63,68 @@ export default function CableNode(props: Props){
return temp
}
const nodeColour = () => {
if (!node.inGrain) return "white";
// const nodeColour = () => {
// if (!node.inGrain) return "white";
// if (node.excluded) return "grey";
const lower = lowerThreshold;
const upper = upperThreshold;
// const lower = lowerThreshold;
// const upper = upperThreshold;
// const temp = node.celcius;
if (lower !== undefined && node.celcius < lower) {
return "#3399ff"; // blue
}
// const GREEN = new Color("#52c41a");
// const BLUE = new Color("#3399ff");
// const RED = new Color("#ff4d4f");
if (upper !== undefined && node.celcius > upper) {
return "#ff4d4f"; // red
}
// if (temp >= lower && temp <= upper) {
// return GREEN.getStyle();
// }
return "#52c41a"; // green
};
// // shared helper logic concept:
// const getCloseness = (distance: number) => {
// const normalized = Math.min(1, distance / colourFade);
// return 1 - normalized; // 1 = near threshold, 0 = far
// };
// const color = new Color();
// // BELOW lower (blue)
// if (temp < lower) {
// const distance = lower - temp;
// const closeness = getCloseness(distance);
// const hsl = { h: 0, s: 1, l: 1 };
// BLUE.getHSL(hsl);
// const lightness = (lightnessRange * closeness) + minimumLightness;
// // 🔥 NEW: saturation as second intensity layer
// const saturation = (saturationRange * (1 - closeness)) + minimumSaturation;
// color.setHSL(hsl.h, saturation, lightness);
// return color.getStyle();
// }
// // ABOVE upper (red)
// if (temp > upper) {
// const distance = temp - upper;
// const closeness = getCloseness(distance);
// const hsl = { h: 0, s: 1, l: 1 };
// RED.getHSL(hsl);
// const lightness = (lightnessRange * closeness) + minimumLightness;
// // 🔥 NEW: saturation as second intensity layer
// const saturation = (saturationRange * (1 - closeness)) + minimumSaturation;
// color.setHSL(hsl.h, saturation, lightness);
// return color.getStyle();
// }
// return GREEN.getStyle();
// };
const rows = useMemo(() => {
const r: { text: string; color: string }[] = [];
@ -153,7 +198,7 @@ export default function CableNode(props: Props){
<React.Fragment>
{(!node.inGrain || !showLabel) &&
<React.Fragment>
<Sphere geometry={geometry} position={node.position} colour={nodeColour()}/>
<Sphere geometry={geometry} position={node.position} colour={tempToColour(node, lowerThreshold, upperThreshold, "node")?.getStyle()}/>
{node.topNode && (
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
)}

View file

@ -0,0 +1,47 @@
import Cylinder, { CylinderGeometry } from "3dModels/Shapes/3D/Cylinder"
import React from "react"
import { Vector3 } from "three"
import CableNode from "./CableNode"
import { Bin } from "models"
import { CableData } from "../../Data/BuildCableData"
import { NodeData } from "../../Data/BuildNodeData"
interface Props {
cable: CableData
nodes: NodeData[]
bin: Bin
binCenter: Vector3
}
export default function GrainCable(props: Props) {
const {cable, nodes, bin, binCenter} = props
const calculateHeight = () => {
return cable.topPosition.distanceTo(cable.bottomPosition)
}
const calculateCenter = () => {
return cable.bottomPosition.clone()
.add(
cable.topPosition.clone()
.sub(cable.bottomPosition)
.multiplyScalar(0.5)
);
};
const geometry = React.useMemo(() => ({
radiusTop: cable.radius,
radiusBottom: cable.radius,
height: calculateHeight(),
radialSegments: cable.radialSegments ?? 10,
heightSegments: cable.heightSegments ?? 2
} as CylinderGeometry), [cable]);
return (
<React.Fragment>
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
{nodes.map((node, i) => (
<CableNode key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
))}
</React.Fragment>
)
}

View file

@ -0,0 +1,16 @@
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 (
<>
</>
)
}

View file

@ -0,0 +1,68 @@
import { Color } from "three";
import { NodeData } from "../Data/BuildNodeData";
type ColourMode = "node" | "heatmap";
const GREEN = new Color("#52c41a");
const BLUE = new Color("#3399ff");
const RED = new Color("#ff4d4f");
const colourFade = 20;
const minimumLightness = 0.3;
const lightnessRange = 0.2;
const minimumSaturation = 0.7;
const saturationRange = 0.8;
const getTemperatureIntensity = (temp: number, lower: number, upper: number) => {
if (temp >= lower && temp <= upper) return 0;
if (temp < lower) return lower - temp;
return temp - upper;
}
const getVisualIntensity = (distance: number, mode: ColourMode) => {
// 1 = near threshold, 0 = far
const intensity = Math.min(1, distance / colourFade);
switch(mode){
case "node":
return 1 - intensity
case "heatmap":
return intensity
}
};
export function tempToColour(
node: NodeData,
lower: number,
upper: number,
mode: ColourMode
): Color | null {
if (!node.inGrain || node.excluded) return null;
const temp = node.celcius;
// in-threshold behavior
if (temp >= lower && temp <= upper) {
return mode === "heatmap" ? null : GREEN;
}
const distance = getTemperatureIntensity(temp, lower, upper);
const intensity = getVisualIntensity(distance, mode);
const color = new Color();
const hsl = { h: 0, s: 1, l: 1 };
if (temp < lower) {
BLUE.getHSL(hsl);
} else {
RED.getHSL(hsl);
}
color.setHSL(
hsl.h,
(saturationRange * intensity) + minimumSaturation,
(lightnessRange * intensity) + minimumLightness
);
return color;
}