diff --git a/src/bin/3dView/Data/BuildCableData.ts b/src/bin/3dView/Data/BuildCableData.ts index 0b0ed03..4b34fb9 100644 --- a/src/bin/3dView/Data/BuildCableData.ts +++ b/src/bin/3dView/Data/BuildCableData.ts @@ -172,8 +172,9 @@ export function BuildCableData(bin: Bin, dims: BinDims): CableData[] { distributed.forEach(orbit => { const count = orbit.assignedCount ?? 0; - // const topY = getTopY(orbit.radius, dims); //this is if we want the cables to start at the roof - const topY = dims.sidewallHeight/2 //this will start the cables at the top of the sidewall + const topY = getTopY(orbit.radius, dims); //this is if we want the cables to start at the roof + //const topY = dims.sidewallHeight/2 //this will start the cables at the top of the sidewall + const bottomY = getBottomY(orbit.radius, dims); if (orbit.orbit === 0 && count > 0) { diff --git a/src/bin/3dView/Scene/Bin3dView.tsx b/src/bin/3dView/Scene/Bin3dView.tsx index e866904..f8107f4 100644 --- a/src/bin/3dView/Scene/Bin3dView.tsx +++ b/src/bin/3dView/Scene/Bin3dView.tsx @@ -92,6 +92,7 @@ export default function Bin3dView(props: Props) { dims.diameter, dims.sidewallHeight, dims.hopperHeight, + dims.roofHeight, fillPercent ); }, [isCableInventory, fillPercent, dims]); diff --git a/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx b/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx index 12ea832..4756c4a 100644 --- a/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx +++ b/src/bin/3dView/Systems/Heatmap/MoistureHeatmap.tsx @@ -133,11 +133,18 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes, const binRadius = binDimensions.diameter / 2; const sidewallHeight = binDimensions.sidewallHeight; const hopperHeight = binDimensions.hopperHeight ?? 0; + const roofHeight = binDimensions.roofHeight ?? 0; const sidewallBaseY = -sidewallHeight / 2; + const sidewallTopY = sidewallHeight / 2; const hopperTipY = sidewallBaseY - hopperHeight; + const roofTipY = sidewallTopY + roofHeight; const maxRadiusAtY = (y: number) => { + if (y > sidewallTopY) { + if (roofHeight <= 0 || y >= roofTipY) return 0; + return binRadius * (1 - (y - sidewallTopY) / roofHeight); + } if (y >= sidewallBaseY) return binRadius; if (hopperHeight <= 0 || y <= hopperTipY) return 0; return binRadius * ((y - hopperTipY) / hopperHeight); @@ -191,7 +198,7 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes, const getSurfaceY = (x: number, z: number): number => { if (surfaceAnchors.length > 0) { const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER); - return Math.max(grainFloorY, Math.min(sidewallHeight / 2, raw)); + return Math.max(grainFloorY, Math.min(roofTipY, raw)); } return flatMaxY ?? sidewallBaseY; }; @@ -415,6 +422,8 @@ export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes, binRadius, targetMoisture, hopperHeight, + roofHeight, + roofTipY, flatMaxY, flatMinY, ]); diff --git a/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx b/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx index b631aea..1b25f0a 100644 --- a/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx +++ b/src/bin/3dView/Systems/Heatmap/TempHeatMap.tsx @@ -133,13 +133,23 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY const binRadius = binDimensions.diameter / 2; const sidewallHeight = binDimensions.sidewallHeight; const hopperHeight = binDimensions.hopperHeight ?? 0; + const roofHeight = binDimensions.roofHeight ?? 0; const upperThreshold = targetTemp; const sidewallBaseY = -sidewallHeight / 2; + const sidewallTopY = sidewallHeight / 2; const hopperTipY = sidewallBaseY - hopperHeight; - + const roofTipY = sidewallTopY + roofHeight; + const maxRadiusAtY = (y: number) => { + // Inside the roof cone — tapers from binRadius at sidewallTopY to 0 at roofTipY + if (y > sidewallTopY) { + if (roofHeight <= 0 || y >= roofTipY) return 0; + return binRadius * (1 - (y - sidewallTopY) / roofHeight); + } + // Straight sidewall if (y >= sidewallBaseY) return binRadius; + // Inside the hopper cone — tapers from binRadius at sidewallBaseY to 0 at hopperTipY if (hopperHeight <= 0 || y <= hopperTipY) return 0; return binRadius * ((y - hopperTipY) / hopperHeight); }; @@ -192,7 +202,7 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY const getSurfaceY = (x: number, z: number): number => { if (surfaceAnchors.length > 0) { const raw = idwSurfaceY(x, z, surfaceAnchors, SURFACE_IDW_POWER); - return Math.max(grainFloorY, Math.min(sidewallHeight / 2, raw)); + return Math.max(grainFloorY, Math.min(roofTipY, raw)); } return flatMaxY ?? sidewallBaseY; }; @@ -416,6 +426,8 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY binRadius, upperThreshold, hopperHeight, + roofHeight, + roofTipY, flatMaxY, flatMinY, ]); @@ -455,5 +467,4 @@ export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY ); -} - +} \ No newline at end of file diff --git a/src/bin/3dView/utils/grainFillBounds.ts b/src/bin/3dView/utils/grainFillBounds.ts index 698fcb0..d1b6fef 100644 --- a/src/bin/3dView/utils/grainFillBounds.ts +++ b/src/bin/3dView/utils/grainFillBounds.ts @@ -1,3 +1,5 @@ +import { ConeVolume, CylinderVolume } from "common/TrigFunctions"; + /** * Y bounds of filled grain volume — matches GrainFillFlat / GrainCableFill fallback volume math. */ @@ -6,28 +8,40 @@ export interface GrainYBounds { maxY: number; } +// Grain never physically fills all the way to the roof tip — cap roof fill +// at this fraction of roofHeight so there is always visible breathing room. +const MAX_ROOF_FILL_FRACTION = 0.75; + export function grainYBoundsFromFillPercent( diameter: number, sidewallHeight: number, hopperHeight: number, + roofHeight: number, fillPercent: number ): GrainYBounds { - const radius = diameter / 2; + const radius = diameter / 2; const sidewallBaseY = -sidewallHeight / 2; - const hopperTipY = sidewallBaseY - hopperHeight; + const hopperTipY = sidewallBaseY - hopperHeight; + const roofBaseY = sidewallHeight / 2; if (fillPercent <= 0) { return { minY: sidewallBaseY, maxY: sidewallBaseY }; } - const cylinderVolume = Math.PI * radius * radius * sidewallHeight; - const hopperVolume = - hopperHeight > 0 ? (1 / 3) * Math.PI * radius * radius * hopperHeight : 0; - const totalVolume = cylinderVolume + hopperVolume; + const cylinderVolume = CylinderVolume(radius, sidewallHeight); + const hopperVolume = hopperHeight > 0 ? ConeVolume(radius, hopperHeight) : 0; + const roofVolume = roofHeight > 0 ? ConeVolume(radius, roofHeight) : 0; + + // Volume of the usable roof portion (up to MAX_ROOF_FILL_FRACTION of height). + // A cone fills as h³ so the capped volume fraction = MAX_ROOF_FILL_FRACTION³. + const roofVolumeCapped = roofVolume * Math.pow(MAX_ROOF_FILL_FRACTION, 3); + + const totalVolume = cylinderVolume + hopperVolume + roofVolumeCapped; const filledVolume = totalVolume * fillPercent; + // Fill the hopper first if (hopperHeight > 0 && filledVolume <= hopperVolume) { - const ratio = filledVolume / hopperVolume; + const ratio = filledVolume / hopperVolume; const hopperFillHeight = hopperHeight * Math.pow(ratio, 1 / 3); return { minY: hopperTipY, @@ -35,15 +49,23 @@ export function grainYBoundsFromFillPercent( }; } - const hopperFillHeight = hopperHeight; - const remainingVolume = filledVolume - hopperVolume; - const cylinderFillHeight = Math.max( - 0, - remainingVolume / (Math.PI * radius * radius) - ); + // Then fill the cylinder + const volumeAfterHopper = filledVolume - hopperVolume; + if (volumeAfterHopper <= cylinderVolume) { + const cylinderFillHeight = volumeAfterHopper / (Math.PI * radius * radius); + return { + minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY, + maxY: sidewallBaseY + cylinderFillHeight, + }; + } + + // Finally fill into the roof cone, capped at MAX_ROOF_FILL_FRACTION + const roofFilledVolume = volumeAfterHopper - cylinderVolume; + const roofRatio = Math.min(1, roofFilledVolume / roofVolumeCapped); + const roofFillHeight = roofHeight * MAX_ROOF_FILL_FRACTION * Math.pow(roofRatio, 1 / 3); return { minY: hopperHeight > 0 ? hopperTipY : sidewallBaseY, - maxY: sidewallBaseY + cylinderFillHeight, + maxY: roofBaseY + roofFillHeight, }; -} +} \ No newline at end of file diff --git a/src/bin/BinPlayer.tsx b/src/bin/BinPlayer.tsx index 9fcd85c..95aabb3 100644 --- a/src/bin/BinPlayer.tsx +++ b/src/bin/BinPlayer.tsx @@ -513,8 +513,8 @@ const dateSelector = () => { {currentCheckpointTime ? currentCheckpointTime.format("MMM D, YYYY h:mm A") : moment().format("MMM D, YYYY h:mm A")} - {startDate.format("MMM D, YYYY")} - {endDate.format("MMM D, YYYY")} + {startDate.format("MMM D, YYYY")} + {endDate.format("MMM D, YYYY")} {/* date range selector */} diff --git a/src/bin/BinTableExpanded.tsx b/src/bin/BinTableExpanded.tsx index cf1def6..ecb7baa 100644 --- a/src/bin/BinTableExpanded.tsx +++ b/src/bin/BinTableExpanded.tsx @@ -81,8 +81,8 @@ export default function BinTableExpanded(props: Props) { const inGrain = nodeIndex < cable.topNode if (inGrain) { - if (raw > targetTemp + 5) color = warning - else if (raw > targetTemp) color = caution + if (raw > targetTemp + 10) color = warning + else if (raw > targetTemp + 5) color = caution else color = inBounds } @@ -102,7 +102,7 @@ export default function BinTableExpanded(props: Props) { const inGrain = nodeIndex < cable.topNode if (inGrain) { if (raw > targetMoisture + 1.5) color = warning - else if (raw > targetMoisture) color = caution + else if (raw > targetMoisture + 0.5) color = caution else color = inBounds } return { display: raw.toFixed(2) + "%", color } @@ -196,15 +196,15 @@ export default function BinTableExpanded(props: Props) { - On or below target + On target - {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target + {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~10 °F" : "+5 °C"} above - {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target + {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~20 °F" : "+10 °C"} above diff --git a/src/bin/binSummary/BinSummary.tsx b/src/bin/binSummary/BinSummary.tsx index 61a71ea..f868712 100644 --- a/src/bin/binSummary/BinSummary.tsx +++ b/src/bin/binSummary/BinSummary.tsx @@ -390,6 +390,7 @@ export default function BinSummary(props: Props){ setOpenGrainDialog(false)} permissions={permissions} updateBin={updateBinCallback}/> {loading ? : isMobile ? inventorySummaryMobile() : inventorySummaryDesktop()} + {!isMobile && {loading ? @@ -406,8 +407,7 @@ export default function BinSummary(props: Props){ binPrefs={binPrefs} updateBinCallback={updateBinCallback} /> - {/* only show the player on desktop */} - {setBin && !isMobile && + {setBin && @@ -415,6 +415,7 @@ export default function BinSummary(props: Props){ } + } {loading ? @@ -428,7 +429,12 @@ export default function BinSummary(props: Props){ linkedComponents={componentMap} permissions={permissions} cables={cables} - plenums={plenums}/> + plenums={plenums} + fans={fans} + heaters={heaters} + binPrefs={binPrefs} + componentMap={componentMap} + setBin={setBin}/> } diff --git a/src/bin/binSummary/components/binDetails.tsx b/src/bin/binSummary/components/binDetails.tsx index 008eed0..1e42eee 100644 --- a/src/bin/binSummary/components/binDetails.tsx +++ b/src/bin/binSummary/components/binDetails.tsx @@ -11,6 +11,9 @@ import BinAnalysisGraphs from "./binAnalysisGraphs"; import { GrainCable } from "models/GrainCable"; import { Plenum } from "models/Plenum"; import { Controller } from "models/Controller"; +import { useMobile } from "hooks"; +import Bin3dVisualizer from "bin/bin3dVisualizer"; +import BinPlayer from "bin/BinPlayer"; interface Props { bin: Bin @@ -21,6 +24,12 @@ interface Props { permissions: pond.Permission[] cables: GrainCable[] plenums: Plenum[] + showBinTab?: boolean + fans?: Controller[] + heaters?: Controller[] + componentMap: Map + binPrefs?: Map + setBin?: React.Dispatch>; } interface TabPanelProps { @@ -51,8 +60,9 @@ function TabPanelMine(props: TabPanelProps) { } export default function BinDetails (props: Props) { - const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums} = props - const [currentTab, setCurrentTab] = useState(0) + const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums, showBinTab, fans, heaters, componentMap, binPrefs, setBin} = props + const isMobile = useMobile() + const [currentTab, setCurrentTab] = useState((isMobile || showBinTab) ? "bin" : "table") //this is where we will have the tabs and tab panels for the table view, alerts, controls, and analysis @@ -65,22 +75,48 @@ export default function BinDetails (props: Props) { textColor="primary" variant="fullWidth" sx={{ flexShrink: 0 }}> - - - - + {isMobile && } + + + + - + {(isMobile || showBinTab) && + + + {setBin && + + + + } + + } + + {setBin && isMobile && + + + + } - + - + - + diff --git a/src/bin/binTableView.tsx b/src/bin/binTableView.tsx index ff3fdb0..33fcf6a 100644 --- a/src/bin/binTableView.tsx +++ b/src/bin/binTableView.tsx @@ -23,7 +23,6 @@ const useStyles = makeStyles((theme: Theme) => ({ headerCellStyle: { fontWeight: 650, backgroundColor: darken(theme.palette.background.paper, 0.2), - fontSize: 15, textAlign: "center", borderTop: "1px solid black", borderLeft: "1px solid black", @@ -32,6 +31,18 @@ const useStyles = makeStyles((theme: Theme) => ({ borderRight: "1px solid black", } }, + mobileHeaderCellStyle: { + fontWeight: 650, + backgroundColor: darken(theme.palette.background.paper, 0.2), + textAlign: "center", + borderTop: "1px solid black", + borderLeft: "1px solid black", + borderBottom: "1px solid black", + "&:last-child": { + borderRight: "1px solid black", + }, + padding: 2 + }, cellStyle: { padding: 2, fontWeight: 650, @@ -164,9 +175,9 @@ export default function BinTableView(props: Props) { if(level.grainTemps.length > 0){ lvlTemp = avg(level.grainTemps) //if the average temp is 5 degrees above the bins threshold - if(lvlTemp > (bin.targetTemp() + 5)){ + if(lvlTemp > (bin.targetTemp() + 10)){ tempColour = warning - }else if(lvlTemp > bin.targetTemp()){ + }else if(lvlTemp > bin.targetTemp() + 5){ //if the average temp is greater than 5 degrees over tempColour = caution }else { @@ -179,7 +190,7 @@ export default function BinTableView(props: Props) { if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){ lvlTemp = celsiusToFahrenheit(lvlTemp) } - tempDisplay = lvlTemp.toFixed(2) + (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C") + tempDisplay = lvlTemp.toFixed(2) } //determine the moisture to show @@ -187,8 +198,7 @@ export default function BinTableView(props: Props) { lvlMoisture = avg(level.grainMoistures) if(lvlMoisture > (bin.targetMoisture() + 1.5)){ moistureColour = warning - }else if(lvlMoisture > bin.targetMoisture()){ - //if the average temp is greater than 5 degrees over + }else if(lvlMoisture > bin.targetMoisture() + 0.5){ moistureColour = caution }else { moistureColour = inBounds @@ -222,11 +232,11 @@ export default function BinTableView(props: Props) { } } - const tempThreshDisplay = () => { + const tempThreshDisplay = (hideUnit?: boolean) => { if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){ - return tempTarget.toFixed(2) + " °F" + return tempTarget.toFixed(2) + (hideUnit ? "" : " °F") } - return tempTarget.toFixed(2) + " °C" + return tempTarget.toFixed(2) + (hideUnit ? "" : " °C") } const LegendSwatch = ({ color }: { color: string }) => ( @@ -287,7 +297,7 @@ export default function BinTableView(props: Props) { return ( - + {expandedTableDialog()} {/* Level Table */} @@ -311,80 +321,80 @@ export default function BinTableView(props: Props) { - - - - - - Targets - - - - - - - {enterTemp ? - updateTempEntry() - : - - {tempThreshDisplay()} - - } - {enterTemp ? - { - setEnterTemp(false) - //and update the bin with the new temp target - update() - }} - > - - - : - { - setEnterTemp(true) - }}> - - - } - - - - - - {enterMoisture ? - updateMoistureEntry() - : - - {bin.targetMoisture()}% - - } - {enterMoisture ? - { - setEnterMoisture(false) - //and update the bin with the new temp target - update() - }} - > - - - : - { - setEnterMoisture(true) - }}> - - - } - - - + + {/* "Targets" cell — hide on mobile when either field is being edited */} + {!(isMobile && (enterTemp || enterMoisture)) && ( + + + + {!isMobile && + + Targets + + } + + + )} + + {/* Temperature cell */} + {!(isMobile && enterMoisture) && ( + + + + {enterTemp ? updateTempEntry() : ( + + {tempThreshDisplay(isMobile)} + + )} + {enterTemp ? ( + { setEnterTemp(false); update(); }}> + + + ) : ( + setEnterTemp(true)}> + + + )} + + + )} + {/* Moisture cell — hide on mobile when temperature is being edited */} + {!(isMobile && enterTemp) && ( + + + + {enterMoisture ? updateMoistureEntry() : ( + + {bin.targetMoisture()}% + + )} + {enterMoisture ? ( + { setEnterMoisture(false); update(); }}> + + + ) : ( + setEnterMoisture(true)}> + + + )} + + + )} + - Level - Temperature - Moisture + Level + + {isMobile ? "T" : "Temperature"}{(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " (°F)" : " (°C)")} + + EMC (%) @@ -402,18 +412,17 @@ export default function BinTableView(props: Props) { - On or below target + On target - {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+10 °F" : "+5 °C"} above target + {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~10 °F" : "+5 °C"} (0.5%) above - {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "+20 °F" : "+10 °C"} above target + {user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "~20 °F" : "+10 °C"} (1.5%) above - ) } \ No newline at end of file