-- WinExec.lua by Egalware s.r.l. 2024/06/13 -- Libreria esecuzione lavorazioni per Serramenti -- Tabella per definizione modulo local WinExec = {} -- Include require( 'EgtBase') -- Carico i dati globali local WinData = require( 'WinData') local WinLib = require( 'WinLib') local ID = require( 'Identity') local FeatureData = require( 'FeatureData') local MachiningLib = require( 'MachiningLib') -- carico librerie di lavorazione _G.package.loaded.Drilling = nil _G.package.loaded.Cutting = nil _G.package.loaded.Pocketing = nil _G.package.loaded.Milling = nil _G.package.loaded.Profiling = nil local Drilling = require( 'Drilling') local Cutting = require( 'Cutting') local Pocketing = require( 'Pocketing') local Milling = require( 'Milling') local Profiling = require( 'Profiling') EgtOutLog( ' WinExec started', 1) EgtMdbSave() ------------------------------------------------------------------------------------------------------------- -- *** variabili globali *** ------------------------------------------------------------------------------------------------------------- TOOLS = nil MACHININGS = nil ------------------------------------------------------------------------------------------------------------- -- *** COSTANTI *** TODO -> DA SPOSTARE IN WINDATA??? ------------------------------------------------------------------------------------------------------------- TH_DIAMETER_HSK63 = 63 TH_LENGTH_HSK63 = 75 SIDEANGLE_DOVETAIL = 15 ------------------------------------------------------------------------------------------------------------- -- *** funzioni di base *** ------------------------------------------------------------------------------------------------------------- local function GetToolTypeNameFromToolTypeID( dToolTypeID) if dToolTypeID == MCH_TY.DRILL_STD then return 'DRILL_STD', 'DRILLBIT' elseif dToolTypeID == MCH_TY.DRILL_LONG then return 'DRILL_LONG', 'DRILLBIT' elseif dToolTypeID == MCH_TY.SAW_STD then return 'SAW_STD', 'SAWBLADE' elseif dToolTypeID == MCH_TY.SAW_FLAT then return 'SAW_FLAT', 'SAWBLADE' elseif dToolTypeID == MCH_TY.MILL_STD then return 'MILL_STD', 'MILL' elseif dToolTypeID == MCH_TY.MILL_NOTIP then return 'MILL_NOTIP', 'MILL' elseif dToolTypeID == MCH_TY.MORTISE_STD then return 'MORTISE_STD', 'MORTISE' else return nil end end ------------------------------------------------------------------------------------------------------------- local function IsToolOk( Tool) -- controllo che i dati necessari siano impostati if Tool.dMaxMaterial and Tool.dDiameter and Tool.dLength and Tool.sHead and Tool.sUUID then -- se DRILLBIT non ho altri dati if Tool.sFamily == 'DRILLBIT' then return true -- altrimenti controllo dati aggiuntivi altre famiglie di utensili elseif Tool.sFamily == 'MORTISE' then if Tool.dCornerRadius then return true end else if Tool.dThickness then return true end end end return false end ------------------------------------------------------------------------------------------------------------- function WinExec.GetToolsFromDB() -- TODO gli utensili profilati devono essere messi in una lista a parte ad accesso diretto TOOLS['Prof1'] in modo da non dover scorrere la lista -- dato che saranno molti e l'applicazione sarà diretta. Non serve ciclarli -- creo lista globale utensili disponibili TOOLS = {} -- lista appoggio utensile local Tool = {} -- recupero tutti gli utensili : punte a forare, lame, frese e motoseghe Tool.sName = EgtTdbGetFirstTool( MCH_TF.DRILLBIT + MCH_TF.SAWBLADE + MCH_TF.MILL + MCH_TF.MORTISE) while Tool.sName ~= '' do -- imposto utensile come corrente per recuperarne i dati EgtTdbSetCurrTool( Tool.sName) -- verifico se utensile disponibile in attrezzaggio attuale e che abbia un tipo ben definito local bToolLoadedOnSetup, sToolTCPos = EgtFindToolInCurrSetup( Tool.sName) local nToolTypeId = EgtTdbGetCurrToolParam( MCH_TP.TYPE) local sToolType, sToolFamily = GetToolTypeNameFromToolTypeID( nToolTypeId) -- se verifica condizioni minime, recupero tutti gli altri dati if bToolLoadedOnSetup and sToolType then Tool.sTcPos = sToolTCPos Tool.sFamily = sToolFamily Tool.sType = sToolType Tool.nTypeId = nToolTypeId Tool.dMaxMaterial = EgtTdbGetCurrToolParam( MCH_TP.MAXMAT) Tool.dDiameter = EgtTdbGetCurrToolParam( MCH_TP.DIAM) Tool.dTotDiameter = EgtTdbGetCurrToolParam( MCH_TP.TOTDIAM) Tool.dLength = EgtTdbGetCurrToolParam( MCH_TP.LEN) Tool.dSpeed = EgtTdbGetCurrToolParam( MCH_TP.SPEED) Tool.bIsCCW = Tool.dSpeed < 0 Tool.Feeds = {} Tool.Feeds.dFeed = EgtTdbGetCurrToolParam( MCH_TP.FEED) Tool.Feeds.dStartFeed = EgtTdbGetCurrToolParam( MCH_TP.STARTFEED) Tool.Feeds.dEndFeed = EgtTdbGetCurrToolParam( MCH_TP.ENDFEED) Tool.Feeds.dTipFeed = EgtTdbGetCurrToolParam( MCH_TP.TIPFEED) Tool.sHead = EgtTdbGetCurrToolParam( MCH_TP.HEAD) Tool.SetupInfo = {} Tool.SetupInfo = WinData.GetSetupInfo( {sHead = Tool.sHead, sTcPos = Tool.sTcPos}) Tool.sUUID = EgtTdbGetCurrToolParam( MCH_TP.UUID) Tool.sUserNotes = EgtTdbGetCurrToolParam( MCH_TP.USERNOTES) Tool.dMaxDepth = EgtTdbGetCurrToolMaxDepth() or Tool.dMaxMaterial Tool.ToolHolder = {} Tool.ToolHolder.dDiameter = EgtTdbGetCurrToolThDiam() or TH_DIAMETER_HSK63 -- diametro standard HSK63 Tool.ToolHolder.dLength = EgtTdbGetCurrToolThLength() or TH_LENGTH_HSK63 -- lunghezza standard HSK63 -- parametri scritti nelle note Tool.nDouble = EgtGetValInNotes( Tool.sUserNotes, 'DOUBLE') -- lettura parametri non comuni ( famiglia DRILLBIT non ha parametri specifici) if sToolFamily ~= 'DRILLBIT' then Tool.dThickness = EgtTdbGetCurrToolParam( MCH_TP.THICK) Tool.dLongitudinalOffset = EgtTdbGetCurrToolParam( MCH_TP.LONOFFSET) Tool.dRadialOffset = EgtTdbGetCurrToolParam( MCH_TP.RADOFFSET) -- recupero parametri propri delle frese if sToolFamily == 'MILL' then Tool.dStemDiameter = EgtTdbGetCurrToolParam( MCH_TP.STEMDIAM) or Tool.ToolHolder.dDiameter -- se non settato, considero diametro come ToolHolder Tool.dSideAngle = EgtTdbGetCurrToolParam( MCH_TP.SIDEANG) or 0 -- verifico che parametri siano compatibili con una fresa a coda di rondine ( angolo di fianco standard Coda di rondine -> 15°) Tool.bIsDoveTail = Tool.Type == 'MILL_NOTIP' and abs( abs( Tool.dSideAngle) - SIDEANGLE_DOVETAIL) < 1 -- verifico che sia una fresa tipo T-Mill o BlockHaus Tool.dSideDepth = EgtGetValInNotes( Tool.sUserNotes, 'SIDEDEPTH') or 0 -- se non settato nell'utensile, dico che non ha massimo affondamento laterale Tool.bIsTMill = Tool.dSideDepth > 0 Tool.dStep = EgtGetValInNotes( Tool.sUserNotes, 'STEP') or ( Tool.dMaxMaterial / 3) -- se non settato nell'utensile, considero metà del tagliente Tool.dSideStep = EgtGetValInNotes( Tool.sUserNotes, 'SIDESTEP') or floor( Tool.dDiameter / 3) -- se non settato nell'utensile, considero metà del diametro Tool.bIsPen = abs( Tool.dSpeed) < 5 -- recupero parametri propri delle lame elseif sToolFamily == 'SAWBLADE' then Tool.bIsUsedForLongCut = EgtGetValInNotes( Tool.sUserNotes, 'LONGCUT') == 1 or false -- false coem valore di default Tool.dStep = EgtGetValInNotes( Tool.sUserNotes, 'STEP') or Tool.dThickness -- se non settato nell'utensile, considero lo spessore lama Tool.dSideStep = EgtGetValInNotes( Tool.sUserNotes, 'SIDESTEP') or Tool.dMaxMaterial -- se non settato nell'utensile, considero un quarto del diametro -- recupero parametri propri delle motoseghe elseif sToolFamily == 'MORTISE' then Tool.dDistance = EgtTdbGetCurrToolParam( MCH_TP.DIST) or 90 -- 90mm dimensione standard aggregato catena Tool.bIsMortise = EgtGetValInNotes( Tool.sUserNotes, 'MORTISE') == 1 Tool.bIsChainSaw = not Tool.bIsMortise Tool.dStep = EgtGetValInNotes( Tool.sUserNotes, 'STEP') or floor( Tool.dMaxMaterial / 3) -- se non settato nell'utensile, considero un terzo della lunghezza Tool.dSideStep = EgtGetValInNotes( Tool.sUserNotes, 'SIDESTEP') or ( Tool.dThickness - 1) -- se non settato nell'utensile, considero spessore catena meno 1mm di sicurezza Tool.dCornerRadius = EgtTdbGetCurrToolParam( MCH_TP.CORNRAD) Tool.dWidth = Tool.dDiameter end end -- se tutti i dati necessari sono disponibili, inserisco utensile nella lista globale degli utensili disponibili if IsToolOk( Tool) then table.insert( TOOLS, Tool) -- altrimenti scrivo nel log che l'utensile non è conforme else EgtOutLog( '*** ' .. Tool.sName .. ' : NOT-COMPLIANT ***', 1) end -- reset dati Tool = {} end -- recupero utensile successivo ( punte a forare, lame, frese e motoseghe) Tool.sName = EgtTdbGetNextTool( MCH_TF.DRILLBIT + MCH_TF.SAWBLADE + MCH_TF.MILL + MCH_TF.MORTISE) end end ------------------------------------------------------------------------------------------------------------- -- *** Inserimento delle lavorazioni nelle travi *** ------------------------------------------------------------------------------------------------------------- local function CollectFeatures( vProc, Part, nIndexPart) -- recupero le feature local LayerId = {} LayerId[1] = WinLib.GetAddGroup( Part.id) LayerId[2] = EgtGetFirstNameInGroup( Part.id or GDB_ID.NULL, 'Processings') for nInd = 1, 2 do local ProcId = EgtGetFirstInGroup( LayerId[nInd] or GDB_ID.NULL) while ProcId do local nEntType = EgtGetType( ProcId) if nEntType == GDB_TY.SRF_MESH or nEntType == GDB_TY.EXT_TEXT or nEntType == GDB_TY.CRV_LINE or nEntType == GDB_TY.CRV_ARC or nEntType == GDB_TY.CRV_BEZ or nEntType == GDB_TY.CRV_COMPO then local sType = EgtGetInfo( ProcId, 'FEATURE_TYPE', 's') local nDo = EgtGetInfo( ProcId, 'DO', 'i') or 1 if sType and nDo == 1 then local Proc = {} Proc.idPart = Part.id Proc.nIndexPart = nIndexPart Proc.id = ProcId Proc.nFlg = 1 Proc.sType = sType Proc.b3Box = EgtGetBBoxGlob( ProcId, GDB_BB.STANDARD) Proc.nPhase = EgtGetInfo( ProcId, 'PHASE', 'i') or nil -- se esiste la geometria if Proc.b3Box and not Proc.b3Box:isEmpty() then -- calcolo dati specifici per tipologia di feature / lavorazione -- se foro if ID.IsDrilling( Proc) then Proc = FeatureData.GetDrillingData( Proc) end -- se taglio if ID.IsCutting( Proc) then Proc = FeatureData.GetCuttingData( Proc) end -- se fresatura if ID.IsMilling( Proc) then Proc = FeatureData.GetMillingData( Proc) end -- se svuotatura if ID.IsPocketing( Proc) then Proc = FeatureData.GetPocketingData( Proc) end -- se profilatura if ID.IsProfiling( Proc) then Proc = FeatureData.GetProfilingData( Proc) end -- informazioni facce Proc.AffectedFaces = WinLib.GetAffectedFaces( Proc, Part) -- inserisco feature in lista table.insert( vProc, Proc) else Proc.nFlg = 0 table.insert( vProc, Proc) EgtOutLog( ' Feature ' .. tostring( Proc.idFeature) .. ' is empty (no geometry)') end end end ProcId = EgtGetNext( ProcId) end end return vProc end ------------------------------------------------------------------------------------------------------------- local function GetFeatureInfoAndDependency( vProc, Part) -- ciclo tutte le feature for i = 1, #vProc do local Proc = vProc[i] -- controllo la feature con tutte le altre per recuperare le dipendenze for j = 1, #vProc do -- non si controlla la feature con se stessa if i ~= j then local ProcB = vProc[j] -- TODO dipendenze da controllare : -- * gruppo di forature con aggregato 2/3 uscite -- * Se 'Left' e 'Out' hanno stesso profilo, vanno concatenati (anche 'Right' se consentito dalla macchina) end end end return vProc end ------------------------------------------------------------------------------------------------------------- -- ottimizza le lavorazioni, considerando di ridurre i cambi utensile local function SortByChangeToolOpt( MACHININGS) local vProcToSortToolOpt = {} vProcToSortToolOpt = MACHININGS for i = 1, #vProcToSortToolOpt do for j = i + 1, #vProcToSortToolOpt do -- se sono su stessa fase if vProcToSortToolOpt[i].AuxiliaryData.nPhase == vProcToSortToolOpt[j].AuxiliaryData.nPhase then -- se stesso utensile if vProcToSortToolOpt[i].Machining.nToolIndex == vProcToSortToolOpt[j].Machining.nToolIndex then if i + 1 == j then -- il confronto riparte dall'ultimo spostato i = j else -- sposto lavorazione confrontata dopo la principale WinLib.ChangeElementPositionInTable( vProcToSortToolOpt, j, i+1) -- il confronto riparte dall'ultimo spostato i = j end end end end end return vProcToSortToolOpt end ------------------------------------------------------------------------------------------------------------- -- Ordina le feature in base a fase di lavorazione -- L'ordine è indicativamente: -- 1) Tagli di testa -- 2) Fori -- 3) Scassi serratura/maniglia -- 4) Profili testa / Minizinken -- 5) Fori per spine dopo profilo di testa -- 6) Profili longitudinali -- 7) Passate pulitura per cambio profilo -- 8) Incontri/scontri (encounter/clash) -- 9) Listello ferma-vetro -- 10) Pausa -- 11) Lavorazioni dopo rimozione ferma-vetro -- TODO Verificare se assegnare le fasi già quando si mette la lavorazione e ordinare solo in base alla fase di lavoro. -- TODO il punto 11) deve essere diviso in altri punti in base alle lavorazioni. Per adesso è un gruppo generico. -- TODO Ridefinire le fasi, prevedere anche la fase di cambio pinzaggio local function OrderMachining( MACHININGS) local vProcToSort = MACHININGS -- funzione di confronto. TRUE = B1 prima di B2. FALSE = B2 prima di B1 local function CompareFeatures( B1, B2) -- se secondo disabilitato, va lasciato dopo if B1.Proc.nFlg ~= 0 and B2.Proc.nFlg == 0 then return true elseif B1.AuxiliaryData.nPhase < B2.AuxiliaryData.nPhase then return true elseif B1.AuxiliaryData.nPhase > B2.AuxiliaryData.nPhase then return false elseif B1.Machining.Geometry == B2.Machining.Geometry then return B1.AuxiliaryData.nIndexMachining < B2.AuxiliaryData.nIndexMachining elseif B1.AuxiliaryData.bIsProfiling and B2.AuxiliaryData.bIsDrilling and B2.AuxiliaryData.bExecAfterProfile then return true elseif B1.AuxiliaryData.bIsDrilling and B1.AuxiliaryData.bExecAfterProfile and B2.AuxiliaryData.bIsProfiling then return false elseif B1.AuxiliaryData.bIsDrilling and B2.AuxiliaryData.bIsProfiling then return true elseif B2.AuxiliaryData.bIsDrilling and B1.AuxiliaryData.bIsProfiling then return false elseif B1.AuxiliaryData.bIsProfiling and B2.AuxiliaryData.bIsProfiling then -- profiling Generic sempre per ultimi if B1.Proc.sProfileInfo == 'Generic' and B2.Proc.sProfileInfo ~= 'Generic' then return false elseif B2.Proc.sProfileInfo == 'Generic' and B1.Proc.sProfileInfo ~= 'Generic' then return true -- profiling Mixed dopo le profilature standard elseif B1.Proc.sProfileInfo == 'Mixed' and B2.Proc.sProfileInfo ~= 'Mixed' then return false elseif B2.Proc.sProfileInfo == 'Mixed' and B1.Proc.sProfileInfo ~= 'Mixed' then return true -- negli altri casi si ordina solo in base alla posizione elseif B1.Proc.b3Box:getCenter():getX() - B2.Proc.b3Box:getCenter():getX() < 10 * GEO.EPS_SMALL then return EgtIf( B1.AuxiliaryData.nPhase == 1, true, false) -- se arrivati a questo controllo, la fase di lavorazione di entrambe è la stessa, quindi mi basta controllarne una else return EgtIf( B1.AuxiliaryData.nPhase == 1, false, true) -- se arrivati a questo controllo, la fase di lavorazione di entrambe è la stessa, quindi mi basta controllarne una end elseif B1.AuxiliaryData.bIsDrilling and B2.AuxiliaryData.bIsDrilling then if B1.Proc.b3Box:getMin():getX() < B2.Proc.b3Box:getMin():getX() then return true else return false end else return false end end -- test della funzione di ordinamento if EgtGetDebugLevel() >= 3 then EgtOutLog( ' CompareFeatures Test ') local bCompTest = true for i = 1, #vProcToSort do for j = i + 1, #vProcToSort do local bComp1 = CompareFeatures( vProcToSort[i], vProcToSort[j]) local bComp2 = CompareFeatures( vProcToSort[j], vProcToSort[i]) if bComp1 == bComp2 then bCompTest = false EgtOutLog( string.format( ' ProcId : %d vs %d --> ERROR', vProcToSort[i].Proc.id, vProcToSort[j].Proc.id)) end end end if bCompTest then EgtOutLog( ' ALL OK') end end -- eseguo un primo ordinamento table.sort( vProcToSort, CompareFeatures) -- ordino per ottimizzare cambio utensile -- MACHININGS = SortByChangeToolOpt( MACHININGS) return true end ------------------------------------------------------------------------------------------------------------- -- applica le lavorazioni local function AddMachinings( vProc, PARTS) local bMachiningOk = true for nFeatureIndex = 1, #vProc do local Proc = vProc[nFeatureIndex] local Part = PARTS[vProc[nFeatureIndex].nIndexPart] if ID.IsDrilling( Proc) then bMachiningOk = Drilling.Make( Proc, Part) end -- se taglio if ID.IsCutting( Proc) then bMachiningOk = Cutting.Make( Proc, Part) end -- se fresatura if ID.IsMilling( Proc) then bMachiningOk = Milling.Make( Proc, Part) end -- se svuotatura if ID.IsPocketing( Proc) then bMachiningOk = Pocketing.Make( Proc, Part) end -- se profilatura if ID.IsProfiling( Proc) then bMachiningOk = Profiling.Make( Proc, Part) end end return bMachiningOk end ------------------------------------------------------------------------------------------------------------- local function PrintFeatures( vProc, PARTS) EgtOutLog( ' === PARTS ====') for i = 1, #PARTS do EgtOutLog( ' RawBox(' .. tostring( i) .. ') =' .. tostring( PARTS[i].RawBox)) end EgtOutLog( ' === FEATURES ====') for i = 1, #vProc do local Proc = vProc[i] local sOut = string.format( 'Id=%3d Flg=%2d Type=%s', Proc.id, Proc.nFlg, Proc.sType) EgtOutLog( sOut) end end ------------------------------------------------------------------------------------------------------------- local function UpdateRawPosition( PARTS) for i = 1, #PARTS do PARTS[i].b3Part = EgtGetBBoxGlob( EgtGetFirstNameInGroup( PARTS[i].id, 'Geo') or GDB_ID.NULL, GDB_BB.STANDARD) PARTS[i].b3RawPart = EgtGetRawPartBBox( PARTS[i].idRaw) end end ------------------------------------------------------------------------------------------------------------- local function CheckAndMovePawPart( nIdRawToMove, vtMove) EgtMoveRawPart( nIdRawToMove, vtMove) end ------------------------------------------------------------------------------------------------------------- function WinExec.ProcessFeatures( PARTS) -- ciclo sui pezzi local nTotErr = 0 local Stats = {} local vProc = {} MACHININGS = {} -- aggiorno posizione grezzi dopo disposizione EgtSetCurrPhase( 1) UpdateRawPosition( PARTS) -- si recuperano tutte le feature di tutti i pezzi in una lista unica for nPart = 1, #PARTS do -- recupero le feature di lavorazione della trave vProc = CollectFeatures( vProc, PARTS[nPart], nPart) -- recupero informazioni ausiliarie feature e dipendenze tra feature dello stesso pezzo vProc = GetFeatureInfoAndDependency( vProc, PARTS[nPart]) end -- debug if EgtGetDebugLevel() >= 1 then PrintFeatures( vProc, PARTS) end EgtOutLog( ' *** AddMachinings ***', 1) -- esegue le lavorazioni e le salva in lista AddMachinings( vProc, PARTS) -- ordina le lavorazioni OrderMachining( MACHININGS) for i = 1, #MACHININGS do if MACHININGS[i].AuxiliaryData.nPhase == 1 then -- aggiunge effettivamente la lavorazione e restituisce gli ingombri local bIsApplyOk, sErr, b3MachEncumbrance = MachiningLib.AddOperation( MACHININGS[i]) -- TODO ingombro lavorazione mi restituisce dati sbagliati. C'è un riferimento? -- se feature di testa, sposto testa in base a ingombro lavorazione if MACHININGS[i].Proc.bHeadProfile and b3MachEncumbrance then PARTS[MACHININGS[i].Proc.nIndexPart].DispOffsets.Phase1.dOffsetX = b3MachEncumbrance:getMax():getX() - PARTS[MACHININGS[i].Proc.nIndexPart].b3Part:getMin():getX() + 5 end end end -- TODO lo spostamento, oltre a controllare le collisioni con il profilo di testa, deve anche considerare il pinzaggio del pezzo nel suo insieme? -- Es.: basterebbe aggiungere un offset di 50mm per la testa, ma se metto 60mm, posso utilizzare una morsa in più che altrimenti avrei dovuto togliere per una collisione con una svuotatura -- sposto pezzo per permettere pinzaggio migliore e non aver colisione in testa for i = 1, #PARTS do -- TODO controllare calcolo ingombro local vtMove = Vector3d( - PARTS[i].DispOffsets.Phase1.dOffsetX, 0, 0) if vtMove ~= V_NULL() then CheckAndMovePawPart( PARTS[i].idRaw, vtMove) end end -- scrivo lavorazioni seconda fase EgtSetCurrPhase( 2) UpdateRawPosition( PARTS) for i = 1, #MACHININGS do if MACHININGS[i].AuxiliaryData.nPhase == 2 then -- aggiunge effettivamente la lavorazione local bIsApplyOk, sErr, b3MachEncumbrance = MachiningLib.AddOperation( MACHININGS[i]) -- TODO ingombro lavorazione mi restituisce dati sbagliati. C'è un riferimento? -- se feature di testa, sposto testa in base a ingombro lavorazione if MACHININGS[i].Proc.bHeadProfile and b3MachEncumbrance then PARTS[MACHININGS[i].Proc.nIndexPart].DispOffsets.Phase2.dOffsetX = PARTS[MACHININGS[i].Proc.nIndexPart].b3Part:getMax():getX() - b3MachEncumbrance:getMin():getX() + 5 end end end -- sposto pezzo per permettere pinzaggio migliore e non aver colisione in testa for i = 1, #PARTS do -- TODO controllare calcolo ingombro local vtMove = Vector3d( PARTS[i].DispOffsets.Phase2.dOffsetX, 0, 0) if vtMove ~= V_NULL() then CheckAndMovePawPart( PARTS[i].idRaw, vtMove) end end EgtOutLog( ' *** End AddMachinings ***', 1) -- Aggiornamento finale di tutto EgtSetCurrPhase( 1) local bApplOk, sApplErrors, sApplWarns = EgtApplyAllMachinings() if not bApplOk then nTotErr = nTotErr + 1 table.insert( Stats, {nErr = 1, sMsg=sApplErrors}) end return ( nTotErr == 0), Stats end ------------------------------------------------------------------------------------------------------------- return WinExec