-- ArchiFrame BVX cnc-writer -- See www.lua.org for Lua manual. -- Extension functions - see manual gScriptUtf8=1 -- We are fully utf --------------------------------------------------------------------------- -- CODE SHARED WITH ARCHIFRAME START --> EPS=0.0001 PI=3.141592653589793 PI2=PI/2.0 PI180=PI/180.0 PI1800=PI/1800.0 -- # Frame machinings types EMcFrAngledBeg = 101 EMcFrBegHiddenShoe = 111 EMcFrAngledBegTenonMort = 112 EMcJointBeg = 113 EMcVCutBeg = 114 EMcFrAngledEnd = 201 EMcFrEndHiddenShoe = 211 EMcFrAngledEndTenonMort = 212 EMcJointEnd = 213 EMcVCutEnd = 214 EMcFrOpening = 300 -- Opening - unused now EMcFrGroove = 301 -- Logsin tapainen vapaa ura EMcFrGrooveTypeDrill = 8 -- Drilling type groove EMcFrDrill = 302 -- Drilling EMcFrMarking = 303 -- Marking EMcFrReinforce = 304 EMcFrSaw = 305 EMcNailGroup = 306 EMcNailLine = 307 EMcFrMortise = 400 -- Also dovetail EMcFrBalkJoint = 401 -- Narrowed balk EMcFrBalkShoe = 1000 EMatProfileXzBeg = "iMatProfileXzBeg" EMatProfileXzMid = "iMatProfileXzMid" EMatProfileXzEnd = "iMatProfileXzEnd" ETypePlank = 1 ETypeBoard = 4 EAFSideNone = 0 -- Used in settings EAFSideTop = 1 EAFSideFront = 2 EAFSideBottom = 3 EAFSideBack = 4 EAFSideBegin = 5 EAFSideEnd = 6 EAFTextXAlignLeft = 1 EAFTextXAlignCenter = 2 EAFTextXAlignRight = 3 EAFTextYAlignBottom = 1 EAFTextYAlignCenter = 2 EAFTextYAlignTop = 3 ECambiumSideTop = 1 -- Same as EAFSideTop but in Cambium ECambiumSideFront = 4 -- Same as EAFSideFront but in Cambium ECambiumSideBottom = 3 -- Same as EAFSideBottom but in Cambium ECambiumSideBack = 2 -- Same as ECambiumSideBack but in Cambium ECambiumSideBegin = 5 -- Same as EAFSideBegin but in Cambium ECambiumSideEnd = 6 -- Same as EAFSideEnd but in Cambium EAlignLeft = "Left" EAlignCenter = "Center" EAlignRight = "Right" EPolygonTypePoint = "point" EPolygonTypeLine = "line" EBvxFeedDirBeg = 1 EBvxFeedDirEnd = 2 -- # Info field definitions (11/2017), same code shared in bvn&btl&btx EInfoNone = 1 -- No text to this field EInfoFullIDPlusUsage= 2 -- Full ID (usage) EInfoShortID = 3 -- For example EW01-07 will give only 07 excluding wall etc id EInfoFullID = 4 -- For example EW01-07 will give full EW01-07 EInfoElementID = 5 -- The owning element's ID part (EW01) EInfoUsage = 6 -- Plank's usage EInfoMatIdFull = 7 -- Full material ID EInfoMatIdShort = 8 -- Material ID everything removed from last space bar EInfoGrade = 9 -- Find grade C24/C18/IMP from material type (any text after plank IDs last space bar) EInfoMatIdIfNotSize = 10 -- Add material ID if different from width x size EInfoPlankRole = 11 -- Role in element EInfoPlankRoleFin = 12 -- Role in element Finnish EInfoPlankRoleNor = 13 -- Role in element Norsk EInfoPlankRoleShort = 14 -- Stud=ST EInfoPlankRoleShortFin= 15 -- Tolppa=TO EInfoPlankRoleShortNor=16 -- Norsk short EInfoPackageOrElemID=17 -- iPackage or if it is empty, EInfoElementID --------------------------------------------------------------------------- -- ARCHIFRAME & ARCHILOGS COMMON FUNCTIONS function toLog(x) ac_environment("tolog", tostring(x)) end -- Dumps given variable into string function DumpTblInt(o) local s if type(o) == 'table' then s = '{ ' for k,v in pairs(o) do if type(k) ~= 'number' then k = '"'..k..'"' end s = s .. '['..k..'] = ' .. DumpTblInt(v) .. ',' end s=s .. '} ' else s=tostring(o) end return s end function DumpTbl(o) ac_environment("tolog", DumpTblInt(o)) end -- Finds key=val from string having \n as separator -- Returns: nil=not found, str=value for requested key function FindKeyVal(strData, keyFind) local i1,i2,s2,firstmsg firstmsg=true i1=1 while i1= nExp then nVal = nVal - nExp sBin = sBin .. "1" else sBin = sBin .. "0" end nExp = nExp / 2 until nExp < 0.9 if string.len(sBin) ~= 32 then error("NumToBin failed") end return sBin end function BitTest(nVal, nBit0based) local s, s2 s = NumToBin(nVal) s2 = string.sub(s, 32 - nBit0based, 32 - nBit0based) if s2 == "0" then return 0 end if s2 == "1" then return 1 end error(string.format("BitTest failed (s=%s, s2=%s)", s, s2)) end -- Table copy values function deepcopy(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[deepcopy(orig_key)] = deepcopy(orig_value) end setmetatable(copy, deepcopy(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end -- Returns 32bit binary number string "010011100..." function NumToBin(nVal) local nExp, sBin nExp=2147483648 sBin="" repeat if nVal+0.1>=nExp then nVal=nVal-nExp sBin=sBin .. "1" else sBin=sBin .. "0" end nExp=nExp/2 until nExp<0.9 if string.len(sBin)~=32 then error( "NumToBin failed" ) end return sBin end -- User entered values may be small if millimeters were used function NormalizeType(nNum) if math.abs(nNum)<0.1 then nNum=Round(nNum*1000) end return nNum end -- Returns element ID, plank ID. For example for EW01-07 will return "EW01","07"" function SplitId(sId, tblPlankInfo) local s = "-" -- This is the master plank not showing the short ID probably: ac_objectget("iShowIDSep") if tblPlankInfo.type == ETypeBoard then s = "#" -- board end local sElemId = sId -- Find element ID --pos=string.find(id, s, 1, true) -- No reverse find in Lua local pos = string.len(sId) while pos >= 1 do if string.sub(sId, pos, pos) == s then break end pos = pos - 1 end sElemId = "" if pos > 1 then sElemId = string.sub(sId, 1, pos - 1) end sId = string.sub(sId, pos + 1) return sElemId, sId end -- Splits material ID to two parts from last space. If no space, last part will be "" function SplitMatId() local s = " " -- Separator local id = ac_objectget("iMatId") local pos = string.len(id) while pos >= 1 do if string.sub(id, pos, pos) == s then break end pos = pos - 1 end local grade = "" if pos > 1 then grade = string.sub(id, pos + 1) id = string.sub(id, 1, pos - 1) end return id, grade end -- COMMON FUNCTION IN A FEW PLACES IN DATA-FOLDER, UPDATE ALL IF UPDATING ONE -- isHor Is it horizontal structure (false/nil=default) -- lang eng/swe/fin -- elemgroup Parameter iElemGroup function GetElemGroupName(isHor, elemgroup) local lang lang=af_request("aflang") if isHor then -- Horizontal structure if elemgroup==nil then elemgroup="" elseif string.match(elemgroup, "^top%a*") or string.match(elemgroup, "^2ndtop%a*") or string.match(elemgroup, "^contour_x.*") then elemgroup="Stud" if lang=="fin" then elemgroup="Tolppa" elseif lang=="swe" then elemgroup="Regel" end elseif string.match(elemgroup, "^bottom%a*") or string.match(elemgroup, "^2ndbottom%a*") then elemgroup="Stud" if lang=="fin" then elemgroup="Tolppa" elseif lang=="swe" then elemgroup="Regel" end elseif string.match(elemgroup, "^vertical_x%a*") or string.match(elemgroup, "^vertical_y%a*") or string.match(elemgroup, "^contourtilted_opening%a*") then elemgroup="Door/win" if lang=="fin" then elemgroup="Ikk/ovi" elseif lang=="swe" then elemgroup="Avväxling" end elseif string.match(elemgroup, "^vertical%a*") then elemgroup="Hor" if lang=="fin" then elemgroup="Vaaka" end elseif string.match(elemgroup, "^contourtilted%a*") then elemgroup="Angled" if lang=="fin" then elemgroup="Vinojuoksu" elseif lang=="swe" then elemgroup="Regel" end elseif string.match(elemgroup, "^balk.*") then elemgroup="Beam" if lang=="fin" then elemgroup="Palkki" elseif lang=="swe" then elemgroup="Bärlina" end elseif string.match(elemgroup, "^lintel.*") then elemgroup="W/D beam" if lang=="fin" then elemgroup="Aukkopalkki" elseif lang=="swe" then elemgroup="Bärplanka" end elseif string.match(elemgroup, "^reinforce.*") then elemgroup="Reinforcement" if lang=="fin" then elemgroup="Vahvike" elseif lang=="swe" then elemgroup="Förstärkning" end elseif string.match(elemgroup, "^nogging.*") then elemgroup="Nogging" if lang=="nor" then elemgroup="Kubbing" elseif lang=="fin" then elemgroup="Nurjahdustuki" elseif lang=="swe" then elemgroup="Kortling" end elseif elemgroup=="sideleft_spacing" or elemgroup=="sideright_spacing" then elemgroup="Side beams" if lang=="fin" then elemgroup="Sivupalkki" elseif lang=="swe" then elemgroup="Ändkortling" end else elemgroup="" -- Just leave it empty, earlier was "? (elemgroup)" end else -- Vertical structure if elemgroup==nil then elemgroup="" elseif string.match(elemgroup, "^top%a*") or string.match(elemgroup, "^2ndtop%a*") then elemgroup="Top plate" if lang=="fin" then elemgroup="Yläjuoksu" elseif lang=="swe" then elemgroup="HB" end elseif string.match(elemgroup, "^bottom%a*") or string.match(elemgroup, "^2ndbottom%a*") then elemgroup="Bottom plate" if lang=="fin" then elemgroup="Alajuoksu" elseif lang=="swe" then elemgroup="Syll" end elseif string.match(elemgroup, "^contour_x.*") then elemgroup="Top/bottom plate" if lang=="fin" then elemgroup="Ylä/alajuoksu" elseif lang=="swe" then elemgroup="HB/Syll" end elseif string.match(elemgroup, "^vertical_x%a*") or string.match(elemgroup, "^contourtilted_opening%a*") then elemgroup="Win/door" if lang=="fin" then elemgroup="Ikk/ovi" elseif lang=="swe" then elemgroup="Avväxling" end elseif string.match(elemgroup, "^vertical%a*") or string.match(elemgroup, "^contour_y.*") then elemgroup="Stud" if lang=="fin" then elemgroup="Tolppa" elseif lang=="swe" then elemgroup="Regel" end elseif string.match(elemgroup, "^contourtilted%a*") then elemgroup="Angled" if lang=="fin" then elemgroup="Vinojuoksu" elseif lang=="swe" then elemgroup="Regel" end elseif string.match(elemgroup, "^balktop.*") then elemgroup="Beam top" if lang=="fin" then elemgroup="Palkki ylä" elseif lang=="swe" then elemgroup="Bärlina topp" end elseif string.match(elemgroup, "^balkbot.*") then elemgroup="Beam bottom" if lang=="fin" then elemgroup="Palkki ala" elseif lang=="swe" then elemgroup="Bärlina botten" end elseif string.match(elemgroup, "^lintel.*") then elemgroup="W/D beam" if lang=="fin" then elemgroup="Aukkopalkki" elseif lang=="swe" then elemgroup="Bärplanka" end elseif string.match(elemgroup, "^nogging.*") then elemgroup="Nogging" if lang=="nor" then elemgroup="Kubbing" elseif lang=="fin" then elemgroup="Nurjahdustuki" elseif lang=="swe" then elemgroup="Kortling" end elseif elemgroup=="sideleft_spacing" or elemgroup=="sideright_spacing" then elemgroup="Side beams" if lang=="fin" then elemgroup="Sivupalkki" elseif lang=="swe" then elemgroup="Ändkortling" end else elemgroup="" -- Just leave it empty, earlier was "? (elemgroup)" end end return elemgroup end function GetOppositeSide(isAFSide, nSide) if isAFSide then if nSide == EAFSideTop then return EAFSideBottom elseif nSide == EAFSideFront then return EAFSideBack elseif nSide == EAFSideBottom then return EAFSideTop elseif nSide == EAFSideBack then return EAFSideFront elseif nSide == EAFSideBegin then return EAFSideEnd elseif nSide == EAFSideEnd then return EAFSideBegin end else if nSide == ECambiumSideTop then return ECambiumSideBottom elseif nSide == ECambiumSideFront then return ECambiumSideBack elseif nSide == ECambiumSideBottom then return ECambiumSideTop elseif nSide == ECambiumSideBack then return ECambiumSideFront elseif nSide == ECambiumSideBegin then return ECambiumSideEnd elseif nSide == ECambiumSideEnd then return ECambiumSideBegin end end end -- nInfo One of EInfoXxxx function GetInfoStr(nInfo, tblPlankInfo) local sInfo = "" if nInfo == EInfoFullIDPlusUsage then sInfo = ac_objectget("iUsageId") if not sInfo then sInfo = "" end if sInfo ~= "" then sInfo = string.format("%s (%s)", gsCurrentLogId, sInfo) else sInfo = gsCurrentLogId end elseif nInfo == EInfoShortID then local _ _, sInfo = SplitId(gsCurrentLogId, tblPlankInfo) elseif nInfo == EInfoFullID then sInfo = gsCurrentLogId elseif nInfo == EInfoElementID then local _ sInfo, _ = SplitId(gsCurrentLogId, tblPlankInfo) elseif nInfo == EInfoPackageOrElemID then sInfo = ac_objectget("iCncPackage") if sInfo == nil or sInfo == "" then local _ sInfo, _ = SplitId(gsCurrentLogId, tblPlankInfo) end elseif nInfo == EInfoGrade then local _ _, sInfo = SplitMatId() elseif nInfo == EInfoUsage then sInfo = ac_objectget("iUsageId") elseif nInfo == EInfoMatIdFull then sInfo = ac_objectget("iMatId") elseif nInfo == EInfoMatIdShort then local _ sInfo, _ = SplitMatId() elseif nInfo == EInfoMatIdIfNotSize then sInfo = ac_objectget("iMatId") if sInfo == string.format("%.0fx%.0f", gnCurrWidth * 1000, gnCurrHeight * 1000) then sInfo = "" end elseif nInfo == EInfoPlankRole then sInfo = ac_objectget("iElemGroup") sInfo = GetElemGroupName(false, sInfo) elseif nInfo == EInfoPlankRoleShort then sInfo = ac_objectget("iElemGroup") if sInfo == nil then sInfo = "" elseif string.match(sInfo, "^top%a*") or string.match(sInfo, "^2ndtop%a*") then sInfo = "TP" elseif string.match(sInfo, "^bottom%a*") or string.match(sInfo, "^2ndbottom%a*") then sInfo = "BP" elseif string.match(sInfo, "^vertical_x%a*") or string.match(sInfo, "^contourtilted_opening%a*") then sInfo = "HO" elseif string.match(sInfo, "^vertical%a*") then sInfo = "ST" elseif string.match(sInfo, "^contourtilted%a*") then sInfo = "AN" elseif string.match(sInfo, "^balk%a*") then sInfo = "BE" elseif string.match(sInfo, "^lintel.*") then sInfo = "LI" else sInfo = "" end elseif nInfo == EInfoPlankRoleFin then sInfo = ac_objectget("iElemGroup") if sInfo == nil then sInfo = "" elseif string.match(sInfo, "^top%a*") or string.match(sInfo, "^2ndtop%a*") then sInfo = "Yläjuoksu" elseif string.match(sInfo, "^bottom%a*") or string.match(sInfo, "^2ndbottom%a*") then sInfo = "Alajuoksu" elseif string.match(sInfo, "^vertical_x%a*") or string.match(sInfo, "^contourtilted_opening%a*") then sInfo = "Ikk/ovi" elseif string.match(sInfo, "^vertical%a*") then sInfo = "Tolppa" elseif string.match(sInfo, "^contourtilted%a*") then sInfo = "Vinojuoksu" elseif string.match(sInfo, "^balk%a*") then sInfo = "Palkki" elseif string.match(sInfo, "^lintel.*") then sInfo = "Aukkopalkki" end elseif nInfo == EInfoPlankRoleShortFin then sInfo = ac_objectget("iElemGroup") if sInfo == nil then sInfo = "" elseif string.match(sInfo, "^top%a*") or string.match(sInfo, "^2ndtop%a*") then sInfo = "YJ" elseif string.match(sInfo, "^bottom%a*") or string.match(sInfo, "^2ndbottom%a*") then sInfo = "AJ" elseif string.match(sInfo, "^vertical_x%a*") or string.match(sInfo, "^contourtilted_opening%a*") then sInfo = "VA" elseif string.match(sInfo, "^vertical%a*") then sInfo = "T" elseif string.match(sInfo, "^contourtilted%a*") then sInfo = "VI" elseif string.match(sInfo, "^balk%a*") then sInfo = "PA" elseif string.match(sInfo, "^lintel.*") then sInfo = "AU" else sInfo = "" end elseif nInfo == EInfoPlankRoleNor then sInfo = ac_objectget("iElemGroup") if sInfo == nil then sInfo = "" elseif string.match(sInfo, "^top%a*") or string.match(sInfo, "^2ndtop%a*") then sInfo = "Toppsvill" elseif string.match(sInfo, "^bottom%a*") or string.match(sInfo, "^2ndbottom%a*") then sInfo = "Bunnsvill" elseif string.match(sInfo, "^vertical_x%a*") or string.match(sInfo, "^contourtilted_opening%a*") then sInfo = "Vindu/dør" elseif string.match(sInfo, "^vertical%a*") then sInfo = "Stender" elseif string.match(sInfo, "^contourtilted%a*") then sInfo = "Vinkel" elseif string.match(sInfo, "^balk%a*") then sInfo = "Bjelke" elseif string.match(sInfo, "^lintel.*") then sInfo = "Overdekning" end elseif nInfo == EInfoPlankRoleShortNor then sInfo = ac_objectget("iElemGroup") if sInfo == nil then sInfo = "" elseif string.match(sInfo, "^top%a*") or string.match(sInfo, "^2ndtop%a*") then sInfo = "TS" elseif string.match(sInfo, "^bottom%a*") or string.match(sInfo, "^2ndbottom%a*") then sInfo = "BS" elseif string.match(sInfo, "^vertical_x%a*") or string.match(sInfo, "^contourtilted_opening%a*") then sInfo = "LH" elseif string.match(sInfo, "^vertical%a*") then sInfo = "ST" elseif string.match(sInfo, "^contourtilted%a*") then sInfo = "LH" elseif string.match(sInfo, "^balk%a*") then sInfo = "BJ" elseif string.match(sInfo, "^lintel.*") then sInfo = "OD" else sInfo = "" end end if sInfo == nil then sInfo = "" end return sInfo end ----------------------------------------------------------------------------- -- Edge Mcs shared in a few scripts -- in: -- mEdgeIndex Current index to iMcEdge -- out, nil=edge not found, table having fields: -- x1,y1,x2,y2 Edge line right pointing outside the board -- dx,dy,len Line vector and length -- angle Angle of the line 0...360 function FrMcEdgeFind(mEdgeIndex) -- Old param block? Ninth added 2021 local nEdgeCount = ac_objectget("iPolygon", 0, -1) if not nEdgeCount or nEdgeCount < 9 then return end local nContourBeg = 1 local nRow = 1 local nEdgeId = ac_objectget("iMcEdge", mEdgeIndex, 2) if not nEdgeId then return end while true do local nPolygonId = ac_objectget("iPolygon", nRow, 9) if not nPolygonId then break end if nPolygonId == nEdgeId then -- Edge found, from this pt to next nPolygonId = nRow + 1 if ac_objectget("iPolygon", nRow, 3) == -1 then nPolygonId = nContourBeg end local result = {} result.nX1Meters = ac_objectget("iPolygon", nRow, 1) result.nY1Meters = ac_objectget("iPolygon", nRow, 2) result.nX2Meters = ac_objectget("iPolygon", nPolygonId, 1) result.nY2Meters = ac_objectget("iPolygon", nPolygonId, 2) local dx = result.nX2Meters - result.nX1Meters local dy = result.nY2Meters - result.nY1Meters -- Lets make the x-axis point downward instead of rightward (ie. 0 degrees will be downwards) result.nAngleRad = (math.atan2(dy, dx) + Deg2Rad(90)) % Deg2Rad(360) return result end if ac_objectget("iPolygon", nRow, 3) == -1 then nContourBeg = nRow + 1 -- End of contour, end pt not duplicated (and no mask code from this pt to next) end nRow = nRow + 1 end end -- Needs to be called after BvxGetPlankBasicParams function has been called function RotateElementIfNeeded(isBoard) if isBoard then -- If this is changed to EAFSideFront, boards will not have right cad positions. -- TODO: make it work both ways? gnCurrentFeedTableSide = EAFSideBack elseif gnCurrentFeedTableSide == EAFSideNone then -- Lets find the orientation with the widest side facing the table if gnCurrWidth < gnCurrHeight then gnCurrentFeedTableSide = EAFSideBack else gnCurrentFeedTableSide = EAFSideBottom end end end function CheckGroWarn(nX1Millis, nY1Millis, nX2Millis, nY2Millis, nGroWidthMillis, nXMaxMillis, nYMaxMillis) if gnBvxGrooveWidthWarnThresholdMillis < EPS or gnBvxGrooveWidthWarnThresholdMillis < (nGroWidthMillis + EPS) then return end if math.abs(nX2Millis - nX1Millis) < 1 then local isOverXBorder = (((nX1Millis - nGroWidthMillis - 1) < 0 or nXMaxMillis < (nX1Millis + 1))) if isOverXBorder then return end end if math.abs(nY2Millis - nY1Millis) < 1 then local isOverYBorder = ((nY1Millis - nGroWidthMillis - 0.001) < 0) or (nYMaxMillis < (nY1Millis + 0.001)) if isOverYBorder then return end end AddCncErr( Millis2Meters(nX1Millis), string.format( "WARNING: Groove width is %s - please check", ac_environment("ntos", Millis2Meters(nGroWidthMillis), "length", "work") ) ) end -- Copy pasted from BTL/BVN-cnc writer -- ### Suoran ja tason leikkauspiste -- in: -- ta,tb,tc,td Tason yhtälö, a-c=kohtisuora ja d on vakio -- x1, y1, z1 Suora -- x2, y2, z2 Suora -- out: -- hasX Oliko leikkausta -- x, y, z Piste -- dist Etäisyys pisteestä x1,y1,z1 suoran suuntaisesti (suhteessa suoran pituuteen 0...1!) function LinePlaneX(ta, tb, tc, td, x1, y1, z1, x2, y2, z2 ) -- Tuollainen koodi "käännetty" wykobi-kirjastosta -- template -- inline point3d intersection_point(const line& line, -- const plane& plane) -- { -- vector3d linevec = line[1] - line[0]; -- T denom = dot_product(linevec,plane.normal); -- point3d ipoint = degenerate_point3d(); -- if (not_equal(denom,T(0.0))) -- { -- T t = -distance(line[0],plane) / denom; -- ipoint = line[0] + t * (line[1] - line[0]); -- } -- return ipoint; -- } local x, y, z, dist, denom dist=0 x=x2-x1 y=y2-y1 z=z2-z1 denom=x*ta + y*tb + z*tc -- Pistetulo suora ja tason normaali if math.abs(denom)<1E-5 then -- Tason suuntainen suora return false end -- Pisteen etäisyys tasolta -- (plane.normal.x * point.x + plane.normal.y * point.y + plane.normal.z * point.z ) - plane.constant; dist=-(ta*x1 + tb*y1 + tc*z1 + td)/denom -- Huom-- td eri etumerkillä kuin wykobissa (kun siellä oli puki) x=x1 + x*dist y=y1 + y*dist z=z1 + z*dist return true, x, y, z, dist -- To return correct dist: return true, x, y, z, dist*math.abs(denom) end function DirToAngles(dirUpX, dirUpY, dirUpZ) local rotZ=0.0 if math.abs(dirUpX)>EPS or math.abs(dirUpY)>EPS then rotZ=-math.atan2(-dirUpY, -dirUpX) end local rotY=math.asin(dirUpZ)-PI/2 return Rad2Deg(rotZ), Rad2Deg(rotY) end -- Elems (planks, boards) with different guid can can have all the same machinings, except they are located in different CAD positions function AreSimilarElems(sPlank1Guid, sPlank2Guid) if (sPlank1Guid == nil) or (sPlank2Guid == nil) then return false elseif ac_getobjparam(sPlank1Guid, "#id") ~= ac_getobjparam(sPlank2Guid, "#id") then return false elseif ac_getobjparam(sPlank1Guid, "#libguid") ~= ac_getobjparam(sPlank2Guid, "#libguid") then return false else local iCncPackage1 = ac_getobjparam(sPlank1Guid, "iCncPackage") local iCncPackage2 = ac_getobjparam(sPlank2Guid, "iCncPackage") if iCncPackage1 and iCncPackage2 and (iCncPackage1 ~= iCncPackage2) then return false end end return true end function AreCoordsInSamePlace(nX1Millis, nY1Millis, nX2Millis, nY2Millis) if nX1Millis == nil or nY1Millis == nil or nX2Millis == nil or nY2Millis == nil then return false end -- if points are within 0.5 millis of each other, lets consider them to be ne near enough local THRESHOLD = 0.5 return (math.abs(nX1Millis - nX2Millis) < THRESHOLD) and (math.abs(nY1Millis - nY2Millis) < THRESHOLD) end -- Returns the xml tag with correct indentation and line change at the end function FormatXmlTag(sTag, nLevel) nLevel = nLevel or 0 if nLevel > 0 then local nSpacesPerIndentation = 2 local nSpaces = nSpacesPerIndentation * nLevel return string.format("%" .. nSpaces .. "s%s\n", " ", sTag) else return string.format("%s\n", sTag) end end function BoolToNumber(bool) return TernaryIf(bool, 1, 0) end function IsTopFrontBottomOrBackSide(nSide) return nSide < 5 end function IsTopOrBottomSide(nSide) return (nSide == EAFSideTop) or (nSide == EAFSideBottom) end -- Degrees to radians function Deg2Rad(nDeg) return nDeg * PI / 180.0 end -- Radians to degress function Rad2Deg(nRad) return nRad * 180.0 / PI end -- Lua doesn't have math.round, so lets use math.floor function Round(num) return math.floor(num + 0.5) end function Meters2Millis(nMeters) return nMeters * 1000 end function Millis2Meters(nMillis) return nMillis / 1000 end function IsRoughlyEqual(num1, num2) return math.abs(num1 - num2) <= EPS end function CalcDist(x1, y1, x2, y2) local xDiff = x2 - x1 local yDiff = y2 - y1 return math.sqrt(xDiff * xDiff + yDiff * yDiff) end function ToggleLeftRightAlign(sAlign) if sAlign == EAlignRight then return EAlignLeft elseif sAlign == EAlignLeft then return EAlignRight else return EAlignCenter end end function TranslateAlign(sAlign) if gnCurrentFeedDir == EBvxFeedDirEnd then return ToggleLeftRightAlign(sAlign) end -- No need to do any translation when normal feed direction return sAlign end function CreateTblRotations(x, y, z) return { x=x, y=y, z=z } end function CreateTblCoords(x, y) return { x=x, y=y } end OperationGroove = {} function OperationGroove:new(tblArgs) local nObjectRow = tblArgs.nObjectRow local nX1Millis = tblArgs.nX1Millis local nY1Millis = tblArgs.nY1Millis local nX2Millis = tblArgs.nX2Millis local nY2Millis = tblArgs.nY2Millis local nGroDepthMillis = tblArgs.nGroDepthMillis local nGroWidthMillis = tblArgs.nGroWidthMillis local nGroAngleDeg = tblArgs.nGroAngleDeg local nSide = tblArgs.nSide local nGroAngleLenDeg = tblArgs.nGroAngleLenDeg local nZMillis = tblArgs.nZMillis local nGroType = tblArgs.nGroType local tblGroove = {} if nObjectRow ~= nil then tblGroove.nX1Millis = Meters2Millis(ac_objectget("iMc", nObjectRow, 2)) tblGroove.nY1Millis = Meters2Millis(ac_objectget("iMc", nObjectRow, 3)) tblGroove.nX2Millis = Meters2Millis(ac_objectget("iMc", nObjectRow, 4)) tblGroove.nY2Millis = Meters2Millis(ac_objectget("iMc", nObjectRow, 5)) tblGroove.nGroDepthMillis = Meters2Millis(ac_objectget("iMc", nObjectRow, 6)) tblGroove.nGroWidthMillis = Meters2Millis(ac_objectget("iMc", nObjectRow, 7)) tblGroove.nGroAngleDeg = ac_objectget("iMc", nObjectRow, 8) tblGroove.nSide = ac_objectget("iMc", nObjectRow, 9) tblGroove.nGroAngleLenDeg = ac_objectget("iMc", nObjectRow, 10) tblGroove.nGroType=ac_objectget("iMc", nObjectRow, 11) tblGroove.nZMillis = 0 -- Petteri added - we need to know if full groove later on local tblGro={} tblGro.guid = gsCurrentLogGuid tblGro.x1 = ac_objectget("iMc", nObjectRow, 2) tblGro.y1 = ac_objectget("iMc", nObjectRow, 3) tblGro.x2 = ac_objectget("iMc", nObjectRow, 4) tblGro.y2 = ac_objectget("iMc", nObjectRow, 5) tblGro.depth = ac_objectget("iMc", nObjectRow, 6) tblGro.width = ac_objectget("iMc", nObjectRow, 7) tblGro.groAngleRad = Deg2Rad(ac_objectget("iMc", nObjectRow, 8)) tblGro.face = ac_objectget("iMc", nObjectRow, 9) tblGro.groTiltAngleRad = Deg2Rad(ac_objectget("iMc", nObjectRow, 10)) tblGroove.tblGroInfo = af_request("mc_getgroinfo", tblGro) else tblGroove.nX1Millis = nX1Millis tblGroove.nY1Millis = nY1Millis tblGroove.nX2Millis = nX2Millis tblGroove.nY2Millis = nY2Millis tblGroove.nGroDepthMillis = nGroDepthMillis tblGroove.nGroWidthMillis = nGroWidthMillis tblGroove.nGroAngleDeg = nGroAngleDeg tblGroove.nSide = nSide tblGroove.nGroAngleLenDeg = nGroAngleLenDeg tblGroove.nZMillis = nZMillis tblGroove.nGroType = nGroType end setmetatable(tblGroove, self) self.__index = self return tblGroove end -- Translate coordinates from ArchiFrame coordinate system to cambium. -- Also translate rotations so they work with different orientations. -- tblElem is either tblPart or tblBoard -- tblCoordsMillis and tblRotationsDeg are optional. -- Returns nonsense placeholder values if they are not given, which should be ignored. -- tblPart and nAFSide are required. -- Values returned depend on the orientation of the machining (ie. gnCurrentFeedDir and gnCurrentFeedTableSide). -- Z-coordinate doesn't need translation since it's always the same in Cambium and AF function TranslateCoordsAndRotations(tblElem, nAFSide, tblCoordsMillis, tblRotationsDeg) local tblTranslatedCoords, nOriginalX, nOriginalY local tblDimension = tblElem.tblDimension if tblCoordsMillis then nOriginalX = tblCoordsMillis.x nOriginalY = tblCoordsMillis.y tblTranslatedCoords = CreateTblCoords(nOriginalX, nOriginalY) else -- We don't need to translate coordinates if tblCoordsMillis is not given. -- 0 is just a placeholder so we don't get errors we would get otherwise if counting with nils. -- Otherwise we would need to check for nil values in every place. nOriginalX = 0 nOriginalY = 0 tblTranslatedCoords = CreateTblCoords(0, 0) end local tblTranslatedRotations, nOriginalRotationX, nOriginalRotationY, nOriginalRotationZ if tblRotationsDeg then nOriginalRotationX = tblRotationsDeg.x nOriginalRotationY = tblRotationsDeg.y nOriginalRotationZ = tblRotationsDeg.z tblTranslatedRotations = CreateTblRotations(nOriginalRotationX, nOriginalRotationY, nOriginalRotationZ) else -- We don't need to translate rotations if tblRotationsDeg is not given. -- 0 is just a placeholder so we don't get errors we would get otherwise if counting with nils. -- Otherwise we would need to check for nil values in every place. nOriginalRotationX = 0 nOriginalRotationY = 0 nOriginalRotationZ = 0 tblTranslatedRotations = CreateTblRotations(0, 0, 0) end local nCambiumFrameId = nAFSide if tblElem.isBoard and gbBvxRotateBoard90 then if nAFSide == EAFSideBack then tblTranslatedCoords.x, tblTranslatedCoords.y = nOriginalY, tblDimension.y - nOriginalX tblTranslatedRotations.z = nOriginalRotationZ - 90 nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideFront then tblTranslatedCoords.x, tblTranslatedCoords.y = nOriginalY, nOriginalX tblTranslatedRotations.z = nOriginalRotationZ + 90 nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideBottom then tblTranslatedCoords.x, tblTranslatedCoords.y = tblDimension.y - nOriginalX, tblElem.nThickness-nOriginalY tblTranslatedRotations.z = nOriginalRotationZ + 180 nCambiumFrameId = ECambiumSideBegin elseif nAFSide == EAFSideTop then tblTranslatedCoords.x, tblTranslatedCoords.y = tblDimension.y - nOriginalX, nOriginalY tblTranslatedRotations.z = nOriginalRotationZ + 180 nCambiumFrameId = ECambiumSideEnd elseif nAFSide == EAFSideBegin then tblTranslatedCoords.x = nOriginalY tblTranslatedCoords.y = nOriginalX tblTranslatedRotations.z = nOriginalRotationZ - 90 nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideEnd then tblTranslatedCoords.x = nOriginalY tblTranslatedCoords.y = tblDimension.z - nOriginalX tblTranslatedRotations.z = nOriginalRotationZ + 90 nCambiumFrameId = ECambiumSideFront end elseif gnCurrentFeedDir == EBvxFeedDirBeg then ------------------------------------- -- Feeding the plank begin side first ------------------------------------- if gnCurrentFeedTableSide == EAFSideTop then -- Feeding the plank so that its top is facing the table, ie. upside down -- This causes the top <==> bottom, and front <==> back to switch sides if nAFSide == EAFSideTop then tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideFront then tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideBottom then nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideBack then nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideBegin then tblTranslatedCoords.y = tblDimension.z - nOriginalY tblTranslatedRotations.z = nOriginalRotationZ - 180 nCambiumFrameId = ECambiumSideBegin elseif nAFSide == EAFSideEnd then tblTranslatedRotations.z = nOriginalRotationZ - 180 nCambiumFrameId = ECambiumSideEnd end elseif gnCurrentFeedTableSide == EAFSideFront then -- Feeding the plank so that its front is facing the table, ie. rotated 90 degrees clockwise -- This causes all the sides to move as the one next to them clockwise -- (ie. top becomes front, front becomes bottom, bottom becomes back, and back becomes top) if nAFSide == EAFSideTop then tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideFront then tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideBottom then nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideBack then nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideBegin then tblTranslatedCoords.x = tblDimension.y - nOriginalY tblTranslatedCoords.y = tblDimension.z - nOriginalX tblTranslatedRotations.z = nOriginalRotationZ + 90 nCambiumFrameId = ECambiumSideBegin elseif nAFSide == EAFSideEnd then tblTranslatedCoords.x = tblDimension.y - nOriginalY tblTranslatedRotations.z = nOriginalRotationZ - 90 tblTranslatedCoords.y = nOriginalX nCambiumFrameId = ECambiumSideEnd end elseif gnCurrentFeedTableSide == EAFSideBottom then -- Feeding the plank so that its bottom is facing the table. -- This is the default scenario so no rotations are needed for the sides, -- lets just convert the AF side ids to match Cambium frame ids if nAFSide == EAFSideTop then tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideFront then tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideBottom then nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideBack then nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideBegin then tblTranslatedCoords.x = tblDimension.y - nOriginalX nCambiumFrameId = ECambiumSideBegin elseif nAFSide == EAFSideEnd then tblTranslatedCoords.x = tblDimension.y - nOriginalX tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideEnd end elseif gnCurrentFeedTableSide == EAFSideBack then -- Feeding the plank so that its back is facing the table, ie. rotated 90 degrees counterclockwise -- This causes all the sides to move as the one next to them counterclockwise -- (ie. top becomes back, back becomes bottom, bottom becomes front, and front becomes top) if nAFSide == EAFSideTop then tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideFront then tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideBottom then nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideBack then nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideBegin then tblTranslatedCoords.x = nOriginalY tblTranslatedCoords.y = nOriginalX tblTranslatedRotations.z = nOriginalRotationZ - 90 nCambiumFrameId = ECambiumSideBegin elseif nAFSide == EAFSideEnd then tblTranslatedCoords.x = nOriginalY tblTranslatedCoords.y = tblDimension.z - nOriginalX tblTranslatedRotations.z = nOriginalRotationZ + 90 nCambiumFrameId = ECambiumSideEnd end else RaiseError("Invalid gnCurrentFeedTableSide value") end else ----------------------------------- -- Feeding the plank end side first ----------------------------------- tblTranslatedRotations.x = -nOriginalRotationX tblTranslatedRotations.y = -nOriginalRotationY if gnCurrentFeedTableSide == EAFSideTop then -- Feeding the plank so that its top is facing the table, ie. upside down -- This causes the top <==> bottom, and front <==> back to switch sides -- However, since we are feeding it end first, front and back switch sides if nAFSide == EAFSideTop then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideFront then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideBottom then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideBack then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideBegin then tblTranslatedCoords.x = tblDimension.y - nOriginalX tblTranslatedRotations.z = nOriginalRotationZ - 180 nCambiumFrameId = ECambiumSideEnd elseif nAFSide == EAFSideEnd then tblTranslatedCoords.x = tblDimension.y - nOriginalX tblTranslatedCoords.y = tblDimension.z - nOriginalY tblTranslatedRotations.z = nOriginalRotationZ - 180 nCambiumFrameId = ECambiumSideBegin end elseif gnCurrentFeedTableSide == EAFSideFront then -- Feeding the plank so that its front is facing the table, ie. rotated 90 degrees clockwise -- This causes all the sides to move as the one next to them clockwise -- (ie. top becomes front, front becomes bottom, bottom becomes back, and back becomes top) -- However, since we are feeding it end first, front and back switch sides if nAFSide == EAFSideTop then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideFront then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideBottom then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideBack then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideBegin then tblTranslatedCoords.x = nOriginalY tblTranslatedCoords.y = nOriginalX tblTranslatedRotations.z = nOriginalRotationZ + 90 nCambiumFrameId = ECambiumSideEnd elseif nAFSide == EAFSideEnd then tblTranslatedCoords.x = nOriginalY tblTranslatedCoords.y = tblDimension.z - nOriginalX tblTranslatedRotations.z = nOriginalRotationZ - 90 nCambiumFrameId = ECambiumSideBegin end elseif gnCurrentFeedTableSide == EAFSideBottom then -- Feeding the plank so that its bottom is facing the table. -- This is the default scenario so no rotations would be needed, however since -- we are feeding the plank end first, front and back switch sides if nAFSide == EAFSideTop then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideFront then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideBottom then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideBack then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideBegin then tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideEnd elseif nAFSide == EAFSideEnd then nCambiumFrameId = ECambiumSideBegin end elseif gnCurrentFeedTableSide == EAFSideBack then -- Feeding the plank so that its back is facing the table, ie. rotated 90 degrees counterclockwise -- This causes all the sides to move as the one next to them counterclockwise -- (ie. top becomes back, back becomes bottom, bottom becomes front, and front becomes top) -- However, since we are feeding it end first, front and back switch sides if nAFSide == EAFSideTop then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideFront elseif nAFSide == EAFSideFront then tblTranslatedCoords.x = tblDimension.x - nOriginalX nCambiumFrameId = ECambiumSideTop elseif nAFSide == EAFSideBottom then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.z - nOriginalY nCambiumFrameId = ECambiumSideBack elseif nAFSide == EAFSideBack then tblTranslatedCoords.x = tblDimension.x - nOriginalX tblTranslatedCoords.y = tblDimension.y - nOriginalY nCambiumFrameId = ECambiumSideBottom elseif nAFSide == EAFSideBegin then tblTranslatedCoords.x = tblDimension.y - nOriginalY tblTranslatedCoords.y = tblDimension.z - nOriginalX tblTranslatedRotations.z = nOriginalRotationZ - 90 nCambiumFrameId = ECambiumSideEnd elseif nAFSide == EAFSideEnd then tblTranslatedCoords.x = tblDimension.y - nOriginalY tblTranslatedCoords.y = nOriginalX tblTranslatedRotations.z = nOriginalRotationZ + 90 nCambiumFrameId = ECambiumSideBegin end else RaiseError("Invalid gnCurrentFeedTableSide value") end end return nCambiumFrameId, tblTranslatedCoords, tblTranslatedRotations end function IsTopOrBottomPlank(sPlankGuid) local sElemGroup = ac_getobjparam(sPlankGuid, "iElemGroup") return string.match(sElemGroup, "^bottom_.*") or string.match(sElemGroup, "^2ndbottom_.*") or string.match(sElemGroup, "^top_.*") or string.match(sElemGroup, "^2ndtop_.*") end -- Simulate the ternary operator that can be found in many other programming languages -- eg. isTrue ? "Value was true" : "Value was false" function TernaryIf(condition, value1, value2) if condition then return value1 else return value2 end end -- Removes spaces from beg and end function TrimStr(str) local res, start, endIndex, len len=string.len(str) start=1 while start<=len and string.sub(str,start,start)==" " do start=start+1 end endIndex=len while endIndex>0 and string.sub(str,endIndex,endIndex)==" " do endIndex=endIndex-1 end return string.sub(str,start, endIndex) end -- Aiheuttaa virheilmoituksen, jonka Logs osaa tulkita käyttäjälle näytettäväksi viestiksi -- Ilmoitus voi olla tyhjä, jolloin ei näytetä mitään (peruttu) function RaiseError(msg) msg="###>" .. msg .. "<###" error( msg ) end gnCncErrCount=0 -- Notices for the log -- Adds cnc error message to gTblCncErr function AddErrMsg(sLogId, nPos, sText) if nPos==nil then error("AddCncErr/nPos") end if sText==nil then error("AddCncErr/sText") end if gnCncErrCount==0 then gTblCncErr={} end gnCncErrCount=gnCncErrCount+1 gTblCncErr[gnCncErrCount] = {} gTblCncErr[gnCncErrCount].guid=gsCurrentLogGuid gTblCncErr[gnCncErrCount].logid=sLogId gTblCncErr[gnCncErrCount].pos=nPos gTblCncErr[gnCncErrCount].text=sText end -- If guid is nil, the board is already opened with ac_objectopen(). Otherwise the part is opened here -- Returns: true=not a board or board going to cnc, false=was a board having iCnc==0 function BvxExportPart(guid) local info, cnc, res if guid then ac_objectopen(guid) end res = true info = af_request("plankinfo") if info and info.type == 4 then cnc = ac_objectget("iCnc") if cnc and cnc == 0 then res = false end end if guid then ac_objectclose() end return res end function GetSortedBoardGuids() local tblBoardGuids = {} local tblSources = af_request("getsellisting") if not tblSources then return tblBoardGuids end for i, tblSource in ipairs(tblSources) do if tblSource.type == ETypeBoard and BvxExportPart(tblSource.guid) then table.insert(tblBoardGuids, tblSource.guid) end end -- Sort so we have boards with same id next to each other, this allows us to count the correct machining quantities table.sort(tblBoardGuids, function (sBoardGuid1, sBoardGuid2) return ac_getobjparam(sBoardGuid1, "#id") < ac_getobjparam(sBoardGuid2, "#id") end) return tblBoardGuids end -- Adds cnc error message to gTblCncErr function AddCncErr(nPos, sText) AddErrMsg(gsCurrentLogId, nPos, sText) end gnUuidSeedMultiplier = 1 function UuidV4() local seconds = os.time() math.randomseed(seconds * gnUuidSeedMultiplier) gnUuidSeedMultiplier = gnUuidSeedMultiplier + 1 local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' return string.gsub(template, '[xy]', function (c) local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) return string.format('%X', v) end) end function EscapeString(str) str = string.gsub(str, "<", "<") str = string.gsub(str, ">", ">") return str end -- Returns width,height function BvxGetPlankSize() local width,height width=ac_objectget("iWidth") if width==nil or width==0.0 then width=ac_objectget("A") end height=ac_objectget("iHeight") if height==nil or height==0.0 then height=ac_objectget("B") end return width,height end -- Gets frame plank basic parameters: -- gsCurrentLogId Use same global as ArchiLogs to ease showing error msg -- gnCurrTotLen -- gnCurrHeight -- gnCurrWidth -- gnBegExtra function BvxGetPlankBasicParams(sPlankGuid) gsCurrentLogId = ac_objectget("#id") gsCurrentLogGuid = sPlankGuid -- Pituus täytyy laskea local tiltAngle = ac_objectget("iTiltAngle") local cosTiltAngle = math.cos(tiltAngle) if math.abs(cosTiltAngle) < 0.005 then -- Vertical gnCurrTotLen = ac_objectget("zzyzx") else local dx = ac_objectget("iEndX") - ac_objectget("iBegX") local dy = ac_objectget("iEndY") - ac_objectget("iBegY") local dz = math.tan(tiltAngle) * math.sqrt(dx * dx + dy * dy) gnCurrTotLen = math.sqrt(dx * dx + dy * dy + dz * dz) end gnCurrWidth, gnCurrHeight = BvxGetPlankSize() local tblInfo = af_request("plankinfo", gsCurrentLogGuid) gnBegExtra = tblInfo.begoff end -- Gets frame board basic parameters: -- gsCurrentLogId Use same global as ArchiLogs to ease showing error msg -- gnCurrTotLen -- gnCurrHeight -- gnCurrWidth -- gnBegExtra function BvxGetBoardBasicParams(sBoardGuid) gsCurrentLogId = ac_objectget("#id") gsCurrentLogGuid = sBoardGuid local tblPolyInfo = af_request ("getpoly") gnCurrTotLen = math.abs(tblPolyInfo.x2 - tblPolyInfo.x1) gnCurrHeight = math.abs(tblPolyInfo.y2 - tblPolyInfo.y1) gnCurrWidth = ac_objectget("iThickness") gnBegExtra = 0 end -- Called before writing new cnc-file - resets globals function BvxOnInit() gnCncErrCount=0 gnBoardPieceNum=500000 end -- Called before save as dialog is showed -- strPlnFileName -> Full path file name of current pln WITHOUT extension (.pln removed) -- Return values (multiple ret values): -- 1=listing file name WITH extension, ""=Just prompt with file dlg, starting with *=result contains full path name - do not prompt -- 2=file extension to be used in save as dialog function OnInit(strPlnFileName) BvxOnInit() sFileName=strPlnFileName .. ".bvx" sExt="bvx" return sFileName, sExt end function SettingsDlg() local tblSettings = {} tblSettings[1] = {} tblSettings[1].cfgonly = 1 tblSettings[1].type = 2 tblSettings[1].prompt = "Write splinter free codes" tblSettings[1].key = "splinterfree" tblSettings[1].defvalue = BoolToNumber(gbBvxWriteSplinterFree) tblSettings[2] = {} tblSettings[2].cfgonly = 1 tblSettings[2].type = 2 tblSettings[2].prompt = "Write also top and bottom pieces" tblSettings[2].key = "topbot" tblSettings[2].defvalue = BoolToNumber(gbBvxWriteTopBottom) tblSettings[3] = {} tblSettings[3].cfgonly = 1 tblSettings[3].type = 2 tblSettings[3].prompt = "Save into separate files based on element IDs" tblSettings[3].key = "sepfiles" tblSettings[3].defvalue = BoolToNumber(gbBvxWriteSepFiles) tblSettings[4] = {} tblSettings[4].cfgonly = 1 tblSettings[4].type = 2 tblSettings[4].prompt = "For element-related pieces save just core-layer" tblSettings[4].key = "onlycore" tblSettings[4].defvalue = BoolToNumber(gbBvxWriteOnlyCore) -- Info fields local sInfos = '"1:Nothing","2:Full ID plus usage (e.g. EW01-07 (ELEM))","3:Short ID (e.g. 07)",' .. '"4:Full ID (e.g. EW01-07)","5:Element ID (e.g. EW01)","6:Usage (e.g. ELEM)",' .. '"7:Full mat id (e.g. 48x198 C24)","8:Short mat id cut to last space (e.g. 48x198)",' .. '"9:Grade after last space from mat id (e.g. C24)",' .. '"10:Material ID if different from width x size (e.g. SAPCODE)",' .. '"11:Plank role in element (e.g. Stud)","12:Plank role in element Finnish (e.g. Tolppa)",' .. '"13:Plank role in element Norwegian (e.g. Stender)","14:Plank role in element short (e.g. ST)",' .. '"15:Plank role in element short Finnish (e.g. TO)","16:Plank role in element short Norwegian (e.g. ST)",' .. '"17:CNC-Package or if it is empty the Element ID"' tblSettings[5] = {} tblSettings[5].cfgonly = 1 tblSettings[5].type = 1 tblSettings[5].prompt = "Part name content" tblSettings[5].key = "partinfo" tblSettings[5].defvalue = gnBvxInfoName tblSettings[5].valuelist = sInfos tblSettings[6] = {} tblSettings[6].cfgonly = 1 tblSettings[6].type = 1 tblSettings[6].prompt = "Unit field content" tblSettings[6].key = "unitinfo" tblSettings[6].defvalue = gnBvxInfoUnit tblSettings[6].valuelist = sInfos tblSettings[7] = {} tblSettings[7].cfgonly = 1 tblSettings[7].type = 1 tblSettings[7].prompt = "Grade field content" tblSettings[7].key = "gradeinfo" tblSettings[7].defvalue = gnBvxInfoGrade tblSettings[7].valuelist = sInfos tblSettings[8] = {} tblSettings[8].cfgonly = 1 tblSettings[8].type = 1 tblSettings[8].prompt = "Comments field content" tblSettings[8].key = "commentsinfo" tblSettings[8].defvalue = gnBvxInfoComments tblSettings[8].valuelist = sInfos tblSettings[9] = {} tblSettings[9].cfgonly = 1 tblSettings[9].type = 1 tblSettings[9].prompt = "Profile field content" tblSettings[9].key = "profileinfo" tblSettings[9].defvalue = gnBvxInfoProfile tblSettings[9].valuelist = sInfos tblSettings[10] = {} tblSettings[10].cfgonly = 1 tblSettings[10].type = 1 tblSettings[10].prompt = "Material field content" tblSettings[10].key = "materialinfo" tblSettings[10].defvalue = gnBvxInfoMaterial tblSettings[10].valuelist = sInfos tblSettings[11] = {} tblSettings[11].cfgonly = 1 tblSettings[11].type = 2 tblSettings[11].prompt = "Write reinforcements" tblSettings[11].key = "writereinforce" tblSettings[11].defvalue = BoolToNumber(gbBvxWriteReinforce) tblSettings[12] = {} tblSettings[12].cfgonly = 1 tblSettings[12].type = 2 tblSettings[12].prompt = "Write reinforcement marking lines" tblSettings[12].key = "marklines" tblSettings[12].defvalue = BoolToNumber(gbBvxMarkReinforce) tblSettings[13] = {} tblSettings[13].cfgonly = 1 tblSettings[13].type = 1 tblSettings[13].prompt = "Plank's feeding direction" tblSettings[13].key = "feeddir" tblSettings[13].defvalue = gnBvxFeedDir tblSettings[13].valuelist = "\"1:Begin first\",\"2:End first\"" tblSettings[14] = {} tblSettings[14].cfgonly = 1 tblSettings[14].type = 1 tblSettings[14].prompt = "Plank's feeding orientation" tblSettings[14].key = "feedorientation" tblSettings[14].defvalue = gnBvxFeedTableSide + 1 tblSettings[14].valuelist = "\"1:Widest down\",\"2:Top down\",\"3:Front down\",\"4:Bottom down\",\"5:Back down\"" tblSettings[15] = {} tblSettings[15].cfgonly = 0 tblSettings[15].type = 3 tblSettings[15].prompt = "Warn if groove narrower, usually 40 mm, 0=No" tblSettings[15].key = "warngro" tblSettings[15].defvalue = Millis2Meters(gnBvxGrooveWidthWarnThresholdMillis) tblSettings[16] = {} tblSettings[16].cfgonly = 1 tblSettings[16].type = 1 tblSettings[16].prompt = "Print part ID content to side" tblSettings[16].key = "idside" tblSettings[16].defvalue = gnBvxPrintPartIdSide + 1 tblSettings[16].valuelist = "\"1:Do not print\",\"2:Top\",\"3:Front\",\"4:Bottom\",\"5:Back\"" tblSettings[17] = {} tblSettings[17].cfgonly = 1 tblSettings[17].type = 4 tblSettings[17].prompt = "Part ID content to print" tblSettings[17].key = "idstring" tblSettings[17].defvalue = gsBvxPrintPartIdContent tblSettings[18] = {} tblSettings[18].cfgonly = 1 tblSettings[18].type = 3 tblSettings[18].prompt = "Part ID text height" tblSettings[18].key = "idsize" tblSettings[18].defvalue = Millis2Meters(gnBvxPrintPartIdTextHeightMillis) tblSettings[19] = {} tblSettings[19].cfgonly = 1 tblSettings[19].type = 3 tblSettings[19].prompt = "Force height of marking texts (0=no)" tblSettings[19].key = "txtsize" tblSettings[19].defvalue = Millis2Meters(gnBvxMarkingTextHeightMillis) tblSettings[20] = {} tblSettings[20].cfgonly = 1 tblSettings[20].type = 2 tblSettings[20].prompt = "Rotate board 90 degrees clockwise" tblSettings[20].key = "rotateboard90" tblSettings[20].defvalue = BoolToNumber(gbBvxRotateBoard90) gHelpAnchor = "afdlgbvnexport" local bRes = ac_optiondlg("LDBV", "BVX-writing settings", tblSettings) if not bRes then return false end gbBvxWriteSplinterFree = (tblSettings[1].value == 1) gbBvxWriteTopBottom = (tblSettings[2].value == 1) gbBvxWriteSepFiles = (tblSettings[3].value == 1) gbBvxWriteOnlyCore = (tblSettings[4].value == 1) gnBvxInfoName = tblSettings[5].value gnBvxInfoUnit = tblSettings[6].value gnBvxInfoGrade = tblSettings[7].value gnBvxInfoComments = tblSettings[8].value gnBvxInfoProfile = tblSettings[9].value gnBvxInfoMaterial = tblSettings[10].value gbBvxWriteReinforce = (tblSettings[11].value == 1) gbBvxMarkReinforce = (tblSettings[12].value == 1) gnBvxFeedDir = tblSettings[13].value gnBvxFeedTableSide = tblSettings[14].value - 1 gnBvxGrooveWidthWarnThresholdMillis = Meters2Millis(tblSettings[15].value) gnBvxPrintPartIdSide = tblSettings[16].value - 1 gsBvxPrintPartIdContent = tblSettings[17].value gnBvxPrintPartIdTextHeightMillis = Meters2Millis(tblSettings[18].value) gnBvxMarkingTextHeightMillis = Meters2Millis(tblSettings[19].value) gbBvxRotateBoard90 = (tblSettings[20].value == 1) gsSplinterFree = TernaryIf(gbBvxWriteSplinterFree, "sfToolPath", "sfNone") return true end -- Returns VECTOR x,y,z transformed function MatTransformVec( x, y, z, tblTran ) local xnew, ynew, znew xnew=x*tblTran[1]+ y*tblTran[2]+ z*tblTran[3] ynew=x*tblTran[5]+ y*tblTran[6]+ z*tblTran[7] znew=x*tblTran[9]+ y*tblTran[10]+ z*tblTran[11] return xnew, ynew, znew end -- Returns point x,y,z transformed function MatTransformPt( x, y, z, tblTran ) return MatTransformVec( x+tblTran[4], y+tblTran[8], z+tblTran[12], tblTran ) end -- Returns path,file name and extension. nil=cannot find function SplitFileName(strPathName) local path, fname, ext local i, c, extIndex i=string.len(strPathName) extIndex=i while i>0 do c=string.sub(strPathName, i, i) if ext==nil and c=="." then ext=string.sub(strPathName, i, string.len(strPathName)) extIndex=i-1 elseif fname==nil and (c=="\\" or c=="/") then fname=string.sub(strPathName, i+1, extIndex) path=string.sub(strPathName, 1, i) break end i=i-1 end return path,fname,ext end function DoFileName(strFileName, strAdd) local path, fname, ext path, fname, ext=SplitFileName(strFileName, strNum) return string.format("%s%s%s%s", path, fname, strAdd, ext) end function GetLibGuidFirstPart() local sGuid = string.lower(ac_objectget("#libguid")) local nMatchStartIndex = string.find(sGuid, "}-{", 1, true) if nMatchStartIndex == nil then error("Bad guid: " .. sGuid) end return string.sub(sGuid, 1, nMatchStartIndex) end function CreateMarkingOperation( nX1Millis, nY1Millis, nX2Millis, nY2Millis, nTextXAlign, nTextYAlign, nFontSize, nSide,shouldInsertTextParallelToLine, shouldDrawLine, sText ) return { ["nX1Millis"] = nX1Millis, ["nY1Millis"] = nY1Millis, ["nX2Millis"] = nX2Millis, ["nY2Millis"] = nY2Millis, ["nTextXAlign"] = nTextXAlign, ["nTextYAlign"] = nTextYAlign, ["nFontSize"] = nFontSize, ["nSide"] = nSide, ["shouldInsertTextParallelToLine"] = shouldInsertTextParallelToLine, ["shouldDrawLine"] = shouldDrawLine, ["sText"] = sText, } end function Add3D(v1, v2) return { x = v1.x + v2.x, y = v1.y + v2.y, z = v1.z + v2.z } end function Sub3D(v2, v1) return { x = v2.x - v1.x, y = v2.y - v1.y, z = v2.z - v1.z } end function Length3D(v) return math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z) end function Scale3D(v, scalar) return { x = v.x * scalar, y = v.y * scalar, z = v.z * scalar } end function Flip3D(v) return Scale3D(v, -1) end function Distance3D(v1, v2) return Length3D(Sub3D(v2, v1)) end -- The transformation of a part to the corresponding CAD-Position is defined with a point P0 (x, y, z). -- The orientation of the part is defined by two 3D-vectors V1 defines the direction of the x-axis and v2 the direction of y-axis. -- Z-axis is automatically defined by Cambium to be orthogonal to these two vectors in a right-handed coordinate system. function CreateCadPositionTbl(rotate90, plankInfo) local tblPosition = {} tblPosition.key = UuidV4() local vecX = plankInfo.vecx local vecY = plankInfo.vecy local vecZ = plankInfo.vecz if rotate90 then -- Only adjust world position here. -- Actual "rotation" is done when creating the board polygon by swapping x/y coordinates -- Move to center relative to element if plankInfo.ownerelemguid ~= nil then local plane = af_request("plankinfo", plankInfo.ownerelemguid) local elemdata = af_request("plankinfo", plankInfo.guid, plane).elemdata local vx = Scale3D(plane.vecx, elemdata.z1) local vy = Scale3D(plane.vecy, elemdata.y1) local vz = Scale3D(plane.vecz, -elemdata.x1) local offset = Add3D(vx, Add3D(vy, vz)) tblPosition.p0 = Add3D(plane.begc, offset) tblPosition.p0 = Add3D(tblPosition.p0, Scale3D(plane.vecy, plane.width * 0.5)) tblPosition.p0 = Add3D(tblPosition.p0, Scale3D(plane.vecz, (plane.height + plane.len) * 0.5)) tblPosition.p0 = Add3D(tblPosition.p0, Scale3D(plane.vecx, (plane.len - plane.height) * 0.5)) -- Move to own center else tblPosition.p0 = plankInfo.begc tblPosition.p0 = Add3D(tblPosition.p0, Scale3D(vecY, gnCurrWidth * 0.5)) tblPosition.p0 = Add3D(tblPosition.p0, Scale3D(vecZ, (gnCurrTotLen + gnCurrHeight) * 0.5)) tblPosition.p0 = Add3D(tblPosition.p0, Scale3D(vecX, (gnCurrTotLen - gnCurrHeight) * 0.5)) end tblPosition.p0 = Add3D(tblPosition.p0, Scale3D(vecZ, -gnCurrTotLen)) -- Compensate for polygon offset tblPosition.v1 = vecX tblPosition.v2 = vecZ else local isFeedBegFirst = (gnCurrentFeedDir == EBvxFeedDirBeg) local x, y, z if isFeedBegFirst then x = plankInfo.begc.x y = plankInfo.begc.y z = plankInfo.begc.z else -- Feeding end side first, reverse X and Y-axis vecX.x = -vecX.x vecX.y = -vecX.y vecX.z = -vecX.z vecY.x = -vecY.x vecY.y = -vecY.y vecY.z = -vecY.z x = plankInfo.endc.x y = plankInfo.endc.y z = plankInfo.endc.z end -- Vector V1 defines the x-axis direction (x, y, z) tblPosition.v1 = vecX -- Vector V2 defines the y-axis direction (x, y, z) tblPosition.v2 = {} -- Vector P0 defines the plank position (x, y, z). -- These are the actual coordinates of the plank. tblPosition.p0 = {} -- In AF the point (begc.x, begc.y, begc.z) is in the bottom middle of the plank -- (orientation being bottom down, beginning side first). -- In Cambium it is in the lower left of the plank, so we need to translate the point to the same place as in AF, -- given the current feed orientation. -- AF uses left-handed coordinate system and Cambium uses right-handed, -- this means the y-axis will point to opposite directions if gnCurrentFeedTableSide == EAFSideTop then -- Feeding the plank so that its top is facing the table, ie. upside down -- Y-vector switches direction, since we are upside down tblPosition.v2.x = -vecY.x tblPosition.v2.y = -vecY.y tblPosition.v2.z = -vecY.z -- Since the plank is upside down, we need to compensate for the full height of the plank in Z-axis direction -- We need to compensate for half of the width of the plank in Y-axis direction tblPosition.p0.x = x + vecY.x * plankInfo.width * 0.5 + vecZ.x * plankInfo.height tblPosition.p0.y = y + vecY.y * plankInfo.width * 0.5 + vecZ.y * plankInfo.height tblPosition.p0.z = z + vecY.z * plankInfo.width * 0.5 + vecZ.z * plankInfo.height elseif ((gnCurrentFeedTableSide == EAFSideFront) and isFeedBegFirst) or ((gnCurrentFeedTableSide == EAFSideBack) and (not isFeedBegFirst)) then -- Feeding the plank so that its front is facing the table. -- In reverse feed direction this is the same as its back facing the table. -- The plank is sideways so the AF Z-vector becomes the Y-vector tblPosition.v2.x = -vecZ.x tblPosition.v2.y = -vecZ.y tblPosition.v2.z = -vecZ.z -- Since we are sideways, we need to compensate for half the width of the plank in Y-axis direction -- We need to compensate for the height of the plank in Z-axis direction tblPosition.p0.x = x - vecY.x * plankInfo.width * 0.5 + vecZ.x * plankInfo.height tblPosition.p0.y = y - vecY.y * plankInfo.width * 0.5 + vecZ.y * plankInfo.height tblPosition.p0.z = z - vecY.z * plankInfo.width * 0.5 + vecZ.z * plankInfo.height elseif gnCurrentFeedTableSide == EAFSideBottom then -- Feeding the plank so that its bottom is facing the table. -- This is the default scenario tblPosition.v2.x = vecY.x tblPosition.v2.y = vecY.y tblPosition.v2.z = vecY.z -- We need to compensate half the width of the planks in Y-axis direction so they go to the right position tblPosition.p0.x = x - vecY.x * plankInfo.width * 0.5 tblPosition.p0.y = y - vecY.y * plankInfo.width * 0.5 tblPosition.p0.z = z - vecY.z * plankInfo.width * 0.5 elseif ((gnCurrentFeedTableSide == EAFSideBack) and isFeedBegFirst) or ((gnCurrentFeedTableSide == EAFSideFront) and (not isFeedBegFirst)) then -- Feeding the plank so that its back is facing the table. -- In reverse feed direction this is the same as its front facing the table. tblPosition.v2.x = vecZ.x tblPosition.v2.y = vecZ.y tblPosition.v2.z = vecZ.z -- Since we are sideways, we need to compensate for half the width of the plank in Y-axis direction -- We need to compensate for the height of the plank in Z-axis direction tblPosition.p0.x = x + vecY.x * plankInfo.width * 0.5 tblPosition.p0.y = y + vecY.y * plankInfo.width * 0.5 tblPosition.p0.z = z + vecY.z * plankInfo.width * 0.5 end end tblPosition.p0.x = Meters2Millis(tblPosition.p0.x) tblPosition.p0.y = Meters2Millis(tblPosition.p0.y) tblPosition.p0.z = Meters2Millis(tblPosition.p0.z) return tblPosition end function WriteOperationsXml(tblElem, nIndentLvl) local sOperationsXml = "" sOperationsXml = sOperationsXml .. FormatXmlTag('', nIndentLvl) if tblElem.isPlank then -- Beginning and ending cuts so the plank has correct length for i, nXPosition in ipairs({ 0, tblElem.tblDimension.x }) do sOperationsXml = sOperationsXml .. WriteEndingCutsXml(i == 1, nXPosition) end if gnBvxPrintPartIdSide ~= EAFSideNone and gsBvxPrintPartIdContent ~= "" then sOperationsXml = sOperationsXml .. WritePartIdToSide(tblElem) end sOperationsXml = sOperationsXml .. WriteContoursXml(tblElem) end if tblElem.isBoard then local nEdgeMachiningsCount = ac_objectget("iMcEdge", -1) if nEdgeMachiningsCount then -- PH added to support old library versions/pla-files for i=1, nEdgeMachiningsCount do local nType = NormalizeType(ac_objectget("iMcEdge", i, 1)) if nType == 2000 then sOperationsXml = sOperationsXml .. WriteBoardEdgeGroove(i, tblElem) end end end end local nMachiningsCount=ac_objectget("iMc" ,-1) for i=1, nMachiningsCount do local nType = NormalizeType(ac_objectget("iMc", i, 1)) if (nType == EMcFrAngledBeg) or (nType == EMcFrAngledEnd) then sOperationsXml = sOperationsXml .. WriteAngledEndingsXml(i, tblElem, nType) elseif nType == EMcFrGroove then local tblGroove=OperationGroove:new({ nObjectRow = i }) if tblGroove.nGroType==EMcFrGrooveTypeDrill then sOperationsXml = sOperationsXml .. WriteGrooveAsDrillingXml(tblElem, tblGroove) else sOperationsXml = sOperationsXml .. WriteGrooveXml(tblElem, tblGroove) end elseif nType == EMcFrDrill then sOperationsXml = sOperationsXml .. WriteDrillXml(i, tblElem) elseif (nType == EMcVCutBeg) or (nType == EMcVCutEnd) then sOperationsXml = sOperationsXml .. WriteVCutXml(i, tblElem, nType) elseif nType == EMcFrSaw then sOperationsXml = sOperationsXml .. WriteSawXml(i, tblElem) elseif (nType == EMcFrBegHiddenShoe) or (nType == EMcFrEndHiddenShoe) then sOperationsXml = sOperationsXml .. WriteHiddenShoeXml(i, tblElem, nType) elseif nType == EMcFrBalkJoint then sOperationsXml = sOperationsXml .. WriteBalkJointXml(i, tblElem) elseif nType == EMcFrMarking then sOperationsXml = sOperationsXml .. WriteMarkingXml(i, tblElem) elseif nType == EMcFrMortise then sOperationsXml = sOperationsXml .. WriteMortiseXml(i, tblElem) elseif (nType == EMcFrAngledBegTenonMort) or (nType == EMcFrAngledEndTenonMort) then sOperationsXml = sOperationsXml .. WriteAngledTenonMortXml(i, tblElem, nType) elseif nType == EMcFrReinforce then sOperationsXml = sOperationsXml .. WriteReinforceXml(i, tblElem) elseif (nType == EMcJointBeg) or (nType == EMcJointEnd) then sOperationsXml = sOperationsXml .. WriteJointBegOrEndXml(i, tblElem, nType) elseif nType ~= 0 then if nType ~=EMcNailGroup and nType ~= EMcNailLine then -- PH, let these be there, will be handled by framing station AddCncErr(ac_objectget("iMc", i, 2), string.format("UNSUPPORTED MACHINING %d", nType)) end end end sOperationsXml = sOperationsXml .. FormatXmlTag("", nIndentLvl) return sOperationsXml end function WriteEndingCutsXml(isBeginning, nXPosition) local nRotationZDeg = TernaryIf(isBeginning, 0, 180) local sSawCutXmlTag = string.format('', nRotationZDeg) return FormatXmlTag(sSawCutXmlTag, gn_OPERATION_INDENTATION) end function WritePartIdToSide(tblPart) local tblPlankInfo =af_request("plankinfo") local sMarkingText = gsBvxPrintPartIdContent sMarkingText = ac_environment("strreplace", sMarkingText, "#fullid#", gsCurrentLogId) sMarkingText = ac_environment("strreplace", sMarkingText, "#projnum#", ac_environment("parsetext", "")) sMarkingText = ac_environment("strreplace", sMarkingText, "#projid#", ac_environment("parsetext", "")) sMarkingText = ac_environment("strreplace", sMarkingText, "#shortid#", GetInfoStr(EInfoShortID, tblPlankInfo)) sMarkingText = ac_environment("strreplace", sMarkingText, "#plankrole#", GetInfoStr(EInfoPlankRole, tblPlankInfo)) sMarkingText = ac_environment("strreplace", sMarkingText, "#plankrolefin#", GetInfoStr(EInfoPlankRoleFin, tblPlankInfo)) sMarkingText = ac_environment("strreplace", sMarkingText, "#plankrolenor#", GetInfoStr(EInfoPlankRoleNor, tblPlankInfo)) sMarkingText = ac_environment("strreplace", sMarkingText, "#plankroleshort#", GetInfoStr(EInfoPlankRoleShort, tblPlankInfo)) sMarkingText = ac_environment("strreplace", sMarkingText, "#plankroleshortfin#", GetInfoStr(EInfoPlankRoleShortFin, tblPlankInfo)) sMarkingText = ac_environment("strreplace", sMarkingText, "#plankroleshortnor#", GetInfoStr(EInfoPlankRoleShortNor, tblPlankInfo)) -- Adjust so that does not intersect with grooves and markings if gnBvxPrintPartIdTextHeightMillis < 1 then gnBvxPrintPartIdTextHeightMillis = 1 end local nWidthMillis = string.len(sMarkingText) * gnBvxPrintPartIdTextHeightMillis * 0.67 -- 20 mm per character for 30 mm high one nWidthMillis = nWidthMillis + 100 local nXMillis = Meters2Millis(af_request("mc_findtextpos", gnBvxPrintPartIdSide, 0, nWidthMillis / 1000)) local nOriginalSettingValue = gnBvxMarkingTextHeightMillis gnBvxMarkingTextHeightMillis = gnBvxPrintPartIdTextHeightMillis local tblMarkingData = CreateMarkingOperation( nXMillis, 0, nXMillis + nWidthMillis, 0, EAFTextXAlignRight, EAFTextYAlignTop, 0, gnBvxPrintPartIdSide, false, false, sMarkingText ) local sResultXml = WriteMarkingXml(nil, tblPart, tblMarkingData) gnBvxMarkingTextHeightMillis = nOriginalSettingValue return sResultXml end function WriteBoardEdgeGroove(i, tblBoard) local nEdgeId = ac_objectget('iMcEdge', i, 2) -- [2] edge id, iPolygon[row][9] local nSide = ac_objectget('iMcEdge', i, 3) -- [3] Anchored either to front or back side local nWidthMeters = ac_objectget('iMcEdge', i, 4) -- [4] From the edge towards inside (logical if at middle of the edge), if zero, will cut the side surface away always no matter to the angle local nDepthMeters = ac_objectget('iMcEdge', i, 5) -- [5] If at surface this is the "depth" from side surface local nOffFromSide = ac_objectget('iMcEdge', i, 6) -- [6] Offset from side, negative=middle of the groove in the surface to the middle of the board edge local nAngleCrossDeg = ac_objectget('iMcEdge', i, 7) -- [7] Tilt angle difference from 90 degree ange: positive tilts groove inside the board, negative outside local nExtBegMeters = ac_objectget('iMcEdge', i, 8) -- [8] Extend groove at begin local nExtEndMeters = ac_objectget('iMcEdge', i, 9) -- [9] Extend groove at end local tblEdgeInfo = FrMcEdgeFind(i) if not tblEdgeInfo then RaiseError("ERROR: Edge data was not found for edge id: " .. nEdgeId) end local nGroAngleDeg = -nAngleCrossDeg local nZMillis = 0 if nOffFromSide < 0 then -- When groove is in the middle of the edge of the board, width and depth switch places nDepthMeters, nWidthMeters = nWidthMeters, nDepthMeters nZMillis = Meters2Millis(gnCurrWidth / 2 + nWidthMeters / 2) nGroAngleDeg = nGroAngleDeg + 90 end local nX1Millis = Meters2Millis(tblEdgeInfo.nX1Meters - math.sin(tblEdgeInfo.nAngleRad) * nExtBegMeters) local nY1Millis = Meters2Millis(tblEdgeInfo.nY1Meters + math.cos(tblEdgeInfo.nAngleRad) * nExtBegMeters) local nX2Millis = Meters2Millis(tblEdgeInfo.nX2Meters + math.sin(tblEdgeInfo.nAngleRad) * nExtEndMeters) local nY2Millis = Meters2Millis(tblEdgeInfo.nY2Meters - math.cos(tblEdgeInfo.nAngleRad) * nExtEndMeters) local tblOperationGroove = OperationGroove:new({ nX1Millis = nX1Millis, nY1Millis = nY1Millis, nX2Millis = nX2Millis, nY2Millis = nY2Millis, nGroDepthMillis = Meters2Millis(nDepthMeters), nGroWidthMillis = Meters2Millis(nWidthMeters), nGroAngleDeg = nGroAngleDeg, nGroAngleLenDeg = 0, nSide = nSide, nZMillis = nZMillis, }) return WriteGrooveXml(tblBoard, tblOperationGroove) end function WriteAngledEndingsXml(i, tblPart, nType) local nAngleDeg = ac_objectget("iMc", i, 2) local nBevelDeg = ac_objectget("iMc", i, 3) local nXMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nYMillis = Meters2Millis(ac_objectget("iMc", i, 5)) local nZMillis = Meters2Millis(ac_objectget("iMc", i, 6)) local x = nXMillis local y = Meters2Millis(gnCurrWidth) / 2 + nYMillis local nRotationXDeg = 0 local nRotationYDeg = 180 - nBevelDeg local nRotationZDeg = -90 + nAngleDeg local isEnd = (nType == EMcFrAngledEnd) if isEnd then x = Meters2Millis(gnCurrTotLen) - x nRotationYDeg = -nRotationYDeg nRotationZDeg = -nRotationZDeg end local nFrameId, tblTranslatedCoords, tblTranslatedRotations = TranslateCoordsAndRotations( tblPart, EAFSideBottom, CreateTblCoords(x, y), CreateTblRotations(nRotationXDeg, nRotationYDeg, nRotationZDeg) ) local sSawCutXmlTag = string.format('', tblTranslatedRotations.z) return FormatXmlTag(sSawCutXmlTag, gn_OPERATION_INDENTATION) end function WriteGrooveAsDrillingXml(part, groove) local info = af_request("mc_getgroinfo", { guid = gsCurrentLogGuid, x1 = groove.nX1Millis, x2 = groove.nX2Millis, y1 = groove.nY1Millis, y2 = groove.nY2Millis, depth = groove.nGroDepthMillis, width = groove.nGroWidthMillis, groAngleRad = Deg2Rad(groove.nGroAngleDeg), face = groove.nSide, groTiltAngleRad = Deg2Rad(groove.nGroAngleLenDeg) }) local midPtBotX=groove.nX1Millis+info.vecDownSurf.x*groove.nGroDepthMillis + info.vecLeftSurf.x*groove.nGroWidthMillis/2 + info.vecLenSurf.x*info.lenBot/2 local midPtBotY=groove.nY1Millis+info.vecDownSurf.y*groove.nGroDepthMillis + info.vecLeftSurf.y*groove.nGroWidthMillis/2 + info.vecLenSurf.y*info.lenBot/2 local midPtBotZ= info.vecDownSurf.z*groove.nGroDepthMillis + info.vecLeftSurf.z*groove.nGroWidthMillis/2 + info.vecLenSurf.z*info.lenBot/2 local hasX, x, y, _z, distanceToPlane=LinePlaneX( 0, 0, 1, 0, midPtBotX, midPtBotY, midPtBotZ, midPtBotX-info.vecDownSurf.x, midPtBotY-info.vecDownSurf.y, midPtBotZ-info.vecDownSurf.z ) if not hasX then AddCncErr(groove.nX1Millis, string.format("ERROR: Drilling does intersect plane")) return end local vecUpX=-info.vecDownSurf.x local vecUpY=-info.vecDownSurf.y local vecUpZ=-info.vecDownSurf.z if groove.nSide==EAFSideBack or groove.nSide==EAFSideBottom then vecUpY=-vecUpY end if gnCurrentFeedDir==EBvxFeedDirBeg then vecUpX=-vecUpX vecUpY=-vecUpY end local rotZ, rotY=DirToAngles(vecUpX, vecUpY, vecUpZ) local frameId, translatedCoords = TranslateCoordsAndRotations( part, groove.nSide, CreateTblCoords(x, y) ) local diameter=TernaryIf(info.lenBot', gsSplinterFree), gn_OPERATION_INDENTATION) end function WriteGrooveXml(tblPart, tblOperationGroove) local nX1Millis = tblOperationGroove.nX1Millis local nY1Millis = tblOperationGroove.nY1Millis local nX2Millis = tblOperationGroove.nX2Millis local nY2Millis = tblOperationGroove.nY2Millis local nGroDepthMillis = tblOperationGroove.nGroDepthMillis local nGroWidthMillis = tblOperationGroove.nGroWidthMillis local nGroAngleDeg= tblOperationGroove.nGroAngleDeg local nSide = tblOperationGroove.nSide local nGroAngleLenDeg= tblOperationGroove.nGroAngleLenDeg if IsTopFrontBottomOrBackSide(nSide) then nX1Millis = nX1Millis + Meters2Millis(gnBegExtra) nX2Millis = nX2Millis + Meters2Millis(gnBegExtra) end local nRotationYDeg = -nGroAngleDeg local nRotationZDeg = Rad2Deg(math.atan((nX2Millis - nX1Millis) / (nY2Millis - nY1Millis))) local nRotationXDeg if nSide == EAFSideTop then nRotationXDeg = -nGroAngleLenDeg CheckGroWarn(nX1Millis, nY1Millis, nX2Millis, nY2Millis, nGroWidthMillis, Meters2Millis(gnCurrTotLen), Meters2Millis(gnCurrWidth)) elseif nSide == EAFSideFront then nRotationXDeg = -nGroAngleLenDeg CheckGroWarn(nX1Millis, nY1Millis, nX2Millis, nY2Millis, nGroWidthMillis, Meters2Millis(gnCurrTotLen), Meters2Millis(gnCurrHeight)) elseif nSide == EAFSideBottom then nRotationXDeg = nGroAngleLenDeg nRotationZDeg = -nRotationZDeg CheckGroWarn(nX1Millis, nY1Millis, nX2Millis, nY2Millis, nGroWidthMillis, Meters2Millis(gnCurrTotLen), Meters2Millis(gnCurrWidth)) elseif nSide == EAFSideBack then nRotationXDeg = nGroAngleLenDeg nRotationZDeg = -nRotationZDeg CheckGroWarn(nX1Millis, nY1Millis, nX2Millis, nY2Millis, nGroWidthMillis, Meters2Millis(gnCurrTotLen), Meters2Millis(gnCurrHeight)) elseif nSide == EAFSideBegin then nRotationXDeg = -nGroAngleLenDeg nRotationZDeg = nRotationZDeg CheckGroWarn(nX1Millis, nY1Millis, nX2Millis, nY2Millis, nGroWidthMillis, Meters2Millis(gnCurrWidth), Meters2Millis(gnCurrHeight)) elseif nSide == EAFSideEnd then nRotationXDeg = nGroAngleLenDeg nRotationZDeg = -nRotationZDeg CheckGroWarn(nX1Millis, nY1Millis, nX2Millis, nY2Millis, nGroWidthMillis, Meters2Millis(gnCurrWidth), Meters2Millis(gnCurrHeight)) end -- Reference points that we use to determine how to align the part local referencePoint1, referencePoint2 if (nSide == EAFSideBegin) or (nSide == EAFSideEnd) then if nY2Millis < nY1Millis then referencePoint1 = nX1Millis referencePoint2 = nX2Millis else referencePoint1 = nX2Millis referencePoint2 = nX1Millis end else referencePoint1 = nY1Millis referencePoint2 = nY2Millis if referencePoint1 == referencePoint2 then -- In this case alignments would go wrong when they are equal, -- so lets subtract an arbitrary number from referencePoint1 so it becomes smaller than referencePoint2 referencePoint1 = referencePoint1 - EPS end end local sAlignX if referencePoint1 < referencePoint2 then sAlignX = EAlignRight else sAlignX = EAlignLeft nRotationXDeg = -nRotationXDeg nRotationYDeg = -nRotationYDeg end local nDimensionY = CalcDist(nX1Millis, nY1Millis, nX2Millis, nY2Millis) local nFrameId, tblTranslatedCoords, tblTranslatedRotations = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(nX1Millis, nY1Millis), CreateTblRotations(nRotationXDeg, nRotationYDeg, nRotationZDeg) ) sAlignX = TranslateAlign(sAlignX) local sAlignY = sAlignX if (nSide == EAFSideBack) or (nSide == EAFSideBottom) or (nSide == EAFSideEnd) then -- Align y is the same as align x, except for these sides it's the opposite sAlignY = ToggleLeftRightAlign(sAlignY) end local sOpTag -- Petteri added 5/2024: If not a full groove, use Slit, ot if not tblOperationGroove.tblGroInfo or tblOperationGroove.tblGroInfo.begIn~=0 or tblOperationGroove.tblGroInfo.endIn~=0 then sOpTag = string.format('', nGroDepthMillis, gsSplinterFree) else sOpTag = string.format('', nGroDepthMillis, gsSplinterFree) end return FormatXmlTag(sOpTag, gn_OPERATION_INDENTATION) end function WriteDrillXml(i, tblPart) local nXMillis = Meters2Millis(ac_objectget("iMc", i, 2)) local nYMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nDiameterMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nDepthMillis = Meters2Millis(ac_objectget("iMc", i, 5)) local nSide = ac_objectget("iMc", i, 6) if IsTopFrontBottomOrBackSide(nSide) then nXMillis = nXMillis + Meters2Millis(gnBegExtra) end local sDepthUsing = TernaryIf(nDepthMillis == 0, "Infinite", "Value") local nFrameId, tblTranslatedCoords = TranslateCoordsAndRotations(tblPart, nSide, CreateTblCoords(nXMillis, nYMillis)) local sDrillingTag = string.format('', sDepthUsing, nDepthMillis, 0) return FormatXmlTag(sDrillingTag, gn_OPERATION_INDENTATION) end function WriteVCutUsingOpenSlitMachiningsXml(nFrameId, x, z, nBevelLeftDeg, nBevelRightDeg, isBeginning) local dimensionY = 8.0 local depth = 0.0 local rotOffset = TernaryIf(isBeginning, 90, -90) local sVOpenSlitRight = string.format('', gsSplinterFree) local sVOpenSlitLeft = string.format('', gsSplinterFree) return FormatXmlTag(sVOpenSlitRight, gn_OPERATION_INDENTATION) .. FormatXmlTag(sVOpenSlitLeft, gn_OPERATION_INDENTATION) end function WriteVCutXml(i, tblPart, nType) local nSide = ac_objectget("iMc", i, 2) local nXMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nYMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nAngleRightDeg = ac_objectget("iMc", i, 5) local nBevelRightDeg = ac_objectget("iMc", i, 6) local nAngleLeftDeg = ac_objectget("iMc", i, 7) local nBevelLeftDeg = ac_objectget("iMc", i, 8) local hasInvalidAngle = (nAngleRightDeg < 0.2) or (179 < nAngleRightDeg) or (nAngleLeftDeg < 0.2) or (179 < nAngleLeftDeg) if hasInvalidAngle then AddCncErr(0, "Invalid angle for V-Cut. Value must be between [0.2, 179]") return "" end -- PH rukkasi nBevelRightDeg < 1 ja nBevelLeftDeg < 0 nollaksi -- if math.abs(nBevelRightDeg)<0.1 then -- nBevelRightDeg=0 -- end -- if math.abs(nBevelLeftDeg)<0.1 then -- nBevelLeftDeg=0 -- end local hasInvalidBevel = (nBevelRightDeg <= -180) or (179.8 < nBevelRightDeg) or (nBevelLeftDeg <= -180) or (179.8 < nBevelLeftDeg) if hasInvalidBevel then AddCncErr(0, string.format("Invalid bevel angle for V-Cut. Value must be between [-180.0, 179.8]: nBevelRightDeg=%f nBevelLeftDeg=%f", nBevelRightDeg, nBevelLeftDeg)) return "" end -- We need to take feed direction into account here local isBeginning = (nType == EMcVCutBeg and gnCurrentFeedDir == EBvxFeedDirBeg) or (nType == EMcVCutEnd and gnCurrentFeedDir == EBvxFeedDirEnd) local x = TernaryIf(isBeginning, nXMillis, tblPart.tblDimension.x - nXMillis) local nNormalizedSide, y, z -- For some reason the sides in v-cut are not the same as in other machinings. (eg. top side actually refers back side) -- We need to normalize this value to match other machinings, so when -- translating coordinates, they go correctly with all combinations of gnCurrentFeedTableSide and gnCurrentFeedDir. if nSide == EAFSideTop then -- For V-cuts, EAFSideTop is the same as EAFSideBack in other machinings nNormalizedSide = EAFSideBack y = Meters2Millis(gnCurrHeight) z = TernaryIf(isBeginning, Meters2Millis(gnCurrWidth) / 2 + nYMillis, Meters2Millis(gnCurrWidth) / 2 - nYMillis) elseif nSide == EAFSideFront then -- For V-cuts, EAFSideFront is the same as EAFSideTop in other machinings nNormalizedSide = EAFSideTop y = Meters2Millis(gnCurrWidth) z = TernaryIf(isBeginning, Meters2Millis(gnCurrHeight) / 2 + nYMillis, Meters2Millis(gnCurrHeight) / 2 - nYMillis) elseif nSide == EAFSideBottom then -- For V-cuts, EAFSideBottom is the same as EAFSideFront in other machinings nNormalizedSide = EAFSideFront y = Meters2Millis(gnCurrHeight) z = TernaryIf(isBeginning, Meters2Millis(gnCurrWidth) / 2 + nYMillis, Meters2Millis(gnCurrWidth) / 2 - nYMillis) else -- For V-cuts, EAFSideBack is the same as EAFSideBottom in other machinings nNormalizedSide = EAFSideBottom y = Meters2Millis(gnCurrWidth) z = TernaryIf(isBeginning, Meters2Millis(gnCurrHeight) / 2 + nYMillis, Meters2Millis(gnCurrHeight) / 2 - nYMillis) end local sPosition = TernaryIf(isBeginning, "Start", "End") local nAngleSideDeg, nBevelSideDeg, nAngleOppDeg, nBevelOppDeg if isBeginning then nAngleSideDeg = 180 - nAngleRightDeg nBevelSideDeg = nBevelRightDeg nAngleOppDeg = 180 - nAngleLeftDeg nBevelOppDeg = nBevelLeftDeg else nAngleSideDeg = 180 - nAngleLeftDeg nBevelSideDeg = nBevelLeftDeg nAngleOppDeg = 180 - nAngleRightDeg nBevelOppDeg = nBevelRightDeg end -- The x and y coordinates are v-cut specific so they don't need to be translated -- as they are always correct and we have already taken into account the gnCurrentFeedDir. -- FrameId however needs to be translated local nFrameId = TranslateCoordsAndRotations(tblPart, nNormalizedSide) -- FrameId is always "the opposite" when we are feeding the plank with end side first. if gnCurrentFeedDir == EBvxFeedDirEnd then nFrameId = GetOppositeSide(false, nFrameId) end if nBevelRightDeg < 0 or nBevelLeftDeg < 0 then return WriteVCutUsingOpenSlitMachiningsXml(nFrameId, x, z, nBevelLeftDeg, nBevelRightDeg, isBeginning) end local sVCutTag = string.format('', nAngleOppDeg, nBevelOppDeg, gsSplinterFree) return FormatXmlTag(sVCutTag, gn_OPERATION_INDENTATION) end function WriteSawXml(i, tblPart) -- Lengthwise sawing local nSide = ac_objectget("iMc", i, 2) local nXStartMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nLengthMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nDepthMillis = Meters2Millis(ac_objectget("iMc", i, 5)) -- variable A in AF local nYOffsetMillis = Meters2Millis(ac_objectget("iMc", i, 6)) -- variable B in AF local nCDeg = ac_objectget("iMc", i, 7) local nDDeg = ac_objectget("iMc", i, 8) if IsTopFrontBottomOrBackSide(nSide) then nXStartMillis = nXStartMillis + Meters2Millis(gnBegExtra) end local isFeedBegFirst = (gnCurrentFeedDir == EBvxFeedDirBeg) local y if (nSide == EAFSideTop) or (nSide == EAFSideBottom) then y = Meters2Millis(gnCurrWidth) / 2 + nYOffsetMillis else y = Meters2Millis(gnCurrHeight) / 2 + nYOffsetMillis end local nXEndMillis = nLengthMillis + nXStartMillis if (nLengthMillis == 0) or (tblPart.tblDimension.x < nXEndMillis) then -- Saw cuts that go from start point to the very end of the plank local nHeightMillis, nAngle1Deg, nAngle2Deg if (nSide == EAFSideTop) then nHeightMillis = Meters2Millis(gnCurrHeight) - nDepthMillis nAngle1Deg = TernaryIf(isFeedBegFirst, -nCDeg, -nDDeg) nAngle2Deg = TernaryIf(isFeedBegFirst, -nDDeg, -nCDeg) elseif nSide == EAFSideFront then nHeightMillis = Meters2Millis(gnCurrWidth) - nDepthMillis nAngle1Deg = TernaryIf(isFeedBegFirst, -nCDeg, -nDDeg) nAngle2Deg = TernaryIf(isFeedBegFirst, -nDDeg, -nCDeg) elseif(nSide == EAFSideBottom) then nHeightMillis = Meters2Millis(gnCurrHeight) - nDepthMillis nAngle1Deg = TernaryIf(isFeedBegFirst, -nDDeg, -nCDeg) nAngle2Deg = TernaryIf(isFeedBegFirst, -nCDeg, -nDDeg) else nHeightMillis = Meters2Millis(gnCurrWidth) - nDepthMillis nAngle1Deg = TernaryIf(isFeedBegFirst, -nDDeg, -nCDeg) nAngle2Deg = TernaryIf(isFeedBegFirst, -nCDeg, -nDDeg) end local sArea = TernaryIf(isFeedBegFirst, "FromLength", "UpToLength") local nFrameId, tblCoords = TranslateCoordsAndRotations(tblPart, nSide, CreateTblCoords(nXStartMillis, y)) local sValleyCutTag = string.format('', nHeightMillis, nAngle1Deg, nAngle2Deg) return FormatXmlTag(sValleyCutTag, gn_OPERATION_INDENTATION) else -- Saw cuts that don't reach the end of the plank can be done with two slots local isTopOrFront = (nSide == EAFSideTop) or (nSide == EAFSideFront) local x = nXStartMillis + nLengthMillis / 2 local z = nDepthMillis local nRotationX1 = TernaryIf(isTopOrFront, -nCDeg, nCDeg) local nRotationX2 = TernaryIf(isTopOrFront, nDDeg, -nDDeg) local nFrameId, tblCoords, tblRotations1 = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(x, y), CreateTblRotations(nRotationX1, 0, 0) ) local sSlotTag1 = string.format('', gsSplinterFree) local sResultXml = FormatXmlTag(sSlotTag1, gn_OPERATION_INDENTATION) local _, _, tblRotations2 = TranslateCoordsAndRotations( tblPart, nSide, nil, CreateTblRotations(nRotationX2, 0, 0) ) local sSlotTag2 = string.format('', gsSplinterFree) sResultXml = sResultXml .. FormatXmlTag(sSlotTag2, gn_OPERATION_INDENTATION) return sResultXml end end function WriteHiddenShoeXml(i, tblPart, nType) local nHoleWidthMillis = Meters2Millis(ac_objectget("iMc", i, 2)) local nHoleHeightMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nHoleDepthMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nGroDepthMillis = Meters2Millis(ac_objectget("iMc", i, 5)) local nGroWidthMillis = Meters2Millis(ac_objectget("iMc", i, 6)) local nDrillOffsetMillis = Meters2Millis(ac_objectget("iMc", i, 7)) local nDrillFromBegOrEndMillis = Meters2Millis(ac_objectget("iMc", i, 8)) local nDrillSizeMillis = Meters2Millis(ac_objectget("iMc", i, 9)) local nDrillSpacingMillis = Meters2Millis(ac_objectget("iMc", i, 10)) local nDrillCount = ac_objectget("iMc", i, 11) local nSide = ac_objectget("iMc", i, 12) local isBeginning = (nType == EMcFrBegHiddenShoe) local sAlignX = EAlignCenter local sAlignY = TernaryIf(isBeginning, EAlignRight, EAlignLeft) local nGroX, nGroY, nRotationZDeg if nSide == EAFSideTop then nGroX = Meters2Millis(gnCurrWidth) / 2 nGroY = Meters2Millis(gnCurrHeight) nRotationZDeg = 0 elseif nSide == EAFSideFront then nGroX = Meters2Millis(gnCurrWidth) nGroY = Meters2Millis(gnCurrHeight) / 2 nRotationZDeg = TernaryIf(isBeginning, 90, -90) elseif nSide == EAFSideBottom then nGroX = Meters2Millis(gnCurrWidth) / 2 nGroY = 0 nRotationZDeg = 180 elseif nSide == EAFSideBack then nGroX = 0 nGroY = Meters2Millis(gnCurrHeight) / 2 nRotationZDeg = TernaryIf(isBeginning, -90, 90) end local nGroSide = TernaryIf(isBeginning, EAFSideBegin, EAFSideEnd) local nGroFrameId, tblHoleCoords, tblHoleRotations = TranslateCoordsAndRotations( tblPart, nGroSide, CreateTblCoords(nGroX, nGroY), CreateTblRotations(0, 0, nRotationZDeg) ) sAlignX = TranslateAlign(sAlignX) sAlignY = TranslateAlign(sAlignY) local sHoleTag = string.format('', nHoleDepthMillis, gsSplinterFree) local sResultXml = FormatXmlTag(sHoleTag, gn_OPERATION_INDENTATION) local sGrooveTag = string.format('', nGroDepthMillis, gsSplinterFree) sResultXml = sResultXml .. FormatXmlTag(sGrooveTag, gn_OPERATION_INDENTATION) for j = 0, nDrillCount - 1 do local nDrillX = TernaryIf(isBeginning, nDrillFromBegOrEndMillis, Meters2Millis(gnCurrTotLen) - nDrillFromBegOrEndMillis) local nDrillSide, nDrillY if nSide == EAFSideTop then nDrillSide = EAFSideFront nDrillY = Meters2Millis(gnCurrHeight) - nDrillOffsetMillis - nDrillSpacingMillis * j elseif nSide == EAFSideFront then nDrillSide = EAFSideBottom nDrillY = nDrillOffsetMillis + nDrillSpacingMillis * j elseif nSide == EAFSideBottom then nDrillSide = EAFSideFront nDrillY = nDrillOffsetMillis + nDrillSpacingMillis * j elseif nSide == EAFSideBack then nDrillSide = EAFSideBottom nDrillY = Meters2Millis(gnCurrWidth) - nDrillOffsetMillis - nDrillSpacingMillis * j end local nDrillFrameId, tblDrillCoords = TranslateCoordsAndRotations( tblPart, nDrillSide, CreateTblCoords(nDrillX, nDrillY) ) local sDrillingTag = string.format('', gsSplinterFree) sResultXml = sResultXml .. FormatXmlTag(sDrillingTag, gn_OPERATION_INDENTATION) end return sResultXml end function WriteBalkJointXml(i, tblPart) local nXMillis = Meters2Millis(ac_objectget("iMc", i, 2)) + Meters2Millis(gnBegExtra) local nWidthMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nDepthTopMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nDepthBotMillis = Meters2Millis(ac_objectget("iMc", i, 5)) local nDepthSidesMillis = Meters2Millis(ac_objectget("iMc", i, 6)) local sResultXml = "" local sideDepthPairs = { { EAFSideTop, nDepthTopMillis }, { EAFSideFront, nDepthSidesMillis }, { EAFSideBottom, nDepthBotMillis }, { EAFSideBack, nDepthSidesMillis }, } for _, tblPair in ipairs(sideDepthPairs) do local nSide = tblPair[1] local nDepthMillis = tblPair[2] -- Y can be anything, it doesn't matter with this machining local nFrameId, tblCoords = TranslateCoordsAndRotations(tblPart, nSide, CreateTblCoords(nXMillis, 0)) local sSlotTag = string.format('', nDepthMillis, gsSplinterFree) sResultXml = sResultXml .. FormatXmlTag(sSlotTag, gn_OPERATION_INDENTATION) end return sResultXml end function WriteMarkingXml(i, tblPart, tblMarkingData) local nX1Millis, nY1Millis, nX2Millis, nY2Millis, nTextXAlign, nTextYAlign, nFontSize, nSide, shouldInsertTextParallelToLine, shouldDrawLine, sText if tblMarkingData then nX1Millis = tblMarkingData.nX1Millis nY1Millis = tblMarkingData.nY1Millis nX2Millis = tblMarkingData.nX1Millis nY2Millis = tblMarkingData.nY2Millis nTextXAlign = tblMarkingData.nTextXAlign nTextYAlign = tblMarkingData.nTextYAlign nFontSize = tblMarkingData.nFontSize nSide = tblMarkingData.nSide shouldInsertTextParallelToLine = tblMarkingData.shouldInsertTextParallelToLine shouldDrawLine = tblMarkingData.shouldDrawLine sText = tblMarkingData.sText else nX1Millis = Meters2Millis(ac_objectget("iMc", i, 2)) nY1Millis = Meters2Millis(ac_objectget("iMc", i, 3)) nX2Millis = Meters2Millis(ac_objectget("iMc", i, 4)) nY2Millis = Meters2Millis(ac_objectget("iMc", i, 5)) nTextXAlign = ac_objectget("iMc", i, 6) nTextYAlign = ac_objectget("iMc", i, 7) nFontSize = ac_objectget("iMc", i, 8) nSide = ac_objectget("iMc", i, 9) shouldInsertTextParallelToLine = BitTest(ac_objectget("iMc", i, 11), 1) == 1 shouldDrawLine = BitTest(ac_objectget("iMc", i, 11), 2) == 0 sText = ac_objectget("iMcStr", i, 1) end if IsTopFrontBottomOrBackSide(nSide) then nX1Millis = nX1Millis + Meters2Millis(gnBegExtra) nX2Millis = nX2Millis + Meters2Millis(gnBegExtra) end local sResultXml = "" if sText ~= "" then sText = EscapeString(sText) local x = (nX1Millis + nX2Millis) / 2 local y = (nY1Millis + nY2Millis) / 2 -- Font size 0 in AF is the same as font size 10 in AF local nTextHeightMillis if gnBvxMarkingTextHeightMillis > 0 then nTextHeightMillis = gnBvxMarkingTextHeightMillis else local nNormalizedFontSize = TernaryIf(nFontSize == 0, 10, nFontSize) -- One font size in AF is about 16.5 millis in Cambium nTextHeightMillis = nNormalizedFontSize * 16.5 end local nRotationZDeg = 0 if shouldInsertTextParallelToLine then nRotationZDeg = Rad2Deg(math.atan((nY2Millis - nY1Millis)/ (nX2Millis - nX1Millis))) end local isFeedBegFirst = gnCurrentFeedDir == EBvxFeedDirBeg local function SwitchTextAlign(nTextAlign) return 4 - nTextAlign -- translates 1 ==> 3, 2 ==> 2, and 3 ==> 1 end local sTextDirection if nSide == EAFSideTop then sTextDirection = TernaryIf(isFeedBegFirst, "Normal", "UpsideDown") elseif nSide == EAFSideFront then sTextDirection = TernaryIf(isFeedBegFirst, "Normal", "UpsideDown") nRotationZDeg = -nRotationZDeg elseif nSide == EAFSideBottom then sTextDirection = TernaryIf(isFeedBegFirst, "UpsideDown", "Normal") nTextYAlign = SwitchTextAlign(nTextYAlign) elseif nSide == EAFSideBack then sTextDirection = TernaryIf(isFeedBegFirst, "UpsideDown", "Normal") nTextYAlign = SwitchTextAlign(nTextYAlign) elseif nSide == EAFSideBegin then sTextDirection = TernaryIf(isFeedBegFirst, "UpsideDown", "Normal") nTextYAlign = SwitchTextAlign(nTextYAlign) nTextXAlign = SwitchTextAlign(nTextXAlign) nRotationZDeg = -nRotationZDeg elseif nSide == EAFSideEnd then sTextDirection = TernaryIf(isFeedBegFirst, "Normal", "UpsideDown") nTextXAlign = SwitchTextAlign(nTextXAlign) end local nFrameId, tblCoords, tblRotations = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(x, y), CreateTblRotations(0, 0, nRotationZDeg) ) if not isFeedBegFirst then nTextXAlign = SwitchTextAlign(nTextXAlign) nTextYAlign = SwitchTextAlign(nTextYAlign) end local tblAFTextXAlignToCambium = { [1] = "pmMin", [2] = "pmCenter", [3] = "pmMax" } local tblAFTextYAlignToCambium = { [1] = "pmMax", [2] = "pmCenter", [3] = "pmMin" } local sTextPlacementX = tblAFTextXAlignToCambium[nTextXAlign] local sTextPlacementY = tblAFTextYAlignToCambium[nTextYAlign] local sTextOutputTag = string.format('') sResultXml = sResultXml .. FormatXmlTag(sTextOutputTag, gn_OPERATION_INDENTATION) end if shouldDrawLine then local nFrameId, tblCoords1 = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(nX1Millis, nY1Millis) ) local _, tblCoords2 = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(nX2Millis, nY2Millis) ) local nHeightMillis = math.abs(tblCoords2.y - tblCoords1.y) -- math.atan works correctly even if we are diving by zero, since it results to infinity and that -- results to an angle of 90 or -90 local nAngleDeg = Rad2Deg(math.atan((tblCoords2.x - tblCoords1.x) / (tblCoords2.y - tblCoords1.y))) if nAngleDeg < -89.9 or 89.9 < nAngleDeg then -- Angle is limited between [-89.9, 89.9] in Cambium, so we can't draw exactly vertical lines -- except with using custom frames with rotation, but it causes the coordinate system to change -- and that would require quite a lot more work than this solution. -- So now we just round the values to -89.9 or 89.9 because it shouldn't matter too much when -- the markings are short as they usually are. -- Results of atan are limited between [-90, 90], so we don't need to worry about the value being -- too far off local sWarningText = "WARNING: Marking is at a vertical angle which is not possible in BVX. " .. "The angle will be rounded to 89.9 degrees." AddCncErr(Millis2Meters(nX1Millis), sWarningText) nAngleDeg = TernaryIf(nAngleDeg < -89.9, -89.9, 89.9) -- We also need to calculate the height, otherwise it would be 0, since y1 and y2 are equal to each other local nLineLengthMillis = CalcDist(nX1Millis, nY1Millis, nX2Millis, nY2Millis) nHeightMillis = math.abs(math.cos(Deg2Rad(nAngleDeg)) * nLineLengthMillis) end local isY1Smaller = tblCoords1.y < tblCoords2.y local x = TernaryIf(isY1Smaller, tblCoords1.x, tblCoords2.x) local y = TernaryIf(isY1Smaller, tblCoords1.y, tblCoords2.y) local sMarkingTag = string.format('', nHeightMillis) sResultXml = sResultXml .. FormatXmlTag(sMarkingTag, gn_OPERATION_INDENTATION) end return sResultXml end function WriteMortiseXml(i, tblPart) local isNewTenon = ac_objectget("iMc", i, 13) % 1 -- bit0: Version control if not isNewTenon then AddCncErr(0, "Old tenons are not supported") return "" end local nXMillis = Meters2Millis(ac_objectget("iMc", i, 2)) local nYMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nLengthMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nWidthMillis = Meters2Millis(ac_objectget("iMc", i, 5)) local nHeightMillis = Meters2Millis(ac_objectget("iMc", i, 6)) local nRotationAngleDeg = ac_objectget("iMc", i, 7) local nHousingDepthMillis = Meters2Millis(ac_objectget("iMc", i, 8)) local nRoundingRadiusMillis = Meters2Millis(ac_objectget("iMc", i, 9)) local nSide = ac_objectget("iMc", i, 10) local isDove = ac_objectget("iMc", i, 11) == 2 local nConeAngleDeg = ac_objectget("iMc", i, 12) if IsTopFrontBottomOrBackSide(nSide) then nXMillis = nXMillis + Meters2Millis(gnBegExtra) end local nDepthMillis = nLengthMillis + nHousingDepthMillis local nTotalHeightMillis = nHeightMillis + nRoundingRadiusMillis local nRotationZDeg, doesNotReachTop if nSide == EAFSideTop then nRotationZDeg = -90 - nRotationAngleDeg doesNotReachTop = (nYMillis + nTotalHeightMillis) <= Meters2Millis(gnCurrWidth) elseif nSide == EAFSideFront then nRotationZDeg = -90 - nRotationAngleDeg doesNotReachTop = (nYMillis + nTotalHeightMillis) <= Meters2Millis(gnCurrHeight) elseif nSide == EAFSideBottom then nRotationZDeg = 90 + nRotationAngleDeg doesNotReachTop = (nYMillis + nTotalHeightMillis) <= Meters2Millis(gnCurrWidth) elseif nSide == EAFSideBack then nRotationZDeg = 90 + nRotationAngleDeg doesNotReachTop = (nYMillis + nTotalHeightMillis) <= Meters2Millis(gnCurrHeight) elseif nSide == EAFSideBegin then nRotationZDeg = 90 - nRotationAngleDeg doesNotReachTop = (nYMillis + nTotalHeightMillis) <= Meters2Millis(gnCurrHeight) elseif nSide == EAFSideEnd then nRotationZDeg = -90 + nRotationAngleDeg doesNotReachTop = (nYMillis + nTotalHeightMillis) <= Meters2Millis(gnCurrHeight) end local nFrameId, tblCoords, tblRotations = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(nXMillis, nYMillis), CreateTblRotations(0, 0, nRotationZDeg) ) if gnCurrentFeedDir == EBvxFeedDirEnd then -- Rotation translations work a bit differently for this machining, -- that's why we need to rotate extra 180 when feeding end first tblRotations.z = tblRotations.z - 180 end if isDove then if not IsRoughlyEqual(nRoundingRadiusMillis, nWidthMillis / 2) then -- When rounding is half of width, it is the same rounding that Cambium has --[[ Not set in ArchiFrame currently - do not warn AddCncErr( Millis2Meters(nXMillis), "Invalid Dovetail Mortise: Rounding radius must be half of width. Otherwise the output will not match the source." ) ]] end if doesNotReachTop then -- You can't create dovetail mortises that look the same in Cambium as in AF when they don't reach the top -- nRotationAngleDeg does not matter here. -- In case mortise reaches the top with angle 0, it will reach it with other angles also. AddCncErr( Millis2Meters(nXMillis), "Invalid Dovetail Mortise: Mortise must reach to the top of the plank. Otherwise the output will not match the source" ) end local sDovetailMortiseTag = string.format('') return FormatXmlTag(sDovetailMortiseTag, gn_OPERATION_INDENTATION) else if nConeAngleDeg ~= 0 then AddCncErr( Millis2Meters(nXMillis), "Invalid Mortise: Cone degree must be 0. Otherwise the output will not match the source." ) end local nXMortiseOffset = math.sin(Deg2Rad(nRotationAngleDeg)) * nHeightMillis / 2 local nYMortiseOffset = math.cos(Deg2Rad(nRotationAngleDeg)) * nHeightMillis / 2 local nXMortise = nXMillis - nXMortiseOffset local nYMortise = nYMillis + nYMortiseOffset local _, tblCoordsMortise = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(nXMortise, nYMortise) ) local sMortiseTag = string.format('') return FormatXmlTag(sMortiseTag, gn_OPERATION_INDENTATION) end end function WriteAngledTenonMortXml( i, tblPart, nType, nAngleTopDeg, nAngleSideDeg, nXMillis, nYMillis, nZMillis, nSide, nEndType, nRotationAngleDeg, nDepthMillis, nWidthMillis, nHeightMillis, nRoundingRadiusMillis, nAngleConeDeg, nMortiseHousingMillis ) if i ~= nil then nAngleTopDeg = ac_objectget("iMc", i, 2) nAngleSideDeg = ac_objectget("iMc", i, 3) -- bevel nXMillis = Meters2Millis(ac_objectget("iMc", i, 4)) nYMillis = Meters2Millis(ac_objectget("iMc", i, 5)) nZMillis = Meters2Millis(ac_objectget("iMc", i, 6)) nSide = ac_objectget("iMc", i, 7) nEndType = ac_objectget("iMc", i, 8) nRotationAngleDeg = ac_objectget("iMc", i, 9) nDepthMillis = Meters2Millis(ac_objectget("iMc", i, 10)) nWidthMillis = Meters2Millis(ac_objectget("iMc", i, 11)) nHeightMillis = Meters2Millis(ac_objectget("iMc", i, 12)) nRoundingRadiusMillis = Meters2Millis(ac_objectget("iMc", i, 13)) nAngleConeDeg = ac_objectget("iMc", i, 14) nMortiseHousingMillis = 0 local sMortiseHousingValue = FindKeyVal(ac_objectget("iMcStr", i, 1), "housing") if sMortiseHousingValue then nMortiseHousingMillis = Meters2Millis(tonumber(sMortiseHousingValue)) end end local isBeginning = (nType == EMcFrAngledBegTenonMort) local isFeedBegFirst = (gnCurrentFeedDir == EBvxFeedDirBeg) if nEndType == 1 then -- Tenon (tappi) if nAngleConeDeg ~= 0 then AddCncErr(Millis2Meters(nXMillis), "Invalid Angled tenon/mort: Cone degree must be 0. Otherwise the output will not match the source.") end local x = TernaryIf(isBeginning, nXMillis, Meters2Millis(gnCurrTotLen) - nXMillis) local z = nZMillis local y if nSide == EAFSideTop then y = Meters2Millis(gnCurrWidth) / 2 - nYMillis elseif nSide == EAFSideFront then y = Meters2Millis(gnCurrHeight) / 2 - nYMillis elseif nSide == EAFSideBottom then y = Meters2Millis(gnCurrWidth) / 2 + nYMillis elseif nSide == EAFSideBack then y = Meters2Millis(gnCurrHeight) / 2 + nYMillis end local nRotationXDeg = 0 local nRotationYDeg = 180 - nAngleSideDeg local nRotationZDeg = TernaryIf(isBeginning, -90 + nAngleTopDeg, -90 - nAngleTopDeg) local nFrameId, tblCoords, tblRotation = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(x, y), CreateTblRotations(nRotationXDeg, nRotationYDeg, nRotationZDeg) ) local sAlignLength = TernaryIf(isFeedBegFirst, EAlignLeft, EAlignRight) local sTenonTag = string.format('', nRoundingRadiusMillis) return FormatXmlTag(sTenonTag, gn_OPERATION_INDENTATION) elseif nEndType == 2 then -- Dovetail Tenon (lohenpyrstö tappi) if not IsRoughlyEqual(nRoundingRadiusMillis, nWidthMillis / 2) then --[[ Not set in ArchiFrame currently - do not warn -- When rounding is half of width, it is the same rounding that Cambium has AddCncErr( Millis2Meters(nXMillis), "Invalid Dovetail tenon: Rounding radius must be half of width. Otherwise the output will not match the source." ) ]] end local sResultXml = "" if nAngleConeDeg < 0 then -- If cone angle is under 0, it means that it's a straight dovetail with rounded top and bottom -- This is done by writing two dovetail tenons at opposite angles nAngleConeDeg = 0 if not IsRoughlyEqual(nRotationAngleDeg, 0) then AddCncErr(Millis2Meters(nXMillis), "Invalid Dovetail tenon: Double rounded dovetail does not support rotation. It will be written as straight.") nRotationAngleDeg = 0 end if (not IsRoughlyEqual(nAngleTopDeg, 90)) or (not IsRoughlyEqual(nAngleSideDeg, 90)) then AddCncErr(Millis2Meters(nXMillis), "Invalid Dovetail tenon: Double rounded dovetail top and side angles. The other end will not be rounded.") else local nNewSide = GetOppositeSide(true, nSide) local nNewYMillis = -nYMillis local nMaxZMeters = TernaryIf(IsTopOrBottomSide(nNewSide), gnCurrHeight, gnCurrWidth) local nNewZMillis = Meters2Millis(nMaxZMeters) - nZMillis - nHeightMillis -- It's not important what is the actual height as long as it's tall enough, -- the extra will be cut away by the opposite tenon. -- 100 000 is the maximum allowed value for height, so lets set height at that. nHeightMillis = 100000 sResultXml = sResultXml .. WriteAngledTenonMortXml( nil, tblPart, nType, nAngleTopDeg, nAngleSideDeg, nXMillis, nNewYMillis, nNewZMillis, nNewSide, nEndType, nRotationAngleDeg, nDepthMillis, nWidthMillis, nHeightMillis, nRoundingRadiusMillis, nAngleConeDeg, nMortiseHousingMillis ) end end local x = TernaryIf(isBeginning, nXMillis, Meters2Millis(gnCurrTotLen) - nXMillis) local z = nZMillis local y if nSide == EAFSideTop then y = Meters2Millis(gnCurrWidth) / 2 - nYMillis elseif nSide == EAFSideFront then y = Meters2Millis(gnCurrHeight) / 2 - nYMillis elseif nSide == EAFSideBottom then y = Meters2Millis(gnCurrWidth) / 2 + nYMillis elseif nSide == EAFSideBack then y = Meters2Millis(gnCurrHeight) / 2 + nYMillis end local nRotationXDeg = 0 local nRotationYDeg = 180 - nAngleSideDeg local nRotationZDeg = TernaryIf(isBeginning, -90 + nAngleTopDeg, -90 - nAngleTopDeg) local nFrameId, tblCoords, tblRotation = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(x, y), CreateTblRotations(nRotationXDeg, nRotationYDeg, nRotationZDeg) ) if not isFeedBegFirst then nRotationAngleDeg = nRotationAngleDeg + 180 end local sDovetailTenonTag = string.format('') sResultXml = sResultXml .. FormatXmlTag(sDovetailTenonTag, gn_OPERATION_INDENTATION) return sResultXml elseif nEndType == 3 then -- Mortise (tappitasku) if nAngleConeDeg ~= 0 then AddCncErr(Millis2Meters(nXMillis), "Invalid Angled tenon/mort: Cone degree must be 0. Otherwise the output will not match the source.") end if nRotationAngleDeg ~= 0 then AddCncErr(Millis2Meters(nXMillis), "Invalid Angled tenon/mort: Mortise doesn't support rotation angle.") return "" end -- Mortise in Cambium is aligned from the center height-wise, whereas in AF it is aligned from the bottom. -- So we need to calculate the correct x, y, z offsets for the center of the mortise, -- given nAngleTopDeg and nAngleSideDeg. -- Calculation is based on https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates local nRadius = nHeightMillis / 2 local theta = Deg2Rad(90 - nAngleSideDeg) local phi = Deg2Rad(90 - nAngleTopDeg) local nXMortiseOffset = nRadius * math.cos(phi) * math.sin(theta) local nYMortiseOffset = nRadius * math.sin(phi) * math.sin(theta) local nZMortiseOffset = nRadius * math.cos(theta) local nXSaw = TernaryIf(isBeginning, nXMillis + nMortiseHousingMillis, Meters2Millis(gnCurrTotLen) - nXMillis - nMortiseHousingMillis) local nXMortise = TernaryIf(isBeginning, nXSaw + nXMortiseOffset, nXSaw - nXMortiseOffset) local nZSaw = nZMillis local nZMortise = nZMillis + nZMortiseOffset local nYSaw, nYMortise if nSide == EAFSideTop then nYSaw = Meters2Millis(gnCurrWidth) / 2 - nYMillis nYMortise = nYSaw + nYMortiseOffset elseif nSide == EAFSideFront then nYSaw = Meters2Millis(gnCurrHeight) / 2 - nYMillis nYMortise = nYSaw + nYMortiseOffset elseif nSide == EAFSideBottom then nYSaw = Meters2Millis(gnCurrWidth) / 2 + nYMillis nYMortise = nYSaw - nYMortiseOffset elseif nSide == EAFSideBack then nYSaw = Meters2Millis(gnCurrHeight) / 2 + nYMillis nYMortise = nYSaw - nYMortiseOffset end local nRotationXDeg = 0 local nRotationYDeg = 180 - nAngleSideDeg local nRotationZDeg = TernaryIf(isBeginning, -90 + nAngleTopDeg, -90 - nAngleTopDeg) local nTotalDepthMillis = nDepthMillis + nMortiseHousingMillis local nFrameId, tblCoordsMortise, tblRotation = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(nXMortise, nYMortise), CreateTblRotations(nRotationXDeg, nRotationYDeg, nRotationZDeg) ) local sMortiseTag = string.format('') local sResultXml = FormatXmlTag(sMortiseTag, gn_OPERATION_INDENTATION) if nAngleTopDeg ~= 90 or nAngleSideDeg ~= 90 then -- We need to add the sawing separately since it's not included in Mortise like it's included in AF local _, tblCoordsSaw = TranslateCoordsAndRotations(tblPart, nSide, CreateTblCoords(nXSaw, nYSaw)) local sSawCutTag = string.format('', tblRotation.x, tblRotation.y, tblRotation.z) sResultXml = sResultXml .. FormatXmlTag(sSawCutTag, gn_OPERATION_INDENTATION) end return sResultXml elseif nEndType == 4 then -- Dovetail Mortise (lohenpyrstötasku) if not IsRoughlyEqual(nRoundingRadiusMillis, nWidthMillis / 2) then --[[ Not set in ArchiFrame currently - do not warn -- When rounding is half of width, it is the same rounding that Cambium has AddCncErr( Millis2Meters(nXMillis), "Invalid Dovetail Mortise beg/end: Rounding radius must be half of width. Otherwise the output will not match the source." ) ]] end local x = TernaryIf(isBeginning, nXMillis + nMortiseHousingMillis, Meters2Millis(gnCurrTotLen) - nXMillis - nMortiseHousingMillis) local z = nZMillis local nTotalHeightMillis = nHeightMillis + nRoundingRadiusMillis local y, doesNotReachTop if nSide == EAFSideTop then y = Meters2Millis(gnCurrWidth) / 2 - nYMillis doesNotReachTop = nTotalHeightMillis < nZMillis elseif nSide == EAFSideFront then y = Meters2Millis(gnCurrHeight) / 2 - nYMillis doesNotReachTop = false -- always reaches top with this side elseif nSide == EAFSideBottom then y = Meters2Millis(gnCurrWidth) / 2 + nYMillis doesNotReachTop = (nZMillis + nTotalHeightMillis) <= Meters2Millis(gnCurrHeight) elseif nSide == EAFSideBack then y = Meters2Millis(gnCurrHeight) / 2 + nYMillis doesNotReachTop = false -- always reaches top with this side end if doesNotReachTop then -- You can't create dovetail mortises that look the same in Cambium as in AF -- when they don't reach the top of the plank. -- nRotationAngleDeg does not matter here. -- In case mortise reaches the top with angle 0, it will reach it with other angles also. AddCncErr( Millis2Meters(nXMillis), "Invalid Dovetail Mortise: Mortise must reach to the top of the plank. Otherwise the output will not match the source" ) end local nRotationXDeg = 0 local nRotationYDeg = 180 - nAngleSideDeg local nRotationZDeg = TernaryIf(isBeginning, -90 + nAngleTopDeg, -90 - nAngleTopDeg) local nFrameId, tblCoords, tblRotation = TranslateCoordsAndRotations( tblPart, nSide, CreateTblCoords(x, y), CreateTblRotations(nRotationXDeg, nRotationYDeg, nRotationZDeg) ) if isFeedBegFirst then nRotationAngleDeg = nRotationAngleDeg + 180 end local nTotalDepthMillis = nDepthMillis + nMortiseHousingMillis local sDovetailMortiseTag = string.format('') local sResultXml = FormatXmlTag(sDovetailMortiseTag, gn_OPERATION_INDENTATION) if nAngleTopDeg ~= 90 or nAngleSideDeg ~= 90 then -- We need to add the sawing separately since it's not included in Mortise like it's included in AF local sSawCutTag = string.format('', tblRotation.x, tblRotation.y, tblRotation.z) sResultXml = sResultXml .. FormatXmlTag(sSawCutTag, gn_OPERATION_INDENTATION) end return sResultXml end end function WriteReinforceXml(i, tblPart) if not gbBvxMarkReinforce then return "" end local nXMillis = Meters2Millis(ac_objectget("iMc", i, 2)) local nYMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nLengthMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nWidthMillis = Meters2Millis(ac_objectget("iMc", i, 5)) local nSide = ac_objectget("iMc", i, 7) local isBothSides = ac_objectget("iMc", i, 8) ~= 0 local tblSidesToReinforce if isBothSides then tblSidesToReinforce = {nSide, gTblOppositeSides[nSide]} else tblSidesToReinforce = {nSide} end local sResultXml = "" for _, nCurrentSide in ipairs(tblSidesToReinforce) do local nX1 = nXMillis - nLengthMillis / 2 local nX2 = nXMillis + nLengthMillis / 2 local nY1 = nYMillis - nWidthMillis / 2 local nY2 = nYMillis + nWidthMillis / 2 local nFrameId, tblCoords1 = TranslateCoordsAndRotations(tblPart, nCurrentSide, CreateTblCoords(nX1, nY1)) local _, tblCoords2 = TranslateCoordsAndRotations(tblPart, nCurrentSide, CreateTblCoords(nX2, nY2)) local y = math.min(tblCoords1.y, tblCoords2.y) local nHeightMillis = math.abs(tblCoords2.y - tblCoords1.y) local sMarkingHorizontalTag1 = string.format('', nHeightMillis) sResultXml = sResultXml .. FormatXmlTag(sMarkingHorizontalTag1, gn_OPERATION_INDENTATION) local sMarkingHorizontalTag2 = string.format('', nHeightMillis) sResultXml = sResultXml .. FormatXmlTag(sMarkingHorizontalTag2, gn_OPERATION_INDENTATION) end return sResultXml end function WriteJointBegOrEndXml(i, tblPart, nType) local nSide = ac_objectget("iMc", i, 2) local nXMillis = Meters2Millis(ac_objectget("iMc", i, 3)) local nLengthMillis = Meters2Millis(ac_objectget("iMc", i, 4)) local nDepthMillis = Meters2Millis(ac_objectget("iMc", i, 5)) local nJointType = ac_objectget("iMc", i, 8) % 8 local isBeginning = ((nType==EMcJointBeg) and (gnCurrentFeedDir == EBvxFeedDirBeg)) or ((nType==EMcJointEnd) and (gnCurrentFeedDir == EBvxFeedDirEnd)) local x = TernaryIf(isBeginning, nXMillis, Meters2Millis(gnCurrTotLen) - nXMillis) local sPosition = TernaryIf(isBeginning, "Start", "End") -- In Cambium the side is the opposite of what it is in AF local nFrameId = TranslateCoordsAndRotations(tblPart, GetOppositeSide(true, nSide)) if nJointType == 0 then local sScarfTag = string.format('') return FormatXmlTag(sScarfTag, gn_OPERATION_INDENTATION) elseif nJointType == 1 then local sScarfTag = string.format('') return FormatXmlTag(sScarfTag, gn_OPERATION_INDENTATION) end AddCncErr(0, string.format("ERROR: UNSUPPORTED BEG/END JOINT TYPE (%d) -SKIPPED", nJointType)) end function WriteContoursXml(tblPlank) local sResultXml = "" local tblContourBegInfo = CreateTblContourInfo(EMatProfileXzBeg, 0) if tblContourBegInfo then sResultXml = sResultXml .. WriteContourXml(tblPlank, tblContourBegInfo) end local tblContourEndInfo = CreateTblContourInfo(EMatProfileXzEnd, Meters2Millis(gnCurrTotLen)) if tblContourEndInfo then sResultXml = sResultXml .. WriteContourXml(tblPlank, tblContourEndInfo) end -- Bounding box for middle profile. -- Middle profile works by repeating the same pattern over and over again over its defined bounding box. -- Bounding box is either from the end of start contour to the start of end contour or from start to the end of plank. -- These lines works like ternary operators, so we don't throw errors in case tblContourBeg/EndInfo don't exist local nBoundingBoxMinX = tblContourBegInfo and tblContourBegInfo.tblBoundingBox.nMaxXMillis or 0 local nBoundingBoxMaxX = tblContourEndInfo and tblContourEndInfo.tblBoundingBox.nMinXMillis or Meters2Millis(gnCurrTotLen) local tblContourMidInfo = CreateTblContourInfo(EMatProfileXzMid, nBoundingBoxMinX) while (tblContourMidInfo and (tblContourMidInfo.tblBoundingBox.nMaxXMillis - EPS) < nBoundingBoxMaxX) do sResultXml = sResultXml .. WriteContourXml(tblPlank, tblContourMidInfo) tblContourMidInfo = CreateTblContourInfo(EMatProfileXzMid, tblContourMidInfo.tblBoundingBox.nMaxXMillis) end return sResultXml end function WriteContourXml(tblPlank, tblContourInfo) local sResultXml = "" -- Archiframe uses the coordinate system of the plank back side for this machining local n_CONTOUR_SIDE = EAFSideBack local nFrameId, tblStartCoords = TranslateCoordsAndRotations( tblPlank, n_CONTOUR_SIDE, CreateTblCoords(tblContourInfo.tblEdges[1].nX1Millis, tblContourInfo.tblEdges[1].nY1Millis) ) local sContourTag = string.format('', tblContourInfo.sAlign) -- TODO: which tool to use? sResultXml = sResultXml .. FormatXmlTag(sContourTag, gn_OPERATION_INDENTATION) local sContourPointTag = string.format('', tblStartCoords.x, tblStartCoords.y) sResultXml = sResultXml .. FormatXmlTag(sContourPointTag, gn_OPERATION_INDENTATION + 1) for i, tblEdge in ipairs(tblContourInfo.tblEdges) do local _, tblCoords = TranslateCoordsAndRotations( tblPlank, n_CONTOUR_SIDE, CreateTblCoords(tblEdge.nX2Millis, tblEdge.nY2Millis) ) local sTag if tblEdge.isArc then sTag = string.format('', tblEdge.sArcDirection) else sTag = string.format('', tblCoords.x, tblCoords.y) end sResultXml = sResultXml .. FormatXmlTag(sTag, gn_OPERATION_INDENTATION + 1) end sResultXml = sResultXml .. FormatXmlTag("", gn_OPERATION_INDENTATION) return sResultXml end function CreateTblContourInfo(sMatProfile, nContourStartXMillis) local nPolygonElemCount = ac_objectget(sMatProfile, -1) local REQUIRED_ELEMS = 3 * 3 if not nPolygonElemCount or nPolygonElemCount < REQUIRED_ELEMS then return nil end -- initialize values as +/- infinities local tblBoundingBox = { nMinXMillis = 1 / 0, nMinYMillis = 1 / 0, nMaxXMillis = -1 / 0, nMaxYMillis = -1 / 0, } local tblEdges = {} local nPrevEndpointX = nil local nPrevEndpointY = nil -- End contour is done in reverse, ie. when x increases the absolute coordinate decreases. local isEndContour = (sMatProfile == EMatProfileXzEnd) -- The polygon x and y coordinates are relative to the the contour start position -- (eg. contour can start from the begin, somewhere in the middle or end of the plank. -- So we need to translate these to absolute coordinates from the plank origin (0,0). -- Y-coordinate start point is always 0, so we don't need to do anything to that, but x coordinate varies so lets fix it. local function GetAbsoluteX(nXMillis) return TernaryIf(isEndContour, nContourStartXMillis - nXMillis, nContourStartXMillis + nXMillis) end local function UpdateBoundingBox(nXMillis, nYMillis) if nXMillis < tblBoundingBox.nMinXMillis then tblBoundingBox.nMinXMillis = nXMillis end if nYMillis < tblBoundingBox.nMinYMillis then tblBoundingBox.nMinYMillis = nYMillis end if tblBoundingBox.nMaxXMillis < nXMillis then tblBoundingBox.nMaxXMillis = nXMillis end if tblBoundingBox.nMaxYMillis < nYMillis then tblBoundingBox.nMaxYMillis = nYMillis end end local function AddEdge(nXMillis, nYMillis, nRadiusMillis, sArcDirection) local nAbsoluteXMillis = GetAbsoluteX(nXMillis) if nPrevEndpointX ~= nil then table.insert(tblEdges, { nX1Millis = GetAbsoluteX(nPrevEndpointX), nY1Millis = nPrevEndpointY, nX2Millis = nAbsoluteXMillis, nY2Millis = nYMillis, isArc = nRadiusMillis ~= nil, nRadiusMillis = nRadiusMillis, sArcDirection = sArcDirection, }) end UpdateBoundingBox(nAbsoluteXMillis, nYMillis) nPrevEndpointX = nXMillis nPrevEndpointY = nYMillis end for i = 1, nPolygonElemCount, 3 do local nStatus = ac_objectget(sMatProfile, i + 2) if nStatus < 100 then -- A normal straight line or point if it's the first element. local nXMillis = Meters2Millis(ac_objectget(sMatProfile, i)) local nYMillis = Meters2Millis(ac_objectget(sMatProfile, i + 1)) AddEdge(nXMillis, nYMillis) elseif nStatus >= 900 and nStatus < 1000 then -- Do nothing here, just check it so we don't throw unnecessary errors about unsupported status codes elseif nStatus >= 4000 and nStatus < 4100 then -- Arc/full circle, y=angledeg, if angle==360 then x=radius local nArcAngleDeg = ac_objectget(sMatProfile, i + 1) if math.abs(nArcAngleDeg) > 359 then AddCncErr(0, string.format("Full circles are not currently supported in table %s", sMatProfile)) else local nArcStartXMillis = nPrevEndpointX local nArcStartYMillis = nPrevEndpointY -- x and y coordinates of the arc center point were defined -- in the previous polygonal item (ie. the previous 3 elements) local nArcCenterXMillis = Meters2Millis(ac_objectget(sMatProfile, i - 3)) local nArcCenterYMillis = Meters2Millis(ac_objectget(sMatProfile, i - 2)) local nArcRadiusMillis = CalcDist(nArcStartXMillis, nArcStartYMillis, nArcCenterXMillis, nArcCenterYMillis) local nXDeltaMillis = nArcStartXMillis - nArcCenterXMillis local nYDeltaMillis = nArcStartYMillis - nArcCenterYMillis local nTotalArcAngleRad = math.atan2(nYDeltaMillis, nXDeltaMillis) + Deg2Rad(nArcAngleDeg) local nArcEndXMillis = nArcCenterXMillis + math.cos(nTotalArcAngleRad) * nArcRadiusMillis local nArcEndYMillis = nArcCenterYMillis + math.sin(nTotalArcAngleRad) * nArcRadiusMillis -- clockwise or counterclockwise local sArcDirection = TernaryIf(nArcAngleDeg < 0, "CW", "CCW") if isEndContour then sArcDirection = TernaryIf(sArcDirection == "CW", "CCW", "CW") end AddEdge(nArcEndXMillis, nArcEndYMillis, nArcRadiusMillis, sArcDirection) end else AddCncErr(0, string.format("Unsupported length shape code %d in table %s", nStatus, sMatProfile)) end if nStatus < 0 then -- End of the polygon, this should be the last element anyway but lets double check that the loop stops here break end end -- Lets filter out edges that we actually don't want to machine. -- These include straight lines on top or bottom of plank and lines that go vertically straight through the plank. -- Also lets skip lines that end in the same endpoint as the previous one. local tblFilteredEdges = {} for i, tblEdge in ipairs(tblEdges) do if tblEdge.isArc then -- Lets not skip any arcs table.insert(tblFilteredEdges, tblEdge) else local nEdgeMinX = math.min(tblEdge.nX1Millis, tblEdge.nX2Millis) local nEdgeMinY = math.min(tblEdge.nY1Millis, tblEdge.nY2Millis) local nEdgeMaxX = math.max(tblEdge.nX1Millis, tblEdge.nX2Millis) local nEdgeMaxY = math.max(tblEdge.nY1Millis, tblEdge.nY2Millis) local isTopOrBottomHorizontalEdge = IsRoughlyEqual(nEdgeMinY, nEdgeMaxY) and IsRoughlyEqual(nEdgeMinX, tblBoundingBox.nMinXMillis) and IsRoughlyEqual(nEdgeMaxX, tblBoundingBox.nMaxXMillis) and (IsRoughlyEqual(nEdgeMinY, 0) or IsRoughlyEqual(nEdgeMinY, Meters2Millis(gnCurrHeight))) local isFullHeightVerticalEdge = IsRoughlyEqual(nEdgeMinX, nEdgeMaxX) and IsRoughlyEqual(nEdgeMinY, 0) and IsRoughlyEqual(nEdgeMaxY, Meters2Millis(gnCurrHeight)) local shouldFilterEdge = isTopOrBottomHorizontalEdge or isFullHeightVerticalEdge or AreCoordsInSamePlace(tblEdge.nX1Millis, tblEdge.nY1Millis, tblEdge.nX2Millis, tblEdge.nY2Millis) if not shouldFilterEdge then table.insert(tblFilteredEdges, tblEdge) end end end return { tblEdges = tblFilteredEdges, tblBoundingBox = tblBoundingBox, sAlign = TernaryIf(isEndContour, "Right", "Left") } end function CreateTblBoard(nBoardQuantity) local tblPlankInfo = af_request("plankinfo") local tblBoard = {} tblBoard.isBoard = true tblBoard.sKey = gsCurrentLogGuid tblBoard.sName = GetInfoStr(gnBvxInfoName, tblPlankInfo) tblBoard.nReqQuantity = nBoardQuantity tblBoard.nThickness = Meters2Millis(gnCurrWidth) tblBoard.tblCadPosition = CreateCadPositionTbl(gbBvxRotateBoard90, tblPlankInfo) tblBoard.tblDimension = CreateTblDimension(gbBvxRotateBoard90) tblBoard.tblOutline = CreateBoardTblOutline(gbBvxRotateBoard90) tblBoard.tblInlines = CreateBoardTblInlines(gbBvxRotateBoard90) return tblBoard end function CreateBoardTblInlines(rotate90) local tblInlines = {} local tblPolyInfo = af_request ("getpoly") local tblInline = nil local nFirstInlineElemIndex = 0 for i, tblPointInfo in ipairs(tblPolyInfo.poly) do local sType = EPolygonTypeLine if tblPointInfo.isbeghole then tblInline = {} nFirstInlineElemIndex = i sType = EPolygonTypePoint end if tblInline ~= nil then local tblPolygonElem = CreateTblPolygonElem(sType, tblPointInfo.x, tblPointInfo.y, rotate90) table.insert(tblInline, tblPolygonElem) if tblPointInfo.isendcontour then -- line from last point back to the beginning local tblFirstPointInfo = tblPolyInfo.poly[nFirstInlineElemIndex] local tblLastLine = CreateTblPolygonElem(EPolygonTypeLine, tblFirstPointInfo.x, tblFirstPointInfo.y, rotate90) table.insert(tblInline, tblLastLine) table.insert(tblInlines, tblInline) tblInline = nil end end end return tblInlines end function CreateBoardTblOutline(rotate90) local tblPolyInfo = af_request("getpoly") local tblOutline = {} for i, tblPointInfo in ipairs(tblPolyInfo.poly) do local sType = TernaryIf(i == 1, EPolygonTypePoint, EPolygonTypeLine) local tblPolygonElem = CreateTblPolygonElem(sType, tblPointInfo.x, tblPointInfo.y, rotate90) table.insert(tblOutline, tblPolygonElem) if tblPointInfo.isendcontour then -- line from last point back to the beginning local tblLastLine = CreateTblPolygonElem(EPolygonTypeLine, tblPolyInfo.poly[1].x, tblPolyInfo.poly[1].y, rotate90) table.insert(tblOutline, tblLastLine) -- There are polys for possible windows and holes etc in this table, so lets not add them to the outline table break end end return tblOutline end function CreateTblPolygonElem(sType, nXMeters, nYMeters, rotate90) if rotate90 then nXMeters, nYMeters=nYMeters, gnCurrTotLen - nXMeters -- gnCurrTotLen added to match machining positions end return { ["type"] = sType, ["x"] = Meters2Millis(nXMeters), ["y"] = Meters2Millis(nYMeters), } end function CreateTblPart(nPlankQuantity) local tblPlankInfo = af_request("plankinfo") local tblPart = {} tblPart.isPlank = true tblPart.name = GetInfoStr(gnBvxInfoName, tblPlankInfo) tblPart.unit = GetInfoStr(gnBvxInfoUnit, tblPlankInfo) tblPart.grade = GetInfoStr(gnBvxInfoGrade, tblPlankInfo) tblPart.comments = GetInfoStr(gnBvxInfoComments, tblPlankInfo) tblPart.profile = GetInfoStr(gnBvxInfoProfile, tblPlankInfo) tblPart.material = GetInfoStr(gnBvxInfoMaterial, tblPlankInfo) tblPart.reqQuantity = nPlankQuantity tblPart.key = gsCurrentLogGuid tblPart.tblDimension = CreateTblDimension(false) tblPart.tblCadPosition = CreateCadPositionTbl(false, tblPlankInfo) return tblPart end function CreateTblDimension(rotate90) local tblDimension = {} if rotate90 then tblDimension.x = Meters2Millis(gnCurrHeight) tblDimension.y = Meters2Millis(gnCurrTotLen) tblDimension.z = Meters2Millis(gnCurrWidth) elseif gnCurrentFeedTableSide == EAFSideTop or gnCurrentFeedTableSide == EAFSideBottom then tblDimension.x = Meters2Millis(gnCurrTotLen) tblDimension.y = Meters2Millis(gnCurrWidth) tblDimension.z = Meters2Millis(gnCurrHeight) else tblDimension.x = Meters2Millis(gnCurrTotLen) tblDimension.y = Meters2Millis(gnCurrHeight) tblDimension.z = Meters2Millis(gnCurrWidth) end return tblDimension end function WriteInlinesXml(tblInlines, nIndentLvl) local sResultXml = FormatXmlTag("", nIndentLvl) for _, tblInline in ipairs(tblInlines) do sResultXml = sResultXml .. FormatXmlTag("", nIndentLvl + 1) for _, tblPolygonElem in ipairs(tblInline) do local sTag if tblPolygonElem.type == EPolygonTypeLine then sTag = string.format('', tblPolygonElem.x, tblPolygonElem.y) elseif tblPolygonElem.type == EPolygonTypePoint then sTag = string.format('', tblPolygonElem.x, tblPolygonElem.y) end sResultXml = sResultXml .. FormatXmlTag(sTag, nIndentLvl + 2) end sResultXml = sResultXml .. FormatXmlTag("", nIndentLvl + 1) end sResultXml = sResultXml .. FormatXmlTag("", nIndentLvl) return sResultXml end function WriteCadPositionXml(tblCadPosition, nIndentLvl) local sResultXml = FormatXmlTag('', nIndentLvl) local sCadPositionTag = string.format('', tblCadPosition.p0.x, tblCadPosition.p0.y, tblCadPosition.p0.z) sResultXml = sResultXml .. FormatXmlTag(sCadPositionTag, nIndentLvl + 1) sResultXml = sResultXml .. FormatXmlTag('', nIndentLvl) return sResultXml end function WriteOutlineXml(tblOutline, nIndentLvl) local sResultXml = FormatXmlTag("", nIndentLvl) for _, tblPolygonElem in ipairs(tblOutline) do local sTag if tblPolygonElem.type == EPolygonTypeLine then sTag = string.format('', tblPolygonElem.x, tblPolygonElem.y) elseif tblPolygonElem.type == EPolygonTypePoint then sTag = string.format('', tblPolygonElem.x, tblPolygonElem.y) end sResultXml = sResultXml .. FormatXmlTag(sTag, nIndentLvl + 1) end sResultXml = sResultXml .. FormatXmlTag("", nIndentLvl) return sResultXml end function WriteBoardXml(tblBoard) local sResultXml = "" local sPolygonalPartStartTag = string.format('') sResultXml = sResultXml .. FormatXmlTag(sPolygonalPartStartTag, gn_PART_INDENTATION) local n_INDENT_LVL_1 = gn_PART_INDENTATION + 1 sResultXml = sResultXml .. WriteFrameBoxXml(tblBoard.tblDimension, n_INDENT_LVL_1) sResultXml = sResultXml .. WriteOutlineXml(tblBoard.tblOutline, n_INDENT_LVL_1) sResultXml = sResultXml .. WriteInlinesXml(tblBoard.tblInlines, n_INDENT_LVL_1) sResultXml = sResultXml .. WriteOperationsXml(tblBoard, n_INDENT_LVL_1) sResultXml = sResultXml .. WriteCadPositionXml(tblBoard.tblCadPosition, n_INDENT_LVL_1) sResultXml = sResultXml .. FormatXmlTag('', gn_PART_INDENTATION) return sResultXml end -- tblPart is a part that is created using CreateTblPart function function WritePartXml(tblPart) local sResultXml = "" local tblDimension = tblPart.tblDimension local sRectangularPartTag = string.format('') sResultXml = sResultXml .. FormatXmlTag(sRectangularPartTag, gn_PART_INDENTATION) local n_INDENT_LVL_1 = gn_PART_INDENTATION + 1 sResultXml = sResultXml .. WriteFrameBoxXml(tblDimension, n_INDENT_LVL_1) sResultXml = sResultXml .. WriteOperationsXml(tblPart, n_INDENT_LVL_1) sResultXml = sResultXml .. WriteCadPositionXml(tblPart.tblCadPosition, n_INDENT_LVL_1) sResultXml = sResultXml .. FormatXmlTag("", gn_PART_INDENTATION) return sResultXml end function WriteFrameBoxXml(tblDimension, nIndentLvl) local sFrameBoxTag = string.format('', tblDimension.y, tblDimension.z) return FormatXmlTag(sFrameBoxTag, nIndentLvl) end function FilterNonCorePlanks() -- Filter out anything related to elements but not into core layer local tblIsCoreElem = {} local tblCorePlanks = {} for _, sPlankGuid in ipairs(gTblPlanks) do ac_objectopen(sPlankGuid) local tblPlankInfo = af_request("plankinfo", sPlankGuid) ac_objectclose() local sOwnerElemGuid = tblPlankInfo.ownerelemguid local isChildOfCoreElem = false if sOwnerElemGuid ~= nil then -- Check that owner element type is core isChildOfCoreElem = tblIsCoreElem[sOwnerElemGuid] if isChildOfCoreElem == nil then -- Not known yet - open the element local tblElem = af_request("elem_openparent", sOwnerElemGuid) for _, tblElem in ipairs(tblElem.tblelems) do tblIsCoreElem[tblElem.guid] = (tblElem.type == "core") end isChildOfCoreElem = tblIsCoreElem[sOwnerElemGuid] end end if (not sOwnerElemGuid) or isChildOfCoreElem then table.insert(tblCorePlanks, sPlankGuid) end end gTblPlanks = tblCorePlanks end -- Called to do all that needs to be done -- sFilePath is full path name for the result file function OnSaveList(sFilePath) if not SettingsDlg() then gbCancel=true return end local tblFeedDirs, tblFeedTableSides if gb_DEBUG_WRITE_ALL_FEED_ORIENTATIONS then tblFeedDirs = { EBvxFeedDirBeg, EBvxFeedDirEnd } tblFeedTableSides = { EAFSideTop, EAFSideFront, EAFSideBottom, EAFSideBack } else tblFeedDirs = { gnBvxFeedDir } tblFeedTableSides = { gnBvxFeedTableSide } end for nFeedDirCounter, nFeedDir in ipairs(tblFeedDirs) do gnCurrentFeedDir = nFeedDir for nFeedTableSideCounter, nFeedTableSide in ipairs(tblFeedTableSides) do gnCurrentFeedTableSide = nFeedTableSide local isFirstOrientation= (nFeedDirCounter == 1) and (nFeedTableSideCounter == 1) WriteCurrentFeedOrientation(sFilePath, isFirstOrientation) end end end function WriteReinforcementPlanks(sFilePath, isFirstOrientation) for _, sPlankGuid in ipairs(gTblPlanks) do if isFirstOrientation then -- This allows mc_reinforce2tbl request to change gTblPlanks into reinforcement planks. -- Only do this once per plank, otherwise the same reinforcement will appear multiple times in gtBLPlanks -- (in case we are debugging with gb_DEBUG_WRITE_ALL_FEED_ORIENTATIONS). af_request("mc_reinforce2planks", sPlankGuid) end end -- If writing reinforcements, they will replace actual planks in gTblPlanks so lets keep this in memory -- in case we are debugging and writing multiple orientations at once. local tblOriginalPlanks = gTblPlanks -- This request combined with the previous requests to mc_reinforce2planks -- will replace the normal planks in gTblPlanks with reinforcement planks instead. af_request("mc_reinforce2tbl", "REINF/[parentid]-001") local sReinforceFilePath = DoFileName(sFilePath, "_reinforce") OnSaveListIntElem(sReinforceFilePath) -- Lets restore the normal planks so we can write other orientations if needed gTblPlanks = tblOriginalPlanks end function WriteMultipleFiles(sFilePath) -- Choose file for each plank, planks are sorted by id -- Fields: numPlanks=number of collected planks, tblPlanks=plank list, sortkey local OWNER_NONE ="$$$NO OWNER$$$" local tblElements = {} -- { ownerId => tblElement } for _, sPlankGuid in ipairs(gTblPlanks) do ac_objectopen(sPlankGuid) -- Related to any element? local tblPlankInfo = af_request("plankinfo", sPlankGuid) local ownerElemId = tblPlankInfo.ownerelemid if ownerElemId == nil then ownerElemId = OWNER_NONE end local tblElement = tblElements[ownerElemId] if tblElement == nil then tblElement = {} tblElement.tblPlanks = {} tblElement.sortkey = ownerElemId tblElement.owner = ownerElemId tblElements[ownerElemId] = tblElement end table.insert(tblElement.tblPlanks, sPlankGuid) ac_objectclose(sPlankGuid) end -- First we need to put the elements into normal table with numbers as indices -- because you can't sort table with strings as keys local tblSortedElems ={} for _, tblElement in pairs(tblElements) do table.insert(tblSortedElems, tblElement) end -- And now the table can be sorted table.sort(tblSortedElems, function (tblElem1, tblElem2) return tblElem1.sortkey < tblElem2.sortkey end) for _, tblElement in ipairs(tblSortedElems) do gTblPlanks= tblElement.tblPlanks if tblElement.owner == OWNER_NONE then --Write without any addition to the file name OnSaveListIntElem(sFilePath) else local sElemSpecificFilePath = DoFileName(sFilePath, "_" .. tblElement.owner) OnSaveListIntElem(sElemSpecificFilePath) end end end function WriteCurrentFeedOrientation(sFilePath, isFirstOrientation) if gb_DEBUG_WRITE_ALL_FEED_ORIENTATIONS then sFilePath = DoFileName(sFilePath, string.format("_%d%d", gnCurrentFeedDir, gnCurrentFeedTableSide)) end if gbBvxWriteOnlyCore then FilterNonCorePlanks() end -- Write if gbBvxWriteSepFiles then WriteMultipleFiles(sFilePath) else -- All into single file OnSaveListIntElem(sFilePath) end if gbBvxWriteReinforce then WriteReinforcementPlanks(sFilePath, isFirstOrientation) end end function GetMachiningQuantity(tblElemGuids, tblUsedIds, nCurrElemIdx, FuncShouldWriteElem) local nMachiningQuantity = 0 local sCurrElemGuid = tblElemGuids[nCurrElemIdx] local sElemId = ac_objectget('#id') -- There may be multiple planks with the same id, they are next to each other in gTblPlanks. -- This means the planks will have the same machinings but they are in different CAD positions. if (not tblUsedIds[sElemId]) or (not AreSimilarElems(sCurrElemGuid, tblElemGuids[nCurrElemIdx - 1])) then nMachiningQuantity = 1 local nSimilarElemCount = 1 while AreSimilarElems(sCurrElemGuid, tblElemGuids[nCurrElemIdx + nSimilarElemCount]) do if FuncShouldWriteElem(tblElemGuids[nCurrElemIdx + nSimilarElemCount]) then nMachiningQuantity = nMachiningQuantity + 1 end nSimilarElemCount = nSimilarElemCount + 1 end end return nMachiningQuantity end -- Saves btl as element -- strFileName is full path name for the result file function OnSaveListIntElem(strFileName) -- Write -- Frame-kapulat -> btl local hFile=io.open(strFileName, "utf8,wt") if not hFile then AddErrMsg("(NO PART)", 0, string.format("ERROR: Opening file %s writing failed", strFileName)) return end io.output( hFile ) io.write(FormatXmlTag('', 0)) io.write(FormatXmlTag("", 0)) io.write(FormatXmlTag("", 0)) io.write(FormatXmlTag('', 0)) io.write(FormatXmlTag("", 1)) -- Write planks/reinforcement planks (both are in gTblPlanks array) local tblUsedIds = {} for i, sPlankGuid in ipairs(gTblPlanks) do ac_objectopen(sPlankGuid) local function ShouldWritePlank(sPlankGuid) return gbBvxWriteTopBottom or (not IsTopOrBottomPlank(sPlankGuid)) end local nPlankMachiningQuantity = GetMachiningQuantity(gTblPlanks, tblUsedIds, i, ShouldWritePlank) local sLibGuid = GetLibGuidFirstPart() if sLibGuid == "{b42736c9-1a6b-4166-bc7c-6f3634e90c73}" then -- Frame plank if ShouldWritePlank(sPlankGuid) then BvxGetPlankBasicParams(sPlankGuid) RotateElementIfNeeded(false) local tblPart = CreateTblPart(nPlankMachiningQuantity) io.write(WritePartXml(tblPart)) tblUsedIds[ac_objectget('#id')] = true end end ac_objectclose() end -- Write boards local tblBoardGuids = GetSortedBoardGuids() tblUsedIds = {} for i, sBoardGuid in ipairs(tblBoardGuids) do ac_objectopen(sBoardGuid) local function ShouldWriteBoard() return true end local nBoardMachiningQuantity = GetMachiningQuantity(tblBoardGuids, tblUsedIds, i, ShouldWriteBoard) BvxGetBoardBasicParams(sBoardGuid) RotateElementIfNeeded(true) local tblBoard = CreateTblBoard(nBoardMachiningQuantity) io.write(WriteBoardXml(tblBoard)) tblUsedIds[ac_objectget('#id')] = true ac_objectclose() end io.write(FormatXmlTag("", 1)) io.write(FormatXmlTag("", 0)) io.close( hFile ) end -- Globals gTblOppositeSides = { [EAFSideTop] = EAFSideBottom, [EAFSideBottom] = EAFSideTop, [EAFSideFront] = EAFSideBack, [EAFSideBack] = EAFSideFront, [EAFSideBegin] = EAFSideEnd, [EAFSideEnd] = EAFSideBegin, } gn_FLANK_ANGLE_DEG = 15 gn_PART_INDENTATION = 3 gn_OPERATION_INDENTATION = gn_PART_INDENTATION + 2 gb_DEBUG_WRITE_ALL_FEED_ORIENTATIONS = false -- Default values for settings dialog gbBvxWriteSplinterFree = false gbBvxWriteTopBottom = true gbBvxWriteSepFiles = false gbBvxWriteOnlyCore = false gnBvxInfoName = EInfoFullID gnBvxInfoUnit = EInfoUsage gnBvxInfoGrade = EInfoGrade gnBvxInfoComments = EInfoMatIdIfNotSize gnBvxInfoProfile = EInfoNone gnBvxInfoMaterial = EInfoMatIdFull gbBvxWriteReinforce = false gbBvxMarkReinforce = false gnBvxFeedDir = EBvxFeedDirBeg -- Which end goes first to the machine gnBvxFeedTableSide = EAFSideNone -- Which side of the log is against the machine table (none = automatic rotation) gnBvxGrooveWidthWarnThresholdMillis = 0 gnBvxPrintPartIdSide = EAFSideNone gsBvxPrintPartIdContent = "#fullid#" gnBvxPrintPartIdTextHeightMillis = 15 gnBvxMarkingTextHeightMillis = 0 gbBvxRotateBoard90 = false