210 lines
No EOL
7.1 KiB
TypeScript
210 lines
No EOL
7.1 KiB
TypeScript
import { RadiansToDegrees } from "common/TrigFunctions";
|
|
import { Bin } from "models";
|
|
import { GrainCable } from "models/GrainCable";
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Effective bin dimensions used for cable placement.
|
|
* Passed in from Bin3dView so that default fallback values are applied
|
|
* consistently — cables will always match the rendered shell even when
|
|
* the bin has no advanced dimensions configured.
|
|
*/
|
|
export interface BinDims {
|
|
diameter: number
|
|
sidewallHeight: number
|
|
roofHeight: number
|
|
hopperHeight: 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, dims: BinDims) {
|
|
const binRadius = dims.diameter / 2;
|
|
|
|
const distanceFromEdge = binRadius - radiusFromCenter;
|
|
|
|
// Use the real roof angle when available; when dimensions aren't configured
|
|
// the angle will be 0 which gives a flat cable top — correct for a default shape.
|
|
const angleRad = Math.atan((dims.roofHeight) / (binRadius));
|
|
|
|
const roofRise = Math.tan(angleRad) * distanceFromEdge;
|
|
|
|
return dims.sidewallHeight / 2 + roofRise;
|
|
};
|
|
|
|
function getBottomY(radiusFromCenter: number, dims: BinDims) {
|
|
const binRadius = dims.diameter / 2;
|
|
|
|
const distanceFromCenter = binRadius - radiusFromCenter;
|
|
|
|
const angleRad = Math.atan((dims.hopperHeight) / (binRadius))
|
|
|
|
const hopperDrop = Math.tan(angleRad) * distanceFromCenter;
|
|
|
|
const clearance = 60; // cm
|
|
|
|
return -dims.sidewallHeight / 2 - hopperDrop + clearance;
|
|
};
|
|
|
|
|
|
export function BuildCableData(bin: Bin, dims: BinDims): 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(dims.diameter);
|
|
const distributed = distributeCables(cables.length, layout);
|
|
|
|
const cableRadius = 3;
|
|
|
|
const result: CableData[] = [];
|
|
|
|
let cableIndex = 0;
|
|
|
|
distributed.forEach(orbit => {
|
|
const count = orbit.assignedCount ?? 0;
|
|
|
|
const topY = getTopY(orbit.radius, dims);
|
|
const bottomY = getBottomY(orbit.radius, dims);
|
|
|
|
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;
|
|
}; |