did some refactoring so the node and cable data is computed in the data layer so that it is accessible by anything in the bin to make way for a heatmap

This commit is contained in:
csawatzky 2026-04-16 16:27:08 -06:00
parent ff64f8594c
commit 6c87ed03ed
13 changed files with 572 additions and 363 deletions

View file

@ -26,9 +26,9 @@ interface Props {
export default function OrbitCameraControls(props: Props) { export default function OrbitCameraControls(props: Props) {
const { target, clampVerticalRotation, minPhi = 0.01, maxPhi = Math.PI - 0.01 } = props; const { target, clampVerticalRotation, minPhi = 0.01, maxPhi = Math.PI - 0.01 } = props;
const { camera, gl } = useThree(); const { camera, gl } = useThree();
const panPlane = useRef(new Plane()); // const panPlane = useRef(new Plane());
const panStartPoint = useRef(new Vector3()); // const panStartPoint = useRef(new Vector3());
const raycaster = useRef(new Raycaster()); // const raycaster = useRef(new Raycaster());
const mouse = useRef(new Vector2()); const mouse = useRef(new Vector2());
const targetRef = useRef({ const targetRef = useRef({
x: target?.x ?? 0, x: target?.x ?? 0,
@ -39,6 +39,7 @@ export default function OrbitCameraControls(props: Props) {
const isDragging = useRef(false); const isDragging = useRef(false);
const isPanning = useRef(false); const isPanning = useRef(false);
const lastPos = useRef({ x: 0, y: 0 }); const lastPos = useRef({ x: 0, y: 0 });
const panCameraPosition = useRef(new Vector3());
// Spherical coords // Spherical coords
const spherical = useRef({ const spherical = useRef({
@ -57,7 +58,11 @@ export default function OrbitCameraControls(props: Props) {
const y = radius * Math.cos(phi); const y = radius * Math.cos(phi);
const z = radius * Math.sin(phi) * Math.cos(theta); const z = radius * Math.sin(phi) * Math.cos(theta);
camera.position.set(x, y, z); camera.position.set(
targetRef.current.x + x,
targetRef.current.y + y,
targetRef.current.z + z
);
camera.lookAt( camera.lookAt(
targetRef.current.x, targetRef.current.x,
targetRef.current.y, targetRef.current.y,
@ -71,6 +76,7 @@ export default function OrbitCameraControls(props: Props) {
if (e.target !== canvas) return; if (e.target !== canvas) return;
lastPos.current = { x: e.clientX, y: e.clientY }; lastPos.current = { x: e.clientX, y: e.clientY };
panCameraPosition.current.copy(camera.position);
// Left click = rotate // Left click = rotate
if (e.button === 0) { if (e.button === 0) {
@ -87,28 +93,29 @@ export default function OrbitCameraControls(props: Props) {
const camDir = new Vector3(); const camDir = new Vector3();
camera.getWorldDirection(camDir); camera.getWorldDirection(camDir);
panPlane.current.setFromNormalAndCoplanarPoint( // panPlane.current.setFromNormalAndCoplanarPoint(
camDir, // camDir,
new Vector3( // new Vector3(
targetRef.current.x, // targetRef.current.x,
targetRef.current.y, // targetRef.current.y,
targetRef.current.z // targetRef.current.z
) // )
); // );
// starting intersection point // const rect = canvas.getBoundingClientRect();
raycaster.current.setFromCamera(
new Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
-(e.clientY / window.innerHeight) * 2 + 1
),
camera
);
raycaster.current.ray.intersectPlane( // raycaster.current.setFromCamera(
panPlane.current, // new Vector2(
panStartPoint.current // ((e.clientX - rect.left) / rect.width) * 2 - 1,
); // -((e.clientY - rect.top) / rect.height) * 2 + 1
// ),
// camera
// );
// raycaster.current.ray.intersectPlane(
// panPlane.current,
// panStartPoint.current
// );
} }
}; };
@ -132,28 +139,61 @@ export default function OrbitCameraControls(props: Props) {
} }
// PAN // PAN
// if (isPanning.current) {
// const rect = canvas.getBoundingClientRect();
// mouse.current.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
// mouse.current.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
// const currentPoint = new Vector3();
// const tempCamera = camera.clone();
// tempCamera.position.copy(panCameraPosition.current);
// tempCamera.updateMatrixWorld();
// raycaster.current.setFromCamera(mouse.current, tempCamera);
// raycaster.current.ray.intersectPlane(panPlane.current, currentPoint);
// const delta = new Vector3().subVectors(
// panStartPoint.current,
// currentPoint
// );
// targetRef.current.x += delta.x;
// targetRef.current.y += delta.y;
// targetRef.current.z += delta.z;
// panStartPoint.current.copy(currentPoint);
// }
if (isPanning.current) { if (isPanning.current) {
const raycaster = new Raycaster(); const deltaX = e.clientX - lastPos.current.x;
const deltaY = e.clientY - lastPos.current.y;
mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; // distance-based scaling (important for consistent feel)
mouse.current.y = -(e.clientY / window.innerHeight) * 2 + 1; const distance = spherical.current.radius;
raycaster.setFromCamera(mouse.current, camera); const panSpeed = 0.002 * distance;
const currentPoint = new Vector3(); const offset = new Vector3();
raycaster.ray.intersectPlane(panPlane.current, currentPoint); // get camera basis vectors
const right = new Vector3();
const up = new Vector3();
const delta = new Vector3().subVectors( camera.getWorldDirection(offset); // forward
panStartPoint.current, right.crossVectors(offset, camera.up).normalize(); // right
currentPoint up.copy(camera.up).normalize();
);
targetRef.current.x += delta.x; // apply movement
targetRef.current.y += delta.y; right.multiplyScalar(-deltaX * panSpeed);
targetRef.current.z += delta.z; up.multiplyScalar(deltaY * panSpeed);
panStartPoint.current.copy(currentPoint); const pan = new Vector3().addVectors(right, up);
targetRef.current.x += pan.x;
targetRef.current.y += pan.y;
targetRef.current.z += pan.z;
} }
lastPos.current = { x: e.clientX, y: e.clientY }; lastPos.current = { x: e.clientX, y: e.clientY };

View file

@ -1,201 +0,0 @@
import GrainCable, { CableData } from "./GrainCable";
import { avg } from "utils";
import { Vector3 } from "three";
import { useMemo } from "react";
import { Bin } from "models";
import React from "react";
interface CableOrbit {
orbit: number;
radius: number; // in cm
expectedCount: number;
assignedCount?: number;
}
interface Props {
bin: Bin
binCenter: Vector3
}
export default function BinCables(props: Props){
const {bin, binCenter} = props
const getLayoutFromDiameter = (diameterCm: number): CableOrbit[] => {
const diameterFt = diameterCm / 30.48;
let summaries: { Orbit: number; DistanceFromCenter: number; NumberOfCables: number }[] = [];
if (diameterFt < 24) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
} else if (diameterFt < 36) {
summaries.push({ Orbit: 1, DistanceFromCenter: 7, NumberOfCables: 3 });
} else if (diameterFt < 42) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
} else if (diameterFt < 48) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 14, NumberOfCables: 4 });
} else if (diameterFt < 54) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 16, NumberOfCables: 5 });
} else if (diameterFt < 60) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
} else if (diameterFt < 72) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 20, NumberOfCables: 7 });
} else if (diameterFt < 78) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 27, NumberOfCables: 9 });
} else if (diameterFt < 90) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 29, NumberOfCables: 10 });
} else if (diameterFt < 105) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
summaries.push({ Orbit: 2, DistanceFromCenter: 36, NumberOfCables: 12 });
} else {
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
summaries.push({ Orbit: 2, DistanceFromCenter: 26.5, NumberOfCables: 9 });
summaries.push({ Orbit: 3, DistanceFromCenter: 44, NumberOfCables: 14 });
}
return summaries.map(s => ({
orbit: s.Orbit,
radius: s.DistanceFromCenter * 30.48, // ft → cm
expectedCount: s.NumberOfCables
}));
};
const distributeCables = (cableCount: number, layout: CableOrbit[]) => {
const totalExpected = layout.reduce((sum, o) => sum + o.expectedCount, 0);
let assigned = layout.map(o => ({
...o,
assignedCount: Math.round((o.expectedCount / totalExpected) * cableCount)
}));
// normalize rounding
let currentTotal = assigned.reduce((sum, o) => sum + (o.assignedCount ?? 0), 0);
while (currentTotal < cableCount) {
assigned[assigned.length - 1].assignedCount!++;
currentTotal++;
}
while (currentTotal > cableCount) {
assigned[assigned.length - 1].assignedCount!--;
currentTotal--;
}
return assigned;
};
const getTopY = (radiusFromCenter: number) => {
const binRadius = bin.diameter() / 2;
const distanceFromEdge = binRadius - radiusFromCenter;
const angleRad = bin.roofAngle() * (Math.PI / 180);
const roofRise = Math.tan(angleRad) * distanceFromEdge;
return bin.sidewallHeight() / 2 + roofRise;
};
const getBottomY = (radiusFromCenter: number) => {
const binRadius = bin.diameter() / 2;
const distanceFromCenter = binRadius - radiusFromCenter;
const angleRad = bin.hopperAngle() * (Math.PI / 180);
const hopperDrop = Math.tan(angleRad) * distanceFromCenter;
const clearance = 60 //this is in cm
return -bin.sidewallHeight() / 2 - hopperDrop + clearance;
};
const buildCablePositions = (bin: Bin): CableData[] => {
const cables = [...(bin.status.grainCables ?? [])];
if (cables.length === 0) return [];
// optional: sort cables (better visual consistency later)
//cables.sort((a, b) => b.celcius.length - a.celcius.length);
cables.sort((a, b) => {
//if the cables have the same number of nodes
if (b.celcius.length === a.celcius.length) {
//sort by temp only last and any type that has humidity first
if ((avg(a.relativeHumidity) === 0) && (avg(b.relativeHumidity) !== 0)) {
return 1;
} else if ((avg(b.relativeHumidity) === 0) && (avg(a.relativeHumidity) !== 0)) {
return -1;
}
else {
return a.key > b.key ? 1 : -1;
}
}
//otherwise sort by the longest cable first
return b.celcius.length - a.celcius.length;
});
const layout = getLayoutFromDiameter(bin.diameter());
const distributed = distributeCables(cables.length, layout);
const cableRadius = 5;
const result: CableData[] = [];
let cableIndex = 0;
distributed.forEach(orbit => {
const count = orbit.assignedCount ?? 0;
const topY = getTopY(orbit.radius)
const bottomY = getBottomY(orbit.radius)
if (orbit.orbit === 0 && count > 0) {
const cable = cables[cableIndex++];
result.push({
radius: cableRadius,
topPosition: new Vector3(0, topY, 0),
bottomPosition: new Vector3(0, bottomY, 0),
grainCable: cable
// optionally attach cable data later
});
return;
}
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const x = Math.cos(angle) * orbit.radius;
const z = Math.sin(angle) * orbit.radius;
const cable = cables[cableIndex++];
result.push({
radius: cableRadius,
topPosition: new Vector3(x,topY,z),
bottomPosition: new Vector3(x,bottomY,z),
grainCable: cable
});
}
});
return result;
};
const cables3D = useMemo(() => {
return buildCablePositions(bin);
}, [bin]);
return (
<React.Fragment>
{cables3D.map((cable, i) => (
<GrainCable key={"cable " + i} cable={cable} bin={bin} binCenter={binCenter}/>
))}
</React.Fragment>
)
}

View file

@ -1,91 +0,0 @@
import Cylinder, { CylinderGeometry } from "3dModels/Shapes/3D/Cylinder"
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
topPosition: Vector3
bottomPosition: Vector3
grainCable: pond.GrainCable
radialSegments?: number
heightSegments?: number
}
interface Props {
cable: CableData
bin: Bin
binCenter: Vector3
}
export default function GrainCable(props: Props) {
const {cable, bin, binCenter} = props
const calculateHeight = () => {
return cable.topPosition.distanceTo(cable.bottomPosition)
}
const calculateCenter = () => {
return cable.bottomPosition.clone()
.add(
cable.topPosition.clone()
.sub(cable.bottomPosition)
.multiplyScalar(0.5)
);
};
const buildCableNodes = () => {
const nodeRadius = 20
const grainCable = cable.grainCable
const nodeCount = grainCable.celcius.length;
if (nodeCount === 0) return [];
const bottomY = cable.bottomPosition.y;
const topY = cable.topPosition.y;
const height = topY - bottomY;
const spacing = height / nodeCount;
let nodeData: NodeData[] = []
grainCable.celcius.forEach((celcius,i) => {
const nodeY = bottomY + (i * spacing);
const humidity = grainCable.relativeHumidity[i] || undefined;
const moisture = grainCable.moisture[i] || undefined;
nodeData.push({
radius: nodeRadius,
position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z),
celcius: celcius,
humidity: humidity,
moisture: moisture,
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
})
})
return nodeData
}
const geometry = React.useMemo(() => ({
radiusTop: cable.radius,
radiusBottom: cable.radius,
height: calculateHeight(),
radialSegments: cable.radialSegments ?? 10,
heightSegments: cable.heightSegments ?? 2
} as CylinderGeometry), [cable]);
return (
<React.Fragment>
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
{buildCableNodes().map((node, i) => (
<CableNode key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
))}
</React.Fragment>
)
}

View file

@ -0,0 +1,194 @@
import { Bin } from "models";
import { pond } from "protobuf-ts/pond";
import { Vector3 } from "three";
import { avg } from "utils";
export interface CableData {
cableIndex: number
radius: number
topPosition: Vector3
bottomPosition: Vector3
grainCable: pond.GrainCable
radialSegments?: number
heightSegments?: number
}
export interface CableOrbit {
orbit: number;
radius: number; // in cm
expectedCount: number;
assignedCount?: number;
}
function getLayoutFromDiameter(diameterCm: number): CableOrbit[] {
const diameterFt = diameterCm / 30.48;
let summaries: { Orbit: number; DistanceFromCenter: number; NumberOfCables: number }[] = [];
if (diameterFt < 24) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
} else if (diameterFt < 36) {
summaries.push({ Orbit: 1, DistanceFromCenter: 7, NumberOfCables: 3 });
} else if (diameterFt < 42) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
} else if (diameterFt < 48) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 14, NumberOfCables: 4 });
} else if (diameterFt < 54) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 16, NumberOfCables: 5 });
} else if (diameterFt < 60) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
} else if (diameterFt < 72) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 20, NumberOfCables: 7 });
} else if (diameterFt < 78) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 27, NumberOfCables: 9 });
} else if (diameterFt < 90) {
summaries.push({ Orbit: 1, DistanceFromCenter: 10, NumberOfCables: 4 });
summaries.push({ Orbit: 2, DistanceFromCenter: 29, NumberOfCables: 10 });
} else if (diameterFt < 105) {
summaries.push({ Orbit: 0, DistanceFromCenter: 2, NumberOfCables: 1 });
summaries.push({ Orbit: 1, DistanceFromCenter: 18, NumberOfCables: 6 });
summaries.push({ Orbit: 2, DistanceFromCenter: 36, NumberOfCables: 12 });
} else {
summaries.push({ Orbit: 1, DistanceFromCenter: 9, NumberOfCables: 3 });
summaries.push({ Orbit: 2, DistanceFromCenter: 26.5, NumberOfCables: 9 });
summaries.push({ Orbit: 3, DistanceFromCenter: 44, NumberOfCables: 14 });
}
return summaries.map(s => ({
orbit: s.Orbit,
radius: s.DistanceFromCenter * 30.48, // ft → cm
expectedCount: s.NumberOfCables
}));
};
function distributeCables(cableCount: number, layout: CableOrbit[]) {
const totalExpected = layout.reduce((sum, o) => sum + o.expectedCount, 0);
let assigned = layout.map(o => ({
...o,
assignedCount: Math.round((o.expectedCount / totalExpected) * cableCount)
}));
// normalize rounding
let currentTotal = assigned.reduce((sum, o) => sum + (o.assignedCount ?? 0), 0);
while (currentTotal < cableCount) {
assigned[assigned.length - 1].assignedCount!++;
currentTotal++;
}
while (currentTotal > cableCount) {
assigned[assigned.length - 1].assignedCount!--;
currentTotal--;
}
return assigned;
};
function getTopY(radiusFromCenter: number, bin: Bin) {
const binRadius = bin.diameter() / 2;
const distanceFromEdge = binRadius - radiusFromCenter;
const angleRad = bin.roofAngle() * (Math.PI / 180);
const roofRise = Math.tan(angleRad) * distanceFromEdge;
return bin.sidewallHeight() / 2 + roofRise;
};
function getBottomY(radiusFromCenter: number, bin: Bin) {
const binRadius = bin.diameter() / 2;
const distanceFromCenter = binRadius - radiusFromCenter;
const angleRad = bin.hopperAngle() * (Math.PI / 180);
const hopperDrop = Math.tan(angleRad) * distanceFromCenter;
const clearance = 60 //this is in cm
return -bin.sidewallHeight() / 2 - hopperDrop + clearance;
};
export function BuildCableData(bin: Bin): CableData[] {
const cables = [...(bin.status.grainCables ?? [])];
if (cables.length === 0) return [];
// optional: sort cables (better visual consistency later)
//cables.sort((a, b) => b.celcius.length - a.celcius.length);
cables.sort((a, b) => {
//if the cables have the same number of nodes
if (b.celcius.length === a.celcius.length) {
//sort by temp only last and any type that has humidity first
if ((avg(a.relativeHumidity) === 0) && (avg(b.relativeHumidity) !== 0)) {
return 1;
} else if ((avg(b.relativeHumidity) === 0) && (avg(a.relativeHumidity) !== 0)) {
return -1;
}
else {
return a.key > b.key ? 1 : -1;
}
}
//otherwise sort by the longest cable first
return b.celcius.length - a.celcius.length;
});
const layout = getLayoutFromDiameter(bin.diameter());
const distributed = distributeCables(cables.length, layout);
const cableRadius = 5;
const result: CableData[] = [];
let cableIndex = 0;
distributed.forEach(orbit => {
const count = orbit.assignedCount ?? 0;
const topY = getTopY(orbit.radius, bin)
const bottomY = getBottomY(orbit.radius, bin)
if (orbit.orbit === 0 && count > 0) {
const cable = cables[cableIndex];
if (!cable) return
result.push({
cableIndex: cableIndex,
radius: cableRadius,
topPosition: new Vector3(0, topY, 0),
bottomPosition: new Vector3(0, bottomY, 0),
grainCable: cable
});
cableIndex++
return;
}
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const x = Math.cos(angle) * orbit.radius;
const z = Math.sin(angle) * orbit.radius;
const cable = cables[cableIndex];
result.push({
cableIndex: cableIndex,
radius: cableRadius,
topPosition: new Vector3(x,topY,z),
bottomPosition: new Vector3(x,bottomY,z),
grainCable: cable
});
cableIndex++
}
});
return result;
};

View file

@ -0,0 +1,60 @@
import { Vector3 } from "three"
import { CableData } from "./BuildCableData"
export interface NodeData {
cableIndex: number
nodeIndex: number
radius: number
position: Vector3
celcius: number
humidity?: number
moisture?: number
topNode?: boolean
excluded?: boolean
inGrain?: boolean
}
export function BuildNodeData(cables: CableData[]): NodeData[] {
const nodeRadius = 20
let nodeData: NodeData[] = []
cables.forEach((cable, cableIndex) => {
const grainCable = cable.grainCable
const nodeCount = grainCable.celcius.length;
if (nodeCount === 0) return;
const bottomY = cable.bottomPosition.y;
const topY = cable.topPosition.y;
const height = topY - bottomY;
const spacing = height / nodeCount;
grainCable.celcius.forEach((celcius,i) => {
const nodeY = bottomY + (i * spacing);
const humidity = grainCable.relativeHumidity[i] || undefined;
const moisture = grainCable.moisture[i] || undefined;
nodeData.push({
cableIndex: cableIndex,
nodeIndex: i,
radius: nodeRadius,
position: new Vector3(cable.bottomPosition.x, nodeY, cable.bottomPosition.z),
celcius: celcius,
humidity: humidity,
moisture: moisture,
topNode: grainCable.topNode === i+1, //top node tracking starts at 1 not 0 so add one to the index
excluded: grainCable.excludedNodes?.includes(i) ?? false,
inGrain: i+1 <= grainCable.topNode
// optional: (may want to add this to the node data later)
// normalizedHeight: (nodeY - bottomY) / height
})
})
})
return nodeData
}

View file

@ -1,11 +1,13 @@
import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls"; import OrbitCameraControls from "3dModels/CameraControls/OrbitCameraControls";
import { Canvas } from "@react-three/fiber"; import { Canvas } from "@react-three/fiber";
import { Bin } from "models"; import { Bin } from "models";
import BinShell from "./BinParts/BinShell"; import BinShell from "../BinParts/BinShell";
import GrainFillFlat from "./BinInventory/GrainFillFlat"; import GrainFillFlat from "../Systems/Inventory/GrainFillFlat";
import BinCables from "./BinCables/BinCables"; import BinCables from "../Systems/Cables/BinCables";
import { Vector3 } from "three"; import { Vector3 } from "three";
import { useMemo } from "react"; import { useMemo } from "react";
import { BuildCableData } from "../Data/BuildCableData";
import { BuildNodeData } from "../Data/BuildNodeData";
interface Props { interface Props {
/** /**
@ -30,8 +32,8 @@ export default function Bin3dView(props: Props){
// 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), []); const binCenter = useMemo(() => new Vector3(0, 0, 0), []);
const cableData = useMemo(() => BuildCableData(bin), [bin]);
console.log(bin) const nodeData = useMemo(() => BuildNodeData(cableData), [cableData]);
return ( return (
<Canvas> <Canvas>
@ -46,6 +48,7 @@ export default function Bin3dView(props: Props){
sidewallHeight={bin.sidewallHeight()} sidewallHeight={bin.sidewallHeight()}
roofHeight={bin.roofHeight()} roofHeight={bin.roofHeight()}
hopperHeight={bin.hopperHeight()} hopperHeight={bin.hopperHeight()}
/> />
{/* grain - cylinder*/} {/* grain - cylinder*/}
@ -58,7 +61,7 @@ export default function Bin3dView(props: Props){
/> />
)} )}
{/* cables */} {/* cables */}
<BinCables bin={bin} binCenter={binCenter}/> <BinCables cableData={cableData} nodeData={nodeData} bin={bin} binCenter={binCenter}/>
</group> </group>
{/* lighting */} {/* lighting */}

View file

@ -0,0 +1,28 @@
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
}
export default function BinCables(props: Props){
const {bin, binCenter, cableData, nodeData} = props
return (
<React.Fragment>
{cableData.map((cable, i) => {
const cableNodes = nodeData.filter(n => n.cableIndex === cable.cableIndex);
return (
<GrainCable key={"cable " + i} cable={cable} nodes={cableNodes} bin={bin} binCenter={binCenter}/>
)}
)}
</React.Fragment>
)
}

View file

@ -1,24 +1,16 @@
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, useRef, useState } from "react" import React, { useMemo, useState } from "react"
import { Euler, Group, Vector3 } from "three" import { Euler, Group, Vector3 } from "three"
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 { useGlobalState } from "providers"
import { pond } from "protobuf-ts/pond" import { pond } from "protobuf-ts/pond"
import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring" import Ring, { RingGeometry } from "3dModels/Shapes/3D/Ring"
// import { Color } from "three";
export interface NodeData { import { NodeData } from "../../Data/BuildNodeData"
radius: number import { tempToColour } from "bin/3dView/utils/tempToColour"
position: Vector3
celcius: number
humidity?: number
moisture?: number
topNode?: boolean
excluded?: boolean
inGrain?: boolean
}
interface Props { interface Props {
node: NodeData node: NodeData
@ -36,6 +28,13 @@ export default function CableNode(props: Props){
const ROW_HEIGHT = 35; const ROW_HEIGHT = 35;
const PADDING = 20; const PADDING = 20;
//node colour varables for adjusting lighness and saturation for the thresholds
// const colourFade = 20 //this number is the temp diff above or below the thresholds that the node will reach its maximum red or blue intensity
// const minimumLightness = 0.3 //the minimum brightness of the colour, to low would approach black
// const lightnessRange = 0.2 //how much can the node brighten, to high will allow the node to turn white
// const minimumSaturation = 0.7
// const saturationRange = 0.8
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(binCenter) const distance = camera.position.distanceTo(binCenter)
@ -64,22 +63,68 @@ export default function CableNode(props: Props){
return temp return temp
} }
const nodeColour = () => { // const nodeColour = () => {
if (!node.inGrain) return "white"; // if (!node.inGrain) return "white";
// if (node.excluded) return "grey";
const lower = lowerThreshold; // const lower = lowerThreshold;
const upper = upperThreshold; // const upper = upperThreshold;
// const temp = node.celcius;
if (lower !== undefined && node.celcius < lower) { // const GREEN = new Color("#52c41a");
return "#3399ff"; // blue // const BLUE = new Color("#3399ff");
} // const RED = new Color("#ff4d4f");
if (upper !== undefined && node.celcius > upper) { // if (temp >= lower && temp <= upper) {
return "#ff4d4f"; // red // return GREEN.getStyle();
} // }
return "#52c41a"; // green // // shared helper logic concept:
}; // const getCloseness = (distance: number) => {
// const normalized = Math.min(1, distance / colourFade);
// return 1 - normalized; // 1 = near threshold, 0 = far
// };
// const color = new Color();
// // BELOW lower (blue)
// if (temp < lower) {
// const distance = lower - temp;
// const closeness = getCloseness(distance);
// const hsl = { h: 0, s: 1, l: 1 };
// BLUE.getHSL(hsl);
// const lightness = (lightnessRange * closeness) + minimumLightness;
// // 🔥 NEW: saturation as second intensity layer
// const saturation = (saturationRange * (1 - closeness)) + minimumSaturation;
// color.setHSL(hsl.h, saturation, lightness);
// return color.getStyle();
// }
// // ABOVE upper (red)
// if (temp > upper) {
// const distance = temp - upper;
// const closeness = getCloseness(distance);
// const hsl = { h: 0, s: 1, l: 1 };
// RED.getHSL(hsl);
// const lightness = (lightnessRange * closeness) + minimumLightness;
// // 🔥 NEW: saturation as second intensity layer
// const saturation = (saturationRange * (1 - closeness)) + minimumSaturation;
// color.setHSL(hsl.h, saturation, lightness);
// return color.getStyle();
// }
// return GREEN.getStyle();
// };
const rows = useMemo(() => { const rows = useMemo(() => {
const r: { text: string; color: string }[] = []; const r: { text: string; color: string }[] = [];
@ -153,7 +198,7 @@ export default function CableNode(props: Props){
<React.Fragment> <React.Fragment>
{(!node.inGrain || !showLabel) && {(!node.inGrain || !showLabel) &&
<React.Fragment> <React.Fragment>
<Sphere geometry={geometry} position={node.position} colour={nodeColour()}/> <Sphere geometry={geometry} position={node.position} colour={tempToColour(node, lowerThreshold, upperThreshold, "node")?.getStyle()}/>
{node.topNode && ( {node.topNode && (
<Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/> <Ring geometry={topNodeGeo} position={topNodePosition} rotation={topNodeRotation}/>
)} )}

View file

@ -0,0 +1,47 @@
import Cylinder, { CylinderGeometry } from "3dModels/Shapes/3D/Cylinder"
import React from "react"
import { Vector3 } from "three"
import CableNode from "./CableNode"
import { Bin } from "models"
import { CableData } from "../../Data/BuildCableData"
import { NodeData } from "../../Data/BuildNodeData"
interface Props {
cable: CableData
nodes: NodeData[]
bin: Bin
binCenter: Vector3
}
export default function GrainCable(props: Props) {
const {cable, nodes, bin, binCenter} = props
const calculateHeight = () => {
return cable.topPosition.distanceTo(cable.bottomPosition)
}
const calculateCenter = () => {
return cable.bottomPosition.clone()
.add(
cable.topPosition.clone()
.sub(cable.bottomPosition)
.multiplyScalar(0.5)
);
};
const geometry = React.useMemo(() => ({
radiusTop: cable.radius,
radiusBottom: cable.radius,
height: calculateHeight(),
radialSegments: cable.radialSegments ?? 10,
heightSegments: cable.heightSegments ?? 2
} as CylinderGeometry), [cable]);
return (
<React.Fragment>
<Cylinder geometry={geometry} position={calculateCenter()} opacity={0.5}/>
{nodes.map((node, i) => (
<CableNode key={"node " + i} node={node} binCenter={binCenter} lowerThreshold={bin.lowerTempThreshold()} upperThreshold={bin.upperTempThreshold()}/>
))}
</React.Fragment>
)
}

View file

@ -0,0 +1,16 @@
import { NodeData } from "bin/3dView/Data/BuildNodeData";
import { Bin } from "models";
interface Props{
bin: Bin
nodes: NodeData
}
export default function Heatmap(props: Props){
const {bin, nodes} = props
return (
<>
</>
)
}

View file

@ -0,0 +1,68 @@
import { Color } from "three";
import { NodeData } from "../Data/BuildNodeData";
type ColourMode = "node" | "heatmap";
const GREEN = new Color("#52c41a");
const BLUE = new Color("#3399ff");
const RED = new Color("#ff4d4f");
const colourFade = 20;
const minimumLightness = 0.3;
const lightnessRange = 0.2;
const minimumSaturation = 0.7;
const saturationRange = 0.8;
const getTemperatureIntensity = (temp: number, lower: number, upper: number) => {
if (temp >= lower && temp <= upper) return 0;
if (temp < lower) return lower - temp;
return temp - upper;
}
const getVisualIntensity = (distance: number, mode: ColourMode) => {
// 1 = near threshold, 0 = far
const intensity = Math.min(1, distance / colourFade);
switch(mode){
case "node":
return 1 - intensity
case "heatmap":
return intensity
}
};
export function tempToColour(
node: NodeData,
lower: number,
upper: number,
mode: ColourMode
): Color | null {
if (!node.inGrain || node.excluded) return null;
const temp = node.celcius;
// in-threshold behavior
if (temp >= lower && temp <= upper) {
return mode === "heatmap" ? null : GREEN;
}
const distance = getTemperatureIntensity(temp, lower, upper);
const intensity = getVisualIntensity(distance, mode);
const color = new Color();
const hsl = { h: 0, s: 1, l: 1 };
if (temp < lower) {
BLUE.getHSL(hsl);
} else {
RED.getHSL(hsl);
}
color.setHSL(
hsl.h,
(saturationRange * intensity) + minimumSaturation,
(lightnessRange * intensity) + minimumLightness
);
return color;
}

View file

@ -66,7 +66,7 @@ import ButtonGroup from "common/ButtonGroup";
import BinTransactions from "bin/BinTransactions"; import BinTransactions from "bin/BinTransactions";
import { CO2 } from "models/CO2"; import { CO2 } from "models/CO2";
import ObjectInteractions from "objects/objectInteractions/ObjectInteractions"; import ObjectInteractions from "objects/objectInteractions/ObjectInteractions";
import Bin3dView from "bin/3dView/Bin3dView"; import Bin3dView from "bin/3dView/Scene/Bin3dView";
interface TabPanelProps { interface TabPanelProps {
children?: React.ReactNode; children?: React.ReactNode;