some tweaks to the 3d bin and nodes, finished up the table view

This commit is contained in:
csawatzky 2026-05-06 13:08:04 -06:00
parent 325d3ea7a0
commit 619f9b7bf4
9 changed files with 291 additions and 146 deletions

View file

@ -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) => {

View file

@ -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)
}}

View file

@ -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 (
<React.Fragment>
{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) => {

View file

@ -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<Group>(null);
const ROW_HEIGHT = 35;
const PADDING = 20;
const ROW_HEIGHT = 45;
const PADDING = 15;
const LABEL_WIDTH = 220;
const flagRef = React.useRef<Group>(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 (
<React.Fragment>
{(!node.inGrain || !showLabel) &&
<React.Fragment>
<Sphere
geometry={geometry}
position={node.position}
colour={nodeColour()}
onClick={handleClick}
renderOrder={renderOrder}
/>
{node.topNode && (
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation} renderOrder={renderOrder}/>
)}
</React.Fragment>
}
<React.Fragment>
<Sphere
geometry={geometry}
position={node.position}
colour={nodeColour()}
onClick={handleClick}
renderOrder={renderOrder}
/>
{node.topNode && (
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation} renderOrder={renderOrder}/>
)}
</React.Fragment>
{node.inGrain && showLabel &&
<Billboard position={labelPosition}>
<group ref={labelGroupRef} renderOrder={999} onClick={handleClick}>
<mesh position={[0, 0, -1]}>
<planeGeometry args={[200, labelHeight]} />
<meshBasicMaterial
color="white"
transparent
opacity={0.9}
depthWrite={false}
depthTest={false}
/>
</mesh>
{rows.map((row, i) => (
<Text
key={i}
position={[0, getRowY(i), 0]}
fontSize={30}
color={row.color}
anchorX="center"
anchorY="middle"
>
{row.text}
</Text>
))}
</group>
</Billboard>
}
<group ref={flagRef} renderOrder={999} onClick={handleClick}>
<mesh position={[0, 0, -1]}>
<planeGeometry args={[LABEL_WIDTH, labelHeight]} />
<meshBasicMaterial
color="white"
transparent
opacity={0.9}
depthWrite={false}
depthTest={false}
/>
</mesh>
{rows.map((row, i) => (
<Text
key={i}
position={[0, getRowY(i), 0]}
fontSize={35}
fontWeight={650}
color={row.color}
anchorX="center"
anchorY="middle"
>
{row.text}
</Text>
))}
</group>
}
</React.Fragment>
)
}

View file

@ -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) {
<React.Fragment>
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5} renderOrder={renderOrder} depthWrite={false} depthTest={false}/>
{nodes.map((node, i) => (
<CableNode displayTemp={displayTemp} displayMoisture={displayMoisture} 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} targetMoisture={bin.targetMoisture()} upperThreshold={bin.upperTempThreshold()}/>
))}
</React.Fragment>
)