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:
parent
ff64f8594c
commit
6c87ed03ed
13 changed files with 572 additions and 363 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
194
src/bin/3dView/Data/BuildCableData.ts
Normal file
194
src/bin/3dView/Data/BuildCableData.ts
Normal 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;
|
||||
};
|
||||
60
src/bin/3dView/Data/BuildNodeData.ts
Normal file
60
src/bin/3dView/Data/BuildNodeData.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
28
src/bin/3dView/Systems/Cables/BinCables.tsx
Normal file
28
src/bin/3dView/Systems/Cables/BinCables.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}/>
|
||||
)}
|
||||
47
src/bin/3dView/Systems/Cables/GrainCable.tsx
Normal file
47
src/bin/3dView/Systems/Cables/GrainCable.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
16
src/bin/3dView/Systems/Heatmap/Heatmap.tsx
Normal file
16
src/bin/3dView/Systems/Heatmap/Heatmap.tsx
Normal 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 (
|
||||
<>
|
||||
</>
|
||||
)
|
||||
}
|
||||
68
src/bin/3dView/utils/tempToColour.ts
Normal file
68
src/bin/3dView/utils/tempToColour.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue