set up some defaults for bins that are not using advanced dimensions so that they can still have a visual of some sort
This commit is contained in:
parent
1d19a7e993
commit
160d2a9d31
5 changed files with 102 additions and 56 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { RadiansToDegrees } from "common/TrigFunctions";
|
||||||
import { Bin } from "models";
|
import { Bin } from "models";
|
||||||
import { GrainCable } from "models/GrainCable";
|
import { GrainCable } from "models/GrainCable";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
|
@ -21,6 +22,19 @@ export interface CableOrbit {
|
||||||
assignedCount?: number;
|
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[] {
|
function getLayoutFromDiameter(diameterCm: number): CableOrbit[] {
|
||||||
const diameterFt = diameterCm / 30.48;
|
const diameterFt = diameterCm / 30.48;
|
||||||
|
|
@ -93,34 +107,36 @@ function distributeCables(cableCount: number, layout: CableOrbit[]) {
|
||||||
return assigned;
|
return assigned;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getTopY(radiusFromCenter: number, bin: Bin) {
|
function getTopY(radiusFromCenter: number, dims: BinDims) {
|
||||||
const binRadius = bin.diameter() / 2;
|
const binRadius = dims.diameter / 2;
|
||||||
|
|
||||||
const distanceFromEdge = binRadius - radiusFromCenter;
|
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;
|
const roofRise = Math.tan(angleRad) * distanceFromEdge;
|
||||||
|
|
||||||
return bin.sidewallHeight() / 2 + roofRise;
|
return dims.sidewallHeight / 2 + roofRise;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getBottomY(radiusFromCenter: number, bin: Bin) {
|
function getBottomY(radiusFromCenter: number, dims: BinDims) {
|
||||||
const binRadius = bin.diameter() / 2;
|
const binRadius = dims.diameter / 2;
|
||||||
|
|
||||||
const distanceFromCenter = binRadius - radiusFromCenter;
|
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 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 ?? [])];
|
const cables = [...(bin.status.grainCables ?? [])];
|
||||||
|
|
||||||
if (cables.length === 0) return [];
|
if (cables.length === 0) return [];
|
||||||
|
|
@ -144,10 +160,9 @@ export function BuildCableData(bin: Bin): CableData[] {
|
||||||
return b.celcius.length - a.celcius.length;
|
return b.celcius.length - a.celcius.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
const layout = getLayoutFromDiameter(bin.diameter());
|
const layout = getLayoutFromDiameter(dims.diameter);
|
||||||
const distributed = distributeCables(cables.length, layout);
|
const distributed = distributeCables(cables.length, layout);
|
||||||
|
|
||||||
|
|
||||||
const cableRadius = 5;
|
const cableRadius = 5;
|
||||||
|
|
||||||
const result: CableData[] = [];
|
const result: CableData[] = [];
|
||||||
|
|
@ -157,12 +172,12 @@ export function BuildCableData(bin: Bin): CableData[] {
|
||||||
distributed.forEach(orbit => {
|
distributed.forEach(orbit => {
|
||||||
const count = orbit.assignedCount ?? 0;
|
const count = orbit.assignedCount ?? 0;
|
||||||
|
|
||||||
const topY = getTopY(orbit.radius, bin)
|
const topY = getTopY(orbit.radius, dims);
|
||||||
const bottomY = getBottomY(orbit.radius, bin)
|
const bottomY = getBottomY(orbit.radius, dims);
|
||||||
|
|
||||||
if (orbit.orbit === 0 && count > 0) {
|
if (orbit.orbit === 0 && count > 0) {
|
||||||
const cable = cables[cableIndex];
|
const cable = cables[cableIndex];
|
||||||
if (!cable) return
|
if (!cable) return;
|
||||||
result.push({
|
result.push({
|
||||||
cableIndex: cableIndex,
|
cableIndex: cableIndex,
|
||||||
radius: cableRadius,
|
radius: cableRadius,
|
||||||
|
|
@ -170,7 +185,7 @@ export function BuildCableData(bin: Bin): CableData[] {
|
||||||
bottomPosition: new Vector3(0, bottomY, 0),
|
bottomPosition: new Vector3(0, bottomY, 0),
|
||||||
grainCable: cable
|
grainCable: cable
|
||||||
});
|
});
|
||||||
cableIndex++
|
cableIndex++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,11 +198,11 @@ export function BuildCableData(bin: Bin): CableData[] {
|
||||||
result.push({
|
result.push({
|
||||||
cableIndex: cableIndex,
|
cableIndex: cableIndex,
|
||||||
radius: cableRadius,
|
radius: cableRadius,
|
||||||
topPosition: new Vector3(x,topY,z),
|
topPosition: new Vector3(x, topY, z),
|
||||||
bottomPosition: new Vector3(x,bottomY,z),
|
bottomPosition: new Vector3(x, bottomY, z),
|
||||||
grainCable: cable
|
grainCable: cable
|
||||||
});
|
});
|
||||||
cableIndex++
|
cableIndex++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,14 @@ import NodePointCloud from "../Systems/Heatmap/NodePointCloud";
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import MoistureHeatMap from "../Systems/Heatmap/MoistureHeatmap";
|
import MoistureHeatMap from "../Systems/Heatmap/MoistureHeatmap";
|
||||||
import { grainYBoundsFromFillPercent } from "../utils/grainFillBounds";
|
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 {
|
interface Props {
|
||||||
bin: Bin
|
bin: Bin
|
||||||
|
|
@ -46,18 +53,28 @@ interface Props {
|
||||||
export default function Bin3dView(props: Props) {
|
export default function Bin3dView(props: Props) {
|
||||||
const { bin, scale = 100, fillPercent, nodeClick, showTempHeatmap, showGrain, showHotspots, showResetButton, showMoistureHeatmap, showTemp, showMoisture, xOffset } = 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.
|
// Compute the initial camera radius so the full bin fits in frame.
|
||||||
// Uses the perspective camera FOV (default 75°) with a padding factor.
|
// Uses the perspective camera FOV (default 75°) with a padding factor.
|
||||||
// Both the diameter and total height are considered so neither is clipped.
|
// Both the diameter and total height are considered so neither is clipped.
|
||||||
const initialCameraRadius = useMemo(() => {
|
const initialCameraRadius = useMemo(() => {
|
||||||
const totalHeight = (bin.sidewallHeight() + bin.roofHeight() + (bin.hopperHeight() ?? 0)) / scale;
|
const totalHeight = (dims.sidewallHeight + dims.roofHeight + dims.hopperHeight) / scale;
|
||||||
const diameter = bin.diameter() / scale;
|
const diameter = dims.diameter / scale;
|
||||||
const largestDim = Math.max(totalHeight, diameter);
|
const largestDim = Math.max(totalHeight, diameter);
|
||||||
const fovRad = (75 * Math.PI) / 180;
|
const fovRad = (75 * Math.PI) / 180;
|
||||||
const padding = 1.3; // 30% breathing room
|
const padding = 1.3; // 30% breathing room
|
||||||
return (largestDim / (2 * Math.tan(fovRad / 2))) * padding;
|
return (largestDim / (2 * Math.tan(fovRad / 2))) * padding;
|
||||||
}, [bin, scale]);
|
}, [dims, scale]);
|
||||||
const cableData = useMemo(() => BuildCableData(bin), [bin]);
|
|
||||||
|
const cableData = useMemo(() => BuildCableData(bin, dims), [bin, dims]);
|
||||||
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
|
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
|
||||||
|
|
||||||
const isCableInventory =
|
const isCableInventory =
|
||||||
|
|
@ -71,12 +88,12 @@ export default function Bin3dView(props: Props) {
|
||||||
const flatGrainBounds = useMemo(() => {
|
const flatGrainBounds = useMemo(() => {
|
||||||
if (isCableInventory || fillPercent === undefined) return undefined;
|
if (isCableInventory || fillPercent === undefined) return undefined;
|
||||||
return grainYBoundsFromFillPercent(
|
return grainYBoundsFromFillPercent(
|
||||||
bin.diameter(),
|
dims.diameter,
|
||||||
bin.sidewallHeight(),
|
dims.sidewallHeight,
|
||||||
bin.hopperHeight() ?? 0,
|
dims.hopperHeight,
|
||||||
fillPercent
|
fillPercent
|
||||||
);
|
);
|
||||||
}, [isCableInventory, fillPercent, bin]);
|
}, [isCableInventory, fillPercent, dims]);
|
||||||
|
|
||||||
// Internal ref — wired to OrbitCameraControls and called by CameraOverlay
|
// Internal ref — wired to OrbitCameraControls and called by CameraOverlay
|
||||||
const resetCameraFn = React.useRef<(() => void) | null>(null);
|
const resetCameraFn = React.useRef<(() => void) | null>(null);
|
||||||
|
|
@ -85,20 +102,20 @@ export default function Bin3dView(props: Props) {
|
||||||
if (isCableInventory) {
|
if (isCableInventory) {
|
||||||
return (
|
return (
|
||||||
<GrainCableFill
|
<GrainCableFill
|
||||||
diameter={bin.diameter()}
|
diameter={dims.diameter}
|
||||||
nodes={nodeData}
|
nodes={nodeData}
|
||||||
sidewallHeight={bin.sidewallHeight()}
|
sidewallHeight={dims.sidewallHeight}
|
||||||
fallbackFillPercent={fillPercent}
|
fallbackFillPercent={fillPercent}
|
||||||
hopperHeight={bin.hopperHeight()}
|
hopperHeight={dims.hopperHeight}
|
||||||
grainOpacity={0.3}
|
grainOpacity={0.3}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (fillPercent) {
|
} else if (fillPercent) {
|
||||||
return (
|
return (
|
||||||
<GrainFillFlat
|
<GrainFillFlat
|
||||||
diameter={bin.diameter()}
|
diameter={dims.diameter}
|
||||||
sidewallHeight={bin.sidewallHeight()}
|
sidewallHeight={dims.sidewallHeight}
|
||||||
hopperHeight={bin.hopperHeight()}
|
hopperHeight={dims.hopperHeight}
|
||||||
fillPercent={fillPercent}
|
fillPercent={fillPercent}
|
||||||
grainOpacity={0.3}
|
grainOpacity={0.3}
|
||||||
/>
|
/>
|
||||||
|
|
@ -122,10 +139,10 @@ export default function Bin3dView(props: Props) {
|
||||||
binMetalness={0.5}
|
binMetalness={0.5}
|
||||||
binRoughness={0.7}
|
binRoughness={0.7}
|
||||||
binOpacity={0.2}
|
binOpacity={0.2}
|
||||||
diameter={bin.diameter()}
|
diameter={dims.diameter}
|
||||||
sidewallHeight={bin.sidewallHeight()}
|
sidewallHeight={dims.sidewallHeight}
|
||||||
roofHeight={bin.roofHeight()}
|
roofHeight={dims.roofHeight}
|
||||||
hopperHeight={bin.hopperHeight()}
|
hopperHeight={dims.hopperHeight}
|
||||||
renderOrder={4}
|
renderOrder={4}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -145,24 +162,26 @@ export default function Bin3dView(props: Props) {
|
||||||
|
|
||||||
{showHotspots && <NodePointCloud bin={bin} nodes={nodeData} />}
|
{showHotspots && <NodePointCloud bin={bin} nodes={nodeData} />}
|
||||||
|
|
||||||
{/* 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 && (
|
{showTempHeatmap && (
|
||||||
<TempHeatMap
|
<TempHeatMap
|
||||||
bin={bin}
|
binDimensions={dims}
|
||||||
|
targetTemp={bin.targetTemp()}
|
||||||
nodes={nodeData}
|
nodes={nodeData}
|
||||||
flatMaxY={flatGrainBounds?.maxY}
|
flatMaxY={flatGrainBounds?.maxY}
|
||||||
flatMinY={flatGrainBounds?.minY}
|
flatMinY={flatGrainBounds?.minY}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showMoistureHeatmap && (
|
{showMoistureHeatmap && (
|
||||||
<MoistureHeatMap
|
<MoistureHeatMap
|
||||||
bin={bin}
|
binDimensions={dims}
|
||||||
|
targetMoisture={bin.targetMoisture()}
|
||||||
nodes={nodeData}
|
nodes={nodeData}
|
||||||
flatMaxY={flatGrainBounds?.maxY}
|
flatMaxY={flatGrainBounds?.maxY}
|
||||||
flatMinY={flatGrainBounds?.minY}
|
flatMinY={flatGrainBounds?.minY}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<ambientLight intensity={0.2} color={"white"} />
|
<ambientLight intensity={0.2} color={"white"} />
|
||||||
|
|
@ -171,6 +190,10 @@ export default function Bin3dView(props: Props) {
|
||||||
<directionalLight intensity={0.2} color={"white"} position={[5, 0, 0]} />
|
<directionalLight intensity={0.2} color={"white"} position={[5, 0, 0]} />
|
||||||
<directionalLight intensity={0.2} color={"white"} position={[-5, 0, 0]} />
|
<directionalLight intensity={0.2} color={"white"} position={[-5, 0, 0]} />
|
||||||
|
|
||||||
|
{/* Overlay — must be inside <Canvas> so it shares the same stacking context */}
|
||||||
|
{showResetButton && (
|
||||||
|
<CameraOverlay onReset={() => resetCameraFn.current?.()} />
|
||||||
|
)}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ import * as THREE from "three";
|
||||||
import { Bin } from "models";
|
import { Bin } from "models";
|
||||||
import { NodeData } from "../../Data/BuildNodeData";
|
import { NodeData } from "../../Data/BuildNodeData";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { BinDims } from "bin/3dView/Data/BuildCableData";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bin: Bin;
|
binDimensions: BinDims
|
||||||
|
targetMoisture: number
|
||||||
nodes: NodeData[];
|
nodes: NodeData[];
|
||||||
/**
|
/**
|
||||||
* For flat fill percent inventory — a scalar Y ceiling for the heatmap top.
|
* 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;
|
return (x - Math.floor(x)) * 2 - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MoistureHeatMap({ bin, nodes, flatMaxY, flatMinY }: Props) {
|
export default function MoistureHeatMap({ binDimensions, targetMoisture, nodes, flatMaxY, flatMinY }: Props) {
|
||||||
const binRadius = bin.diameter() / 2;
|
const binRadius = binDimensions.diameter / 2;
|
||||||
const sidewallHeight = bin.sidewallHeight();
|
const sidewallHeight = binDimensions.sidewallHeight;
|
||||||
const hopperHeight = bin.hopperHeight() ?? 0;
|
const hopperHeight = binDimensions.hopperHeight ?? 0;
|
||||||
const targetMoisture = bin.targetMoisture();
|
|
||||||
|
|
||||||
const sidewallBaseY = -sidewallHeight / 2;
|
const sidewallBaseY = -sidewallHeight / 2;
|
||||||
const hopperTipY = sidewallBaseY - hopperHeight;
|
const hopperTipY = sidewallBaseY - hopperHeight;
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ import * as THREE from "three";
|
||||||
import { Bin } from "models";
|
import { Bin } from "models";
|
||||||
import { NodeData } from "../../Data/BuildNodeData";
|
import { NodeData } from "../../Data/BuildNodeData";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { BinDims } from "bin/3dView/Data/BuildCableData";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bin: Bin;
|
binDimensions: BinDims
|
||||||
|
targetTemp: number
|
||||||
nodes: NodeData[];
|
nodes: NodeData[];
|
||||||
/**
|
/**
|
||||||
* For flat fill percent inventory — a scalar Y ceiling for the heatmap top.
|
* 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;
|
return (x - Math.floor(x)) * 2 - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TempHeatMap({ bin, nodes, flatMaxY, flatMinY }: Props) {
|
export default function TempHeatMap({ binDimensions, targetTemp, nodes, flatMaxY, flatMinY }: Props) {
|
||||||
const binRadius = bin.diameter() / 2;
|
const binRadius = binDimensions.diameter / 2;
|
||||||
const sidewallHeight = bin.sidewallHeight();
|
const sidewallHeight = binDimensions.sidewallHeight;
|
||||||
const hopperHeight = bin.hopperHeight() ?? 0;
|
const hopperHeight = binDimensions.hopperHeight ?? 0;
|
||||||
const upperThreshold = bin.targetTemp();
|
const upperThreshold = targetTemp;
|
||||||
|
|
||||||
const sidewallBaseY = -sidewallHeight / 2;
|
const sidewallBaseY = -sidewallHeight / 2;
|
||||||
const hopperTipY = sidewallBaseY - hopperHeight;
|
const hopperTipY = sidewallBaseY - hopperHeight;
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,12 @@ export default function BinSummary(props: Props){
|
||||||
fontSize: "0.875rem", // match body2
|
fontSize: "0.875rem", // match body2
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" noWrap sx={{ width: bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ? fillCapWidth : undefined, flexShrink: 0 }}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
noWrap
|
||||||
|
sx={{
|
||||||
|
width: (bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL || bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN) ? fillCapWidth : undefined, flexShrink: 0
|
||||||
|
}}>
|
||||||
{currentFill()}/{bin.grainCapacity(user)}
|
{currentFill()}/{bin.grainCapacity(user)}
|
||||||
</Typography>
|
</Typography>
|
||||||
{bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ?
|
{bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ?
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue