diff --git a/src/3dModels/Shapes/3D/Ring.tsx b/src/3dModels/Shapes/3D/Ring.tsx new file mode 100644 index 0000000..1a79d40 --- /dev/null +++ b/src/3dModels/Shapes/3D/Ring.tsx @@ -0,0 +1,40 @@ +import { useMemo } from "react"; +import * as THREE from "three"; +import BaseMesh, { BaseMeshProps } from "../BaseMesh"; + +export interface RingGeometry{ + // the radius of the ring from the center to the ouside edge, must be larger than thickness + radius: number + // the thickness of the tube, must be smaller than radius + thickness: number + radialSegments?: number + tubularSegments?: number + // central angle in radians + arc?: number +} + +interface Props extends Omit { + geometry: RingGeometry; +} + +export default function Ring(props: Props) { + const { geometry } = props; + + const geo = useMemo(() => { + return new THREE.TorusGeometry( + geometry.radius, + geometry.thickness, + geometry.radialSegments ?? 12, + geometry.tubularSegments ?? 48, + geometry.arc ?? Math.PI * 2 + ); + }, [ + geometry.radius, + geometry.thickness, + geometry.radialSegments, + geometry.tubularSegments, + geometry.arc + ]); + + return ; +} \ No newline at end of file diff --git a/src/bin/3dView/Bin3dView.tsx b/src/bin/3dView/Bin3dView.tsx index 9322e48..6b6213d 100644 --- a/src/bin/3dView/Bin3dView.tsx +++ b/src/bin/3dView/Bin3dView.tsx @@ -4,6 +4,8 @@ import { Bin } from "models"; import BinShell from "./BinParts/BinShell"; import GrainFillFlat from "./BinInventory/GrainFillFlat"; import BinCables from "./BinCables/BinCables"; +import { Vector3 } from "three"; +import { useMemo } from "react"; interface Props { /** @@ -27,6 +29,7 @@ export default function Bin3dView(props: Props){ // and either a cone for the hopper or circle for flat bottom, it is possible to also use lathe geometry for this as well, // it might even work better because we can control the smoothness easier with it being only one mesh rather than 3 const {bin, scale = 100, fillPercent} = props + const binCenter = useMemo(() => new Vector3(0, 0, 0), []); console.log(bin) @@ -55,7 +58,7 @@ export default function Bin3dView(props: Props){ /> )} {/* cables */} - + {/* lighting */} diff --git a/src/bin/3dView/BinCables/BinCables.tsx b/src/bin/3dView/BinCables/BinCables.tsx index a4026d2..e7b5684 100644 --- a/src/bin/3dView/BinCables/BinCables.tsx +++ b/src/bin/3dView/BinCables/BinCables.tsx @@ -14,10 +14,11 @@ interface CableOrbit { interface Props { bin: Bin + binCenter: Vector3 } export default function BinCables(props: Props){ - const {bin} = props + const {bin, binCenter} = props const getLayoutFromDiameter = (diameterCm: number): CableOrbit[] => { const diameterFt = diameterCm / 30.48; @@ -192,8 +193,8 @@ export default function BinCables(props: Props){ return ( - {cables3D.map(cable => ( - + {cables3D.map((cable, i) => ( + ))} ) diff --git a/src/bin/3dView/BinCables/CableNode.tsx b/src/bin/3dView/BinCables/CableNode.tsx index ac3235e..a9803fc 100644 --- a/src/bin/3dView/BinCables/CableNode.tsx +++ b/src/bin/3dView/BinCables/CableNode.tsx @@ -1,11 +1,13 @@ 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 { Vector3 } from "three" -import { cloneDeep } from "lodash" +import React, { useMemo, useRef, useState } from "react" +import { Euler, Group, Vector3 } from "three" import { describeMeasurement } from "pbHelpers/MeasurementDescriber" import { quack } from "protobuf-ts/quack" +import { useGlobalState } from "providers" +import { pond } from "protobuf-ts/pond" +import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring" export interface NodeData { radius: number @@ -14,21 +16,29 @@ export interface NodeData { humidity?: number moisture?: number topNode?: boolean + excluded?: boolean + inGrain?: boolean } interface Props { node: NodeData + binCenter: Vector3 + lowerThreshold: number + upperThreshold: number } export default function CableNode(props: Props){ - const {node} = props + const {node, binCenter, lowerThreshold, upperThreshold} = props const { camera } = useThree(); + const [{user}] = useGlobalState(); const [showLabel, setShowLabel] = useState(false); + const labelGroupRef = React.useRef(null); + const ROW_HEIGHT = 35; + const PADDING = 20; useFrame(() => { //use the cameras distance to the center of the bin - const distance = camera.position.distanceTo(new Vector3(0, 0, 0)) - console.log(distance) + const distance = camera.position.distanceTo(binCenter) const SHOW_DISTANCE = 15; const HIDE_DISTANCE = 16; // hysteresis buffer @@ -38,73 +48,147 @@ export default function CableNode(props: Props){ } else if (showLabel && distance > HIDE_DISTANCE) { setShowLabel(false); } + + const scale = distance * 0.07; + + if (labelGroupRef.current) { + labelGroupRef.current.scale.set(scale, scale, 1); + } }); + const tempDisplay = () => { + let temp = node.celcius.toFixed(2) + "°C" + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + temp = ((node.celcius * 9/5) + 32).toFixed(2) + "°F" + } + return temp + } + + const nodeColour = () => { + if (!node.inGrain) return "white"; + + const lower = lowerThreshold; + const upper = upperThreshold; + + if (lower !== undefined && node.celcius < lower) { + return "#3399ff"; // blue + } + + if (upper !== undefined && node.celcius > upper) { + return "#ff4d4f"; // red + } + + return "#52c41a"; // green + }; + + const rows = useMemo(() => { + const r: { text: string; color: string }[] = []; + + if (node.excluded) { + r.push({ + text: "Excluded", + color: "red" + }); + return r; // nothing else matters + } + + if (node.topNode) { + r.push({ + text: "Top Node", + color: "yellow" + }); + } + + r.push({ + text: tempDisplay(), + color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour() + }); + + if (node.humidity) { + r.push({ + text: `${node.humidity.toFixed(2)}%`, + color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour() + }); + } + + if (node.moisture) { + r.push({ + text: `${node.moisture.toFixed(2)}% EMC`, + color: describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour() + }); + } + + return r; + }, [node, user]); + const geometry = React.useMemo(() => ({ radius: node.radius, } as SphereGeometry), [node]); + const topNodeGeo = React.useMemo(() => ({ + radius: node.radius, + thickness: 5, + } as RingGeometry), [node]) + + const topNodePosition = React.useMemo(() => (new Vector3(node.position.x, node.position.y + 25, node.position.z)), [node]) + + 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(); // push label slightly toward camera labelPosition.add(dir.multiplyScalar(1)); // small offset like 0.5–2 units + const getRowY = (index: number) => { + const totalHeight = rows.length * ROW_HEIGHT; + return totalHeight / 2 - (index * ROW_HEIGHT) - ROW_HEIGHT / 2; + }; + + return ( - {!showLabel ? - - : - - - {/* Background */} - - - - - - {/* Temp Text */} - - {node.celcius.toFixed(2)}°C - - {/* Humidity Text */} - {node.humidity && - - {node.humidity.toFixed(2)}% - - } - {/* Moisture Text */} - {node.moisture && - - {node.moisture.toFixed(2)}% EMC - - } - - + {(!node.inGrain || !showLabel) && + + + {node.topNode && ( + + )} + + } + {node.inGrain && showLabel && + + + {/* Background */} + + + + + {/* Rows */} + {rows.map((row, i) => ( + + {row.text} + + ))} + + } - ) } \ No newline at end of file diff --git a/src/bin/3dView/BinCables/GrainCable.tsx b/src/bin/3dView/BinCables/GrainCable.tsx index d7c70e7..e889893 100644 --- a/src/bin/3dView/BinCables/GrainCable.tsx +++ b/src/bin/3dView/BinCables/GrainCable.tsx @@ -3,6 +3,7 @@ import { pond } from "protobuf-ts/pond" import React from "react" import { Vector3 } from "three" import CableNode, { NodeData } from "./CableNode" +import { Bin } from "models" export interface CableData { radius: number @@ -15,9 +16,11 @@ export interface CableData { interface Props { cable: CableData + bin: Bin + binCenter: Vector3 } export default function GrainCable(props: Props) { - const {cable} = props + const {cable, bin, binCenter} = props const calculateHeight = () => { return cable.topPosition.distanceTo(cable.bottomPosition) @@ -59,7 +62,9 @@ export default function GrainCable(props: Props) { celcius: celcius, humidity: humidity, moisture: moisture, - topNode: grainCable.topNode === i + topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index + excluded: grainCable.excludedNodes.includes(i), + inGrain: i+1 <= grainCable.topNode }) }) @@ -78,8 +83,8 @@ export default function GrainCable(props: Props) { return ( - {buildCableNodes().map(node => ( - + {buildCableNodes().map((node, i) => ( + ))} ) diff --git a/src/bin/BinSVGV2.tsx b/src/bin/BinSVGV2.tsx index ecbeb53..0a24d5c 100644 --- a/src/bin/BinSVGV2.tsx +++ b/src/bin/BinSVGV2.tsx @@ -615,7 +615,9 @@ export default function BinSVGV2(props: Props) { //determine how high to draw the node on the cable const nodeY = nodeSpacingY * (index + 1) + minNodeY; - //if we are using the auto top nodes only show the heatmap for nodes at or below the top node and it is not an ecluded node + //if we are using the auto top nodes only show the heatmap for nodes at or below the top node and it is not an excluded node + //NOTE it is nodeNumber - 1 because the excluded node array uses a 0 based count for the node number but the nodeNumber variable uses a 1 based count + //ie. the bottom node of the cable would be nodeNumber of 1 but in the array it would be 0 if (!cable.excludedNodes.includes(nodeNumber - 1)){ if (cable.topNode > 0) { if (nodeNumber <= cable.topNode) { diff --git a/src/models/Bin.ts b/src/models/Bin.ts index 485358e..475958c 100644 --- a/src/models/Bin.ts +++ b/src/models/Bin.ts @@ -284,4 +284,12 @@ export class Bin { } return c; } + + public upperTempThreshold(): number { + return this.settings.highTemp + } + + public lowerTempThreshold(): number { + return this.settings.lowTemp + } }