From 160d2a9d318644eae1be11d639835ff85d3ed55c Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 3 Jun 2026 15:59:14 -0600 Subject: [PATCH] set up some defaults for bins that are not using advanced dimensions so that they can still have a visual of some sort --- src/bin/3dView/Data/BuildCableData.ts | 53 +++++++++----- src/bin/3dView/Scene/Bin3dView.tsx | 71 ++++++++++++------- .../Systems/Heatmap/MoistureHeatmap.tsx | 13 ++-- .../3dView/Systems/Heatmap/TempHeatMap.tsx | 14 ++-- src/bin/binSummary/BinSummary.tsx | 7 +- 5 files changed, 102 insertions(+), 56 deletions(-) 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 so it shares the same stacking context */} + {showResetButton && ( + resetCameraFn.current?.()} /> + )} ) } 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 ?