-- ############################################ -- ################ GLOBALS ################### -- ############################################ local EPS = 0.0001 local EXTENSION = "cdt" local PI = 3.1415926535 local RAD2DEG = 180/PI local DEG2RAD = PI/180 local ESide = { Top = 1, Front = 2, Bottom = 3, Back = 4, Begin = 5, End = 6 } local EMachiningType = { Drilling = 302, Marking = 303, Nailing = 307 } local ELayerType = { Paneling = 2 } local EType = { Plank = 1, Board = 4 } local EBoardType = { Board = 1, Insulation = 201 } local EFilter = { OnVisibleLayer = 2 } local EExportTarget = { Production = 1, RandekViewer_6_0_0_11 = 2 } local EPiecesToExport = { Selection = 1, SelectionAndRelated = 2, SelectionAndRelatedExclHidden = 3 } -- ############################################ -- ############# ARCHIFRAME HOOKS ############# -- ############################################ function OnInit(plnFileName) gnCncErrCount=0 gtblUsedIds = {} gnBoardPieceNum=500000 local fileName = ac_environment("parsetext", "") if fileName == nil or #fileName == 0 then fileName = plnFileName end return string.format("%s.%s", fileName, EXTENSION), EXTENSION end function OnSaveList(fileName) local fields = {} fields[1] = { cfgonly = 1, type = 1, prompt = "Export target", key = "cdttarget", defvalue = EExportTarget.Production, valuelist = "\"1:Production\"," .. "\"2:Randek viewer 6.0.0.11\"," } fields[2] = { cfgonly = 1, type = 1, prompt = "Pieces to export", key = "piecestoexport", defvalue = EPiecesToExport.Selection, valuelist = "\"1:Selection\"," .. "\"2:Selection + related elements\"," .. "\"3:Selection + related elements (exluding hidden layers)\"" } fields[3] = { cfgonly = 1, type = 2, prompt = "Exclude insulation inside modules", key = "excludeinsulationinsidemodules", defvalue = 0 } fields[4] = { cfgonly = 1, type = 2, prompt = "Generate multiple studs (MSB)", key = "generatemultiplestuds", defvalue = 0 } gHelpAnchor = "afdlgcdtexport" ok, error=ac_optiondlg("LDWW", "CDT 3.1 export settings", fields) if not ok then return end local exportTarget = fields[1].value local piecesToExport = fields[2].value local excludeInsulationInsideModules = fields[3].value local generateMultipleStuds = fields[4].value local projectNumber = ac_environment("parsetext", "") -- Remove extension fileName=fileName:sub(1, -(#EXTENSION+2)) local selection = af_request("getsellisting") if selection == nil then return end for elementGUID, element in pairs(CollectPiecesToExport(selection, piecesToExport)) do if element.layers.core == nil then PrintWarning("No core layer in element, skipping", nil, elementGUID) else WriteElement( element, elementGUID, exportTarget, excludeInsulationInsideModules, generateMultipleStuds, projectNumber, fileName ) end end end -- ############################################ -- ############### MAIN LOGIC ################ -- ############################################ -- From Archiframe to CDT coordinate system -- Position/orientation of pieces will be relative to this plane. -- Modify it further if other orientation is wanted (flipped, rotated etc...) function AFToCDTPlane(plane) plane.vecy, plane.vecz = plane.vecz, plane.vecy plane.vecz = Flip3D(plane.vecz) plane.begc = Add3D(plane.begc, Scale3D(plane.vecz, -plane.width * 0.5)) plane.endc = Add3D(plane.endc, Scale3D(plane.vecz, -plane.width * 0.5)) return plane end function CollectFromSelectionAndRelated(selection, excludeHidden) local result = {} local layerCache = {} for _, item in ipairs(selection) do local GUID = item.guid local info = af_request("plankinfo", GUID) local layerGUID = info.ownerelemguid if layerGUID == nil then PrintWarning("Piece is not part of a layer - skipping", item.id, GUID) else -- New layer if layerCache[layerGUID] == nil then local element = af_request("elem_openparent", layerGUID) if element == nil then PrintWarning("Layer is not part of an element - skipping", nil, layerGUID) else -- New element if result[element.guid] == nil then result[element.guid] = { id = element.id, layers = {} } local elementPlane = AFToCDTPlane(af_request("plankinfo", element.guid)) -- Add planks / boards from every related layer for _, layer in pairs(element.tblelems) do layerCache[layer.guid] = element.guid -- Mark this layer as handled local quantities = af_request("elem_quantities", layer.guid, 0).tblelems[1] result[element.guid].layers[layer.type] = { guid = layer.guid, xsize = layer.xsize, ysize = layer.ysize, thickness = layer.thickness, zoffcore = af_request("plankinfo", layer.guid, elementPlane).elemdata.bz1, xmlutf8 = quantities.xmlutf8, boards = {}, planks = {} } if quantities.layertypenum == 2 and quantities.tblboards ~= nil and quantities.tblplanks == nil then PrintWarning("Panel must be exploded - skipping layer", layer.id, layer.guid) else if quantities.tblboards ~= nil then for _, board in ipairs(quantities.tblboards) do if not excludeHidden or ac_environment ("filterelem", board.guid, EFilter.OnVisibleLayer) then table.insert(result[element.guid].layers[layer.type].boards, af_request("plankinfo", board.guid, elementPlane)) end end end if quantities.tblplanks ~= nil then for _, plank in ipairs(quantities.tblplanks) do if not excludeHidden or ac_environment ("filterelem", plank.guid, EFilter.OnVisibleLayer) then table.insert(result[element.guid].layers[layer.type].planks, af_request("plankinfo", plank.guid, elementPlane)) end end end end end end end end end end return result end function CollectFromSelectionOnly(selection) local layerCache = {} local result = {} for _, item in ipairs(selection) do local GUID = item.guid local info = af_request("plankinfo", GUID) local layerGUID = info.ownerelemguid if layerGUID == nil then PrintWarning("Piece is not part of a layer - skipping", item.id, GUID) else -- New layer if layerCache[layerGUID] == nil then local element = af_request("elem_openparent", layerGUID) if element == nil then PrintWarning("Layer is not part of an element - skipping", nil, layerGUID) else -- New element if result[element.guid] == nil then result[element.guid] = { id = element.id, plane = AFToCDTPlane(af_request("plankinfo", element.guid)), layers = {} } end local elementPlane = result[element.guid].plane for _, layer in pairs(element.tblelems) do if layer.guid == layerGUID then local quantities = af_request("elem_quantities", layer.guid, 0).tblelems[1] if quantities.layertypenum == 2 and quantities.tblboards ~= nil and quantities.tblplanks == nil then PrintWarning("Panel must be exploded - skipping layer", layer.id, layer.guid) else layerCache[layerGUID] = { elementGUID = element.guid, layerType = layer.type } result[element.guid].layers[layer.type] = { guid = layer.guid, xsize = layer.xsize, ysize = layer.ysize, thickness = layer.thickness, zoffcore = af_request("plankinfo", layer.guid, elementPlane).elemdata.bz1, xmlutf8 = quantities.xmlutf8, boards = {}, planks = {}, } end end end end end local cached = layerCache[layerGUID] if cached ~= nil then local layerType = cached.layerType local elementGUID = cached.elementGUID local layer = result[elementGUID].layers[layerType] local elementPlane = result[elementGUID].plane if info.type == EType.Plank then table.insert(layer.planks, af_request("plankinfo", GUID, elementPlane)) elseif info.type == EType.Board then table.insert(layer.boards, af_request("plankinfo", GUID, elementPlane)) else PrintWarning("Only planks and boards are supported - skipping piece", item.id, GUID) end end end end return result end -- Element->Layer->Piece (plank/board) function CollectPiecesToExport(selection, piecesToExport) if piecesToExport == EPiecesToExport.SelectionAndRelated or piecesToExport == EPiecesToExport.SelectionAndRelatedExclHidden then return CollectFromSelectionAndRelated(selection, piecesToExport == EPiecesToExport.SelectionAndRelatedExclHidden) end return CollectFromSelectionOnly(selection) end function WriteElement( element, elementGUID, exportTarget, excludeInsulationInsideModules, generateMultipleStuds, projectNumber, fileName) local writer = CDTFileWriter:New() ac_objectopen(elementGUID) local elementId = ac_objectget("#id") ac_objectclose() local plating, studs, topPlateNailgun, bottomPlateNailgun, insulationPieces, boarding, branders, sawcuts = GetComponents(element, excludeInsulationInsideModules, generateMultipleStuds) if plating == nil then ac_environment("tolog", string.format("(CDT): An error occured - skipping element (id=%s, guid=%s)", elementId, elementGUID)) return end -- CDT Header if exportTarget == EExportTarget.Production then writer:AppendVersion("3,1") elseif exportTarget == EExportTarget.RandekViewer_6_0_0_11 then writer:AppendVersion("3.1") else assert(false) end writer:AppendTXT("Exported by ArchiFrame") writer:AppendPNO(projectNumber) writer:AppendELD("EXTERIOR") writer:AppendDNO(elementId) writer:AppendELF("OUTSIDE") -- Examples only include OUTSIDE writer:AppendELM( element.layers.core.xsize, element.layers.core.ysize, element.layers.core.thickness, 1 ) writer:AppendNewLine() writer:AppendTXT("FRAMING STATION") writer:AppendTS(CDTAABB(plating.topPlate), "-", plating.topPlate.matname, "q") writer:AppendBS(CDTAABB(plating.bottomPlate), "-", plating.bottomPlate.matname, "q") AppendElement (studs, nil, writer) AppendSubElements (studs.subElements, writer) AppendOutsideBoards (boarding.outside.boards, writer) AppendInsideBoards (boarding.inside.boards, writer) AppendBranders (branders, writer) AppendInsulationPieces (insulationPieces, writer) AppendSawLines (sawcuts, 1, writer) AppendDrillings (plating.topPlateDrillings, "DRILLING_TOPRAIL", 0, writer) AppendDrillings (plating.bottomPlateDrillings, "DRILLING_BOTTOMRAIL", 0, writer) AppendPlatingNailings ( plating.topPlate, plating.topPlateIntersections, topPlateNailgun, plating.bottomPlate, plating.bottomPlateIntersections, bottomPlateNailgun, writer ) AppendTopPlateMarkings (plating.topPlateMarkings, writer) AppendNailLines (boarding.outside.nailLines, "NAILING_BOARD_OUTSIDE", writer) AppendNailLines (boarding.inside.nailLines, "NAILING_BOARD_INSIDE", writer) writer:AppendEOF() -- Replace invalid filename characters in elementId with an underscore elementId = elementId:gsub('[<>:"/\\|?*]', '_') fileName = string.format("%s_%s.%s", fileName, elementId, EXTENSION) writer:WriteToFile(fileName) end function IsPlateNailgunStrValid(str) if #str == 0 then return true end if #str > 4 then return false end local number = tonumber(str) if number == nil or math.floor(number) ~= number or number < 0 then return false end for i = 1, #str do local number = tonumber(str:sub(i,i)) if number == 0 or number > 4 then return false end end return true end function GetPlatingNailguns(xmlutf8) local nailgun = string.match(xmlutf8, " cnc_nailgun=\"(.-)\"") if nailgun == nil then return "", "" end local top = "" local bottom = "" for expr in string.gmatch(nailgun, "%S+") do local key, val = string.match(expr, "(.+)=(.*)") if string.lower(key) == "top" then top = val elseif string.lower(key) == "bottom" then bottom = val else PrintError(string.format("Invalid plate nailgun '%s' in cnc_nailgun for core layer, should either be 'top' or 'bottom'", key)) return nil end if #top ~= 0 and #bottom~=0 then return top, bottom end end return top, bottom end function GetComponents(element, excludeInsulationInsideModules, generateMultipleStuds) local topPlateNailgun = "" local bottomPlateNailgun = "" local plating = nil local studs = nil local insulationPieces = nil local branders = {} local sawcuts = {} local boarding = { inside = { boards = {}, nailLines = {} }, outside = { boards = {}, nailLines = {} } } for name, layer in pairs(element.layers) do if name == "core" then topPlateNailgun, bottomPlateNailgun = GetPlatingNailguns(layer.xmlutf8) if topPlateNailgun == nil then return nil elseif not IsPlateNailgunStrValid(topPlateNailgun) then PrintError(string.format("Invalid top plate nailgun '%s' in cnc_nailgun for core layer, should be a sequence of max 4 numbers between 1-4. Example: 'top=134'", topPlateNailgun)) return nil elseif not IsPlateNailgunStrValid(bottomPlateNailgun) then PrintError(string.format("Invalid bottom plate nailgun '%s' in cnc_nailgun for core layer, should be a sequence of max 4 numbers between 1-4. Example: 'bottom=14'", bottomPlateNailgun)) return nil end plating = GetPlating(layer.planks, layer.xsize) if plating == nil then return nil end studs = GetStuds(layer.planks, plating.bottomPlate, plating.topPlate, generateMultipleStuds) insulationPieces = GetInsulationPieces(layer.boards, studs, excludeInsulationInsideModules) elseif string.match(name, "boarding_ext.*") then local ok = GetBoarding(name, "boarding_ext", layer, boarding.outside) if not ok then return nil end elseif string.match(name, "boarding_int.*") then local ok = GetBoarding(name, "boarding_int", layer, boarding.inside) if not ok then return nil end elseif string.match(name, "finish_ext.*") or string.match(name, "finish_int.*") or string.match(name, "extstud.*") or string.match(name, "intstud.*") then GetBranders(layer.planks, branders) else PrintWarning(string.format("Unknown layer \"%s\", skipping", name), layer.id, layer.guid) end GetSawcuts(layer, element.plane, sawcuts) end return plating, studs, topPlateNailgun, bottomPlateNailgun, insulationPieces, boarding, branders, sawcuts end function FindBottomPlateIdx(planks, layerWidth) local bottomPlateIdx = -1 for i, plank in ipairs(planks) do if bottomPlateIdx == -1 or plank.elemdata.by1 < planks[bottomPlateIdx].elemdata.by1 then bottomPlateIdx = i end end return bottomPlateIdx end function FindTopPlateIdx(planks, layerWidth) local topPlateIdx = -1 for i, plank in ipairs(planks) do if topPlateIdx == -1 or plank.elemdata.by2 > planks[topPlateIdx].elemdata.by2 then topPlateIdx = i end end return topPlateIdx end function GetBottomPlateIntersections(bottomPlate, planks, result, i) if i > 3 then -- Recurse limit return end for _, plank in pairs(planks) do if math.abs(bottomPlate.elemdata.by2 - plank.elemdata.by1) < EPS then if IsVertical(plank) then table.insert(result, { x1 = plank.elemdata.bx1, x2 = plank.elemdata.bx2, z1 = plank.elemdata.bz1, z2 = plank.elemdata.bz2 }) elseif IsHorizontal(plank) then GetBottomPlateIntersections(plank, planks, result, i+1) end end end end function GetTopPlateIntersections(topPlate, planks, result, i) if i > 3 then -- Recurse limit return end for _, plank in pairs(planks) do if math.abs(topPlate.elemdata.by1 - plank.elemdata.by2) < EPS then if IsVertical(plank) then table.insert(result, { x1 = plank.elemdata.bx1, x2 = plank.elemdata.bx2, z1 = plank.elemdata.bz1, z2 = plank.elemdata.bz2 }) elseif IsHorizontal(plank) then GetTopPlateIntersections(plank, planks, result, i + 1) end end end end function GetPlating(planks, layerWidth) local result = {} if #planks < 2 then PrintError("Atleast two planks has to exported") return nil end local bottomPlateIdx = FindBottomPlateIdx(planks, layerWidth) local bottomPlate = planks[bottomPlateIdx] table.remove(planks, bottomPlateIdx) if not IsValidPlate(bottomPlate, layerWidth) then PrintError("Invalid bottom plate, make sure the bottom plate is horizontal and spans the whole element", bottomPlate.id, bottomPlate.guid) return nil end local bottomPlateDrillings, bottomPlateMarkings = GetPlateOperations(bottomPlate) local topPlateIdx = FindTopPlateIdx(planks, layerWidth) local topPlate = planks[topPlateIdx] table.remove(planks, topPlateIdx) if not IsValidPlate(topPlate, layerWidth) then PrintError("Invalid top plate, make sure the top plate is horizontal and spans the whole element", topPlate.id, topPlate.guid) return nil end local topPlateDrillings, topPlateMarkings = GetPlateOperations(topPlate) -- Studs intersecting with top/bottom plate local topPlateIntersections = {} local bottomPlateIntersections = {} GetTopPlateIntersections(topPlate, planks, topPlateIntersections, 1) GetBottomPlateIntersections(bottomPlate, planks, bottomPlateIntersections, 1) return { bottomPlate = bottomPlate, bottomPlateDrillings = bottomPlateDrillings, bottomPlateMarkings = bottomPlateMarkings, bottomPlateIntersections = bottomPlateIntersections, topPlate = topPlate, topPlateDrillings = topPlateDrillings, topPlateMarkings = topPlateMarkings, topPlateIntersections = topPlateIntersections } end function IsBitSet(value, i) local bits = {} while value > 0 do if value % 2 == 1 then table.insert(bits, true) else table.insert(bits, false) end value = math.floor(value * 0.5) end i = i + 1 if i > #bits then return false end return bits[i] end function GetSideDepth(plank, side) if side == ESide.Top or side == ESide.Bottom then return plank.height elseif side == ESide.Front or side == ESide.Back then return plank.width elseif side == ESide.Begin or side == ESide.End then return plank.len else assert(false) end return plank.len end function GetPlateOperations(plate) local drillings = {} local markings = {} ac_objectopen(plate.guid) local count = ac_objectget("iMc" ,-1) if count == nil then return end for i = 1, count do local type_ = ac_objectget("iMc", i, 1) if type_ == EMachiningType.Drilling then if plate.elemdata.tblSides == nil then PrintWarning("Cannot export drillings, elemdata.tblSides is not available. Please upgrade to latest ArchiFrame version.", plate.id, plate.guid) else local x = ac_objectget("iMc", i, 2) local y = ac_objectget("iMc", i, 3) local diameter = ac_objectget("iMc", i, 4) local depth = ac_objectget("iMc", i, 5) local side = ac_objectget("iMc", i, 6) if math.abs(depth) < EPS then depth = GetSideDepth(plate, side) end local plane = plate.elemdata.tblSides[side] local p1 = Add3D(plane.origc, Add3D( Scale3D(plane.vecx, x), Scale3D(plane.vecy, y) ) ) local p2 = Add3D(p1, Scale3D(plane.vecz, -depth)) table.insert(drillings, {p1 = p1, p2 = p2, diameter = diameter}) end elseif type_ == EMachiningType.Marking then local x1 = ac_objectget("iMc", i, 2) -- local y1 = ac_objectget("iMc", i, 3) -- local x2 = ac_objectget("iMc", i, 4) -- local y2 = ac_objectget("iMc", i, 5) -- local xPosText = ac_objectget("iMc", i, 6) -- local yPosText = ac_objectget("iMc", i, 7) -- local fontSize = ac_objectget("iMc", i, 8) -- local groFace = ac_objectget("iMc", i, 9) -- local textOffset = ac_objectget("iMc", i, 10) -- local flags = ac_objectget("iMc", i, 11) -- local ArchiFrameManages = IsBitSet(flags, 0) -- local hasLine = not IsBitSet(flags, 2) -- --- ^ -- --- |___Strange -- -- CDT 3.1 only supports text markings local text = ac_objectget("iMcStr", i, 1) if text ~= nil and #text > 0 then table.insert(markings, {x = x1, text = text}) end end end ac_objectclose() return drillings, markings end function InsertStud(plank, target) if IsHorizontal(plank) then table.insert(target.horizontal, plank) elseif IsVertical(plank) then table.insert(target.vertical, plank) else table.insert(target.angled, plank) end end function GenerateMultipleStuds(verticalStuds) -- table.sort(verticalStuds, function(a, b) return a.elemdata.bx1 < b.elemdata.bx1 end) local multipleStuds = {} local currGroup = {} local zMin = 0.0 local zMax = 0.0 local i = 1 while i < #verticalStuds do local curr = verticalStuds[i] local next_ = verticalStuds[i + 1] if next_.elemdata.bx1 - curr.elemdata.bx2 < EPS and math.abs(curr.elemdata.y1 - next_.elemdata.y1) < EPS and math.abs(curr.elemdata.y2 - next_.elemdata.y2) < EPS then -- New group if #currGroup == 0 then zMin = curr.elemdata.bz1 zMax = curr.elemdata.bz2 else zMin = Min(zMin, next_.elemdata.bz1) zMax = Max(zMax, next_.elemdata.bz2) end table.insert(currGroup, curr) table.remove(verticalStuds, i) elseif #currGroup > 0 then table.insert(currGroup, curr) table.remove(verticalStuds, i) table.insert(multipleStuds, { studs = currGroup, zMin = zMin, zMax = zMax}) currGroup = {} else i = i + 1 -- Only increment if an entry is not removed from studs.vertical end end if #currGroup > 0 then table.insert(currGroup, verticalStuds[#verticalStuds]) table.remove(verticalStuds, #verticalStuds) table.insert(multipleStuds, { studs = currGroup, zMin = zMin, zMax = zMax}) end return multipleStuds end function GetStuds(planks, bottomPlate, topPlate, generateMultipleStuds) local studs = { horizontal = {}, vertical = {}, angled = {}, modules = {}, subElements = {}, multipleStuds = {} } table.sort(planks, function(a, b) return a.elemdata.bx1 < b.elemdata.bx1 end) for _, plank in pairs(planks) do ac_objectopen(plank.guid) local moduleId = ac_objectget("iElemModule") local subElementId = ac_objectget("iSubElementId") ac_objectclose() local target = studs if subElementId ~= nil and subElementId ~= "" then -- New sub element if target.subElements[subElementId] == nil then target.subElements[subElementId] = { horizontal = {}, vertical = {}, angled = {}, modules = {}, multipleStuds = {}, xMin = plank.elemdata.bx1, yMin = plank.elemdata.by1, zMin = plank.elemdata.bz1, xMax = plank.elemdata.bx2, yMax = plank.elemdata.by2, zMax = plank.elemdata.bz2 } target = target.subElements[subElementId] else target = target.subElements[subElementId] -- Extend sub element box target.xMin = Min(plank.elemdata.bx1, target.xMin) target.yMin = Min(plank.elemdata.by1, target.yMin) target.zMin = Min(plank.elemdata.bz1, target.zMin) target.xMax = Max(plank.elemdata.bx2, target.xMax) target.yMax = Max(plank.elemdata.by2, target.yMax) target.zMax = Max(plank.elemdata.bz2, target.zMax) end end if moduleId ~= nil and moduleId ~= "" then -- New module if target.modules[moduleId] == nil then target.modules[moduleId] = { horizontal = {}, vertical = {}, angled = {}, xMin = plank.elemdata.bx1, yMin = plank.elemdata.by1, zMin = plank.elemdata.bz1, xMax = plank.elemdata.bx2, yMax = plank.elemdata.by2, zMax = plank.elemdata.bz2 } target = target.modules[moduleId] else target = target.modules[moduleId] -- Extend module box target.xMin = Min(plank.elemdata.bx1, target.xMin) target.yMin = Min(plank.elemdata.by1, target.yMin) target.zMin = Min(plank.elemdata.bz1, target.zMin) target.xMax = Max(plank.elemdata.bx2, target.xMax) target.yMax = Max(plank.elemdata.by2, target.yMax) target.zMax = Max(plank.elemdata.bz2, target.zMax) end end InsertStud(plank, target) end if generateMultipleStuds == 1 then studs.multipleStuds = GenerateMultipleStuds(studs.vertical) for _, subElement in pairs(studs.subElements) do subElement.multipleStuds = GenerateMultipleStuds(subElement.vertical) end end return studs end function IsOutsideModules(board, modules) for _, module_ in pairs(modules) do if board.elemdata.bx1 > module_.xMin - EPS and board.elemdata.by1 > module_.yMin - EPS and board.elemdata.bz1 > module_.zMin - EPS and board.elemdata.bx2 < module_.xMax + EPS and board.elemdata.by2 < module_.yMax + EPS and board.elemdata.bz2 < module_.zMax + EPS then return false end end return true end function IsOutsideAllModules(board, subElements, modules) for _, subElement in pairs(subElements) do if not IsOutsideModules(board, subElement.modules) then return false end end return IsOutsideModules(board, modules) end function GetInsulationPieces(boards, studs, excludeInsulationInsideModules) local result = {} for _, board in pairs(boards) do ac_objectopen(board.guid) local cnc = ac_objectget("iCnc") local typeCode = ac_objectget("iTypeCode") -- Apparently can be nil, in that case handle as board if typeCode ~= nil and typeCode == EBoardType.Insulation and (cnc == nil or cnc == 1) and (excludeInsulationInsideModules == 0 or IsOutsideAllModules(board, studs.subElements, studs.modules)) then table.insert(result, board) end ac_objectclose() end return result end -- Interior function GetBoarding(layerName, pattern, layer, result) local layerIdxStr = string.match(layerName, pattern .. "(%d*)") local layerIdx = 2 if #layerIdxStr ~= 0 then layerIdx = tonumber(layerIdxStr) if layerIdx == nil or layerIdx < 2 then PrintError(string.format("Invalid boarding layer '%s', should be a number larger than 1", layerName)) return false end end local nailgunToolIdx = 11 local nailgunToolIdxStr = string.match(layer.xmlutf8, " cnc_nailgun=\"(.-)\"") if nailgunToolIdxStr ~= nil then nailgunToolIdx = tonumber(nailgunToolIdxStr) if nailgunToolIdx == nil or nailgunToolIdx < 0 then PrintError(string.format("Invalid cnc_nailgun '%s' in '%s', should be a number larger than 0", nailgunToolIdxStr, layerName)) return false end end if result.boards[layerIdx] == nil then result.boards[layerIdx] = {} end for _, board in pairs(layer.boards) do ac_objectopen(board.guid) local cnc = ac_objectget("iCnc") local typeCode = ac_objectget("iTypeCode") -- Apparently can be nil, in that case handle as board if (typeCode == nil or typeCode <= EBoardType.Board) and (cnc == nil or cnc == 1) then GetNailLines(result.nailLines, board, nailgunToolIdx) table.insert(result.boards[layerIdx], board) end ac_objectclose() end return true end function GetNailLines(result, board, toolIndex) local count = ac_objectget("iMc" ,-1) if count == nil then return end for i = 1, count do local type_ = ac_objectget("iMc", i, 1) if type_ == EMachiningType.Nailing then if board.elemdata.tblSides == nil then PrintWarning("Cannot export board nail lines, elemdata.tblSides is not available. Please upgrade to latest ArchiFrame version.", board.id, board.guid) return else local x1 = ac_objectget("iMc", i, 2) local y1 = ac_objectget("iMc", i, 3) local x2 = ac_objectget("iMc", i, 4) local y2 = ac_objectget("iMc", i, 5) local spacing = ac_objectget("iMc", i, 6) --local flags = ac_objectget("iMc", i, 7) local sideNr = ac_objectget("iMc", i, 8) local dx = ac_objectget("iMc", i, 9) local side = board.elemdata.tblSides[sideNr] local p1 = Add3D(side.origc, Add3D( Scale3D(side.vecx, x1), Scale3D(side.vecy, y1) ) ) local p2 = Add3D(side.origc, Add3D( Scale3D(side.vecx, x2), Scale3D(side.vecy, y2) ) ) table.insert(result, {p1 = p1, p2 = p2, spacing = spacing, toolIndex = toolIndex}) end end end end function GetBranders(planks, result) for _, plank in pairs(planks) do table.insert(result, plank) end end function GetSawcuts(layer, elementPlane, result) local sawcuts = af_request ("mc_sawcuts", layer.guid) if sawcuts == nil then return end for _, sawcut in pairs(sawcuts) do local depth = sawcut.depth if math.abs(depth) < EPS then depth = layer.thickness end local z1 = layer.zoffcore local z2 = z1 + depth -- Z1 should always furthest away from core if math.abs(z2) > math.abs(z1) then z1, z2 = z2, z1 end table.insert( result, { p1 = { x = sawcut.x1, y = sawcut.y1, z = z1 }, p2 = { x = sawcut.x2, y = sawcut.y2, z = z2 }, toolIndex=1 } ) end end function AppendStuds(studs, writer) for _, stud in pairs(studs.vertical) do writer:AppendST(CDTAABB(stud), "-", stud.matname, "q") end for _, stud in pairs(studs.horizontal) do writer:AppendSTL(CDTAABB(stud), "-", stud.matname, "q") end for _, stud in pairs(studs.angled) do writer:AppendSTA(CDTOBB(stud), "-", stud.matname, "q") writer:AppendSTE() end end function AppendSubElements(subElements, writer) for id, element in pairs(subElements) do writer:AppendSPB({ x = element.xMin, y = element.yMin, z = element.zMin, xSize = element.xMax - element.xMin, ySize = element.yMax - element.yMin, zSize = element.zMax - element.zMin }, id) AppendElement(element, id, writer) writer:AppendSPE() end end function AppendElement(element, id, writer) AppendStuds(element, writer) for _, multipleStuds in pairs(element.multipleStuds) do writer:AppendMSB({ x = multipleStuds.studs[1].elemdata.bx1, y = multipleStuds.studs[1].elemdata.by1, z = multipleStuds.zMin, xSize = multipleStuds.studs[#multipleStuds.studs].elemdata.bx2 - multipleStuds.studs[1].elemdata.bx1, ySize = GetYSize(multipleStuds.studs[1]), zSize = multipleStuds.zMax - multipleStuds.zMin }, "-") for _, stud in pairs(multipleStuds.studs) do writer:AppendST(CDTAABB(stud), "-", stud.matname, "q") end writer:AppendMSE() end for id, module_ in pairs(element.modules) do -- If combining elements generated module ids will are not unique since they start at 'A', -- This makes sure they are unique in the combined element if elementId ~= nil then id = string.format("%s-%s", elementId, id) end writer:AppendSUB({ x = module_.xMin, y = module_.yMin, z = module_.zMin, xSize = module_.xMax - module_.xMin, ySize = module_.yMax - module_.yMin, zSize = module_.zMax - module_.zMin }, id) AppendStuds(module_, writer) writer:AppendSUE() end end function AppendOutsideBoards(boards, writer) for idx, layer in pairs(boards) do if idx < 6 then for _, board in pairs(layer) do writer:AppendBOOx(idx - 1, CDTAABB(board), board.matname) writer:Append("BE") end else PrintWarning(string.format("Exterior board layer '%d' skipped, maximum layer id is 5", idx)) end end end function AppendInsideBoards(boards, writer) for idx, layer in pairs(boards) do if idx < 6 then for _, board in pairs(layer) do writer:AppendBOIx(idx - 1, CDTAABB(board), board.matname) writer:Append("BE") end else PrintWarning(string.format("Interior board layer '%d' skipped, maximum layer id is 5", idx)) end end end function AppendInsulationPieces(insulations, writer) for _, insulation in pairs(insulations) do writer:AppendINS(CDTAABB(insulation), insulation.matname) end end function AppendBranders(branders, writer) for _, brander in pairs(branders) do writer:AppendBRA(CDTAABB(brander)) end end function AppendSawLines(sawlines, toolIndex, writer) if #sawlines == 0 then return end writer:AppendNewLine() writer:Append("MILLING_BOARD_OUTSIDE") for _, sawline in pairs(sawlines) do writer:AppendSL(sawline.p1, sawline.p2, toolIndex) end end function AppendDrillings(drillings, header, toolIndex, writer) if #drillings == 0 then return end writer:AppendNewLine() writer:Append(header) for _, drilling in pairs(drillings) do writer:AppendDRI(drilling.p1, drilling.p2, drilling.diameter, toolIndex) end end function AppendTopPlateMarkings(markings, writer) if #markings == 0 then return end writer:AppendNewLine() writer:Append("PANEL_IDENTIFICATION_TOPRAIL") for _, marking in pairs(markings) do writer:AppendINK(marking.x, marking.text) end end function AppendNailLines(nailLines, header, writer) if #nailLines == 0 then return end writer:AppendNewLine() writer:Append(header) for _, nailLine in pairs(nailLines) do writer:AppendNL(nailLine.p1, nailLine.p2, nailLine.spacing, nailLine.toolIndex) end end function AppendPlatingNailings( topPlate, topPlateIntersections, topPlateNailgun, bottomPlate, bottomPlateIntersections, bottomPlateNailgun, writer ) if (#topPlateIntersections == 0 or #topPlateNailgun == 0) and (#bottomPlateIntersections == 0 or #bottomPlateNailgun == 0) then return end writer:AppendNewLine() writer:Append("FRAMING_STATION") AppendPlateNailings( bottomPlateIntersections, bottomPlate.elemdata.by1, bottomPlate.elemdata.bz2 - bottomPlate.elemdata.bz1, bottomPlateNailgun, writer ) AppendPlateNailings( topPlateIntersections, topPlate.elemdata.by2, topPlate.elemdata.bz2 - topPlate.elemdata.bz1, topPlateNailgun, writer ) end function AppendPlateNailings(intersections, plateY, plateZSize, nailgun, writer) if #nailgun == 0 then return end -- CDTViewer adds ~18mm to the x value, probably as an attempt to center the nailings? -- Remove it and center ourselves since the width can vary local xOffset = 0.018 for _, intersection in pairs(intersections) do local xSize = intersection.x2 - intersection.x1 local zSize = intersection.z2 - intersection.z1 local x1 = intersection.x1 - xOffset local x2 = intersection.x2 - xOffset if math.abs(plateZSize - zSize) < EPS then writer:AppendNPA(x1 + xSize * 0.5, plateY, nailgun) else -- Calculate which nailing indices (1-4) will intersect if intersection is not overlapping plate completely local spacing = plateZSize / 5 local autoNailgun = "" for i = 1, 4 do local nailZ = i * spacing if intersection.z1 < nailZ + EPS and nailZ < intersection.z2 + EPS then autoNailgun = autoNailgun .. tostring(i) end end if #autoNailgun ~= 0 then if xSize < zSize + EPS then writer:AppendNPA(x1 + xSize * 0.5, plateY, autoNailgun) else writer:AppendNPA(x1 + zSize * 0.5, plateY, autoNailgun) writer:AppendNPA(x2 - zSize * 0.5, plateY, autoNailgun) end end end end end -- Axis Aligned Bounding Box function CDTAABB(AFPart) return { x = AFPart.elemdata.bx1, y = AFPart.elemdata.by1, z = AFPart.elemdata.bz1, xSize = GetXSize(AFPart), ySize = GetYSize(AFPart), zSize = GetZSize(AFPart) } end -- Oriented Bounding Box function CDTOBB(AFPart) -- Convert to CDT coordinate system local vecx = Flip3D(AFPart.elemdata.vecy) local vecy = AFPart.elemdata.vecx local vecz = Flip3D(AFPart.elemdata.vecz) -- https://en.wikipedia.org/wiki/Euler_angles, under Tait–Bryan angles order ZYX local zRot = math.atan2(vecy.x, -vecx.x) * RAD2DEG local yRot = math.asin(-vecz.x) * RAD2DEG local xRot = math.atan2(-vecz.y, vecz.z) * RAD2DEG local point = { x = AFPart.elemdata.x1, y = AFPart.elemdata.y1, z = AFPart.elemdata.z1 } point = Add3D(point, Add3D( Scale3D(vecx, -AFPart.width * 0.5), Scale3D(vecz, -AFPart.height) ) ) return { x = point.x, y = point.y, z = point.z, xSize = AFPart.width, ySize = AFPart.len, zSize = AFPart.height, xRot = xRot, yRot = yRot, zRot = -zRot } end function GetXSize(plank) return plank.elemdata.bx2 - plank.elemdata.bx1 end function GetYSize(plank) return plank.elemdata.by2 - plank.elemdata.by1 end function GetZSize(plank) return plank.elemdata.bz2 - plank.elemdata.bz1 end function IsHorizontal(plank) return AlmostEq(math.abs(plank.elemdata.vecx.x), 1.0) and (AlmostEq(math.abs(plank.elemdata.vecz.z), 1.0) or AlmostEq(math.abs(plank.elemdata.vecz.y), 1.0)) end -- Similar to IsHorizontal but has to be laying 'flat' and same x-size as the layer function IsValidPlate(plank, layerWidth) return AlmostEq(math.abs(plank.elemdata.vecx.x), 1.0) and AlmostEq(math.abs(plank.elemdata.vecz.z), 1.0) -- AlmostEq(GetXSize(plank), layerWidth) end function IsVertical(plank) return AlmostEq(math.abs(plank.elemdata.vecx.y), 1.0) and (AlmostEq(math.abs(plank.elemdata.vecz.x), 1.0) or AlmostEq(math.abs(plank.elemdata.vecz.z), 1.0)) end -- ############################################ -- ########### CDT file handling ############## -- ############################################ CDTFileWriter={} function CDTFileWriter:New() local newObj = { text = "" } self.__index = self return setmetatable(newObj, self) end function CDTFileWriter:Append(text) self.text = self.text .. string.format("%s;", text) self:AppendNewLine() end function CDTFileWriter:AppendCommand(command, args) self:Append(string.format("%s:%s", command, args)) end function CDTFileWriter:AppendVersion(version) self:AppendCommand("VERSION", version) end function CDTFileWriter:AppendTXT(txt) self:AppendCommand("TXT", txt) end function CDTFileWriter:AppendPNO(pno) self:AppendCommand("PNO", pno) end function CDTFileWriter:AppendELD(eld) self:AppendCommand("ELD", eld) end function CDTFileWriter:AppendELF(elf) self:AppendCommand("ELF", elf) end function CDTFileWriter:AppendDNO(dno) self:AppendCommand("DNO", dno) end -- Output: DRI:xStart:yStart:zStart:xEnd:yEnd:zEnd:diameter:index; function CDTFileWriter:AppendDRI(p1, p2, diameter, toolIndex) self:AppendCommand( "DRI", string.format( "%s:%s:%s:%s:%s:%s:%s:%d", CDTFormatMeters(p1.x), CDTFormatMeters(p1.y), CDTFormatMeters(p1.z), CDTFormatMeters(p2.x), CDTFormatMeters(p2.y), CDTFormatMeters(p2.z), CDTFormatMeters(diameter), toolIndex ) ) end -- Output: INK:x:text; function CDTFileWriter:AppendINK(x, text) self:AppendCommand( "INK", string.format( "%s:%s", CDTFormatMeters(x), text ) ) end -- Output: NL:xStart:yStart:zStart:xEnd:yEnd:zEnd:nailDistance:toolIndex; function CDTFileWriter:AppendNL(p1, p2, nailDistance, toolIndex) self:AppendCommand( "NL", string.format( "%s:%s:%s:%s:%s:%s:%s:%d", CDTFormatMeters(p1.x), CDTFormatMeters(p1.y), CDTFormatMeters(p1.z), CDTFormatMeters(p2.x), CDTFormatMeters(p2.y), CDTFormatMeters(p2.z), CDTFormatMeters(nailDistance), toolIndex ) ) end -- Saw line -- Output: SL:xStart:yStart:zStart:xEnd:yEnd:zEnd:Inclination:toolIndex; function CDTFileWriter:AppendSL(p1, p2, toolIndex) self:AppendCommand( "SL", string.format( "%s:%s:%s:%s:%s:%s:%s:%d", CDTFormatMeters(p1.x), CDTFormatMeters(p1.y), CDTFormatMeters(p1.z), CDTFormatMeters(p2.x), CDTFormatMeters(p2.y), CDTFormatMeters(p2.z), 900, toolIndex ) ) end --FST:xStart:yStart:zStart:xEnd:yEnd:zEnd:toolIndex:comment; function CDTFileWriter:AppendFST(p1, p2, toolIndex, comment) self:AppendCommand( "FST", string.format( "%s:%s:%s:%s:%s:%s:%d:%s", CDTFormatMeters(p1.x), CDTFormatMeters(p1.y), CDTFormatMeters(p1.z), CDTFormatMeters(p2.x), CDTFormatMeters(p2.y), CDTFormatMeters(p2.z), toolIndex, comment ) ) end -- NPA Fastener (Only recognized in CDTViewer, not recognized in Randek Viewer) -- Output: NPA:2772:0:13; function CDTFileWriter:AppendNPA(x, y, zArrangement) self:AppendCommand( "NPA", string.format( "%s:%s:%s", CDTFormatMeters(x), CDTFormatMeters(y), zArrangement ) ) end -- Sub assembly -- Output: SUB:xSize:ySize:zSize:x:y:z:name function CDTFileWriter:AppendSUB(aabb, name) self:AppendCommand( "SUB", string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Sub assembly end function CDTFileWriter:AppendSUE() self:Append("SUE") end -- Multiple stud group -- Output: MSB:xSize:ySize:zSize:x:y:z:name function CDTFileWriter:AppendMSB(aabb, name) self:AppendCommand( "MSB", string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Multiple stud group end function CDTFileWriter:AppendMSE() self:Append("MSE") end -- Sub panel -- Output: SPB:xSize:ySize:zSize:x:y:z:name function CDTFileWriter:AppendSPB(aabb, name) self:AppendCommand( "SPB", string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Sub panel end function CDTFileWriter:AppendSPE() self:Append("SPE") end -- Insulation -- Output: INS:xSize:ySize:zSize:x:y:z:name function CDTFileWriter:AppendINS(aabb, name) self:AppendCommand( "INS", string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Branders -- Output: BRA:xSize:ySize:zSize:x:y:z; function CDTFileWriter:AppendBRA(aabb) self:AppendCommand( "BRA", string.format( "%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z) ) ) end -- Interior boarding -- Output: BOI1:xSize:ySize:zSize:x:y:z:name; function CDTFileWriter:AppendBOIx(idx, aabb, name) self:AppendCommand( string.format("BOI%d", idx), string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Exterior boarding -- Output: BOI1:xSize:ySize:zSize:x:y:z:name; function CDTFileWriter:AppendBOOx(idx, aabb, name) self:AppendCommand( string.format("BOO%d", idx), string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Interior boarding -- Output: BOI1:xSize:ySize:zSize:x:y:z:name; function CDTFileWriter:AppendBOI1(aabb, name) self:AppendCommand( "BOI1", string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Exterior boarding -- Output: BOI1:xSize:ySize:zSize:x:y:z:name; function CDTFileWriter:AppendBOO1(aabb, name) self:AppendCommand( "BOO1", string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), name ) ) end -- Angular stud -- Output: STA:xSize:ySize:zSize:x:y:z:av:ah:ar:type:length:measurements:quality; function CDTFileWriter:AppendSTA(obb, type_, measurements, quality) self:AppendCommand( "STA", string.format( "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(obb.xSize), CDTFormatMeters(obb.ySize), CDTFormatMeters(obb.zSize), CDTFormatMeters(obb.x), CDTFormatMeters(obb.y), CDTFormatMeters(obb.z), CDTFormatNumber(obb.zRot), CDTFormatNumber(obb.xRot), CDTFormatNumber(obb.yRot), type_, CDTFormatMeters(obb.ySize), measurements, quality ) ) end -- Angular stud end function CDTFileWriter:AppendSTE() self:Append("STE") end -- Top plate -- Output: TS:xSize:ySize:zSize:x:y:z:type:length:measurements:quality; function CDTFileWriter:AppendTS(aabb, type_, measurements, quality) self:AppendCommand( "TS", string.format( "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), type_, CDTFormatMeters(aabb.xSize), measurements, quality ) ) end -- Bottom plate -- Output: BS:xSize:ySize:zSize:x:y:z:type:length:measurements:quality; function CDTFileWriter:AppendBS(aabb, type_, measurements, quality) self:AppendCommand( "BS", string.format( "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), type_, CDTFormatMeters(aabb.xSize), measurements, quality ) ) end -- Vertical stud -- Output: ST:xSize:ySize:zSize:x:y:z:type:length:measurements:quality; function CDTFileWriter:AppendST(aabb, type_, measurements, quality) self:AppendCommand( "ST", string.format( "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), type_, CDTFormatMeters(aabb.ySize), measurements, quality ) ) end -- Horizontal stud -- Output: STL:xSize:ySize:zSize:x:y:z:type:length:measurements:quality; function CDTFileWriter:AppendSTL(aabb, type_, measurements, quality) self:AppendCommand( "STL", string.format( "%s:%s:%s:%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(aabb.xSize), CDTFormatMeters(aabb.ySize), CDTFormatMeters(aabb.zSize), CDTFormatMeters(aabb.x), CDTFormatMeters(aabb.y), CDTFormatMeters(aabb.z), type_, CDTFormatMeters(aabb.xSize), measurements, quality ) ) end -- Element dimensions -- Output: ELM:lx:by:hz:t:l:m:q; function CDTFileWriter:AppendELM(lx, by, hz, type_) self:AppendCommand( "ELM", string.format( "%s:%s:%s:%s:%s:%s:%s", CDTFormatMeters(lx), CDTFormatMeters(by), CDTFormatMeters(hz), type_, CDTFormatMeters(lx), -- Length is the same as lx CDTFormatMeters(by), CDTFormatMeters(hz) ) ) end function CDTFileWriter:AppendNewLine() self.text = self.text .. "\r\n" end function CDTFileWriter:AppendEOF() self.text = self.text .. "EOF;" end function CDTFileWriter:WriteToFile(fileName) local file = io.open(fileName, "wb") file:write(self.text) file:close() end -- Rounds and converts to string function CDTFormatNumber(number) return tostring(Round(number)) -- local number=Round(number)/100 -- return string.format(Round(number) -- -- If its an integer -- if number == math.floor(number) then -- return string.format("%d", number) -- end --return string.format("%0.2f", number) end -- Converts to meters and rounds to int function CDTFormatMeters(meters) return CDTFormatNumber(MToMM(meters)) end -- ############################################ -- ############## MATH UTILITIES ############### -- ############################################ function Flip3D(vec) return { x = -vec.x, y = -vec.y, z = -vec.z } end function Add3D(vecA, vecB) return { x = vecA.x + vecB.x, y = vecA.y + vecB.y, z = vecA.z + vecB.z, } end function Sub3D(vecA, vecB) return { x = vecA.x - vecB.x, y = vecA.y - vecB.y, z = vecA.z - vecB.z, } end function Scale3D(vec, value) return { x = vec.x * value, y = vec.y * value, z = vec.z * value, } end function Magnitude3D(vec) return math.sqrt(vec.x*vec.x + vec.y*vec.y + vec.z*vec.z) end function Dir3D(p1, p2) local vec = Sub3D(p2, p1) local magnutide = Magnitude3D(vec) return { x = vec.x / magnutide, y = vec.y / magnutide, z = vec.z / magnutide, } end function Round(number) return math.floor(number+0.5) end function AlmostEq(v1, v2) return math.abs(v1 - v2) < EPS end function MToMM(number) return number*1000.0 end -- ############################################ -- ############## LUA UTILITIES ############### -- ############################################ function Min(a, b) return TernaryIf(a < b, a, b) end function Max(a, b) return TernaryIf(a > b, a, b) end function TernaryIf(cond, T, F) if cond then return T else return F end end function Format(o, nIndent) local s,k,v if type(o) == 'table' then s = '{\n' for i = 0, nIndent do s = s .. '\t' end for k,v in pairs(o) do if type(k) ~= 'number' then k = '"'..k..'"' end s = s .. '['..k..'] = ' .. Format(v, nIndent + 1) .. ',\n' for i = 0, nIndent do s = s .. '\t' end end if string.len(s) > 1 then if string.sub(s, -1) == '\t' then s = string.sub(s, 1, -2) end end s=s .. '}' else s = "" for i = 0, nIndent do s = s .. '\t' end if type(o) == 'string' then s = '"' .. tostring(o) .. '"' elseif type(o) == 'number' then -- Less digits to be able to compare results s = '"' .. string.format("%0.4f", o) .. '"' else s = tostring(o) end end return s end function Print(value) ac_environment("tolog", Format(value, 0)) end function PrintWarning(message, id, guid) if id~=nil and guid ~=nil then ac_environment("tolog", string.format("(CDT Warning): %s (id=%s, guid=%s)", message, id, guid)) elseif id~=nil then ac_environment("tolog", string.format("(CDT Warning): %s (id=%s)", message, id)) elseif guid~=nil then ac_environment("tolog", string.format("(CDT Warning): %s (guid=%s)", message, guid)) else ac_environment("tolog", string.format("(CDT Warning): %s ", message)) end end function PrintError(message, id, guid) if id~=nil and guid ~=nil then ac_environment("tolog", string.format("(CDT Error): %s (id=%s, guid=%s)", message, id, guid)) elseif id~=nil then ac_environment("tolog", string.format("(CDT Error): %s (id=%s)", message, id)) elseif guid~=nil then ac_environment("tolog", string.format("(CDT Error): %s (guid=%s)", message, guid)) else ac_environment("tolog", string.format("(CDT Error): %s ", message)) end end function PrintCncError(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