diff --git a/src/bin/3dView/Data/BuildCableData.ts b/src/bin/3dView/Data/BuildCableData.ts
index 3b2c673..7fce813 100644
--- a/src/bin/3dView/Data/BuildCableData.ts
+++ b/src/bin/3dView/Data/BuildCableData.ts
@@ -1,3 +1,4 @@
+import { RadiansToDegrees } from "common/TrigFunctions";
import { Bin } from "models";
import { GrainCable } from "models/GrainCable";
import { pond } from "protobuf-ts/pond";
@@ -21,6 +22,19 @@ export interface CableOrbit {
assignedCount?: number;
}
+/**
+ * Effective bin dimensions used for cable placement.
+ * Passed in from Bin3dView so that default fallback values are applied
+ * consistently — cables will always match the rendered shell even when
+ * the bin has no advanced dimensions configured.
+ */
+export interface BinDims {
+ diameter: number
+ sidewallHeight: number
+ roofHeight: number
+ hopperHeight: number
+}
+
function getLayoutFromDiameter(diameterCm: number): CableOrbit[] {
const diameterFt = diameterCm / 30.48;
@@ -93,34 +107,36 @@ function distributeCables(cableCount: number, layout: CableOrbit[]) {
return assigned;
};
-function getTopY(radiusFromCenter: number, bin: Bin) {
- const binRadius = bin.diameter() / 2;
+function getTopY(radiusFromCenter: number, dims: BinDims) {
+ const binRadius = dims.diameter / 2;
const distanceFromEdge = binRadius - radiusFromCenter;
- const angleRad = bin.roofAngle() * (Math.PI / 180);
+ // Use the real roof angle when available; when dimensions aren't configured
+ // the angle will be 0 which gives a flat cable top — correct for a default shape.
+ const angleRad = Math.atan((dims.roofHeight) / (binRadius));
const roofRise = Math.tan(angleRad) * distanceFromEdge;
- return bin.sidewallHeight() / 2 + roofRise;
+ return dims.sidewallHeight / 2 + roofRise;
};
-function getBottomY(radiusFromCenter: number, bin: Bin) {
- const binRadius = bin.diameter() / 2;
+function getBottomY(radiusFromCenter: number, dims: BinDims) {
+ const binRadius = dims.diameter / 2;
const distanceFromCenter = binRadius - radiusFromCenter;
- const angleRad = bin.hopperAngle() * (Math.PI / 180);
+ const angleRad = Math.atan((dims.hopperHeight) / (binRadius))
const hopperDrop = Math.tan(angleRad) * distanceFromCenter;
- const clearance = 60 //this is in cm
+ const clearance = 60; // cm
- return -bin.sidewallHeight() / 2 - hopperDrop + clearance;
+ return -dims.sidewallHeight / 2 - hopperDrop + clearance;
};
-export function BuildCableData(bin: Bin): CableData[] {
+export function BuildCableData(bin: Bin, dims: BinDims): CableData[] {
const cables = [...(bin.status.grainCables ?? [])];
if (cables.length === 0) return [];
@@ -144,10 +160,9 @@ export function BuildCableData(bin: Bin): CableData[] {
return b.celcius.length - a.celcius.length;
});
- const layout = getLayoutFromDiameter(bin.diameter());
+ const layout = getLayoutFromDiameter(dims.diameter);
const distributed = distributeCables(cables.length, layout);
-
const cableRadius = 5;
const result: CableData[] = [];
@@ -157,12 +172,12 @@ export function BuildCableData(bin: Bin): CableData[] {
distributed.forEach(orbit => {
const count = orbit.assignedCount ?? 0;
- const topY = getTopY(orbit.radius, bin)
- const bottomY = getBottomY(orbit.radius, bin)
+ const topY = getTopY(orbit.radius, dims);
+ const bottomY = getBottomY(orbit.radius, dims);
if (orbit.orbit === 0 && count > 0) {
const cable = cables[cableIndex];
- if (!cable) return
+ if (!cable) return;
result.push({
cableIndex: cableIndex,
radius: cableRadius,
@@ -170,7 +185,7 @@ export function BuildCableData(bin: Bin): CableData[] {
bottomPosition: new Vector3(0, bottomY, 0),
grainCable: cable
});
- cableIndex++
+ cableIndex++;
return;
}
@@ -183,11 +198,11 @@ export function BuildCableData(bin: Bin): CableData[] {
result.push({
cableIndex: cableIndex,
radius: cableRadius,
- topPosition: new Vector3(x,topY,z),
- bottomPosition: new Vector3(x,bottomY,z),
+ topPosition: new Vector3(x, topY, z),
+ bottomPosition: new Vector3(x, bottomY, z),
grainCable: cable
});
- cableIndex++
+ cableIndex++;
}
});
diff --git a/src/bin/3dView/Scene/Bin3dView.tsx b/src/bin/3dView/Scene/Bin3dView.tsx
index 3db6980..f93c239 100644
--- a/src/bin/3dView/Scene/Bin3dView.tsx
+++ b/src/bin/3dView/Scene/Bin3dView.tsx
@@ -17,7 +17,14 @@ import NodePointCloud from "../Systems/Heatmap/NodePointCloud";
import { Box } from "@mui/material";
import MoistureHeatMap from "../Systems/Heatmap/MoistureHeatmap";
import { grainYBoundsFromFillPercent } from "../utils/grainFillBounds";
-// import { GrainCable } from "models/GrainCable";
+import { RadiansToDegrees } from "common/TrigFunctions";
+// import { GrainCable } from "models/GrainCable"
+
+// Fallback dimensions used when a bin has no advanced dimensions configured.
+// Chosen to resemble a typical mid-size grain bin.
+const DEFAULT_ROOF_HEIGHT_CM = 200; // 2 m
+const DEFAULT_FLAT_HEIGHT_CM = 0; // flat bottom
+const DEFAULT_HOPPER_HEIGHT_CM = 280; // hopper bottom
interface Props {
bin: Bin
@@ -46,18 +53,28 @@ interface Props {
export default function Bin3dView(props: Props) {
const { bin, scale = 100, fillPercent, nodeClick, showTempHeatmap, showGrain, showHotspots, showResetButton, showMoistureHeatmap, showTemp, showMoisture, xOffset } = props
+ // Resolve effective dimensions, falling back to defaults when advanced
+ // dimensions have not been configured (bin methods return 0 in that case).
+ const dims = useMemo(() => ({
+ diameter: bin.diameter(),
+ sidewallHeight: bin.sidewallHeight() || bin.diameter() + 50, //calculated using the diameter so 'default' bins always have the same shape
+ roofHeight: bin.roofHeight() || DEFAULT_ROOF_HEIGHT_CM,
+ hopperHeight: bin.hopperHeight() || (bin.binShape() === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? DEFAULT_HOPPER_HEIGHT_CM : DEFAULT_FLAT_HEIGHT_CM),
+ }), [bin]);
+
// Compute the initial camera radius so the full bin fits in frame.
// Uses the perspective camera FOV (default 75°) with a padding factor.
// Both the diameter and total height are considered so neither is clipped.
const initialCameraRadius = useMemo(() => {
- const totalHeight = (bin.sidewallHeight() + bin.roofHeight() + (bin.hopperHeight() ?? 0)) / scale;
- const diameter = bin.diameter() / scale;
+ const totalHeight = (dims.sidewallHeight + dims.roofHeight + dims.hopperHeight) / scale;
+ const diameter = dims.diameter / scale;
const largestDim = Math.max(totalHeight, diameter);
const fovRad = (75 * Math.PI) / 180;
const padding = 1.3; // 30% breathing room
return (largestDim / (2 * Math.tan(fovRad / 2))) * padding;
- }, [bin, scale]);
- const cableData = useMemo(() => BuildCableData(bin), [bin]);
+ }, [dims, scale]);
+
+ const cableData = useMemo(() => BuildCableData(bin, dims), [bin, dims]);
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
const isCableInventory =
@@ -71,12 +88,12 @@ export default function Bin3dView(props: Props) {
const flatGrainBounds = useMemo(() => {
if (isCableInventory || fillPercent === undefined) return undefined;
return grainYBoundsFromFillPercent(
- bin.diameter(),
- bin.sidewallHeight(),
- bin.hopperHeight() ?? 0,
+ dims.diameter,
+ dims.sidewallHeight,
+ dims.hopperHeight,
fillPercent
);
- }, [isCableInventory, fillPercent, bin]);
+ }, [isCableInventory, fillPercent, dims]);
// Internal ref — wired to OrbitCameraControls and called by CameraOverlay
const resetCameraFn = React.useRef<(() => void) | null>(null);
@@ -85,20 +102,20 @@ export default function Bin3dView(props: Props) {
if (isCableInventory) {
return (
)
} else if (fillPercent) {
return (
@@ -122,10 +139,10 @@ export default function Bin3dView(props: Props) {
binMetalness={0.5}
binRoughness={0.7}
binOpacity={0.2}
- diameter={bin.diameter()}
- sidewallHeight={bin.sidewallHeight()}
- roofHeight={bin.roofHeight()}
- hopperHeight={bin.hopperHeight()}
+ diameter={dims.diameter}
+ sidewallHeight={dims.sidewallHeight}
+ roofHeight={dims.roofHeight}
+ hopperHeight={dims.hopperHeight}
renderOrder={4}
/>
@@ -145,24 +162,26 @@ export default function Bin3dView(props: Props) {
{showHotspots && }
- {/* note that the heatmaps insternally use render order 1,2, and 3 for the three seperate coloured meshes */}
+ {/* note that the heatmaps internally use render order 1,2, and 3 for the three separate coloured meshes */}
{showTempHeatmap && (
)}
{showMoistureHeatmap && (
-
)}
-
+
@@ -171,6 +190,10 @@ export default function Bin3dView(props: Props) {
+ {/* Overlay — must be inside
)
}
diff --git a/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx b/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx
index 19157af..12ea832 100644
--- a/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx
+++ b/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx
@@ -3,9 +3,11 @@ import * as THREE from "three";
import { Bin } from "models";
import { NodeData } from "../../Data/BuildNodeData";
import React from "react";
+import { BinDims } from "bin/3dView/Data/BuildCableData";
interface Props {
- bin: Bin;
+ binDimensions: BinDims
+ targetMoisture: number
nodes: NodeData[];
/**
* For flat fill percent inventory — a scalar Y ceiling for the heatmap top.
@@ -127,11 +129,10 @@ function hashNoise(a: number, b: number, c: number) {
return (x - Math.floor(x)) * 2 - 1;
}
-export default function MoistureHeatMap({ bin, nodes, flatMaxY, flatMinY }: Props) {
- const binRadius = bin.diameter() / 2;
- const sidewallHeight = bin.sidewallHeight();
- const hopperHeight = bin.hopperHeight() ?? 0;
- const targetMoisture = bin.targetMoisture();
+export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes, flatMaxY, flatMinY }: Props) {
+ const binRadius = binDimensions.diameter / 2;
+ const sidewallHeight = binDimensions.sidewallHeight;
+ const hopperHeight = binDimensions.hopperHeight ?? 0;
const sidewallBaseY = -sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight;
diff --git a/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx b/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx
index d64b744..b631aea 100644
--- a/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx
+++ b/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx
@@ -3,9 +3,11 @@ import * as THREE from "three";
import { Bin } from "models";
import { NodeData } from "../../Data/BuildNodeData";
import React from "react";
+import { BinDims } from "bin/3dView/Data/BuildCableData";
interface Props {
- bin: Bin;
+ binDimensions: BinDims
+ targetTemp: number
nodes: NodeData[];
/**
* For flat fill percent inventory — a scalar Y ceiling for the heatmap top.
@@ -127,11 +129,11 @@ function hashNoise(a: number, b: number, c: number) {
return (x - Math.floor(x)) * 2 - 1;
}
-export default function TempHeatMap({ bin, nodes, flatMaxY, flatMinY }: Props) {
- const binRadius = bin.diameter() / 2;
- const sidewallHeight = bin.sidewallHeight();
- const hopperHeight = bin.hopperHeight() ?? 0;
- const upperThreshold = bin.targetTemp();
+export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY, flatMinY }: Props) {
+ const binRadius = binDimensions.diameter / 2;
+ const sidewallHeight = binDimensions.sidewallHeight;
+ const hopperHeight = binDimensions.hopperHeight ?? 0;
+ const upperThreshold = targetTemp;
const sidewallBaseY = -sidewallHeight / 2;
const hopperTipY = sidewallBaseY - hopperHeight;
diff --git a/src/bin/binSummary/BinSummary.tsx b/src/bin/binSummary/BinSummary.tsx
index 3bfc671..de3f73d 100644
--- a/src/bin/binSummary/BinSummary.tsx
+++ b/src/bin/binSummary/BinSummary.tsx
@@ -161,7 +161,12 @@ export default function BinSummary(props: Props){
fontSize: "0.875rem", // match body2
}}
/>
-
+
{currentFill()}/{bin.grainCapacity(user)}
{bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ?