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; };