diff --git a/src/bin/3dView/Data/BuildNodeData.ts b/src/bin/3dView/Data/BuildNodeData.ts index 4103ce5..fd3f5dd 100644 --- a/src/bin/3dView/Data/BuildNodeData.ts +++ b/src/bin/3dView/Data/BuildNodeData.ts @@ -21,7 +21,7 @@ export interface NodeData { } export function BuildNodeData(cables: CableData[]): NodeData[] { - const nodeRadius = 20 + const nodeRadius = 15 let nodeData: NodeData[] = [] cables.forEach((cable, cableIndex) => { diff --git a/src/bin/3dView/Scene/Bin3dView.tsx b/src/bin/3dView/Scene/Bin3dView.tsx index 41ef6d6..cc2ae26 100644 --- a/src/bin/3dView/Scene/Bin3dView.tsx +++ b/src/bin/3dView/Scene/Bin3dView.tsx @@ -14,6 +14,7 @@ import { pond } from "protobuf-ts/pond"; import GrainCableFill from "../Systems/Inventory/GrainCableFill"; import TempHeatMap from "../Systems/Heatmap/TempHeatMap"; import NodePointCloud from "../Systems/Heatmap/NodePointCloud"; +import { Box } from "@mui/material"; interface Props { bin: Bin @@ -35,8 +36,6 @@ interface Props { export default function Bin3dView(props: 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. @@ -129,7 +128,6 @@ export default function Bin3dView(props: Props) { cableData={cableData} nodeData={nodeData} bin={bin} - binCenter={binCenter} onNodeClick={(node, cable) => { if (nodeClick) nodeClick(node, cable) }} diff --git a/src/bin/3dView/Systems/Cables/BinCables.tsx b/src/bin/3dView/Systems/Cables/BinCables.tsx index 7480e46..1901178 100644 --- a/src/bin/3dView/Systems/Cables/BinCables.tsx +++ b/src/bin/3dView/Systems/Cables/BinCables.tsx @@ -1,15 +1,13 @@ import GrainCable from "./GrainCable"; -import { Vector3 } from "three"; import { Bin } from "models"; import React from "react"; import { CableData } from "../../Data/BuildCableData"; import { NodeData } from "../../Data/BuildNodeData"; interface Props { - cableData: CableData[] - nodeData: NodeData[] - bin: Bin - binCenter: Vector3, + cableData: CableData[], + nodeData: NodeData[], + bin: Bin, renderOrder?: number, displayTemp?: boolean, displayMoisture?: boolean, @@ -17,7 +15,7 @@ interface Props { } export default function BinCables(props: Props){ - const {bin, binCenter, cableData, nodeData, onNodeClick, renderOrder, displayTemp, displayMoisture} = props + const {bin, cableData, nodeData, onNodeClick, renderOrder, displayTemp, displayMoisture} = props return ( {cableData.map((cable, i) => { @@ -29,7 +27,6 @@ export default function BinCables(props: Props){ cable={cable} nodes={cableNodes} bin={bin} - binCenter={binCenter} displayTemp={displayTemp} displayMoisture={displayMoisture} onNodeClick={(node) => { diff --git a/src/bin/3dView/Systems/Cables/CableNode.tsx b/src/bin/3dView/Systems/Cables/CableNode.tsx index 99c5824..ecaaf18 100644 --- a/src/bin/3dView/Systems/Cables/CableNode.tsx +++ b/src/bin/3dView/Systems/Cables/CableNode.tsx @@ -1,21 +1,18 @@ import Sphere, { SphereGeometry } from "3dModels/Shapes/3D/Sphere" import { useFrame, useThree } from "@react-three/fiber" -import { Billboard, Text } from "@react-three/drei" -import React, { useMemo, useState } from "react" +import { Text } from "@react-three/drei" +import React, { useMemo } from "react" import { Euler, Group, Vector3 } from "three" -import { describeMeasurement } from "pbHelpers/MeasurementDescriber" -import { quack } from "protobuf-ts/quack" +import { green, grey, orange, red } from "@mui/material/colors"; import { useGlobalState } from "providers" import { pond } from "protobuf-ts/pond" import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring" import { NodeData } from "../../Data/BuildNodeData" -import { TempToColour } from "bin/3dView/utils/tempToColour" interface Props { node: NodeData - binCenter: Vector3 - lowerThreshold: number upperThreshold: number, + targetMoisture: number, renderOrder?: number, displayTemp?: boolean, displayMoisture?: boolean, @@ -23,33 +20,31 @@ interface Props { } export default function CableNode(props: Props) { - const { node, binCenter, lowerThreshold, upperThreshold, onNodeClick, renderOrder, displayTemp, displayMoisture } = props + const { node, upperThreshold, targetMoisture, onNodeClick, renderOrder, displayTemp, displayMoisture } = props const { camera } = useThree(); const [{ user }] = useGlobalState(); // const [showLabel, setShowLabel] = useState(false); - const labelGroupRef = React.useRef(null); - const ROW_HEIGHT = 35; - const PADDING = 20; + const ROW_HEIGHT = 45; + const PADDING = 15; + const LABEL_WIDTH = 220; + const flagRef = React.useRef(null); - //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; - - // if (!showLabel && distance < SHOW_DISTANCE) { - // setShowLabel(true); - // } else if (showLabel && distance > HIDE_DISTANCE) { - // setShowLabel(false); - // } - - // const scale = distance * 0.07; - - // if (labelGroupRef.current) { - // labelGroupRef.current.scale.set(scale, scale, 1); - // } - // }); + useFrame(() => { + if (!flagRef.current || !showLabel) return; + + // Get camera's right direction in world space + const cameraRight = new Vector3(); + camera.getWorldDirection(cameraRight); // forward + const up = new Vector3(0, 1, 0); + cameraRight.crossVectors(cameraRight, up).normalize(); // right = forward × up + + const pos = node.position.clone(); + pos.addScaledVector(cameraRight, LABEL_WIDTH / 2); // offset right in screen space + pos.addScaledVector(camera.position.clone().sub(node.position).normalize(), 1); // nudge toward camera + + flagRef.current.position.copy(pos); + flagRef.current.quaternion.copy(camera.quaternion); + }); const tempDisplay = useMemo(() => { if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { @@ -63,7 +58,7 @@ export default function CableNode(props: Props) { if (node.excluded) { if (!anyToggleOn) return []; - return [{ text: "Excluded", color: "red" }]; + return [{ text: "Excluded", color: grey[700] }]; } const r: { text: string; color: string }[] = []; @@ -72,7 +67,9 @@ export default function CableNode(props: Props) { if (displayTemp && node.celcius !== 0) { r.push({ text: tempDisplay, - color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour() + // color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour() + //will be green when below threshold, orange when it goeas above but by less than 5 degrees, and red if more than 5 over + color: node.celcius > upperThreshold ? node.celcius > upperThreshold + 5 ? red[700] : orange[800]: green[800] }); } @@ -81,21 +78,25 @@ export default function CableNode(props: Props) { if (node.moisture && node.moisture > 0) { r.push({ text: `${node.moisture.toFixed(2)}% EMC`, - color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour() + // color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour() + color: node.moisture > targetMoisture ? node.moisture > targetMoisture + 0.5 ? red[700] : orange[800]: green[800] }); - } else if (node.humidity && node.humidity > 0) { - r.push({ - text: `${node.humidity.toFixed(2)}%`, - color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour() - }); - } + } + //apparently we dont want to display humidity at all + // else if (node.humidity && node.humidity > 0) { + // r.push({ + // text: `${node.humidity.toFixed(2)}%`, + // // color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour() + // color: "black" + // }); + // } } 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" }); + r.unshift({ text: "Top Node", color: grey[700] }); } return r; @@ -117,12 +118,8 @@ export default function CableNode(props: Props) { const topNodeRotation = React.useMemo(() => new Euler(-Math.PI / 2, 0, 0), []); - const labelPosition = node.position.clone(); const labelHeight = rows.length * ROW_HEIGHT + PADDING; - const dir = camera.position.clone().sub(node.position).normalize(); - labelPosition.add(dir.multiplyScalar(1)); - const getRowY = (index: number) => { const totalHeight = rows.length * ROW_HEIGHT; return totalHeight / 2 - (index * ROW_HEIGHT) - ROW_HEIGHT / 2; @@ -130,9 +127,7 @@ export default function CableNode(props: Props) { const nodeColour = () => { if (node.excluded) return "grey"; - if (!node.inGrain) return "white"; - const c = TempToColour(node, lowerThreshold, upperThreshold, "node"); - return c !== null ? c.getStyle() : "white"; + return "white"; }; const handleClick = (e: any) => { @@ -144,49 +139,46 @@ export default function CableNode(props: Props) { return ( - {(!node.inGrain || !showLabel) && - - - {node.topNode && ( - - )} - - } + + + {node.topNode && ( + + )} + {node.inGrain && showLabel && - - - - - - - {rows.map((row, i) => ( - - {row.text} - - ))} - - - } + + + + + + {rows.map((row, i) => ( + + {row.text} + + ))} + +} ) } \ No newline at end of file diff --git a/src/bin/3dView/Systems/Cables/GrainCable.tsx b/src/bin/3dView/Systems/Cables/GrainCable.tsx index 73add6a..7c3aa3b 100644 --- a/src/bin/3dView/Systems/Cables/GrainCable.tsx +++ b/src/bin/3dView/Systems/Cables/GrainCable.tsx @@ -7,17 +7,16 @@ import { CableData } from "../../Data/BuildCableData" import { NodeData } from "../../Data/BuildNodeData" interface Props { - cable: CableData - nodes: NodeData[] - bin: Bin - binCenter: Vector3, + cable: CableData, + nodes: NodeData[], + bin: Bin, renderOrder?: number, displayTemp?: boolean, - displayMoisture?: boolean + displayMoisture?: boolean, onNodeClick?: (node: NodeData) => void } export default function GrainCable(props: Props) { - const {cable, nodes, bin, binCenter, onNodeClick, renderOrder, displayTemp, displayMoisture} = props + const {cable, nodes, bin, onNodeClick, renderOrder, displayTemp, displayMoisture} = props const calculateHeight = () => { return cable.topPosition.distanceTo(cable.bottomPosition) @@ -44,7 +43,7 @@ export default function GrainCable(props: Props) { {nodes.map((node, i) => ( - + ))} ) diff --git a/src/bin/binSummary/BinSummary.tsx b/src/bin/binSummary/BinSummary.tsx index a4777e7..737f125 100644 --- a/src/bin/binSummary/BinSummary.tsx +++ b/src/bin/binSummary/BinSummary.tsx @@ -47,12 +47,12 @@ export default function BinSummary(props: Props){ - + {bin3dView()} - + {tableView()} diff --git a/src/bin/binSummary/components/bin3dVisualizer.tsx b/src/bin/binSummary/components/bin3dVisualizer.tsx index 62cf814..5da0a3e 100644 --- a/src/bin/binSummary/components/bin3dVisualizer.tsx +++ b/src/bin/binSummary/components/bin3dVisualizer.tsx @@ -1,6 +1,7 @@ -import { Box, Checkbox, Grid2, FormControlLabel } from "@mui/material"; +import { Box, Checkbox, FormControlLabel, List, ListItem } from "@mui/material"; import Bin3dView from "bin/3dView/Scene/Bin3dView"; import { Bin } from "models"; +import React from "react"; import { useState } from "react"; interface Props { @@ -19,13 +20,13 @@ export default function bin3dVisualizer(props: Props){ return 0 } - - return ( - - - - { + return ( + + + + - + + - + + - + + - + + + - - - - - - - + + + + + + + + ) + } + + const binViewer = () => { + return ( + + ) + } + + return ( + + + {binViewer()} + + {controlsOverlay()} + + ) } \ No newline at end of file diff --git a/src/bin/binSummary/components/binTableView.tsx b/src/bin/binSummary/components/binTableView.tsx index 42d4fb9..daa76ff 100644 --- a/src/bin/binSummary/components/binTableView.tsx +++ b/src/bin/binSummary/components/binTableView.tsx @@ -1,37 +1,165 @@ -import { Box } from "@mui/material"; +import { Box, Table, TableBody, TableCell, TableHead, TableRow } from "@mui/material"; +import { green, grey, orange, red } from "@mui/material/colors"; import { Bin } from "models"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers"; +import React from "react"; import { useMemo } from "react"; +import { avg, celsiusToFahrenheit } from "utils"; interface Props { bin: Bin } -interface LevelAvg { - temp: number - moisture: number - inGrain: boolean - +interface BinLevel { + grainTemps: number[] + airTemps: number[] + grainMoistures: number[] + airMoistures: number[] } export default function BinTableView(props: Props) { const {bin} = props + const [{user}] = useGlobalState() - const cableLevelAverages = useMemo(() => { + const binLevels = useMemo(() => { const cables = bin.status.grainCables - let levels: LevelAvg[] = [] - console.log(cables) + let levels: BinLevel[] = [] //note for future self, the first item in the measurement arrays is the lowest node on the cable //the zones we are creating here will match with the node number because most cables will either have the same number of nodes or only have a difference of 1 or 2 //example: cable 1 has 5 nodes cable 2 has 7 nodes, the first five nodes of the cables will match up and average together for each zone //and then zones 6 and 7 will be made using only cable 2 because cable 1 doesn't have them cables.forEach(cable => { - + cable.celcius.forEach((c, index) => { + //remember index will be 0 based but the top node starts at 1 for the lowest cable + let inGrain = index < cable.topNode + if (levels[index]){ + if(cable.excludedNodes.includes(index+1)) return + if(inGrain){ + levels[index].grainTemps.push(c) + //need to check that the cable not only has the moisture mutation on it, but also that it reads humidity + //cables that dont read humidity return an array of 0 values so if the average of all of the values is 0 then it is a temp only cable + if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ + //based on all the other checks and the fact that the indexes between the array should match this should never be 0 but put a guard here just in case + //also note that we are only doing moisture and not humidity, it seems like users dont care about humidity + levels[index].grainMoistures.push(cable.moisture[index] ?? 0) + } + }else{ + levels[index].airTemps.push(c) + //need to check that the cable not only has the moisture mutation on it, but also that it reads humidity + //cables that dont read humidity return an array of 0 values so if the average of all of the values is 0 then it is a temp only cable + if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ + //based on all the other checks and the fact that the indexes between the array should match this should never be 0 but put a guard here just in case + levels[index].airMoistures.push(cable.moisture[index] ?? 0) + } + } + }else{ + let newlevel: BinLevel = { + grainTemps: [], + grainMoistures: [], + airTemps: [], + airMoistures: [] + } + if(!cable.excludedNodes.includes(index)){ + if(inGrain){ + newlevel.grainTemps.push(c) + if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ + newlevel.grainMoistures.push(cable.moisture[index] ?? 0) + } + }else{ + newlevel.airTemps.push(c) + if(cable.moisture.length > 0 && avg(cable.moisture) > 0){ + newlevel.airMoistures.push(cable.moisture[index] ?? 0) + } + } + } + levels.push(newlevel) + } + + }) }) + //flip the levels so that the highest nodes are on the top + levels.reverse() return levels },[bin]) + const levelDisplay = (level: BinLevel) => { + let tempDisplay = "--" + let moistureDisplay = "--" + let lvlTemp + let lvlMoisture + let tempColour: string = grey[600] + let moistureColour: string = grey[600] + + //determine the temp + if(level.grainTemps.length > 0){ + lvlTemp = avg(level.grainTemps) + //if the average temp is 5 degrees above the bins threshold + if(lvlTemp > (bin.upperTempThreshold() + 5)){ + tempColour = red[700] + }else if(lvlTemp > bin.upperTempThreshold()){ + //if the average temp is greater than 5 degrees over + tempColour = orange[600] + }else { + tempColour = green[300] + } + }else if (level.airTemps.length > 0){ + lvlTemp = avg(level.airTemps) + } + if (lvlTemp){ + if(user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){ + lvlTemp = celsiusToFahrenheit(lvlTemp) + } + tempDisplay = lvlTemp.toFixed(2) + (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " F" : " C") + } + + //determine the moisture to show + if(level.grainMoistures.length > 0){ + lvlMoisture = avg(level.grainMoistures) + if(lvlMoisture > (bin.targetMoisture() + 1.5)){ + moistureColour = red[700] + }else if(lvlMoisture > bin.targetMoisture()){ + //if the average temp is greater than 5 degrees over + moistureColour = orange[600] + }else { + moistureColour = green[300] + } + }else if (level.airMoistures.length > 0){ + lvlMoisture = avg(level.airMoistures) + } + if(lvlMoisture){ + moistureDisplay = lvlMoisture.toFixed(2) + "%" + } + return ( + + {tempDisplay} + {moistureDisplay} + + ) + } + + + return ( - Table View + + + + + Level + Temperature + Moisture + + + + {binLevels.map((level, i) => ( + + {binLevels.length - i} + {levelDisplay(level)} + + ))} + +
+
) } \ No newline at end of file diff --git a/src/models/Bin.ts b/src/models/Bin.ts index 475958c..6aa2389 100644 --- a/src/models/Bin.ts +++ b/src/models/Bin.ts @@ -292,4 +292,8 @@ export class Bin { public lowerTempThreshold(): number { return this.settings.lowTemp } + + public targetMoisture(): number { + return this.settings.inventory?.targetMoisture ?? 0 + } }