-- Processore macchina Essetre-WALL by EgalTech s.r.l.   2022/03/08
-- Funzioni generiche indipendenti dal controllo

-- 2020/07/21 ver 2.3g5 DS Modificata ExecScrapRemove per maggiore precisione nel riconoscere i veri sfridi.
-- 2020/09/16 ver 2.3i1 DS Correzione a ExecScrapRemove per evitare crash (va approfondita).
-- 2020/09/16 ver 2.3i2 DS Riportato ExecScrapRemove e gestione collisioni da Wall_90480026 (cliente portoghese).
-- 2020/10/22 ver 2.3j4 DS Corretto problema mancato posizionamento B1=90 e C1=90 quando si seleziona H14.
-- 2022/01/20 ver 2.4a3 DS In ExecScrapRemove migliorato calcolo centro di ogni parte del Vmill.
-- 2022/01/20 ver 2.4a3 DS Aggiunta gestione ingombri riferimenti per collisioni.
-- 2022/03/08 ver 2.4c1 DS In ExecScrapRemove si accettano pezzi fino a 19% dell'originale.

-- Intestazioni
require( 'EmtGenerator')
EgtEnableDebug( false)


---------------------------------------------------------------------
-- *** GENERATION ***
---------------------------------------------------------------------
local sBaseDir = EgtGetCurrMachineDir()
if NumericalControl == 'NUM' then
   dofile( sBaseDir .. '\\Essetre-WALL.NUM.mlpe')
elseif NumericalControl == 'TPA' then
   error( 'Numerical Control error : TPA not yet managed')
   --dofile( sBaseDir .. 'Essetre-WALL.TPA.mlpe')
else
   error( 'Numerical Control error : unkwnown type')
end

---------------------------------------------------------------------
-- *** SIMULATION ***
---------------------------------------------------------------------
local COLL_SAFE_DIST = 2

---------------------------------------------------------------------
function OnSimulStart()
   -- Carico gli utensili sulle barre portautensili
   local vTcPos = EgtGetAllTcPosNames()
   if vTcPos then
      for i = 1, #vTcPos do
         local vTools = EgtGetToolsInCurrSetupPos( vTcPos[i])
         if vTools and vTools[1] then
            EgtLoadTool( vTcPos[i], 1, vTools[1])
            ShowToolInTcPos( vTcPos[i], true)
         end
      end
   end
   -- Se reset o home, esco
   if EMT.SIM1ST then return end
   -- Non effettuate lavorazioni con sega a catena sul fianco
   EMT.CSAWFIRST = true
   -- Non effettuata pausa per rimozione sfridi
   EMT.SCRAPFIRST = true
   -- Creo o svuoto gruppo per copia finale degli oggetti virtual milling
   local nVmGrpId = EgtGetFirstNameInGroup( GDB_ID.ROOT, 'VMill')
   if nVmGrpId then
      EgtSetStatus( nVmGrpId, GDB_ST.ON)
      EgtEmptyGroup( nVmGrpId)
   else
      nVmGrpId = EgtGroup( GDB_ID.ROOT)
      EgtSetName( nVmGrpId, 'VMill')
      EgtSetLevel( nVmGrpId, GDB_LV.TEMP)
   end
   -- Preparo lista oggetti da verificare per collisioni
   EMT.COLLOBJ = {}
   AddToCollisionCheck( 'B1', 'COLLISION', EMT.COLLOBJ)
   AddToCollisionCheck( 'C1', 'COLLISION', EMT.COLLOBJ)
   AddToCollisionCheck( 'C2', 'COLLISION', EMT.COLLOBJ)
   DumpCollisionCheck( EMT.COLLOBJ, 'Collision Objects :', 4)
   -- Preparo lista solidi macchina con cui possono collidere gli oggetti sopra riportati (in aggiunta a VMill)
   EMT.MCODET = {}
   EMT.MCODET[1] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'Y'), 'COLLISION'), 'TRAV')
   EMT.MCODET[2] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'Y'), 'COLLISION'), 'COL1')
   EMT.MCODET[3] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'Y'), 'COLLISION'), 'COL2')
   EMT.MCODET[4] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'Y'), 'COLLISION'), 'TC1')
   EMT.MCODET[5] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'Y'), 'COLLISION'), 'TC2')
   EMT.MCODET[6] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'Y'), 'COLLISION'), 'TC3')
   EMT.MCODET[7] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'Y'), 'COLLISION'), 'TC4')
   EMT.MCODET[8] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetAxisId( 'X1'), 'COLLISION'), 'TRAV')
   EMT.MCODET[9] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'STM')
   EMT.MCODET[10] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref1_1')
   EMT.MCODET[11] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref1_2')
   EMT.MCODET[12] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref1_3')
   EMT.MCODET[13] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref1_4')
   EMT.MCODET[14] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref1_5')
   EMT.MCODET[15] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref3_1')
   EMT.MCODET[16] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref3_2')
   EMT.MCODET[17] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref3_3')
   EMT.MCODET[18] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref3_4')
   EMT.MCODET[19] = EgtGetFirstNameInGroup( EgtGetFirstNameInGroup( EgtGetBaseId( 'Base'), 'COLLISION'), 'Ref3_5')
   for i = #EMT.MCODET, 1, -1 do
      if not EMT.MCODET[i] then
         table.remove( EMT.MCODET, i)
         EgtOutLog( 'Warning : MCODET element (' .. tostring( i) .. ') is null')
      end
   end
   -- Preparo lista collisioni vuota
   EMT.COLLIDE = {}
end

---------------------------------------------------------------------
function OnSimulEnd()
   if EMT.SCRAPFIRST then
      EMT.SCRAPFIRST = false
      ExecScrapRemove()
   end
   ExecUnloading()
end

---------------------------------------------------------------------
--function OnSimulDispositionStarting()
--end

---------------------------------------------------------------------
function OnSimulDispositionStart()

   EMT.OPEISDISP = true

   -- dati del grezzo
   local nRawId = EgtGetFirstRawPart()
   local b3Raw = EgtGetRawPartBBox( nRawId or GDB_ID.NULL)

   -- Se prima disposizione (dovrebbe essere l'unica)
   if EMT.PHASE == 1 then
      -- Assegno dimensioni del grezzo
      EMT.LB = 0
      EMT.SB = 0
      EMT.HB = 0
      if b3Raw then
         EMT.LB = b3Raw:getDimX()
         EMT.SB = b3Raw:getDimY()
         EMT.HB = b3Raw:getDimZ()
      end
      -- Carico primo utensile sulla testa 1
      local sTool = FindFirstToolOnHead( 'H1')
      if sTool then
         EMT.TOOL_1 = sTool
         --EgtLoadTool( 'H1', 1, EMT.TOOL_1)
         EgtSetCalcTool( EMT.TOOL_1, 'H1', 1)
         EgtResetAxisPos( 'C1')
         EgtResetAxisPos( 'B1')
         EMT.TCPOS_1 = GetToolTcPos( EMT.TOOL_1)
         ShowToolInTcPos( EMT.TCPOS_1, false)
      else
         EgtUnloadTool( 'H1', 1)
      end
      -- Se vero inizio, abilitato e trovato grezzo, creazione Zmap
      if not EMT.SIM1ST and EgtGetInfo( EgtGetCurrMachGroup(), 'Vm', 'b') and nRawId then
         -- elimino eventuale vecchio Zmap
         EgtErase( EgtGetFirstNameInGroup( nRawId, 'VMill') or GDB_ID.NULL)
         -- determino la risoluzione dello Zmap
         local dArea = b3Raw:getDimX() * b3Raw:getDimY() + b3Raw:getDimX() * b3Raw:getDimZ() + b3Raw:getDimY() * b3Raw:getDimZ()
         local dTol = 3.81
         if dArea < CoeffVM * 6.0e6 then
            dTol = 2.03
         elseif dArea < CoeffVM * 12.0e6 then
            dTol = 2.57
         elseif dArea < CoeffVM * 24.0e6 then
            dTol = 3.11
         end
         local nOperCnt = EgtGetGroupObjs( EgtGetFirstNameInGroup( EgtGetCurrMachGroup(), 'Opers'))
         if nOperCnt > 500 then
            dTol = 5.71
         end
         -- verifico se grezzo ruotato
         local refRaw = EgtGetGroupGlobFrame( nRawId)
         local bRefXYZ = AreSameVectorApprox( refRaw:getVersX(), X_AX()) and AreSameVectorApprox( refRaw:getVersY(), Y_AX())
         -- creo lo Zmap
         if bRefXYZ then
            EMT.VMILL = EgtVolZmapBox( nRawId, b3Raw:getMin(), b3Raw:getDimX(), b3Raw:getDimY(), b3Raw:getDimZ(), dTol, true, GDB_RT.GLOB)
         else
            local nSolId = EgtGetFirstNameInGroup( nRawId, 'RawSolid')
            EMT.VMILL = EgtVolZmapFromSurfTm( nRawId, nSolId, dTol, true)
         end
         if EMT.VMILL then
            EgtSetName( EMT.VMILL, 'VMill')
            EgtSetLevel( EMT.VMILL, GDB_LV.TEMP)
            local nSolId = EgtGetFirstNameInGroup( nRawId, 'RawSolid')
            EgtSetColor( EMT.VMILL, EgtGetColor( nSolId), false)
            -- nascondo le altre geometrie
            local nId = EgtGetFirstInGroup( nRawId)
            while nId do
               if nId ~= EMT.VMILL then
                  EgtSetStatus( nId, GDB_ST.OFF)
               end
               nId = EgtGetNext( nId)
            end
            EgtSetStatus( EMT.SCRAP or GDB_ID.NULL, GDB_ST.OFF)
            -- imposto eventuale utensile corrente
            SetToolForVmill( EMT.TOOL, EMT.HEAD, EMT.EXIT, EMT.VMILL)
         end
      end
   end
   -- se vero inizio, assegno solidi per verifica collisione
   if not EMT.SIM1ST then
      EMT.CODET = {}
      for i = 1, #EMT.MCODET do
         EMT.CODET[i] = EMT.MCODET[i]
      end
      if EMT.VMILL then
         table.insert( EMT.CODET, EMT.VMILL)
      end
   end
   -- Nascondo tutte le lavorazioni
   local nMchId = EgtGetFirstOperation()
   while nMchId do
      if EgtGetOperationType( nMchId) ~= MCH_OY.DISP then
         EgtSetOperationStatus( nMchId, false)
      end
      nMchId = EgtGetNextOperation( nMchId)
   end
end

---------------------------------------------------------------------
function OnSimulDispositionEnd()
   if EMT.UNLOADING or EMT.FALL then
      EMT.UNLOADING = false
      EMT.FALL = false
   end
   EMT.OPEISDISP = false
end

---------------------------------------------------------------------
function OnSimulToolSelect()
   -- se utensile non definito, è disposizione ed esco
   if EMT.TOOL == '' then return end
   -- recupero dati utensile
   EMT.TOOLTYPE = EgtTdbGetCurrToolParam( MCH_TP.TYPE)
   EMT.TOOLTOTLEN = EgtTdbGetCurrToolParam( MCH_TP.TOTLEN)
   -- se non lama su rinvio o graffettatrici, carico utensile 
   if EMT.HEAD == 'H1' or EMT.HEAD == 'H3' then
      -- se sega a catena, imposto subito angolo scelto per asse virtuale A
      if EMT.HEAD == 'H3' then
         -- recupero la lavorazione successiva
         local NextMchId
         if EMT.MCHID then
            NextMchId = EgtGetNextActiveOperation( EMT.MCHID)
         else
            NextMchId = EgtGetFirstActiveOperation()
         end
         if EgtGetOperationType( NextMchId) == MCH_OY.DISP then
            NextMchId = EgtGetNextActiveOperation( NextMchId)
         end
         EgtSetCurrMachining( NextMchId)
         -- leggo e imposto il valore di A1
         local sVal = EgtGetMachiningParam( MCH_MP.BLOCKEDAXIS)
         local CSawPosA = EgtGetVal( sVal, 'A1', 'd') or 0
         EgtSetAxisPos( 'A1', CSawPosA)
         EmtModifyAxisHome( 'A1', CSawPosA)
         EgtSetAxisPos( 'C1', GetChainSawCHomeFromVirtualAxis( CSawPosA))
      -- se lama grande
      elseif EMT.HEAD == 'H1' and EMT.TCPOS == 'T203' then
         EgtSetAxisPos( 'X1', -2200)
      end
      EgtResetAxisPos( 'C1')
      EgtResetAxisPos( 'B1')
      -- breve pausa
      EgtPause( 100)
      EgtOutText( '')
      EMT.TOOL_1 = EMT.TOOL
      EMT.TCPOS_1 = EMT.TCPOS
      -- lo nascondo sul portautensili
      ShowToolInTcPos( EMT.TCPOS, false)
   -- se altrimenti graffettatrice su testa 1, tolgo utensile da testa e imposto opportunamente B e C
   elseif EMT.HEAD == 'H14' then
      -- nascondo utensile su testa e lo visualizzo su TcPos
      EgtSetStatus( EgtGetHeadId( 'H1'), GDB_ST.OFF)
      ShowToolInTcPos( EMT.TCPOS_1, true)
      EMT.TOOL_1 = nil
      EMT.TCPOS_1 = nil
      -- imposto assi rotanti B90 C90
      EgtSetAxisPos( 'C1', 90)
      EgtSetAxisPos( 'B1', 90)
   end
   -- se attivo Vmill
   SetToolForVmill( EMT.TOOL, EMT.HEAD, EMT.EXIT, EMT.VMILL)
   -- se attivo Collision Check
   EMT.SAFEDIST = COLL_SAFE_DIST
   if EMT.COLLOBJ then
      local nInd = EgtIf( EMT.HEAD ~= 'H2', 1001, 1011)
      AddToolToCollisionObj( EMT.TOOL, EMT.HEAD, EMT.EXIT, nInd)
      AddToolHolderToCollisionObj( EMT.TOOL, EMT.HEAD, EMT.EXIT, nInd + 1)
      for i, Coll in ipairs( EMT.COLLOBJ) do
         EmtAddCollisionObjEx( i, Coll.Fr, Coll.Ty, Coll.Mv, Coll.P1, Coll.P2, Coll.P3)
      end
   end
end

---------------------------------------------------------------------
function OnSimulToolDeselect()
   -- Step di simulazione
   local RapSimStep = 4 * EMT.SIMSTEP
   local RapSimRotStep = RapSimStep / 10
   local CollSimRotStep = min( RapSimRotStep, 5)
   -- porto la testa in home
   if EMT.HEAD == 'H2' or EMT.HEAD == 'H21' or EMT.HEAD == 'H22' or EMT.HEAD == 'H23' then
      SimulMoveAxes( 'X2', MaxX2, RapSimStep, 'C2', EgtGetAxisHomePos( 'C2'), RapSimRotStep, 'B2', EgtGetAxisHomePos( 'B2'), RapSimRotStep)
   else
      local ParkX1 = EgtIf( EMT.HEAD == 'H3', ( MinX1 + MaxX1) / 2, MinX1)
      SimulMoveAxes( 'X1', ParkX1, RapSimStep, 'C1', EgtGetAxisHomePos( 'C1'), RapSimRotStep, 'B1', EgtGetAxisHomePos( 'B1'), RapSimRotStep)
   end
   -- se testa attuale è sega a catena devo depositarla
   if EMT.HEAD == 'H3' then
      ShowToolInTcPos( EMT.TCPOS_1, true)
      EgtUnloadTool( 'H3', 1)
      EMT.TOOL_1 = nil
      EMT.TCPOS_1 = nil
   end
   -- deposito utensile se non lama su rinvio o graffettatrici
   if ( EMT.NEXTHEAD == 'H1' or EMT.NEXTHEAD == 'H3') and ( EMT.HEAD == 'H1' or EMT.HEAD == 'H3') then
      if EMT.NEXTTOOL ~= EMT.TOOL_1 then
         EgtOutText( 'Tool change in progress...')
         -- simulo movimento
         SimulMoveAxes( 'B', 90, CollSimRotStep, 'C', -90, CollSimRotStep)
         SimulMoveAxis( 'X', 0, RapSimStep)
         -- breve pausa
         EgtPause( 100)
         ShowToolInTcPos( EMT.TCPOS_1, true)
         -- se segue sega a catena
         if EMT.NEXTHEAD == 'H3' then
            -- nascondo l'utensile corrente
            EgtSetStatus( EgtGetHeadId( EMT.HEAD), GDB_ST.OFF)
            -- eseguo movimento opportuno
            SimulMoveAxes( 'B', 0, CollSimRotStep, 'C', -90, CollSimRotStep)
         end
      else
         EMT.TOOL_1 = nil
         EMT.TCPOS_1 = nil
      end
   end
end

---------------------------------------------------------------------
function OnSimulMachiningStart()
   -- recupero alcuni dati della lavorazione
   EMT.MCHNAME = EgtGetOperationName( EMT.MCHID)
   EMT.MCHTYPE = EgtGetMachiningParam( MCH_MP.TYPE)
   local sNotes = EgtGetMachiningParam( MCH_MP.USERNOTES)
   EMT.VMRS = false
   -- recupero CUTID e TASKID della feature lavorata
   local vId = EgtGetMachiningGeometry()
   if vId and #vId > 0 and #vId[1] > 0 then
      local nPartId = EgtGetParent( EgtGetParent( vId[1][1]) or GDB_ID.NULL)
      EMT.CUTID = EgtGetInfo( nPartId or GDB_ID.NULL, 'CUTID', 'i') or 0
      EMT.TASKID = EgtGetInfo( vId[1][1], 'TASKID', 'i') or 0 ;
   else
      EMT.CUTID = 0
      EMT.TASKID = 0
   end
   -- eventuale pausa per rimozione sfridi
   if EMT.SCRAPFIRST then
      if ( EMT.HEAD == 'H3' and string.sub( EMT.MCHNAME, 1, 5) == 'Csaw_') or EgtExistsInfo( EMT.MCHID, 'MOVE_AFTER') then
         EMT.SCRAPFIRST = false
         EgtOutText( 'Pause for Scrap Remove')
         ParkForScrapRemove()
         ExecScrapRemove()
         EgtPause( 500)
         EgtOutText( '')
      end
   end
   -- se chiodatura in doppio, imposto attivazione testa associata 12
   local nDouble = GetValNotes( sNotes, 'Dbl', 'i')
   if EMT.HEAD == 'H14' and nDouble and nDouble > 0 then
      EMT.DOUBLE = nDouble
      EMT.DELTA = GetValNotes( sNotes, 'Dlt', 'd')
      EgtOutText( 'Machining in Double (D='..EgtNumToString( EMT.DELTA, 1)..')')
   else
      EgtOutText( '')
   end
   -- non ancora iniziata la lavorazione
   EMT.MCHFIRST = true
end

---------------------------------------------------------------------
function OnSimulMachiningEnd()
   if EMT.UNLOADING or EMT.FALL then
      EMT.UNLOADING = false
      EMT.FALL = false
   end
   EMT.DOUBLE = nil
   EMT.DELTA = nil
end

---------------------------------------------------------------------
--function OnSimulPathStart()
--end

---------------------------------------------------------------------
function OnSimulPathEnd()
   -- se attivo VMILL, lavorazione ed è richiesto di eliminare gli sfridi
   if EMT.VMILL and not EMT.OPEISDISP and EMT.VMRS then
      EgtOutLog( 'OnSimulPathEnd', 5)
      local nPart = EgtVolZmapPartCount( EMT.VMILL)
      if nPart > 1 then
         -- ricerca del pezzo con massimo volume
         local nPartMax = 0
         local dVolMax = 0
         for i = 1, nPart do
            local dVol = EgtVolZmapPartVolume( EMT.VMILL, i - 1)
            if dVol > dVolMax then
               dVolMax = dVol
               nPartMax = i
            end
         end
         -- eliminazione di tutti i pezzi piccoli
         for i = nPart, 1, -1 do
            if i ~= nPartMax then
               local b3Vmill = EgtVolZmapGetPartBBoxGlob( EMT.VMILL, i - 1, GDB_BB.STANDARD)
               if b3Vmill:getDimX() < 1200 then
                  EgtRemoveVolZmapPart( EMT.VMILL, i - 1)
               end
            end
         end
         -- aggiorno visualizzazione
         EgtDraw()
      end
   end
end

---------------------------------------------------------------------
function OnSimulPathStartAux()
   --EgtOutLog( 'OnSimulPathStartAux - ' .. EgtNumToString( EMT.AUXIND, 0) .. ' - ' .. EMT.AUX)
   EgtOutLog( 'OnSimulPathStartAux', 5)
   -- eseguo il comando
   --ExecAuxCmd( EMT.AUX)
end

---------------------------------------------------------------------
function OnSimulPathEndAux()
   --EgtOutLog( 'OnSimulPathEndAux - ' .. EgtNumToString( EMT.AUXIND, 0) .. ' - ' .. EMT.AUX)
   EgtOutLog( 'OnSimulPathEndAux', 5)
   -- eseguo il comando
   --ExecAuxCmd( EMT.AUX)
end

---------------------------------------------------------------------
--function OnSimulMoveStart()
--end

---------------------------------------------------------------------
--function OnSimulMoveEnd()
--end

---------------------------------------------------------------------
function OnSimulCollision()
   -- se prima collisione della lavorazione, la segnalo
   if EMT.MCHNAME ~= EMT.LAST_MCHNAME_COLLIDE then
      local Class = ''
      if EMT.SIMCOBIND == 1001 or EMT.SIMCOBIND == 1011 then
         Class = 'T_'..EMT.HEAD
      elseif EMT.SIMCOBIND == 1002 or EMT.SIMCOBIND == 1012 then
         Class = 'TH_'..EMT.HEAD
      else
         Class = EMT.COLLOBJ[EMT.SIMCOBIND].Cl
      end
      table.insert( EMT.COLLIDE, { Mc = EMT.MCHNAME, Cl = Class, Vm = EMT.SIMVMID})
      EMT.LAST_MCHNAME_COLLIDE = EMT.MCHNAME
      EMT.ERR = 11
      local sErr = 'CUTID='..tostring( EMT.CUTID)..'; TASKID='..tostring( EMT.TASKID)..'; Mach='..EMT.MCHNAME..'; Class='..Class..'; VMill='..EMT.SIMVMID
      EmtSetLastError( 1221, sErr, true)
      EgtOutLog( 'Collision : ' .. sErr, 1)
   end
end

---------------------------------------------------------------------
function ParkForScrapRemove()
   -- Step di simulazione
   local RapSimStep = 4 * EMT.SIMSTEP
   -- Quota di parcheggio
   local b3Raw = EgtGetRawPartBBox( EgtGetFirstRawPart() or GDB_ID.NULL)
   local ParkY = min( -( b3Raw:getMin():getX() - 2000), -MinY)
   --Eseguo
   SimulMoveAxis( 'Z1', EgtGetAxisHomePos( 'Z1'), RapSimStep)
   SimulMoveAxis( 'Y', ParkY, RapSimStep)
end

---------------------------------------------------------------------
function ExecScrapRemove()
   -- Se non c'è virtual milling attivo, esco
   if not EMT.VMILL then return end
   -- Se non ci sono almeno due parti nel virtual milling, esco
   local nVmPart = EgtVolZmapPartCount( EMT.VMILL)
   if nVmPart < 2 then return end
   -- Recupero box e area dei pezzi
   local vPart = {}
   local nPartId = EgtGetFirstPartInRawPart( EgtGetFirstRawPart() or GDB_ID.NULL)
   while nPartId do
      -- box e area
      local b3Part = EgtGetBBoxGlob( EgtGetFirstNameInGroup( nPartId, 'Box') or GDB_ID.NULL, GDB_BB.STANDARD)
      local dPartArea = b3Part:getDimX() * b3Part:getDimY()
      -- box espanso
      local b3PartExp = BBox3d( b3Part)
      b3PartExp:Add( b3PartExp:getMin() - Vector3d( 10, 10, 0))
      b3PartExp:Add( b3PartExp:getMax() + Vector3d( 10, 10, 0))
      -- salvo i dati
      table.insert( vPart, { Box = b3Part, BoxExp = b3PartExp, Area = dPartArea})
      -- passo al successivo
      nPartId = EgtGetNextPartInRawPart( nPartId)
   end
   -- Recupero box e area delle parti di Vmill
   local vVmPart = {}
   for i = 1, nVmPart do
      -- recupero il box e calcolo l'area della parte come Vmill
      local b3VmPart = EgtVolZmapGetPartBBoxGlob( EMT.VMILL, i - 1, GDB_BB.STANDARD)
      local dVmPartArea = EgtVolZmapPartVolume( EMT.VMILL, i - 1) / b3VmPart:getDimZ()
      -- box espanso
      local b3VmPartExp = BBox3d( b3VmPart)
      b3VmPartExp:Add( b3VmPart:getMin() - Vector3d( 100, 100, 0))
      b3VmPartExp:Add( b3VmPart:getMax() + Vector3d( 100, 100, 0))
      -- salvo i dati
      table.insert( vVmPart, { Box = b3VmPart, BoxExp = b3VmPartExp, Area = dVmPartArea})
   end
   -- Tengo solo le parti del Vmill che contengono almeno un box di un pezzo o che interferiscono e sono abbastanza grandi
   local nValidCnt = 0
   for i = #vVmPart, 1, -1 do
      -- verifiche per parte da eliminare
      local bToRemove = true
      -- se area abbastanza grande, allora da verificare
      if vVmPart[i].Area > 1e4 then
         -- verifico se sono sicuramente valide (i due box si equivalgono entro la tolleranza)
         for j = 1, #vPart do
            if EnclosesXY( vVmPart[i].BoxExp, vPart[j].Box) and EnclosesXY( vPart[j].BoxExp, vVmPart[i].Box) then
               bToRemove = false
               vVmPart[i].Part = j
               nValidCnt = nValidCnt + 1
               break
            end
         end
         -- verifico se sono molto probabilmente valide
         for j = 1, #vPart do
            if bToRemove and ( OverlapsXY( vVmPart[i].Box, vPart[j].Box) and vVmPart[i].Area > 0.19 * vPart[j].Area) then
               bToRemove = false
               break
            end
         end
      end
      -- se richiesta eliminazione, la eseguo
      if bToRemove then
         EgtRemoveVolZmapPart( EMT.VMILL, i - 1)
         table.remove( vVmPart, i)
      end
   end
   -- Se il numero delle parti sicuramente valide uguaglia il numero dei pezzi, elimino le altre ed ho finito
   if nValidCnt == #vPart then
      for i = #vVmPart, 1, -1 do
         if not vVmPart[i].Part then
            EgtRemoveVolZmapPart( EMT.VMILL, i - 1)
            table.remove( vVmPart, i)
         end
      end
      return
   end
   -- Calcolo punti interni alle parti di Vmill non già dichiarate valide
   for i = 1, #vVmPart do
      if not vVmPart[i].Part then
         local vtDir = VectorFromRotated( X_AX(), Z_AX(), 45) 
         local ptP = vVmPart[i].Box:getCenter() - vtDir * ( vVmPart[i].Box:getRadius() + 10)
         local bOk, vIntType, vIntDist = EgtLineVolZmapInters( ptP, vtDir, EMT.VMILL, GDB_RT.GLOB)
         if bOk then
            for j = 1, #vIntType do
               -- se la corrente entra ed esiste la successiva che esce
               if vIntType[j] == GDB_SLT.IN and j + 1 <= #vIntType and vIntType[j + 1] == GDB_SLT.OUT then
                  local ptC = ptP + vtDir * (( vIntDist[j] + vIntDist[j + 1]) / 2)
                  local nPart = EgtVolZmapGetPartMinDist( EMT.VMILL, ptC, GDB_RT.GLOB)
                  if nPart == i - 1 then
                     vVmPart[i].Cen = ptC
                     break
                  end
               end
            end
         end
      end
   end
   -- Verifico le parti di Vmill non già dichiarate valide
   for i = #vVmPart, 1, -1 do
      if not vVmPart[i].Part then
         local bToRemove = true
         if vVmPart[i].Cen then
            for j = 1, #vPart do
               if EnclosesPointXY( vPart[j].Box, vVmPart[i].Cen) then
                  bToRemove = false
                  break
               end
            end
            for k = 1, #vVmPart do
               if k ~= i then
                  local vExpBox = BBox3d( vVmPart[k].Box)
                  -- lo espando in XY per sicurezza
                  vExpBox:Add( vExpBox:getMin() - Vector3d( 100, 100, 0))
                  vExpBox:Add( vExpBox:getMax() + Vector3d( 100, 100, 0))
                  -- verifico contenimento
                  if EnclosesXY( vExpBox, vVmPart[i].Box) then
                     bToRemove = true
                     break
                  end
               end
            end
         end
         -- se richiesta eliminazione, la eseguo
         if bToRemove then
            EgtRemoveVolZmapPart( EMT.VMILL, i - 1)
            table.remove( vVmPart, i)
         end
      end
   end
end

---------------------------------------------------------------------
function ExecUnloading()
   if EMT.VMILL then
      -- gruppo dei Vmill
      local nVmGrpId = EgtGetFirstNameInGroup( GDB_ID.ROOT, 'VMill')
      -- creo un nuovo layer e vi inserisco il nuovo pezzo
      local nLayId = EgtGroup( nVmGrpId, EgtGetGlobFrame( EMT.VMILL))
      EgtRelocate( EMT.VMILL, nLayId)
      EgtSetLevel( EMT.VMILL, GDB_LV.USER)
      -- aggiungo gli spigoli
      local nFirstId, nCount = EgtVolZmapGetEdges( EMT.VMILL, nLayId)
      if nFirstId then
         for nId = nFirstId, nFirstId + nCount - 1 do
            EgtSetColor( nId, Color3d( 96, 96, 96))
         end
      end
      -- rilascio Vmill 
      EMT.VMILL = nil
      -- aggiorno la visualizzazione
      EgtDraw()
      -- se finito
      if EMT.PHASE == EgtGetPhaseCount() then
         -- se impostato di salvare i Vmill, lo faccio
         local sMachIni = EgtGetCurrMachineDir() .. '\\' .. EgtGetCurrMachineName() .. '.ini'
         if EgtGetStringFromIni( 'VMill', 'Save', '', sMachIni) == '1' then
            local sFile = EgtGetCurrFilePath()
            if sFile then
               local sDir, sName, sExt = EgtSplitPath( sFile)
               if sExt and sExt:lower() == ".nge" then
                  sName = sName .. '_VM_' .. EgtGetMachGroupName( EgtGetCurrMachGroup())
                  EgtSetLevel( nVmGrpId, GDB_LV.USER)
                  EgtSaveObjToFile( nVmGrpId, sDir .. sName .. '.Nge')
                  EgtSetLevel( nVmGrpId, GDB_LV.TEMP)
               end
            end
         end
      end
   end
end

---------------------------------------------------------------------
function ShowToolInTcPos( sTcPos, bShow)
   -- recupero il gruppo dell'utensile
   local TcExitId = EgtGetFirstNameInGroup( EgtGetTcPosId( sTcPos or '') or GDB_ID.NULL, 'T1')
   if not TcExitId then return end
   -- imposto lo stato di visualizzazione
   EgtSetStatus( TcExitId, EgtIf( bShow, GDB_ST.ON, GDB_ST.OFF))
end

---------------------------------------------------------------------
function GetToolTcPos( sTool)
   -- salvo stato iniziale
   local CurrTool = EgtTdbGetCurrToolParam( MCH_TP.NAME)
   -- recupero la posizione di cambio utensile dell'utensile indicato
   local sTcPos
   if EgtTdbSetCurrTool( sTool) then
      sTcPos = EgtTdbGetCurrToolParam( MCH_TP.TCPOS)
   end
   -- ripristino stato iniziale
   if CurrTool then
      EgtTdbSetCurrTool( CurrTool)
   else
      EgtTdbSetCurrTool( '')
   end
   -- restituisco risultato
   return sTcPos
end


---------------------------------------------------------------------
--   *** ESTIMATION T&L ***
---------------------------------------------------------------------
local RAPID_X_FEED =  75000    -- mm/min
local RAPID_Y_FEED = 100000    -- mm/min
local RAPID_Z_FEED =  50000    -- mm/min
local RAPID_C_FEED =  15000    -- deg/min
local RAPID_B_FEED =  15000    -- deg/min
local RAPID_MIN_T = 0.1        -- s
local LOAD_T = 2               -- s
local CHAR_ONE_MOVE_T = 1      -- s
local ROTATION_T = 40          -- s
local SPLIT_T = 6              -- s
local UNLOAD_T = 4             -- s
local FALL_T = 2               -- s

---------------------------------------------------------------------
function OnEstimStart()
   EMT.INCHES = not EgtUiUnitsAreMM()      -- unità di misura mm/inches
end

---------------------------------------------------------------------
function OnEstimEnd()
end

---------------------------------------------------------------------
function OnEstimProgramStart()
   -- imposto inizio movimenti da Home
   EMT.L1 = EgtGetAxisHomePos( 'Y')
   EMT.L2 = EgtGetAxisHomePos( 'X1')
   EMT.L3 = EgtGetAxisHomePos( 'Z1')
   EMT.R1 = EgtGetAxisHomePos( 'C1')
   EMT.R2 = EgtGetAxisHomePos( 'B1')
   -- aggiorno valori come precedenti
   EmtUpdatePrev()
   -- totalizzatori tempi e lunghezze
   EMT.TOTCUTLEN = 0
   EMT.TOTCUTTIME = 0
   EMT.TOTEXTLEN = 0
   EMT.TOTEXTTIME = 0
   -- variabile per lunghezza taglio utensili
   EMT.TOOLCUTLEN = {}
   -- intestazioni
   EmtTleStart( EMT.INFO)
end

---------------------------------------------------------------------
function OnEstimProgramEnd()
   -- stampa dei totali delle lavorazioni
   EmtTleAddTotal( EmtSecToHMS( EMT.TOTCUTTIME + EMT.TOTEXTTIME), EmtLenToMF( EMT.TOTCUTLEN))
   -- stampa dei totali degli utensili
   for i = 1, #EMT.TOOLCUTLEN do
      local TCL = EMT.TOOLCUTLEN[i]
      EmtTleAddTool( TCL.Name, EmtLenToMF( TCL.Len))
   end
   -- completo il file
   local _, _, sExt = EgtSplitPath( EMT.FILE)
   EmtTleEnd( sExt:sub( 2))
   -- salvo i dati principali nel progetto
   EgtSetInfo( EgtGetCurrMachGroup(), 'Ttot', EgtNumToString( EMT.TOTCUTTIME + EMT.TOTEXTTIME, 0))
   EgtSetInfo( EgtGetCurrMachGroup(), 'Ltot', EgtNumToString( EMT.TOTCUTLEN, 0))
end

---------------------------------------------------------------------
function OnEstimDispositionStart()
   -- inizio disposizione
   EMT.OPEISDISP = true
   -- sulla prima fase dichiaro carico barra
   if EMT.PHASE == 1 then
      EMT.LOAD = true
   else
      EMT.LOAD = false
   end
end

---------------------------------------------------------------------
function OnEstimDispositionEnd()
   -- emetto dati in sospeso
   if EMT.TLE_NAME then
      EmtTleAddMachining( EMT.TLE_NAME, EmtSecToHMS( EMT.TLE_TIME), '  -  ', '  -  ')
      EMT.TLE_NAME = nil
      EMT.TLE_TIME = nil
   end
   -- termine disposizione
   EMT.OPEISDISP = false
end

---------------------------------------------------------------------
function OnEstimToolSelect()
   -- reset indice utensile in tabella lunghezze
   EMT.TCLIND = 0
   -- verifico che l'utensile sia definito
   if #EMT.TOOL == 0 then return end
   -- cerco l'utensile nella tabella
   for i = 1, #EMT.TOOLCUTLEN do
      if EMT.TOOLCUTLEN[i].Name == EMT.TOOL then
         EMT.TCLIND = i
         break
      end
   end
   -- se non trovato, lo aggiungo
   if EMT.TCLIND == 0 then
      table.insert( EMT.TOOLCUTLEN, { Name = EMT.TOOL, Len = 0})
      EMT.TCLIND = #EMT.TOOLCUTLEN
   end
end

---------------------------------------------------------------------
function OnEstimToolDeselect()
end

---------------------------------------------------------------------
function OnEstimMachiningStart()
   EMT.MCHNAME = EgtGetOperationName( EMT.MCHID)
   EMT.MCHTYPE = EgtGetMachiningParam( MCH_MP.TYPE)
   EgtOutLog( 'Mach : ' .. EMT.MCHNAME, 5)
   -- reset contatori di lavorazione
   EMT.MCHCUTLEN = 0
   EMT.MCHCUTTIME = 0
   EMT.MCHEXTLEN = 0
   EMT.MCHEXTTIME = 0
end

---------------------------------------------------------------------
function OnEstimMachiningEnd()
   -- nel caso di foratura devo dimezzare la lunghezza di taglio perchè comprende anche l'uscita
   if EMT.MCHTYPE == MCH_MY.DRILLING then
      EMT.MCHCUTLEN = EMT.MCHCUTLEN / 2
   end
   local sName = EgtGetName( EMT.MCHID)
   EmtTleAddMachining( sName, EmtSecToHMS( EMT.MCHCUTTIME + EMT.MCHEXTTIME), EmtLenToMF( EMT.MCHCUTLEN), EMT.TOOL)
   -- aggiorno totali e utensili
   EMT.TOTCUTLEN = EMT.TOTCUTLEN + EMT.MCHCUTLEN
   EMT.TOTCUTTIME = EMT.TOTCUTTIME + EMT.MCHCUTTIME
   EMT.TOTEXTLEN = EMT.TOTEXTLEN + EMT.MCHEXTLEN
   EMT.TOTEXTTIME = EMT.TOTEXTTIME + EMT.MCHEXTTIME
   EMT.TOOLCUTLEN[EMT.TCLIND].Len = EMT.TOOLCUTLEN[EMT.TCLIND].Len + EMT.MCHCUTLEN
   -- emetto dati in sospeso
   if EMT.TLE_NAME then
      EmtTleAddMachining( EMT.TLE_NAME, EmtSecToHMS( EMT.TLE_TIME), '  -  ', '  -  ')
      EMT.TLE_NAME = nil
      EMT.TLE_TIME = nil
   end
end

---------------------------------------------------------------------
function OnEstimPathStart()
   EMT.AUXTYPE = nil
   EMT.MCHMOVEFIRST = true
end

---------------------------------------------------------------------
function OnEstimPathEnd()
   EMT.AUXTYPE = nil
end

---------------------------------------------------------------------
function OnEstimPathStartAux()
   -- se richiesto, preparo il carico barra
   if EMT.LOAD then
      if EMT.AUXIND == EMT.AUXTOT then
         local dTime = LOAD_T
         EMT.TOTEXTTIME = EMT.TOTEXTTIME + dTime
         EmtTleAddMachining( 'Loading', EmtSecToHMS( dTime), '  -  ', '  -  ')
         EMT.LOAD = false
      end
   -- se altrimenti carico dopo rotazione
   elseif EMT.POSTROT then
      if EMT.AUXIND == EMT.AUXTOT then
         local dTime = ROTATION_T
         EMT.TOTEXTTIME = EMT.TOTEXTTIME + dTime
         EmtTleAddMachining( 'Rotation', EmtSecToHMS( dTime), '  -  ', '  -  ')
         EMT.POSTROT = false
      end
   -- altrimenti, spostamento carrelli
   else
      if EMT.AUXTOT > 2 and EMT.AUXIND == EMT.AUXTOT then
         local dTime = ( EMT.AUXTOT - 2) * CHAR_ONE_MOVE_T
         EMT.TOTEXTTIME = EMT.TOTEXTTIME + dTime
         EmtTleAddMachining( 'Charriots move', EmtSecToHMS( dTime), '  -  ', '  -  ')
      end
   end
end

---------------------------------------------------------------------
function OnEstimPathEndAux()
   -- verifico tipo di emissione
   if EMT.OPEISDISP then
      if not EMT.AUXTYPE then
         local Cmd = EgtSplitString( EMT.AUX)
         if Cmd[1] == '0'  and  Cmd[2] == 'Unloading' then
            EMT.AUXTYPE = 'U'
         else
            EMT.AUXTYPE = 'R'
         end
      end
   else
      if not EMT.AUXTYPE then
         local Cmd = EgtSplitString( EMT.AUX)
         if Cmd[1] == '0'  and  Cmd[2] == 'Split' then
            EMT.AUXTYPE = 'S'
         elseif Cmd[1] == '0'  and  Cmd[2] == 'Unloading' then
            EMT.AUXTYPE = 'U'
         elseif Cmd[1] == '0'  and  Cmd[2] == 'Fall' then
            EMT.AUXTYPE = 'F'
         else
            EMT.AUXTYPE = 'P'
         end
      end
   end
   -- per lo scarico della rimanenza
   if EMT.AUXTYPE == 'R' then
      if EMT.AUXIND == EMT.AUXTOT then
         local dTime = LOAD_T + UNLOAD_T
         EMT.TOTEXTTIME = EMT.TOTEXTTIME + dTime
         EMT.TLE_NAME = 'Remnant unloading'
         EMT.TLE_TIME = dTime
      end
   -- per lo split
   elseif EMT.AUXTYPE == 'S' then
      if EMT.AUXIND == EMT.AUXTOT then
         local dTime = SPLIT_T
         EMT.TOTEXTTIME = EMT.TOTEXTTIME + dTime
         EMT.TLE_NAME = 'Splitting'
         EMT.TLE_TIME = dTime
      end
   -- per lo scarico
   elseif EMT.AUXTYPE == 'U' then
      if EMT.AUXIND == EMT.AUXTOT then
         local dTime = UNLOAD_T
         EMT.TOTEXTTIME = EMT.TOTEXTTIME + dTime
         EMT.TLE_NAME = 'Unloading'
         EMT.TLE_TIME = dTime
      end
   -- per lo scarico a caduta
   elseif EMT.AUXTYPE == 'F' then
      if EMT.AUXIND == EMT.AUXTOT then
         local dTime = FALL_T
         EMT.TOTEXTTIME = EMT.TOTEXTTIME + dTime
         EMT.TLE_NAME = 'Fall'
         EMT.TLE_TIME = dTime
      end
   -- per la pre-rotazione
   elseif EMT.AUXTYPE == 'P' then
      ; -- calcolato come parte della rotazione
   end
end

---------------------------------------------------------------------
function OnEstimRapid()
   -- dati movimento
   local dL1 = EMT.L1 - ( EMT.L1p or 0)
   local dL2 = EMT.L2 - ( EMT.L2p or 0)
   local dL3 = EMT.L3 - ( EMT.L3p or 0)
   local dR1 = 0
   if EMT.R1 and EMT.R1p then dR1 = EMT.R1 - EMT.R1p end
   local dR2 = 0
   if EMT.R2 and EMT.R2p then dR2 = EMT.R2 - EMT.R2p end
   -- se primo posizionamento della lavorazione il movimento di L1 è già conteggiato in quello dei carrelli
   if EMT.MCHMOVEFIRST then
      EMT.MCHMOVEFIRST = false
      dL1 = 0
   end
   -- calcolo lunghezza
   local dLen = sqrt( dL1 * dL1 + dL2 * dL2 + dL3 * dL3)
   EMT.MCHEXTLEN = EMT.MCHEXTLEN + dLen
   -- calcolo tempo
   local dTime = RAPID_MIN_T
   local dT1 = abs( dL1) / RAPID_X_FEED * 60
   if dT1 > dTime then dTime = dT1 end
   local dT2 = abs( dL2) / RAPID_Y_FEED * 60
   if dT2 > dTime then dTime = dT2 end
   local dT3 = abs( dL3) / RAPID_Z_FEED * 60
   if dT3 > dTime then dTime = dT3 end
   local dT4 = abs( dR1) / RAPID_C_FEED * 60
   if dT4 > dTime then dTime = dT4 end
   local dT5 = abs( dR2) / RAPID_B_FEED * 60
   if dT5 > dTime then dTime = dT5 end
   EMT.MCHEXTTIME = EMT.MCHEXTTIME + dTime
   EgtOutLog( string.format( '  G0 Len=%.0f Rot=%.0f° Time=%.2f', dLen, abs( dR1) + abs( dR2), dTime), 5)
   -- aggiorno valori come precedenti
   EmtUpdatePrev()
end

---------------------------------------------------------------------
function OnEstimLinear()
   -- dati movimento
   local dL1 = EMT.L1 - EMT.L1p
   local dL2 = EMT.L2 - EMT.L2p
   local dL3 = EMT.L3 - EMT.L3p
   -- calcolo lunghezza
   local dLen = sqrt( dL1 * dL1 + dL2 * dL2 + dL3 * dL3)
   EMT.MCHCUTLEN = EMT.MCHCUTLEN + dLen
   -- calcolo tempo
   local dTime = dLen / EMT.F * 60
   EMT.MCHCUTTIME = EMT.MCHCUTTIME + dTime
   EgtOutLog( string.format( '  G1 Len=%.0f Time=%.2f', dLen, dTime), 5)
   -- aggiorno valori come precedenti
   EmtUpdatePrev()
end

---------------------------------------------------------------------
function OnEstimArc()
   -- dati movimento
   local dLxy = EMT.RR * abs( EMT.AC) * pi / 180
   local dLz = abs( ( Point3d( EMT.L1, EMT.L2, EMT.L3) - Point3d( EMT.L1p, EMT.L2p, EMT.L3p)) * Vector3d( EMT.EXTR))
   -- calcolo lunghezza
   local dLen = sqrt( dLxy * dLxy + dLz * dLz)
   EMT.MCHCUTLEN = EMT.MCHCUTLEN + dLen
   -- calcolo tempo
   local dTime = dLen / EMT.F * 60
   EMT.MCHCUTTIME = EMT.MCHCUTTIME + dTime
   EgtOutLog( string.format( '  G2 Len=%.0f Time=%.2f', dLen, dTime), 5)
   -- aggiorno valori come precedenti
   EmtUpdatePrev()
end

---------------------------------------------------------------------
-- *** GENERAL ***
---------------------------------------------------------------------
function FindFirstToolOnHead( sH1)
   -- salvo stato iniziale
   local CurrMachId = EgtGetCurrMachining()
   local CurrTool = EgtTdbGetCurrToolParam( MCH_TP.NAME)
   -- cerco lavorazione con utensile su testa indicata
   local sTool, nTlen
   local OpId = EgtGetFirstActiveOperation()
   while OpId do
      local nType = EgtGetOperationType( OpId)
      if nType ~= MCH_OY.NONE  and  nType ~= MCH_OY.DISP then
         if EgtSetCurrMachining( OpId) then
            local sTest = EgtGetMachiningParam( MCH_MP.TOOL)
            if EgtTdbSetCurrTool( sTest) then
               local sHead = EgtTdbGetCurrToolParam( MCH_TP.HEAD)
               if sHead  and  ( sHead == sH1) then
                  sTool = sTest
                  nTlen = EgtTdbGetCurrToolParam( MCH_TP.TOTLEN)
                  break
               end
            end
         end
      end
      OpId = EgtGetNextActiveOperation( OpId)
   end
   -- ripristino stato iniziale
   if CurrMachId then
      EgtSetCurrMachining( CurrMachId)
   else
      EgtResetCurrMachining()
   end
   if CurrTool then
      EgtTdbSetCurrTool( CurrTool)
   else
      EgtTdbSetCurrTool( '')
   end
   -- restituisco risultato
   return sTool, nTlen
end

---------------------------------------------------------------------
-- *** END GENERAL ***
---------------------------------------------------------------------
