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 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 */}
|
||||
<BinCables bin={bin}/>
|
||||
<BinCables bin={bin} binCenter={binCenter}/>
|
||||
</group>
|
||||
|
||||
{/* lighting */}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
{cables3D.map(cable => (
|
||||
<GrainCable cable={cable}/>
|
||||
{cables3D.map((cable, i) => (
|
||||
<GrainCable key={"cable " + i} cable={cable} bin={bin} binCenter={binCenter}/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Group>(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 (
|
||||
<React.Fragment>
|
||||
{!showLabel ?
|
||||
<Sphere geometry={geometry} position={node.position}/>
|
||||
:
|
||||
<Billboard position={labelPosition}>
|
||||
<group renderOrder={999}>
|
||||
{/* Background */}
|
||||
<mesh position={[0, 0, -1]}>
|
||||
<planeGeometry args={[200, 100]} />
|
||||
<meshBasicMaterial
|
||||
color="black"
|
||||
transparent
|
||||
opacity={0.7}
|
||||
depthWrite={false}
|
||||
depthTest={false}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* Temp Text */}
|
||||
<Text
|
||||
position={[0, 35, 0]}
|
||||
fontSize={30}
|
||||
color={describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour()}
|
||||
anchorX="center"
|
||||
anchorY="middle">
|
||||
{node.celcius.toFixed(2)}°C
|
||||
</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>
|
||||
</Billboard>
|
||||
{(!node.inGrain || !showLabel) &&
|
||||
<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}>
|
||||
<group ref={labelGroupRef} renderOrder={999}>
|
||||
{/* Background */}
|
||||
<mesh position={[0, 0, -1]}>
|
||||
<planeGeometry args={[200, labelHeight]} />
|
||||
<meshBasicMaterial
|
||||
color="black"
|
||||
transparent
|
||||
opacity={0.9}
|
||||
depthWrite={false}
|
||||
depthTest={false}
|
||||
/>
|
||||
</mesh>
|
||||
{/* Rows */}
|
||||
{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>
|
||||
}
|
||||
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<React.Fragment>
|
||||
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
|
||||
{buildCableNodes().map(node => (
|
||||
<CableNode node={node}/>
|
||||
{buildCableNodes().map((node, i) => (
|
||||
<CableNode key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -284,4 +284,12 @@ export class Bin {
|
|||
}
|
||||
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