started work on the bin summary part of the new bin page, built the framework and the new 3d visualizer, still doing some work on the camera to create an overlay with a reset button

This commit is contained in:
csawatzky 2026-05-01 13:11:12 -06:00
parent b3cf5b5e83
commit 4b2c67dfd9
11 changed files with 974 additions and 220 deletions

View file

@ -1,5 +1,6 @@
import React from "react";
import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls";
import CameraOverlay from "3dModels/CameraControls/CameraOverlay";
import { Canvas } from "@react-three/fiber";
import { Bin } from "models";
import BinShell from "../BinParts/BinShell";
@ -13,28 +14,47 @@ import { pond } from "protobuf-ts/pond";
import GrainCableFill from "../Systems/Inventory/GrainCableFill";
import TempHeatMap from "../Systems/Heatmap/TempHeatMap";
import NodePointCloud from "../Systems/Heatmap/NodePointCloud";
interface Props {
bin: Bin
scale: number
fillPercent?: number
nodeClick?: (node: NodeData, cable: CableData) => void
showGrain?: boolean
showHeatmap?: boolean
showTempHeatmap?: boolean
showMoistureHeatmap?: boolean
showHotspots?: boolean
/**
* When true renders a reset camera button overlaid on the 3D view.
*/
showResetButton?: boolean
showTemp?: boolean
showMoisture?: boolean
}
export default function Bin3dView(props: Props) {
const { bin, scale = 100, fillPercent, nodeClick, showHeatmap, showGrain, showHotspots } = props
const { bin, scale = 100, fillPercent, nodeClick, showTempHeatmap, showGrain, showHotspots, showResetButton, showMoistureHeatmap, showTemp, showMoisture } = props
const binCenter = useMemo(() => new Vector3(0, 0, 0), []);
// 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 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]);
const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
const isCableInventory =
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_CABLE;
// For cable inventory, TempHeatMap derives the wavy surface directly from
// the top nodes in nodeData — no scalar needed.
//
@ -42,21 +62,24 @@ export default function Bin3dView(props: Props) {
// using the same volume math as GrainFillFlat and pass it as flatMaxY.
const flatMaxY = useMemo(() => {
if (isCableInventory || fillPercent === undefined) return undefined;
const radius = bin.diameter() / 2;
const sidewallH = bin.sidewallHeight();
const hopperH = bin.hopperHeight() ?? 0;
const cylVolume = Math.PI * radius * radius * sidewallH;
const hopperVol = hopperH > 0 ? (1 / 3) * Math.PI * radius * radius * hopperH : 0;
const filled = (cylVolume + hopperVol) * fillPercent;
const cylinderFillH = hopperH > 0 && filled <= hopperVol
? 0
: Math.max(0, (filled - hopperVol) / (Math.PI * radius * radius));
return -sidewallH / 2 + cylinderFillH;
}, [isCableInventory, fillPercent, bin]);
// Internal ref — wired to OrbitCameraControls and called by CameraOverlay
const resetCameraFn = React.useRef<(() => void) | null>(null);
const grainInventory = () => {
if (isCableInventory) {
return (
@ -81,7 +104,7 @@ export default function Bin3dView(props: Props) {
)
}
}
return (
<Canvas>
<group scale={[1 / scale, 1 / scale, 1 / scale]}>
@ -97,10 +120,12 @@ export default function Bin3dView(props: Props) {
hopperHeight={bin.hopperHeight()}
renderOrder={4}
/>
{showGrain && grainInventory()}
<BinCables
displayTemp={showTemp}
displayMoisture={showMoisture}
cableData={cableData}
nodeData={nodeData}
bin={bin}
@ -110,24 +135,34 @@ export default function Bin3dView(props: Props) {
}}
renderOrder={1}
/>
{showHotspots && <NodePointCloud bin={bin} nodes={nodeData} />}
{showHeatmap && (
{showTempHeatmap && (
<TempHeatMap
bin={bin}
nodes={nodeData}
flatMaxY={flatMaxY}
/>
)}
</group>
<ambientLight intensity={0.2} color={"white"} />
<directionalLight intensity={0.2} color={"white"} position={[0, 0, 5]} />
<directionalLight intensity={0.2} color={"white"} position={[0, 0, -5]} />
<directionalLight intensity={0.2} color={"white"} position={[5, 0, 0]} />
<directionalLight intensity={0.2} color={"white"} position={[-5, 0, 0]} />
<OrbitCameraControls clampVerticalRotation />
<OrbitCameraControls
clampVerticalRotation
initialRadius={initialCameraRadius}
maxRadius={initialCameraRadius * 2}
onReset={fn => { resetCameraFn.current = fn; }}
/>
{/* <CameraOverlay
showResetButton={showResetButton}
onReset={() => resetCameraFn.current?.()}
/> */}
</Canvas>
)
}