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:
parent
ff64f8594c
commit
6c87ed03ed
13 changed files with 572 additions and 363 deletions
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
194
src/bin/3dView/Data/BuildCableData.ts
Normal file
194
src/bin/3dView/Data/BuildCableData.ts
Normal 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;
|
||||||
|
};
|
||||||
60
src/bin/3dView/Data/BuildNodeData.ts
Normal file
60
src/bin/3dView/Data/BuildNodeData.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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 */}
|
||||||
28
src/bin/3dView/Systems/Cables/BinCables.tsx
Normal file
28
src/bin/3dView/Systems/Cables/BinCables.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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}/>
|
||||||
)}
|
)}
|
||||||
47
src/bin/3dView/Systems/Cables/GrainCable.tsx
Normal file
47
src/bin/3dView/Systems/Cables/GrainCable.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
src/bin/3dView/Systems/Heatmap/Heatmap.tsx
Normal file
16
src/bin/3dView/Systems/Heatmap/Heatmap.tsx
Normal 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 (
|
||||||
|
<>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
68
src/bin/3dView/utils/tempToColour.ts
Normal file
68
src/bin/3dView/utils/tempToColour.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue