nodes and labes adjust based on zoom level, colours show based on the bins upper and lower temp limits, working on panning functionality for the camera
This commit is contained in:
parent
ee416db013
commit
f5db701d3c
7 changed files with 209 additions and 66 deletions
40
src/3dModels/Shapes/3D/Ring.tsx
Normal file
40
src/3dModels/Shapes/3D/Ring.tsx
Normal file
|
|
@ -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<BaseMeshProps, "geometry"> {
|
||||||
|
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 <BaseMesh {...props} geometry={geo} />;
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ import { Bin } from "models";
|
||||||
import BinShell from "./BinParts/BinShell";
|
import BinShell from "./BinParts/BinShell";
|
||||||
import GrainFillFlat from "./BinInventory/GrainFillFlat";
|
import GrainFillFlat from "./BinInventory/GrainFillFlat";
|
||||||
import BinCables from "./BinCables/BinCables";
|
import BinCables from "./BinCables/BinCables";
|
||||||
|
import { Vector3 } from "three";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
interface Props {
|
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,
|
// 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
|
// 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 {bin, scale = 100, fillPercent} = props
|
||||||
|
const binCenter = useMemo(() => new Vector3(0, 0, 0), []);
|
||||||
|
|
||||||
console.log(bin)
|
console.log(bin)
|
||||||
|
|
||||||
|
|
@ -55,7 +58,7 @@ export default function Bin3dView(props: Props){
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* cables */}
|
{/* cables */}
|
||||||
<BinCables bin={bin}/>
|
<BinCables bin={bin} binCenter={binCenter}/>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{/* lighting */}
|
{/* lighting */}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ interface CableOrbit {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bin: Bin
|
bin: Bin
|
||||||
|
binCenter: Vector3
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BinCables(props: Props){
|
export default function BinCables(props: Props){
|
||||||
const {bin} = props
|
const {bin, binCenter} = props
|
||||||
const getLayoutFromDiameter = (diameterCm: number): CableOrbit[] => {
|
const getLayoutFromDiameter = (diameterCm: number): CableOrbit[] => {
|
||||||
const diameterFt = diameterCm / 30.48;
|
const diameterFt = diameterCm / 30.48;
|
||||||
|
|
||||||
|
|
@ -192,8 +193,8 @@ export default function BinCables(props: Props){
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{cables3D.map(cable => (
|
{cables3D.map((cable, i) => (
|
||||||
<GrainCable cable={cable}/>
|
<GrainCable key={"cable " + i} cable={cable} bin={bin} binCenter={binCenter}/>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import Sphere, { SphereGeometry } from "3dModels/Shapes/3D/Sphere"
|
import Sphere, { SphereGeometry } from "3dModels/Shapes/3D/Sphere"
|
||||||
import { useFrame, useThree } from "@react-three/fiber"
|
import { useFrame, useThree } from "@react-three/fiber"
|
||||||
import { Billboard, Text } from "@react-three/drei"
|
import { Billboard, Text } from "@react-three/drei"
|
||||||
import React, { useMemo, useState } from "react"
|
import React, { useMemo, useRef, useState } from "react"
|
||||||
import { Vector3 } from "three"
|
import { Euler, Group, Vector3 } from "three"
|
||||||
import { cloneDeep } from "lodash"
|
|
||||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"
|
||||||
import { quack } from "protobuf-ts/quack"
|
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 {
|
export interface NodeData {
|
||||||
radius: number
|
radius: number
|
||||||
|
|
@ -14,21 +16,29 @@ export interface NodeData {
|
||||||
humidity?: number
|
humidity?: number
|
||||||
moisture?: number
|
moisture?: number
|
||||||
topNode?: boolean
|
topNode?: boolean
|
||||||
|
excluded?: boolean
|
||||||
|
inGrain?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
node: NodeData
|
node: NodeData
|
||||||
|
binCenter: Vector3
|
||||||
|
lowerThreshold: number
|
||||||
|
upperThreshold: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CableNode(props: Props){
|
export default function CableNode(props: Props){
|
||||||
const {node} = props
|
const {node, binCenter, lowerThreshold, upperThreshold} = props
|
||||||
const { camera } = useThree();
|
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(() => {
|
useFrame(() => {
|
||||||
//use the cameras distance to the center of the bin
|
//use the cameras distance to the center of the bin
|
||||||
const distance = camera.position.distanceTo(new Vector3(0, 0, 0))
|
const distance = camera.position.distanceTo(binCenter)
|
||||||
console.log(distance)
|
|
||||||
|
|
||||||
const SHOW_DISTANCE = 15;
|
const SHOW_DISTANCE = 15;
|
||||||
const HIDE_DISTANCE = 16; // hysteresis buffer
|
const HIDE_DISTANCE = 16; // hysteresis buffer
|
||||||
|
|
@ -38,73 +48,147 @@ export default function CableNode(props: Props){
|
||||||
} else if (showLabel && distance > HIDE_DISTANCE) {
|
} else if (showLabel && distance > HIDE_DISTANCE) {
|
||||||
setShowLabel(false);
|
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(() => ({
|
const geometry = React.useMemo(() => ({
|
||||||
radius: node.radius,
|
radius: node.radius,
|
||||||
} as SphereGeometry), [node]);
|
} 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 labelPosition = node.position.clone();
|
||||||
|
const labelHeight = rows.length * ROW_HEIGHT + PADDING;
|
||||||
|
|
||||||
const dir = camera.position.clone().sub(node.position).normalize();
|
const dir = camera.position.clone().sub(node.position).normalize();
|
||||||
|
|
||||||
// push label slightly toward camera
|
// push label slightly toward camera
|
||||||
labelPosition.add(dir.multiplyScalar(1)); // small offset like 0.5–2 units
|
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!showLabel ?
|
{(!node.inGrain || !showLabel) &&
|
||||||
<Sphere geometry={geometry} position={node.position}/>
|
<React.Fragment>
|
||||||
:
|
<Sphere geometry={geometry} position={node.position} colour={nodeColour()}/>
|
||||||
|
{node.topNode && (
|
||||||
|
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
{node.inGrain && showLabel &&
|
||||||
<Billboard position={labelPosition}>
|
<Billboard position={labelPosition}>
|
||||||
<group renderOrder={999}>
|
<group ref={labelGroupRef} renderOrder={999}>
|
||||||
{/* Background */}
|
{/* Background */}
|
||||||
<mesh position={[0, 0, -1]}>
|
<mesh position={[0, 0, -1]}>
|
||||||
<planeGeometry args={[200, 100]} />
|
<planeGeometry args={[200, labelHeight]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
color="black"
|
color="black"
|
||||||
transparent
|
transparent
|
||||||
opacity={0.7}
|
opacity={0.9}
|
||||||
depthWrite={false}
|
depthWrite={false}
|
||||||
depthTest={false}
|
depthTest={false}
|
||||||
/>
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
|
{/* Rows */}
|
||||||
{/* Temp Text */}
|
{rows.map((row, i) => (
|
||||||
<Text
|
<Text
|
||||||
position={[0, 35, 0]}
|
key={i}
|
||||||
|
position={[0, getRowY(i), 0]}
|
||||||
fontSize={30}
|
fontSize={30}
|
||||||
color={describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour()}
|
color={row.color}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="middle">
|
anchorY="middle"
|
||||||
{node.celcius.toFixed(2)}°C
|
>
|
||||||
|
{row.text}
|
||||||
</Text>
|
</Text>
|
||||||
{/* Humidity Text */}
|
))}
|
||||||
{node.humidity &&
|
|
||||||
<Text
|
|
||||||
position={[0, 0, 0]}
|
|
||||||
fontSize={30}
|
|
||||||
color={describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour()}
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle">
|
|
||||||
{node.humidity.toFixed(2)}%
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
{/* Moisture Text */}
|
|
||||||
{node.moisture &&
|
|
||||||
<Text
|
|
||||||
position={[0, -35, 0]}
|
|
||||||
fontSize={30}
|
|
||||||
color={describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour()}
|
|
||||||
anchorX="center"
|
|
||||||
anchorY="middle">
|
|
||||||
{node.moisture.toFixed(2)}% EMC
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
</group>
|
</group>
|
||||||
</Billboard>
|
</Billboard>
|
||||||
}
|
}
|
||||||
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { pond } from "protobuf-ts/pond"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Vector3 } from "three"
|
import { Vector3 } from "three"
|
||||||
import CableNode, { NodeData } from "./CableNode"
|
import CableNode, { NodeData } from "./CableNode"
|
||||||
|
import { Bin } from "models"
|
||||||
|
|
||||||
export interface CableData {
|
export interface CableData {
|
||||||
radius: number
|
radius: number
|
||||||
|
|
@ -15,9 +16,11 @@ export interface CableData {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cable: CableData
|
cable: CableData
|
||||||
|
bin: Bin
|
||||||
|
binCenter: Vector3
|
||||||
}
|
}
|
||||||
export default function GrainCable(props: Props) {
|
export default function GrainCable(props: Props) {
|
||||||
const {cable} = props
|
const {cable, bin, binCenter} = props
|
||||||
|
|
||||||
const calculateHeight = () => {
|
const calculateHeight = () => {
|
||||||
return cable.topPosition.distanceTo(cable.bottomPosition)
|
return cable.topPosition.distanceTo(cable.bottomPosition)
|
||||||
|
|
@ -59,7 +62,9 @@ export default function GrainCable(props: Props) {
|
||||||
celcius: celcius,
|
celcius: celcius,
|
||||||
humidity: humidity,
|
humidity: humidity,
|
||||||
moisture: moisture,
|
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
|
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
|
||||||
{buildCableNodes().map(node => (
|
{buildCableNodes().map((node, i) => (
|
||||||
<CableNode node={node}/>
|
<CableNode key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -615,7 +615,9 @@ export default function BinSVGV2(props: Props) {
|
||||||
//determine how high to draw the node on the cable
|
//determine how high to draw the node on the cable
|
||||||
const nodeY = nodeSpacingY * (index + 1) + minNodeY;
|
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.excludedNodes.includes(nodeNumber - 1)){
|
||||||
if (cable.topNode > 0) {
|
if (cable.topNode > 0) {
|
||||||
if (nodeNumber <= cable.topNode) {
|
if (nodeNumber <= cable.topNode) {
|
||||||
|
|
|
||||||
|
|
@ -284,4 +284,12 @@ export class Bin {
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public upperTempThreshold(): number {
|
||||||
|
return this.settings.highTemp
|
||||||
|
}
|
||||||
|
|
||||||
|
public lowerTempThreshold(): number {
|
||||||
|
return this.settings.lowTemp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue