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:
parent
b3cf5b5e83
commit
4b2c67dfd9
11 changed files with 974 additions and 220 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ interface Props {
|
|||
bin: Bin
|
||||
binCenter: Vector3,
|
||||
renderOrder?: number,
|
||||
displayTemp?: boolean,
|
||||
displayMoisture?: boolean,
|
||||
onNodeClick?: (node: NodeData, cable: CableData) => void
|
||||
}
|
||||
|
||||
export default function BinCables(props: Props){
|
||||
const {bin, binCenter, cableData, nodeData, onNodeClick, renderOrder} = props
|
||||
const {bin, binCenter, cableData, nodeData, onNodeClick, renderOrder, displayTemp, displayMoisture} = props
|
||||
return (
|
||||
<React.Fragment>
|
||||
{cableData.map((cable, i) => {
|
||||
|
|
@ -28,6 +30,8 @@ export default function BinCables(props: Props){
|
|||
nodes={cableNodes}
|
||||
bin={bin}
|
||||
binCenter={binCenter}
|
||||
displayTemp={displayTemp}
|
||||
displayMoisture={displayMoisture}
|
||||
onNodeClick={(node) => {
|
||||
if(onNodeClick){
|
||||
onNodeClick(node, cable)
|
||||
|
|
|
|||
|
|
@ -17,36 +17,39 @@ interface Props {
|
|||
lowerThreshold: number
|
||||
upperThreshold: number,
|
||||
renderOrder?: number,
|
||||
displayTemp?: boolean,
|
||||
displayMoisture?: boolean,
|
||||
onNodeClick?: (node: NodeData) => void
|
||||
}
|
||||
|
||||
export default function CableNode(props: Props) {
|
||||
const { node, binCenter, lowerThreshold, upperThreshold, onNodeClick, renderOrder } = props
|
||||
const { node, binCenter, lowerThreshold, upperThreshold, onNodeClick, renderOrder, displayTemp, displayMoisture } = props
|
||||
const { camera } = useThree();
|
||||
const [{ user }] = useGlobalState();
|
||||
const [showLabel, setShowLabel] = useState(false);
|
||||
// const [showLabel, setShowLabel] = useState(false);
|
||||
const labelGroupRef = React.useRef<Group>(null);
|
||||
const ROW_HEIGHT = 35;
|
||||
const PADDING = 20;
|
||||
|
||||
useFrame(() => {
|
||||
const distance = camera.position.distanceTo(binCenter)
|
||||
//this can be used to control label visibility through the cameras zoom level
|
||||
// useFrame(() => {
|
||||
// const distance = camera.position.distanceTo(binCenter)
|
||||
|
||||
const SHOW_DISTANCE = 15;
|
||||
const HIDE_DISTANCE = 16;
|
||||
// const SHOW_DISTANCE = 15;
|
||||
// const HIDE_DISTANCE = 16;
|
||||
|
||||
if (!showLabel && distance < SHOW_DISTANCE) {
|
||||
setShowLabel(true);
|
||||
} else if (showLabel && distance > HIDE_DISTANCE) {
|
||||
setShowLabel(false);
|
||||
}
|
||||
// if (!showLabel && distance < SHOW_DISTANCE) {
|
||||
// setShowLabel(true);
|
||||
// } else if (showLabel && distance > HIDE_DISTANCE) {
|
||||
// setShowLabel(false);
|
||||
// }
|
||||
|
||||
const scale = distance * 0.07;
|
||||
// const scale = distance * 0.07;
|
||||
|
||||
if (labelGroupRef.current) {
|
||||
labelGroupRef.current.scale.set(scale, scale, 1);
|
||||
}
|
||||
});
|
||||
// if (labelGroupRef.current) {
|
||||
// labelGroupRef.current.scale.set(scale, scale, 1);
|
||||
// }
|
||||
// });
|
||||
|
||||
const tempDisplay = useMemo(() => {
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
|
|
@ -56,36 +59,47 @@ export default function CableNode(props: Props) {
|
|||
}, [node.celcius, user]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
const r: { text: string; color: string }[] = [];
|
||||
|
||||
const anyToggleOn = displayTemp || displayMoisture;
|
||||
|
||||
if (node.excluded) {
|
||||
r.push({ text: "Excluded", color: "red" });
|
||||
return r;
|
||||
if (!anyToggleOn) return [];
|
||||
return [{ text: "Excluded", color: "red" }];
|
||||
}
|
||||
|
||||
if (node.topNode) {
|
||||
r.push({ text: "Top Node", color: "yellow" });
|
||||
}
|
||||
|
||||
r.push({
|
||||
text: tempDisplay,
|
||||
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour()
|
||||
});
|
||||
|
||||
if (node.moisture) {
|
||||
|
||||
const r: { text: string; color: string }[] = [];
|
||||
|
||||
// TEMP
|
||||
if (displayTemp && node.celcius !== 0) {
|
||||
r.push({
|
||||
text: `${node.moisture.toFixed(2)}% EMC`,
|
||||
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour()
|
||||
});
|
||||
} else if (node.humidity) {
|
||||
r.push({
|
||||
text: `${node.humidity.toFixed(2)}%`,
|
||||
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour()
|
||||
text: tempDisplay,
|
||||
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// MOISTURE
|
||||
if (displayMoisture) {
|
||||
if (node.moisture && node.moisture > 0) {
|
||||
r.push({
|
||||
text: `${node.moisture.toFixed(2)}% EMC`,
|
||||
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour()
|
||||
});
|
||||
} else if (node.humidity && node.humidity > 0) {
|
||||
r.push({
|
||||
text: `${node.humidity.toFixed(2)}%`,
|
||||
color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const hasData = r.length > 0;
|
||||
|
||||
// Only show top node if there's actual data
|
||||
if (node.topNode && hasData) {
|
||||
r.unshift({ text: "Top Node", color: "yellow" });
|
||||
}
|
||||
|
||||
return r;
|
||||
}, [node, tempDisplay]);
|
||||
}, [node, tempDisplay, displayTemp, displayMoisture]);
|
||||
|
||||
const geometry = React.useMemo(() => ({
|
||||
radius: node.radius,
|
||||
|
|
@ -126,6 +140,8 @@ export default function CableNode(props: Props) {
|
|||
onNodeClick?.(node);
|
||||
};
|
||||
|
||||
const showLabel = rows.length > 0;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{(!node.inGrain || !showLabel) &&
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ interface Props {
|
|||
bin: Bin
|
||||
binCenter: Vector3,
|
||||
renderOrder?: number,
|
||||
displayTemp?: boolean,
|
||||
displayMoisture?: boolean
|
||||
onNodeClick?: (node: NodeData) => void
|
||||
}
|
||||
export default function GrainCable(props: Props) {
|
||||
const {cable, nodes, bin, binCenter, onNodeClick, renderOrder} = props
|
||||
const {cable, nodes, bin, binCenter, onNodeClick, renderOrder, displayTemp, displayMoisture} = props
|
||||
|
||||
const calculateHeight = () => {
|
||||
return cable.topPosition.distanceTo(cable.bottomPosition)
|
||||
|
|
@ -42,7 +44,7 @@ export default function GrainCable(props: Props) {
|
|||
<React.Fragment>
|
||||
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5} renderOrder={renderOrder} depthWrite={false} depthTest={false}/>
|
||||
{nodes.map((node, i) => (
|
||||
<CableNode onNodeClick={onNodeClick} key={"node " + i} renderOrder={renderOrder} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
|
||||
<CableNode displayTemp={displayTemp} displayMoisture={displayMoisture} onNodeClick={onNodeClick} key={"node " + i} renderOrder={renderOrder} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue