diff --git a/NestProcess.lua b/NestProcess.lua index 03a8797..1e47e87 100644 --- a/NestProcess.lua +++ b/NestProcess.lua @@ -7,6 +7,33 @@ _ENV = EgtProtectGlobal() EgtEnableDebug( true) -- 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 = 1000, -- 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 ---------------------------------------------------------------------------------------------------------- -- inventario grezzi @@ -18,9 +45,10 @@ local RawInventory = { function RawInventory:GetNewBeam( dLength) local NewBeam = { dTotalLength = dLength, - dResidual = dLength - NEST.STARTOFFSET, + dResidualLength = dLength - ( NEST.STARTOFFSET or 0), LastOffsetX = { 0, 0, 0, 0}, LastVtN = Vector3d( 1, 0, 0), + vtNXabs = 1, dLastHeadRecess = 0, NestedParts = {} } @@ -55,7 +83,7 @@ function RawInventory:AddActiveBeam( nStockIndex) -- aggiungo una nuova barra attiva local NewBeam = self:GetNewBeam( CurrentStock.dLength) - table.insert( self.ActiveBeams, NewBeam) + t_insert( self.ActiveBeams, NewBeam) return NewBeam end @@ -94,6 +122,11 @@ function PartTemplates:AddPart( id) 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 @@ -116,11 +149,13 @@ function PartTemplates:AddPart( id) State.Head = { OffsetX = OffsetXHead, - vtN = vtNHead + vtN = vtNHead, + vtNXabs = abs( vtN:getX()) } State.Tail = { OffsetX = OffsetXTail, - vtN = vtNTail + vtN = vtNTail, + vtNXabs = abs( vtN:getX()) } -- overlap massimi in testa e in coda per questo pezzo @@ -170,7 +205,7 @@ local function BuildPartTemplatesAndJobPool() for id, nCount in pairs( PART) do PartTemplates:AddPart( id) for _ = 1, nCount do - table.insert( JobPool, { id = id, bNested = false}) + t_insert( JobPool, { id = id, bNested = false}) end end @@ -178,10 +213,185 @@ local function BuildPartTemplatesAndJobPool() 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 dNormalizedLength = dPartLength / dMaxJobLength + local dEfficiency = dPartLength / ( dPartLength + dFutureResidualLength) + local dLengthScore = dNormalizedLength * ( 1 - GlobalProgress) * 1000 + local dEfficiencyScore = dEfficiency * GlobalProgress * 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 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) + 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 + local function AddIfUnique( JobToAdd) + if not JobToAdd then return end + for i = 1, #Candidates do + if Candidates[i] == JobToAdd then return end + end + t_insert( Candidates, JobToAdd) + end + AddIfUnique( BestFitJob1.Job) + AddIfUnique( BestFitJob2.Job) + + -- 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 ---------------------------------------------------------------------------------------------------------- @@ -192,12 +402,29 @@ 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]) @@ -211,7 +438,8 @@ while true do -- 2 Si provano le barre ancora a stock for i = 1, #RawInventory.Stock do if RawInventory.Stock[i].nCount > 0 then - local VirtualBeam = RawInventory:GetNewBeam( RawInventory.Stock[i].dLength) + VirtualBeam.dTotalLength = RawInventory.Stock[i].dLength + VirtualBeam.dResidualLength = RawInventory.Stock[i].dLength - ( NEST.STARTOFFSET or 0) local CurrentMove = FindBestPartForBeam( VirtualBeam) if CurrentMove and CurrentMove.dScore > dHighestScore then dHighestScore = CurrentMove.dScore @@ -224,6 +452,7 @@ while true do -- 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