-- BeamNestProcess.lua by Egalware s.r.l. 2026/05/11 -- Gestione nesting automatico travi anche oblique -- Intestazioni require( 'EgtBase') _ENV = EgtProtectGlobal() EgtEnableDebug( false) -- Include local BeamData = require( 'BeamDataNew') ---------------------------------------------------------------------------------------------------------- -- Parametri di configurazione Nesting local CONFIG = { -- Strategia di ricerca NUM_LONGEST_CANDIDATES = 3, -- Punteggi BONUS_PERFECT_FIT = 500, -- Se lo scarto è quasi zero (es: < 5mm) BONUS_SHARED_CUT = 100, -- Se le inclinazioni combaciano perfettamente PENALTY_TOO_SHORT = 100, -- Se lo scarto è troppo piccolo per essere utile ma non zero PENALTY_BAD_FIT_END_PHASE = 1000, -- Se siamo alla fine (>80%) e si utilizza un pezzo corto con fit mediocre -- Macchina MIN_USABLE_REMNANT = BeamData.MINRAW_S, -- Sotto questa misura lo scarto è considerato "sfrido" MAX_REMNANT_PERFECT_FIT = 20, BLADE_THICKNESS = 5.4 -- TODO questo deve arrivare da interfaccia o da automatismo!!!!! } ---------------------------------------------------------------------------------------------------------- -- variabili per tutto il modulo local dMaxJobLength = 0 local dMaxFillerLength = 0 local dGlobalProgress = 0 local t_insert = table.insert -- si mette via il riferimento locale per evitare continui lookup local JobPool = {} ---------------------------------------------------------------------------------------------------------- -- inventario grezzi local RawInventory = { Stock = {}, ActiveBeams = {} } function RawInventory:GetNewBeam( dLength) local NewBeam = { dTotalLength = dLength, dResidualLength = dLength - ( NEST.STARTOFFSET or 0), LastOffsetX = { 0, 0, 0, 0}, LastVtN = Vector3d( 1, 0, 0), vtNXabs = 1, dLastHeadRecess = 0, NestedParts = {} } return NewBeam end function RawInventory:BuildStock() if #LEN ~= #QTY then error( 'NestProcess: invalid stock data') end for i = 1, #LEN do self.Stock[#self.Stock + 1] = { dLength = LEN[i], nCount = QTY[i] } end return RawInventory end -- aggiunge una nuova barra attiva rimuovendo la corrispondente dalla lista stock disponibili function RawInventory:AddActiveBeam( nStockIndex) local CurrentStock = self.Stock[nStockIndex] -- se barra disponibile posso aggiungerla a quelle attive if CurrentStock and CurrentStock.nCount > 0 then -- update quantità a stock CurrentStock.nCount = CurrentStock.nCount - 1 -- aggiungo una nuova barra attiva local NewBeam = self:GetNewBeam( CurrentStock.dLength) t_insert( self.ActiveBeams, NewBeam) return NewBeam end return nil end -- auditor della bontà del nesting calcolato. Scrive nel log i risultati. function RawInventory:PrintDiagnosticReport() local nBeamsUsed = #self.ActiveBeams local dTotalStockLength = 0 local dTotalPartLength = 0 local dTotalScrap = 0 local dTotalUsableRemnants = 0 for i = 1, nBeamsUsed do local Beam = self.ActiveBeams[i] dTotalStockLength = dTotalStockLength + Beam.dTotalLength -- Classificazione del residuo rimasto in fondo alla barra if Beam.dResidualLength >= CONFIG.MIN_USABLE_REMNANT then dTotalUsableRemnants = dTotalUsableRemnants + Beam.dResidualLength else dTotalScrap = dTotalScrap + Beam.dResidualLength end -- Somma delle lunghezze dei pezzi reali inseriti for j = 1, #Beam.NestedParts do local Part = Beam.NestedParts[j] dTotalPartLength = dTotalPartLength + Part.dLength end end -- Calcolo efficienze percentuali con protezione per divisione per zero local dGrossEfficiency = 0 if dTotalStockLength > 0 then dGrossEfficiency = (dTotalPartLength / dTotalStockLength) * 100 end local dNetEfficiency = 0 local dNetDenominator = dTotalStockLength - dTotalUsableRemnants if dNetDenominator > 0 then dNetEfficiency = (dTotalPartLength / dNetDenominator) * 100 end -- Stampa tramite il sistema di logging proprietario EgtLog EgtOutLog("==========================================================") EgtOutLog(" EGALWARE NESTING ENGINE DIAGNOSTIC ") EgtOutLog("==========================================================") EgtOutLog(string.format("Total Parts Nested : %d", #JobPool)) EgtOutLog(string.format("Total Bars Used : %d", nBeamsUsed)) EgtOutLog(string.format("Total Material Cut : %.2f mm", dTotalPartLength)) EgtOutLog("----------------------------------------------------------") EgtOutLog(string.format("Gross Yield (Rendimento): %.2f%%", dGrossEfficiency)) EgtOutLog(string.format("Net Yield (Riutilizzato): %.2f%%", dNetEfficiency)) EgtOutLog("----------------------------------------------------------") EgtOutLog(string.format("Total Usable Remnants : %.2f mm (Safe for Clamps)", dTotalUsableRemnants)) EgtOutLog(string.format("Total Pure Scrap (Sfrido): %.2f mm (Trash Zone)", dTotalScrap)) EgtOutLog("==========================================================") end ---------------------------------------------------------------------------------------------------------- -- PartTemplates (informazioni geometriche pezzi univoci) -- JobPool (lista di tutti i singoli pezzi, multipli compresi, da nestare) local PartTemplates = {} function PartTemplates:GetInfoFromPart( id, sStateWithSide) local OffsetX = {} local vtN local sInfo = EgtGetInfo( id, sStateWithSide) if not sInfo then return end local Info = EgtSplitString( sInfo, ';') OffsetX = EgtSplitString( Info[1], ',') vtN = VectorFromString( Info[2]) for i = 1, #OffsetX do OffsetX[i] = tonumber( OffsetX[i]) or 0 end return OffsetX, vtN end function PartTemplates:AddPart( id) self[id] = {} self[id].dLength = EgtGetInfo( id, 'L', 'd') self[id].States = {} self[id].dMaxGlobalTailRecess = 0 -- si tiene via la lunghezza del pezzo massimo if self[id].dLength > dMaxJobLength + 10 * GEO.EPS_SMALL then dMaxJobLength = self[id].dLength end local States = { '1000', '0010', '1000_INV', '0010_INV' } for _, sState in ipairs(States) do local OffsetXHead, vtNHead = self:GetInfoFromPart( id, 'ALT' .. sState .. '_H') local OffsetXTail, vtNTail = self:GetInfoFromPart( id, 'ALT' .. sState .. '_T') if OffsetXHead or OffsetXTail then if not OffsetXHead then OffsetXHead = { 0, 0, 0, 0} vtNHead = Vector3d( 1, 0, 0) end if not OffsetXTail then OffsetXTail = { 0, 0, 0, 0} vtNTail = Vector3d( -1, 0, 0) end self[id].States[sState] = {} local State = self[id].States[sState] State.Head = { OffsetX = OffsetXHead, vtN = vtNHead, vtNXabs = abs( vtNHead:getX()) } State.Tail = { OffsetX = OffsetXTail, vtN = vtNTail, vtNXabs = abs( vtNTail:getX()) } -- overlap massimi in testa e in coda per questo pezzo local dMaxHeadRecess = 0 for i = 1, 4 do if State.Head.OffsetX[i] > dMaxHeadRecess + 10 * GEO.EPS_SMALL then dMaxHeadRecess = State.Head.OffsetX[i] end end State.dMaxHeadRecess = dMaxHeadRecess local dMaxTailRecess = 0 for i = 1, 4 do if abs( State.Tail.OffsetX[i]) > dMaxTailRecess + 10 * GEO.EPS_SMALL then dMaxTailRecess = abs( State.Tail.OffsetX[i]) end end State.dMaxTailRecess = dMaxTailRecess if dMaxTailRecess > self[id].dMaxGlobalTailRecess + 10 * GEO.EPS_SMALL then self[id].dMaxGlobalTailRecess = dMaxTailRecess end end end end -- ordinamento JobPool per lunghezza decrescente dei pezzi function JobPool:Sort() local function SortByLengthDescending( Part1, Part2) local dLength1 = PartTemplates[Part1.id].dLength local dLength2 = PartTemplates[Part2.id].dLength if dLength1 > dLength2 + 10 * GEO.EPS_SMALL then return true elseif dLength2 > dLength1 + 10 * GEO.EPS_SMALL then return false end -- tie breaker return Part1.id < Part2.id end table.sort( self, SortByLengthDescending) end -- creazione combinata (si cicla una sola volta) di entrambe le tabelle local function BuildPartTemplatesAndJobPool() for id, nCount in pairs( PART) do PartTemplates:AddPart( id) for _ = 1, nCount do t_insert( JobPool, { id = id, bNested = false}) end end return PartTemplates, JobPool end ---------------------------------------------------------------------------------------------------------- -- calcolo singola Move, ossia combinazione barra-pezzo-stato, con relativo punteggio local function CalculateMove( Beam, dPartLength, sState, State) local Move = {} -- calcolo overlap pezzi (riduzione di lunghezza occupata) local dSafeOverlap = GEO.INFINITO for i = 1, 4 do local dCornerDistance = Beam.LastOffsetX[i] - State.Tail.OffsetX[i] if dCornerDistance < dSafeOverlap then dSafeOverlap = dCornerDistance end end -- il massimo overlap va ridotto dello spessore lama local dProjectedBlade = CONFIG.BLADE_THICKNESS / min( Beam.vtNXabs, State.Tail.vtNXabs) dSafeOverlap = dSafeOverlap - dProjectedBlade -- lunghezza barra rimasta (se negativo non ci sta) local dFutureResidualLength = Beam.dResidualLength + dSafeOverlap - dPartLength if dFutureResidualLength < -10 * GEO.EPS_SMALL then return nil end -- Calcolo punteggio -- All'inizio (Ratio=1) si dà vantaggio ai pezzi più lunghi. Alla fine (Ratio=0) si dà vantaggio ai best fit. local dEfficiency -- barra nuova, si valuta l'efficienza prospettica if #Beam.NestedParts == 0 then -- È una barra vergine dallo stock! Valutiamo la sua efficienza PROSPETTICA -- Quanti pezzi di questa lunghezza ci starebbero al massimo? local nMaxPieces = math.floor(Beam.dResidualLength / dPartLength) if nMaxPieces == 0 then nMaxPieces = 1 end local dUltimateUsed = nMaxPieces * dPartLength dEfficiency = dUltimateUsed / Beam.dTotalLength -- barra avviata: efficienza reale else dEfficiency = dPartLength / ( dPartLength + dFutureResidualLength) end local dNormalizedLength = dPartLength / dMaxJobLength local dLengthScore = dNormalizedLength * ( 1 - dGlobalProgress) * 1000 local dEfficiencyScore = dEfficiency * dGlobalProgress * 1000 local dScore = dLengthScore + dEfficiencyScore -- Bonus Perfect Fit if dFutureResidualLength < CONFIG.MAX_REMNANT_PERFECT_FIT then dScore = dScore + CONFIG.BONUS_PERFECT_FIT -- Penalità Sliver (si evitano sfridi non riutilizzabili) elseif dFutureResidualLength < CONFIG.MIN_USABLE_REMNANT then dScore = dScore - CONFIG.PENALTY_TOO_SHORT end -- Bonus Shared Cut: se le normali sono opposte, si risparmia un taglio/posizionamento if AreOppositeVectorApprox( Beam.LastVtN, State.Tail.vtN) then dScore = dScore + CONFIG.BONUS_SHARED_CUT end -- Protezione finale pezzi corti: se siamo alla fine e il pezzo è corto, penalizziamo se non è un fit quasi perfetto if ( dGlobalProgress >= 0.8) and ( dPartLength < dMaxFillerLength) and ( dFutureResidualLength > CONFIG.MAX_REMNANT_PERFECT_FIT) then dScore = dScore - CONFIG.PENALTY_BAD_FIT_END_PHASE end Move = { sState = sState, dScore = dScore, dSafeOverlap = dSafeOverlap, dFutureResidualLength = dFutureResidualLength } return Move end ---------------------------------------------------------------------------------------------------------- -- trova i migliori pezzi da inserire nella trave (N pezzi più lunghi e 2 pezzi di lunghezza più adeguata al restante della barra) e i migliori stati in cui metterli local function FindBestPartForBeam( Beam) local nLongestParts = 0 local Candidates = {} local JobsAlreadyInCandidates = {} local BestFitJob1 = { Job = nil, dGap = GEO.INFINITO } local BestFitJob2 = { Job = nil, dGap = GEO.INFINITO } -- 1 Scelta candidati -- for i = 1, #JobPool do local Job = JobPool[i] if not Job.bNested then local PartTemplate = PartTemplates[Job.id] local dGap = Beam.dResidualLength + Beam.dLastHeadRecess + PartTemplate.dMaxGlobalTailRecess - PartTemplate.dLength if dGap > - 10 * GEO.EPS_SMALL then -- JobPool è già ordinata: i primi N pezzi che entrano sono i più lunghi if nLongestParts < CONFIG.NUM_LONGEST_CANDIDATES then t_insert( Candidates, Job) JobsAlreadyInCandidates[Job] = true nLongestParts = nLongestParts + 1 end -- si cercano i due pezzi con il best fit nella barra restante if dGap < BestFitJob1.dGap then -- il bestfit1 è il nuovo bestfit 2 BestFitJob2.Job = BestFitJob1.Job BestFitJob2.dGap = BestFitJob1.dGap -- la job corrente è la nuova bestfit1 BestFitJob1.dGap = dGap BestFitJob1.Job = Job elseif dGap < BestFitJob2.dGap then BestFitJob2.Job = Job BestFitJob2.dGap = dGap end end end end -- i pezzi bestfit si aggiungono solo se non corrispondono a pezzi già inseriti if BestFitJob1.Job and not JobsAlreadyInCandidates[BestFitJob1.Job] then table.insert( Candidates, BestFitJob1.Job) JobsAlreadyInCandidates[BestFitJob1.Job] = true end if BestFitJob2.Job and not JobsAlreadyInCandidates[BestFitJob2.Job] then table.insert( Candidates, BestFitJob2.Job) JobsAlreadyInCandidates[BestFitJob2.Job] = true end -- 2 Scelta miglior candidato -- local dHighestScore = -GEO.INFINITO local BestMove for i = 1, #Candidates do local Candidate = Candidates[i] local Template = PartTemplates[Candidate.id] local dPartLength = Template.dLength local dHighestCandidateScore = -GEO.INFINITO local BestCandidateMove -- si trova la Move migliore del singolo candidato (a CalculateMove si passano gli argomenti precalcolati per evitare di rallentare il calcolo) for sState, State in pairs( Template.States) do local Move = CalculateMove( Beam, dPartLength, sState, State) if Move and Move.dScore > dHighestCandidateScore + 10 * GEO.EPS_SMALL then BestCandidateMove = Move dHighestCandidateScore = Move.dScore BestCandidateMove.Job = Candidate end end -- si trova la Move migliore in assoluto if dHighestCandidateScore > dHighestScore + 10 * GEO.EPS_SMALL then BestMove = BestCandidateMove dHighestScore = dHighestCandidateScore end end return BestMove end ---------------------------------------------------------------------------------------------------------- -- Esegue la mossa scelta: aggiorna lo stato della trave e segna il pezzo come nestato local function CommitBestMove( BestMove) local Beam -- recupero o creazione della barra if BestMove.nActiveBeamIndex then Beam = RawInventory.ActiveBeams[BestMove.nActiveBeamIndex] elseif BestMove.nStockIndex then Beam = RawInventory:AddActiveBeam( BestMove.nStockIndex) end if not Beam then return end -- recupero dati pezzo e stato local Job = BestMove.Job local Template = PartTemplates[Job.id] local State = Template.States[BestMove.sState] -- update geometria barra -- la nuova faccia della barra è ora la testa (Head) del pezzo appena inserito Beam.dResidualLength = BestMove.dFutureResidualLength Beam.LastOffsetX = State.Head.OffsetX Beam.LastVtN = State.Head.vtN Beam.vtNXabs = abs( State.Head.vtN:getX()) Beam.dLastHeadRecess = State.dMaxHeadRecess -- registrazione pezzo t_insert( Beam.NestedParts, { id = Job.id, sState = BestMove.sState, dSafeOverlap = BestMove.dSafeOverlap, dLength = Template.dLength }) -- chiusura job Job.bNested = true end ---------------------------------------------------------------------------------------------------------- -- script principale -- preparazione tabelle lista grezzi (RawInventory), lista pezzi univoci (PartTemplates) e lista singoli pezzi da nestare (JobPool) RawInventory:BuildStock() BuildPartTemplatesAndJobPool() JobPool:Sort() -- calcolo lunghezza massima pezzi "filler" local nTotalParts = #JobPool local nFillerIndex = floor( nTotalParts * 0.8) + 1 if nFillerIndex > nTotalParts then nFillerIndex = nTotalParts end dMaxFillerLength = PartTemplates[JobPool[nFillerIndex].id].dLength dMaxFillerLength = EgtClamp( dMaxFillerLength, ( BeamData.MINRAW_S + BeamData.MINRAW_L) / 2, BeamData.LEN_SHORT_PART) -- loop principale: scorre le barre, sia già attive che a stock, e le riempie nel modo migliore possibile -- per ogni giro sceglie la soluzione con punteggio più alto local nDoneParts = 0 local VirtualBeam = RawInventory:GetNewBeam( 0) while true do local BestMove local dHighestScore = -GEO.INFINITO -- progresso calcolo if nTotalParts > 0 then dGlobalProgress = nDoneParts / nTotalParts -- se non ci sono pezzi il calcolo è già finito else dGlobalProgress = 1 end -- 1 Si provano le barre già attive for i = 1, #RawInventory.ActiveBeams do local CurrentMove = FindBestPartForBeam( RawInventory.ActiveBeams[i]) if CurrentMove and CurrentMove.dScore > dHighestScore then dHighestScore = CurrentMove.dScore BestMove = CurrentMove BestMove.nActiveBeamIndex = i end end -- 2 Si provano le barre ancora a stock SOLO SE NON TROVATA SOLUZIONE CON BARRE ATTIVE -- VirtualBeam si resetta invece di crearne una nuova per velocizzare il calcolo if not BestMove then for i = 1, #RawInventory.Stock do if RawInventory.Stock[i].nCount > 0 then VirtualBeam.dTotalLength = RawInventory.Stock[i].dLength VirtualBeam.dResidualLength = RawInventory.Stock[i].dLength - ( NEST.STARTOFFSET or 0) VirtualBeam.LastOffsetX = { 0, 0, 0, 0} VirtualBeam.LastVtN = Vector3d( 1, 0, 0) VirtualBeam.vtNXabs = 1 VirtualBeam.dLastHeadRecess = 0 VirtualBeam.NestedParts = {} local CurrentMove = FindBestPartForBeam( VirtualBeam) if CurrentMove and CurrentMove.dScore > dHighestScore then dHighestScore = CurrentMove.dScore BestMove = CurrentMove BestMove.nStockIndex = i end end end end -- 3 Se BestMove trovata si aggiornano lista pezzi e barre if BestMove then CommitBestMove( BestMove) nDoneParts = nDoneParts + 1 -- se non c'è più niente di compatibile si esce else break end end RawInventory:PrintDiagnosticReport()