| | |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | __title__ = "FreeCAD Draft Workbench - DXF importer/exporter" |
| | __author__ = "Yorik van Havre <yorik@uncreated.net>" |
| | __url__ = "https://www.freecad.org" |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | """ |
| | This script uses a DXF-parsing library created by Stani, |
| | Kitsu and Migius for Blender |
| | |
| | imports: |
| | line, polylines, lwpolylines, arcs, circles, texts, |
| | mtexts, layers (as groups), colors |
| | |
| | exports: |
| | lines, polylines, lwpolylines, circles, arcs, |
| | texts, colors,layers (from groups) |
| | """ |
| | |
| | TEXTSCALING = 1.35 |
| | |
| | CURRENTDXFLIB = 1.42 |
| |
|
| | import sys |
| | import os |
| | import math |
| | import re |
| | import time |
| | import FreeCAD |
| | import Part |
| | import Draft |
| | import Mesh |
| | import DraftVecUtils |
| | import DraftGeomUtils |
| | import WorkingPlane |
| | from FreeCAD import Vector |
| | from FreeCAD import Console as FCC |
| | from Draft import LinearDimension |
| | from draftobjects.dimension import _Dimension |
| | from draftutils import params |
| | from draftutils import utils |
| | from draftutils.utils import pyopen |
| | from PySide import QtCore, QtGui |
| |
|
| | gui = FreeCAD.GuiUp |
| | draftui = None |
| | if gui: |
| | import FreeCADGui |
| |
|
| | try: |
| | draftui = FreeCADGui.draftToolBar |
| | except (AttributeError, NameError): |
| | draftui = None |
| | try: |
| | from draftviewproviders.view_base import ViewProviderDraft |
| | from draftviewproviders.view_wire import ViewProviderWire |
| | from draftviewproviders.view_dimension import ViewProviderLinearDimension |
| | except ImportError: |
| | ViewProviderDraft = None |
| | ViewProviderWire = None |
| | from draftutils.translate import translate |
| | from PySide import QtWidgets |
| | else: |
| |
|
| | def translate(context, txt): |
| | return txt |
| |
|
| |
|
| | dxfReader = None |
| | dxfColorMap = None |
| | dxfLibrary = None |
| |
|
| |
|
| | def errorDXFLib(gui): |
| | """Download the files required to convert DXF files. |
| | |
| | It checks the parameter `'dxfAllowDownload'` to decide whether it |
| | has access to download the required DXF libraries. |
| | |
| | Parameters |
| | ---------- |
| | gui : bool |
| | If `True` it will display error messages in graphical |
| | text boxes; otherwise it will display the messages in the terminal. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | dxfAllowDownload = params.get_param("dxfAllowDownload") |
| | if dxfAllowDownload: |
| | files = ["dxfColorMap.py", "dxfImportObjects.py", "dxfLibrary.py", "dxfReader.py"] |
| |
|
| | baseurl = "https://raw.githubusercontent.com/yorikvanhavre/" |
| | baseurl += "Draft-dxf-importer/master/" |
| | import ArchCommands |
| | from FreeCAD import Base |
| |
|
| | progressbar = Base.ProgressIndicator() |
| | progressbar.start("Downloading files...", 4) |
| | for f in files: |
| | progressbar.next() |
| | p = None |
| | p = ArchCommands.download(baseurl + f, force=True) |
| | if not p: |
| | if gui: |
| | message = translate( |
| | "Draft", |
| | """Download of DXF libraries failed. |
| | Please install the DXF Library addon manually |
| | from menu Tools → Addon Manager""", |
| | ) |
| | QtWidgets.QMessageBox.information(None, "", message) |
| | else: |
| | FCC.PrintWarning( |
| | "The DXF import/export libraries needed by FreeCAD to handle the DXF format are not installed.\n" |
| | ) |
| | FCC.PrintWarning( |
| | "Please install the DXF Library addon from Tools → Addon Manager\n" |
| | ) |
| | break |
| | progressbar.stop() |
| | sys.path.append(FreeCAD.ConfigGet("UserAppData")) |
| | else: |
| | if gui: |
| | message = translate( |
| | "draft", |
| | """The DXF import/export libraries needed by FreeCAD to handle |
| | the DXF format were not found on this system. |
| | Please either allow FreeCAD to download these libraries: |
| | 1 - Load Draft workbench |
| | 2 - Menu Edit → Preferences → Import-Export → DXF → Enable downloads |
| | Or download these libraries manually, as explained on |
| | https://github.com/yorikvanhavre/Draft-dxf-importer |
| | To enabled FreeCAD to download these libraries, answer Yes.""", |
| | ) |
| | reply = QtWidgets.QMessageBox.question( |
| | None, |
| | "", |
| | message, |
| | QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, |
| | QtWidgets.QMessageBox.No, |
| | ) |
| | if reply == QtWidgets.QMessageBox.Yes: |
| | params.set_param("dxfAllowDownload", True) |
| | errorDXFLib(gui) |
| | if reply == QtWidgets.QMessageBox.No: |
| | pass |
| | else: |
| | FCC.PrintWarning( |
| | "The DXF import/export libraries needed by FreeCAD to handle the DXF format are not installed.\n" |
| | ) |
| | _ver = FreeCAD.Version() |
| | _maj = _ver[0] |
| | _min = _ver[1] |
| | if float(_maj + "." + _min) >= 0.17: |
| | FCC.PrintWarning( |
| | "Please install the DXF Library addon from Tools → Addon Manager\n" |
| | ) |
| | else: |
| | FCC.PrintWarning( |
| | "Please check https://github.com/yorikvanhavre/Draft-dxf-importer\n" |
| | ) |
| |
|
| |
|
| | def getDXFlibs(): |
| | """Load the DXF Python libraries. |
| | |
| | It tries loading the global libraries for use in the system |
| | `dxfLibrary`, `dxfColorMap`, `dxfReader`, |
| | If they are not present, they are downloaded. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | try: |
| | if FreeCAD.ConfigGet("UserAppData") not in sys.path: |
| | sys.path.append(FreeCAD.ConfigGet("UserAppData")) |
| | global dxfLibrary, dxfColorMap, dxfReader |
| | import dxfLibrary |
| | import dxfColorMap |
| |
|
| | try: |
| | import dxfReader |
| | except Exception: |
| | libsok = False |
| | except ImportError: |
| | libsok = False |
| | FCC.PrintWarning("DXF libraries not found. Trying to download…\n") |
| | else: |
| | if float(dxfLibrary.__version__[1:5]) >= CURRENTDXFLIB: |
| | libsok = True |
| | else: |
| | FCC.PrintWarning("DXF libraries need to be updated. " "Trying to download…\n") |
| | libsok = False |
| | if not libsok: |
| | errorDXFLib(gui) |
| | try: |
| | import dxfColorMap, dxfLibrary, dxfReader |
| | import importlib |
| |
|
| | importlib.reload(dxfColorMap) |
| | importlib.reload(dxfLibrary) |
| | importlib.reload(dxfReader) |
| | except Exception: |
| | dxfReader = None |
| | dxfLibrary = None |
| | FCC.PrintWarning("DXF libraries not available. Aborting.\n") |
| |
|
| |
|
| | def deformat(text): |
| | """Remove weird formats in texts and wipes UTF characters. |
| | |
| | It removes `{}`, html codes, \\(U...) characters, |
| | |
| | Parameters |
| | ---------- |
| | text : str |
| | The input string. |
| | |
| | Results |
| | ------- |
| | str |
| | The deformatted string. |
| | """ |
| | |
| | |
| | |
| | t = text.strip("{}") |
| | t = re.sub(r"\\\\.*?;", "", t) |
| | |
| | sts = re.split("\\\\(U\\+....)", t) |
| | t = "".join(sts) |
| | |
| | t = re.sub(r"%%d", "°", t) |
| | t = re.sub(r"%%c", "Ø", t) |
| | t = re.sub(r"%%D", "°", t) |
| | t = re.sub(r"%%C", "Ø", t) |
| | |
| | return t |
| |
|
| |
|
| | def locateLayer(wantedLayer, color=None, drawstyle=None, visibility=True): |
| | """Return layer group and create it if needed. |
| | |
| | This function iterates over a global list named `layers`, which is |
| | defined in `processdxf`. |
| | |
| | If no layers are found it looks for the global `dxfUseDraftVisGroup` |
| | variable defined in `readPreferences`, and creates a new `Draft Layer` |
| | with the specified color. |
| | |
| | Otherwise it creates a group (`App::DocumentObjectGroup`) |
| | to use as a layer container. |
| | |
| | Parameters |
| | ---------- |
| | wantedLayer : str |
| | The name of a layer to search in the global `layers` list. |
| | |
| | color : tuple of four floats, optional |
| | It defaults to `None`. |
| | A tuple with color information `(r,g,b,a)`, where each value |
| | is a float between 0 and 1. |
| | |
| | drawstyle : str, optional |
| | It defaults to `None`. In which case "Solid" is used. |
| | "Solid", "Dashed", "Dotted" or "Dashdot". |
| | |
| | Visibility : bool, optional |
| | It defaults to `True`. |
| | Visibility of the new layer. |
| | |
| | Returns |
| | ------- |
| | App::FeaturePython or App::DocumentObjectGroup |
| | If the `wantedLayer` is found in the global list of layers, |
| | it is returned. |
| | Otherwise, a new layer or group is created and returned. |
| | |
| | If the global variable `dxfUseDraftVisGroup` is set, |
| | it creates a `Draft Layer` (`App::FeaturePython`). |
| | Otherwise, it creates a simple group (`App::DocumentObjectGroup`). |
| | |
| | See also |
| | -------- |
| | Draft.make_layer |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | |
| | |
| | if wantedLayer is None: |
| | wantedLayer = "0" |
| | for layer in layers: |
| | if layer.Label == wantedLayer: |
| | return layer |
| | if dxfUseDraftVisGroups: |
| | newLayer = Draft.make_layer( |
| | name=wantedLayer, |
| | line_color=(0.0, 0.0, 0.0) if not color else color, |
| | draw_style="Solid" if not drawstyle else drawstyle, |
| | ) |
| | newLayer.Visibility = visibility |
| | else: |
| | newLayer = doc.addObject("App::DocumentObjectGroup", wantedLayer) |
| | newLayer.Label = wantedLayer |
| | layers.append(newLayer) |
| | return newLayer |
| |
|
| |
|
| | def getdimheight(style): |
| | """Return the dimension text height from the given dimstyle. |
| | |
| | It searches the global variable `drawing.tables.data`, |
| | created in `processdxf`, for a `dimstyle`; then iterates on the data, |
| | and if a `dimstyle` is found, it compares if its raw value with DXF code 2 |
| | (Name) is equal to `style`. |
| | |
| | Parameters |
| | --------- |
| | style : str |
| | A raw value of DXF code 3 (other text or name value). |
| | |
| | Returns |
| | ------- |
| | float |
| | The data of DXF code 140 (DIMSTYLE setting), |
| | or just 1 if no `dimstyle` was found in `drawing.tables.data`. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | for t in drawing.tables.data: |
| | if t.name == "dimstyle": |
| | for a in t.data: |
| | if hasattr(a, "type"): |
| | if a.type == "dimstyle": |
| | if rawValue(a, 2) == style: |
| | return rawValue(a, 140) |
| | return 1 |
| |
|
| |
|
| | def calcBulge(v1, bulge, v2): |
| | """Calculate intermediary vertex for a curved segment. |
| | |
| | Considering an arc of a circle, it can be defined by two vertices `v1` |
| | and `v2`, and a `bulge` value that indicates how curved the arc is. |
| | A `bulge` of 0 is a straight line, while a `bulge` of 1 is the maximum |
| | curvature, or a semicircle. |
| | |
| | A vertex that is in the curve, equidistant to the two vertices, |
| | can be found by finding the sagitta of the arc, that is, |
| | the perpendicular to the chord that goes from `v1` to `v2`. |
| | |
| | It uses the algorithm from http://www.afralisp.net/lisp/Bulges1.htm |
| | |
| | Parameters |
| | ---------- |
| | v1 : Base::Vector3 |
| | The first point. |
| | bulge : float |
| | The bulge is the tangent of 1/4 of the included angle for the arc |
| | between `v1` and `v2`. A negative `bulge` indicates that the arc |
| | goes clockwise from `v1` to `v2`. A `bulge` of 0 indicates |
| | a straight segment, and a `bulge` of 1 is a semicircle. |
| | v2 : Base::Vector3 |
| | The second point. |
| | |
| | Returns |
| | ------- |
| | Base::Vector3 |
| | The new point between `v1` and `v2`. |
| | """ |
| | chord = v2.sub(v1) |
| | sagitta = (bulge * chord.Length) / 2 |
| | perp = chord.cross(Vector(0, 0, 1)) |
| | startpoint = v1.add(chord.multiply(0.5)) |
| | if not DraftVecUtils.isNull(perp): |
| | perp.normalize() |
| | endpoint = perp.multiply(sagitta) |
| | return startpoint.add(endpoint) |
| |
|
| |
|
| | def getGroup(ob): |
| | """Get the name of the group or Draft layer that contains the object. |
| | |
| | It looks for the global `dxfUseDraftVisGroup` variable defined |
| | in `readPreferences`. Then searches all objects of type "Layer" |
| | for the one that contains `ob`. |
| | |
| | Otherwise, it searches all objects derived from |
| | `App::DocumentObjectGroup` for the one that contains `ob`. |
| | |
| | Parameters |
| | ---------- |
| | ob : App::DocumentObject |
| | Any object to test as belonging to a layer or group. |
| | |
| | Returns |
| | ------- |
| | str |
| | The label of the layer, or of the group, if it contains `ob`. |
| | Otherwise, return "0". |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | all_objs = FreeCAD.ActiveDocument.Objects |
| | if dxfUseDraftVisGroups: |
| | for layer in [o for o in all_objs if Draft.getType(o) == "Layer"]: |
| | if ob in layer.Group: |
| | return layer.Label |
| | for i in all_objs: |
| | if i.isDerivedFrom("App::DocumentObjectGroup"): |
| | for j in i.Group: |
| | if j == ob: |
| | return i.Label |
| | return "0" |
| |
|
| |
|
| | def getACI(ob, text=False): |
| | """Get the AutoCAD color index (ACI) color closest to the object's color. |
| | |
| | This function only works if the graphical interface is loaded, |
| | as it checks the `ViewObject` attribute of the object |
| | which only exists when the GUI is available. |
| | |
| | Parameters |
| | ---------- |
| | ob : App::DocumentObject |
| | Any object. |
| | |
| | text : bool, optional |
| | It defaults ot `False`. If `True`, use the `TextColor` |
| | instead of the `LineColor` of the object. |
| | |
| | Returns |
| | ------- |
| | int |
| | The numerical value of the AutoCAD color index (ACI) color, |
| | which goes from 0 to 255. |
| | It returns 0 (black) if no graphical interface is loaded. |
| | It returns 256 (`BYLAYER`) if `ob` is inside a Draft Layer, |
| | and the layer's `OverrideChildren` view property is `True`. |
| | """ |
| | if not gui: |
| | return 0 |
| | else: |
| | |
| | for parent in ob.InList: |
| | if Draft.getType(parent) == "Layer": |
| | if ob in parent.Group: |
| | if hasattr(parent, "ViewObject") and hasattr( |
| | parent.ViewObject, "OverrideChildren" |
| | ): |
| | if parent.ViewObject.OverrideChildren: |
| | return 256 |
| | if text: |
| | col = ob.ViewObject.TextColor |
| | else: |
| | col = ob.ViewObject.LineColor |
| | aci = [0, 442] |
| | for i in range(255, -1, -1): |
| | ref = dxfColorMap.color_map[i] |
| | dist = (ref[0] - col[0]) ** 2 + (ref[1] - col[1]) ** 2 + (ref[2] - col[2]) ** 2 |
| | if dist <= aci[1]: |
| | aci = [i, dist] |
| | return aci[0] |
| |
|
| |
|
| | def rawValue(entity, code): |
| | """Return the value of a DXF code in an entity section. |
| | |
| | Parameters |
| | ---------- |
| | entity : drawing.entities |
| | A DXF entity in the `drawing` data obtained from `processdxf`. |
| | code : int |
| | A numerical value of the code. |
| | |
| | Returns |
| | ------- |
| | float or str |
| | The value corresponding to the code. It may be numeric or a string. |
| | """ |
| | value = None |
| | for pair in entity.data: |
| | if pair[0] == code: |
| | value = pair[1] |
| | return value |
| |
|
| |
|
| | def getMultiplePoints(entity): |
| | """Scan the given entity (paths, leaders, etc.) for multiple points. |
| | |
| | Parameters |
| | ---------- |
| | entity : drawing.entities |
| | A DXF entity in the `drawing` data obtained from `processdxf`. |
| | |
| | Returns |
| | ------- |
| | list of Base::Vector3 |
| | The list of points (vectors). |
| | Each point has three coordinates `(X,Y,Z)`. |
| | If the original point only had two, the third coordinate |
| | is set to zero `(X,Y,0)`. |
| | """ |
| | pts = [] |
| | for d in entity.data: |
| | if d[0] == 10: |
| | pts.append([d[1]]) |
| | elif d[0] in [20, 30]: |
| | pts[-1].append(d[1]) |
| | pts.reverse() |
| | points = [] |
| | for p in pts: |
| | if len(p) == 3: |
| | points.append(Vector(p[0], p[1], p[2])) |
| | else: |
| | points.append(Vector(p[0], p[1], 0)) |
| | return points |
| |
|
| |
|
| | def isBrightBackground(): |
| | """Check if the current viewport's background is a bright color. |
| | |
| | It considers the values of `BackgroundColor` for a solid background, |
| | or a combination of `BackgroundColor2` and `BackgroundColor3` |
| | for a gradient background from the parameter database. |
| | |
| | Returns |
| | ------- |
| | bool |
| | Returns `True` if the value of the color is larger than 128, |
| | which is considered light; otherwise it is considered dark |
| | and returns `False`. |
| | """ |
| | if params.get_param_view("Gradient"): |
| | r1, g1, b1, _ = utils.get_rgba_tuple(params.get_param_view("BackgroundColor2")) |
| | r2, g2, b2, _ = utils.get_rgba_tuple(params.get_param_view("BackgroundColor3")) |
| | v1 = Vector(r1, g1, b1) |
| | v2 = Vector(r2, g2, b2) |
| | v = v2.sub(v1) |
| | v.multiply(0.5) |
| | cv = v1.add(v) |
| | else: |
| | r1, g1, b1, _ = utils.get_rgba_tuple(params.get_param_view("BackgroundColor")) |
| | cv = Vector(r1, g1, b1) |
| | value = cv.x * 0.3 + cv.y * 0.59 + cv.z * 0.11 |
| | if value < 128: |
| | return False |
| | else: |
| | return True |
| |
|
| |
|
| | def getGroupColor(dxfobj, index=False): |
| | """Get the color of the layer. |
| | |
| | It searches the global variable `drawing.tables`, |
| | created in `processdxf`, for a `layer`; then iterates on the data, |
| | and if the layer name matches the layer of `dxfobj`, it will try |
| | to return the color of its layer. |
| | |
| | It searches the global variable `dxfBrightBackground` to determine |
| | if it should return black, or a color from the global |
| | `dxfColorMap.color_map` dictionary. |
| | |
| | Parameters |
| | ---------- |
| | dxfobj : Part::Feature |
| | An imported DXF object. |
| | |
| | index : bool, optional |
| | It defaults to `False`. If it is `True` it will return the layer's |
| | color; otherwise it will check the global variable |
| | `dxfBrightBackground`, and return black or a mapped color. |
| | |
| | Returns |
| | ------- |
| | list of 3 floats |
| | The layer's color as a list `[r, g, b]`, black `[0, 0, 0]` |
| | or the mapped color `dxfColorMap.color_map[color]`. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | name = dxfobj.layer |
| | for table in drawing.tables.get_type("table"): |
| | if table.name == "layer": |
| | for l in table.get_type("layer"): |
| | if l.name == name: |
| | if index: |
| | return l.color |
| | else: |
| | if (l.color == 7) and dxfBrightBackground: |
| | return [0.0, 0.0, 0.0] |
| | else: |
| | if isinstance(l.color, int): |
| | if l.color > 0: |
| | return dxfColorMap.color_map[l.color] |
| | return [0.0, 0.0, 0.0] |
| |
|
| |
|
| | def getColor(): |
| | """Get the Draft color defined in the Draft toolbar or preferences. |
| | |
| | Returns |
| | ------- |
| | tuple of 4 floats |
| | Return the `(r, g, b, 0.0)` tuple with the colors defined |
| | in the Draft toolbar, if the graphical user interface is active. |
| | Otherwise, return the tuple with the color |
| | of the `DefaultShapeLineColor` in the parameter database. |
| | """ |
| | if gui and draftui: |
| | r = float(draftui.color.red() / 255.0) |
| | g = float(draftui.color.green() / 255.0) |
| | b = float(draftui.color.blue() / 255.0) |
| | return (r, g, b, 0.0) |
| | else: |
| | r, g, b, _ = utils.get_rgba_tuple(params.get_param_view("DefaultShapeLineColor")) |
| | return (r, g, b, 0.0) |
| |
|
| |
|
| | def formatObject(obj, dxfobj=None): |
| | """Apply text and line color to an object from a DXF object. |
| | |
| | If `dxfUseDraftVisGroups` is `True` the function returns immediately. |
| | The color of the object then depends on the Draft Layer the object is in. |
| | |
| | Else this function only works if the graphical user interface is loaded |
| | as it needs access to the `ViewObject` attribute of the objects. |
| | |
| | If `dxfobj` and the global variable `dxfGetColors` exist |
| | the `TextColor` and `LineColor` of `obj` will be set to the color |
| | indicated by the global dictionary |
| | `dxfColorMap.color_map[dxfobj.color_index]`. |
| | |
| | If the global `dxfBrightBackground` is set, it will set the `LineColor` |
| | to black. |
| | |
| | If no `dxfobj` is given, `TextColor` and `LineColor` |
| | are set to the global variable `dxfDefaultColor`. |
| | |
| | Parameters |
| | ---------- |
| | obj : App::DocumentObject |
| | Object that will use the DXF color. |
| | |
| | dxfobj : drawing.entities, optional |
| | It defaults to `None`. DXF object from which the color will be taken. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if dxfUseDraftVisGroups: |
| | return |
| |
|
| | if dxfGetColors and dxfobj and hasattr(dxfobj, "color_index"): |
| | if hasattr(obj.ViewObject, "TextColor"): |
| | if dxfobj.color_index == 256: |
| | cm = getGroupColor(dxfobj)[:3] |
| | else: |
| | cm = dxfColorMap.color_map[dxfobj.color_index] |
| | obj.ViewObject.TextColor = (cm[0], cm[1], cm[2]) |
| | elif hasattr(obj.ViewObject, "LineColor"): |
| | if dxfobj.color_index == 256: |
| | cm = getGroupColor(dxfobj) |
| | elif (dxfobj.color_index == 7) and dxfBrightBackground: |
| | cm = [0.0, 0.0, 0.0] |
| | else: |
| | cm = dxfColorMap.color_map[dxfobj.color_index] |
| | obj.ViewObject.LineColor = (cm[0], cm[1], cm[2], 0.0) |
| | else: |
| | if hasattr(obj.ViewObject, "TextColor"): |
| | obj.ViewObject.TextColor = dxfDefaultColor |
| | elif hasattr(obj.ViewObject, "LineColor"): |
| | obj.ViewObject.LineColor = dxfDefaultColor |
| |
|
| |
|
| | def vec(pt): |
| | """Return a rounded and scaled Vector from a DXF point. |
| | |
| | Parameters |
| | ---------- |
| | pt : Base::Vector3, or list of three numerical values, or float, or int |
| | A point with three coordinates `(x, y, z)`, |
| | or just a single numerical value. |
| | |
| | Returns |
| | ------- |
| | Base::Vector3 or float |
| | Each of the components of the vector, or the single numerical value, |
| | is rounded to the precision defined by `prec`, |
| | and scaled by the amount of the global variable `resolvedScale`. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | pre = Draft.precision() |
| | if isinstance(pt, (int, float)): |
| | v = round(pt, pre) |
| | if resolvedScale != 1: |
| | v = v * resolvedScale |
| | else: |
| | v = Vector(round(pt[0], pre), round(pt[1], pre), round(pt[2], pre)) |
| | if resolvedScale != 1: |
| | v.multiply(resolvedScale) |
| | return v |
| |
|
| |
|
| | def placementFromDXFOCS(ent): |
| | """Return the placement of an object from AutoCAD's OCS. |
| | |
| | In AutoCAD DXF's the points of each entity are expressed in terms |
| | of the entity's object coordinate system (OCS). |
| | Then to determine the entity's position in 3D space, |
| | what is needed is a 3D vector defining the Z axis of the OCS, |
| | and the elevation value over it. |
| | |
| | It uses `WorkingPlane.align_to_point_and_axis()` to align the working plane |
| | to the origin and to `ent.extrusion` (the plane's `axis`). |
| | Then it gets the global coordinates of the entity |
| | by using `WorkingPlane.get_global_coords()` |
| | and either `ent.elevation` (Z coordinate) or `ent.loc` a `(x,y,z)` tuple. |
| | |
| | Parameters |
| | ---------- |
| | ent : A DXF entity |
| | It could be of several types, like `lwpolyline`, `polyline`, |
| | and others, and with `ent.extrusion`, `ent.elevation` |
| | or `ent.loc` attributes. |
| | |
| | Returns |
| | ------- |
| | Base::Placement |
| | A placement, comprised of a `Base` (`Base::Vector3`), |
| | and a `Rotation` (`Base::Rotation`). |
| | |
| | See also |
| | -------- |
| | WorkingPlane.align_to_point_and_axis, WorkingPlane.get_global_coords |
| | """ |
| | draftWPlane = WorkingPlane.PlaneBase() |
| | draftWPlane.align_to_point_and_axis(Vector(0.0, 0.0, 0.0), vec(ent.extrusion), 0.0) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if draftWPlane.axis == FreeCAD.Vector(1.0, 0.0, 0.0): |
| | draftWPlane.u = FreeCAD.Vector(0.0, 1.0, 0.0) |
| | draftWPlane.v = FreeCAD.Vector(0.0, 0.0, 1.0) |
| | elif draftWPlane.axis == FreeCAD.Vector(-1.0, 0.0, 0.0): |
| | draftWPlane.u = FreeCAD.Vector(0.0, -1.0, 0.0) |
| | draftWPlane.v = FreeCAD.Vector(0.0, 0.0, 1.0) |
| | else: |
| | if (abs(ent.extrusion[0]) < (1.0 / 64.0)) and (abs(ent.extrusion[1]) < (1.0 / 64.0)): |
| | draftWPlane.u = FreeCAD.Vector(0.0, 1.0, 0.0).cross(draftWPlane.axis) |
| | else: |
| | draftWPlane.u = FreeCAD.Vector(0.0, 0.0, 1.0).cross(draftWPlane.axis) |
| | draftWPlane.u.normalize() |
| | draftWPlane.v = draftWPlane.axis.cross(draftWPlane.u) |
| | draftWPlane.v.normalize() |
| | draftWPlane.position = Vector(0.0, 0.0, 0.0) |
| |
|
| | pl = draftWPlane.get_placement() |
| | if (ent.type == "lwpolyline") or (ent.type == "polyline"): |
| | pl.Base = draftWPlane.get_global_coords(vec([0.0, 0.0, ent.elevation])) |
| | else: |
| | pl.Base = draftWPlane.get_global_coords(vec(ent.loc)) |
| | return pl |
| |
|
| |
|
| | def drawLine(line, forceShape=False): |
| | """Return a Part shape (Wire or Edge) from a DXF line. |
| | |
| | Parameters |
| | ---------- |
| | line : drawing.entities |
| | The DXF object of type `'line'`. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will produce a `Part.Edge`, |
| | otherwise it produces a `Draft Wire`. |
| | |
| | Returns |
| | ------- |
| | Part::Feature or Part::TopoShape ('Edge') |
| | The returned object is normally a `Wire`, if the global |
| | variables `dxfCreateDraft` or `dxfCreateSketch` are set, |
| | and `forceShape` is `False`. |
| | Otherwise it produces a `Part.Edge`. |
| | |
| | It returns `None` if it fails. |
| | |
| | See also |
| | -------- |
| | drawBlock |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if len(line.points) > 1: |
| | v1 = vec(line.points[0]) |
| | v2 = vec(line.points[1]) |
| | if not DraftVecUtils.equals(v1, v2): |
| | try: |
| | if (dxfCreateDraft or dxfCreateSketch) and (not forceShape): |
| | return Draft.make_wire([v1, v2], face=False) |
| | else: |
| | return Part.LineSegment(v1, v2).toShape() |
| | except Part.OCCError: |
| | warn(line) |
| | return None |
| |
|
| |
|
| | def drawPolyline(polyline, forceShape=False, num=None): |
| | """Return a Part shape (Wire, Face, or Shell) from a DXF polyline. |
| | |
| | It traverses the points of the polyline checking for straight edges, |
| | and for curvatures (bulges) between two points. |
| | Then it produces `Part.Edges` and `Part.Arcs`, and decides what to output |
| | at the end based on the options. |
| | |
| | Parameters |
| | ---------- |
| | polyline : drawing.entities |
| | The DXF object of type `'polyline'` or `'lwpolyline'`. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Wire`, otherwise it try to produce a `Draft Wire`. |
| | |
| | num : float, optional |
| | It defaults to `None`. A simple number that identifies this polyline. |
| | |
| | Returns |
| | ------- |
| | Part::Feature or Part::TopoShape ('Wire', 'Face', 'Shell') |
| | It returns `None` if it fails producing a shape. |
| | |
| | If the polyline has a `width` and the global variable |
| | `dxfRenderPolylineWidth` is set, it will try to return a face simulating |
| | a thick line. If the polyline is closed, it will cut the interior loop |
| | to produce the a shell. |
| | |
| | If the polyline doesn't have curvatures, and the global variables |
| | `dxfCreateDraft` or `dxfCreateSketch` are set, and `forceShape` is `False` |
| | it creates a straight `Draft Wire`. |
| | |
| | Otherwise, it will return a `Part.Wire`. |
| | |
| | See also |
| | -------- |
| | drawBlock |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if len(polyline.points) > 1: |
| | edges = [] |
| | curves = False |
| | verts = [] |
| | for p in range(len(polyline.points) - 1): |
| | p1 = polyline.points[p] |
| | p2 = polyline.points[p + 1] |
| | v1 = vec(p1) |
| | v2 = vec(p2) |
| | verts.append(v1) |
| | if not DraftVecUtils.equals(v1, v2): |
| | if polyline.points[p].bulge: |
| | curves = True |
| | cv = calcBulge(v1, polyline.points[p].bulge, v2) |
| | if DraftVecUtils.isColinear([v1, cv, v2]): |
| | try: |
| | edges.append(Part.LineSegment(v1, v2).toShape()) |
| | except Part.OCCError: |
| | warn(polyline, num) |
| | else: |
| | try: |
| | edges.append(Part.Arc(v1, cv, v2).toShape()) |
| | except Part.OCCError: |
| | warn(polyline, num) |
| | else: |
| | try: |
| | edges.append(Part.LineSegment(v1, v2).toShape()) |
| | except Part.OCCError: |
| | warn(polyline, num) |
| | verts.append(v2) |
| | if polyline.closed: |
| | p1 = polyline.points[len(polyline.points) - 1] |
| | p2 = polyline.points[0] |
| | v1 = vec(p1) |
| | v2 = vec(p2) |
| | cv = calcBulge(v1, polyline.points[-1].bulge, v2) |
| | if not DraftVecUtils.equals(v1, v2): |
| | if DraftVecUtils.isColinear([v1, cv, v2]): |
| | try: |
| | edges.append(Part.LineSegment(v1, v2).toShape()) |
| | except Part.OCCError: |
| | warn(polyline, num) |
| | else: |
| | try: |
| | edges.append(Part.Arc(v1, cv, v2).toShape()) |
| | except Part.OCCError: |
| | warn(polyline, num) |
| | if edges: |
| | try: |
| | width = rawValue(polyline, 43) |
| | if width and dxfRenderPolylineWidth: |
| | w = Part.Wire(edges) |
| | w1 = w.makeOffset(width / 2) |
| | if polyline.closed: |
| | w2 = w.makeOffset(-width / 2) |
| | w1 = Part.Face(w1) |
| | w2 = Part.Face(w2) |
| | if w1.BoundBox.DiagonalLength > w2.BoundBox.DiagonalLength: |
| | return w1.cut(w2) |
| | else: |
| | return w2.cut(w1) |
| | else: |
| | return Part.Face(w1) |
| | elif (dxfCreateDraft or dxfCreateSketch) and (not curves) and (not forceShape): |
| | |
| | ob = Draft.make_wire(verts, face=False) |
| | ob.Closed = polyline.closed |
| | ob.Placement = placementFromDXFOCS(polyline) |
| | return ob |
| | else: |
| | w = Part.Wire(edges) |
| | w.Placement = placementFromDXFOCS(polyline) |
| | return w |
| | except Part.OCCError: |
| | warn(polyline, num) |
| | return None |
| |
|
| |
|
| | def drawArc(arc, forceShape=False): |
| | """Return a Part shape (Arc, Edge) from a DXF arc. |
| | |
| | Parameters |
| | ---------- |
| | arc : drawing.entities |
| | The DXF object of type `'arc'`. The `'arc'` object is different from |
| | a `'circle'` because it has different start and end angles. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Edge`, otherwise it tries to produce a `Draft Arc`. |
| | |
| | Returns |
| | ------- |
| | Part::Part2DObject or Part::TopoShape ('Edge') |
| | The returned object is normally a `Draft Arc` with no face, |
| | if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set, |
| | and `forceShape` is `False`. |
| | Otherwise it produces a `Part.Edge`. |
| | |
| | It returns `None` if it fails producing a shape. |
| | |
| | See also |
| | -------- |
| | drawCircle, drawBlock |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | pre = Draft.precision() |
| | pl = placementFromDXFOCS(arc) |
| | rad = vec(arc.radius) |
| | firstangle = round(arc.start_angle % 360, pre) |
| | lastangle = round(arc.end_angle % 360, pre) |
| | try: |
| | if (dxfCreateDraft or dxfCreateSketch) and (not forceShape): |
| | return Draft.make_circle(rad, pl, face=False, startangle=firstangle, endangle=lastangle) |
| | else: |
| | circle = Part.Circle() |
| | circle.Radius = rad |
| | shape = circle.toShape(math.radians(firstangle), math.radians(lastangle)) |
| | shape.Placement = pl |
| | return shape |
| | except Part.OCCError: |
| | warn(arc) |
| | return None |
| |
|
| |
|
| | def drawCircle(circle, forceShape=False): |
| | """Return a Part shape (Circle, Edge) from a DXF circle. |
| | |
| | Parameters |
| | ---------- |
| | circle : drawing.entities |
| | The DXF object of type `'circle'`. The `'circle'` object is different |
| | from an `'arc'` because the circle forms a full circumference. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Edge`, otherwise it tries to produce a `Draft Circle`. |
| | |
| | Returns |
| | ------- |
| | Part::Part2DObject or Part::TopoShape ('Edge') |
| | The returned object is normally a `Draft Circle` with no face, |
| | if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set, |
| | and `forceShape` is `False`. |
| | Otherwise it produces a `Part.Edge`. |
| | |
| | It returns `None` if it fails producing a shape. |
| | |
| | See also |
| | -------- |
| | drawArc, drawBlock |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | pl = placementFromDXFOCS(circle) |
| | rad = vec(circle.radius) |
| | try: |
| | if (dxfCreateDraft or dxfCreateSketch) and (not forceShape): |
| | return Draft.make_circle(rad, pl, face=False) |
| | else: |
| | curve = Part.Circle() |
| | curve.Radius = rad |
| | shape = curve.toShape() |
| | shape.Placement = pl |
| | return shape |
| | except Part.OCCError: |
| | warn(circle) |
| | return None |
| |
|
| |
|
| | def drawEllipse(ellipse, forceShape=False): |
| | """Return a Part shape (Ellipse, Edge) from a DXF ellipse. |
| | |
| | Parameters |
| | ---------- |
| | ellipse : drawing.entities |
| | The DXF object of type `'ellipse'`. The ellipse can be a full ellipse |
| | or an elliptical arc. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Edge`, otherwise it tries to produce a `Draft Ellipse`. |
| | |
| | Returns |
| | ------- |
| | Part::Part2DObject or Part::TopoShape ('Edge') |
| | The returned object is normally a `Draft Ellipse` with a face, |
| | if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set, |
| | and `forceShape` is `False`. |
| | Otherwise it produces a `Part.Edge`. |
| | |
| | It returns `None` if it fails producing a shape. |
| | |
| | See also |
| | -------- |
| | drawArc, drawCircle |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | try: |
| | pre = Draft.precision() |
| | c = vec(ellipse.loc) |
| | start = round(ellipse.start_angle, pre) |
| | end = round(ellipse.end_angle, pre) |
| | majv = vec(ellipse.major) |
| | majr = majv.Length |
| | minr = majr * ellipse.ratio |
| | el = Part.Ellipse(vec((0, 0, 0)), majr, minr) |
| | x = majv.normalize() |
| | z = vec(ellipse.extrusion).normalize() |
| | y = z.cross(x) |
| | m = DraftVecUtils.getPlaneRotation(x, y) |
| | pl = FreeCAD.Placement(m) |
| | pl.move(c) |
| | if (dxfCreateDraft or dxfCreateSketch) and (not forceShape): |
| | if (start != 0.0) or ((end != 0.0) or (end != round(math.pi / 2, pre))): |
| | shape = el.toShape(start, end) |
| | shape.Placement = pl |
| | return shape |
| | else: |
| | return Draft.make_ellipse(majr, minr, pl, face=False) |
| | else: |
| | shape = el.toShape(start, end) |
| | shape.Placement = pl |
| | return shape |
| | except Part.OCCError: |
| | warn(arc) |
| | return None |
| |
|
| |
|
| | def drawFace(face): |
| | """Return a Part face from a list of points. |
| | |
| | Parameters |
| | ---------- |
| | face : drawing.entities |
| | The DXF object of type `'3dface'`. |
| | |
| | Returns |
| | ------- |
| | Part::TopoShape ('Face') |
| | The returned object is a `Part.Face`. |
| | It returns `None` if it fails producing a shape. |
| | """ |
| | pl = [] |
| | for p in face.points: |
| | pl.append(vec(p)) |
| | p1 = face.points[0] |
| | pl.append(vec(p1)) |
| | try: |
| | pol = Part.makePolygon(pl) |
| | return Part.Face(pol) |
| | except Part.OCCError: |
| | warn(face) |
| | return None |
| |
|
| |
|
| | def drawMesh(mesh, forceShape=False): |
| | """Return a Mesh (Mesh, Shell) from a DXF mesh. |
| | |
| | Parameters |
| | ---------- |
| | mesh : drawing.entities |
| | The DXF object of type `'polyline'` or `'lwpolyline'` |
| | with `flags` of 16 (3D polygon mesh) or 64 (polyface mesh). |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Shape` of type `'Shell'`, |
| | otherwise it tries to produce a `Mesh::MeshObject`. |
| | |
| | Returns |
| | ------- |
| | Mesh::MeshObject or Part::TopoShape ('Shell') |
| | The returned object is normally a `Mesh` if `forceShape` is `False`. |
| | Otherwise it produces a `Part.Shape` of type `'Shell'`. |
| | |
| | It returns `None` if it fails producing a shape. |
| | |
| | See also |
| | -------- |
| | drawBlock |
| | """ |
| | md = [] |
| | if mesh.flags == 16: |
| | pts = mesh.points |
| | udim = rawValue(mesh, 71) |
| | vdim = rawValue(mesh, 72) |
| | for u in range(udim - 1): |
| | for v in range(vdim - 1): |
| | b = u + v * udim |
| | p1 = pts[b] |
| | p2 = pts[b + 1] |
| | p3 = pts[b + udim] |
| | p4 = pts[b + udim + 1] |
| | md.append([p1, p2, p4]) |
| | md.append([p1, p4, p3]) |
| | elif mesh.flags == 64: |
| | pts = [] |
| | fcs = [] |
| | for p in mesh.points: |
| | if p.flags == 192: |
| | pts.append(p) |
| | elif p.flags == 128: |
| | fcs.append(p) |
| | |
| | |
| | for f in fcs: |
| | p1 = pts[abs(rawValue(f, 71)) - 1] |
| | p2 = pts[abs(rawValue(f, 72)) - 1] |
| | p3 = pts[abs(rawValue(f, 73)) - 1] |
| | md.append([p1, p2, p3]) |
| | if rawValue(f, 74) is not None: |
| | p4 = pts[abs(rawValue(f, 74)) - 1] |
| | md.append([p1, p3, p4]) |
| | try: |
| | m = Mesh.Mesh(md) |
| | if forceShape: |
| | s = Part.Shape() |
| | s.makeShapeFromMesh(m.Topology, 1) |
| | s = s.removeSplitter() |
| | m = s |
| | except FreeCAD.Base.FreeCADError: |
| | warn(mesh) |
| | else: |
| | return m |
| | return None |
| |
|
| |
|
| | def drawSolid(solid): |
| | """Return a Part shape (Face) from a DXF solid. |
| | |
| | It takes three or four points from a `solid`, if possible. |
| | It adds the first point again to the end of the points list, and creates |
| | a polygon, which is then used to create a face. |
| | |
| | Parameters |
| | ---------- |
| | solid : drawing.entities |
| | The DXF object of type `'solid'`. |
| | |
| | Returns |
| | ------- |
| | Part::TopoShape ('Face') |
| | The returned object is a `Part.Face`. |
| | It returns `None` if it fails producing a shape. |
| | |
| | See also |
| | -------- |
| | drawBlock |
| | """ |
| | p4 = None |
| | p1x = rawValue(solid, 10) |
| | p1y = rawValue(solid, 20) |
| | p1z = rawValue(solid, 30) or 0 |
| | p2x = rawValue(solid, 11) |
| | p2y = rawValue(solid, 21) |
| | p2z = rawValue(solid, 31) or p1z |
| | p3x = rawValue(solid, 12) |
| | p3y = rawValue(solid, 22) |
| | p3z = rawValue(solid, 32) or p1z |
| | p4x = rawValue(solid, 13) |
| | p4y = rawValue(solid, 23) |
| | p4z = rawValue(solid, 33) or p1z |
| | p1 = Vector(p1x, p1y, p1z) |
| | p2 = Vector(p2x, p2y, p2z) |
| | p3 = Vector(p3x, p3y, p3z) |
| | if p4x is not None: |
| | p4 = Vector(p4x, p4y, p4z) |
| | if p4 and (p4 != p3) and (p4 != p2) and (p4 != p1): |
| | try: |
| | return Part.Face(Part.makePolygon([p1, p2, p4, p3, p1])) |
| | except Part.OCCError: |
| | warn(solid) |
| | else: |
| | try: |
| | return Part.Face(Part.makePolygon([p1, p2, p3, p1])) |
| | except Part.OCCError: |
| | warn(solid) |
| | return None |
| |
|
| |
|
| | def drawSplineIterpolation(verts, closed=False, forceShape=False, alwaysDiscretize=False): |
| | """Return a wire or spline, opened or closed. |
| | |
| | Parameters |
| | ---------- |
| | verts : Base::Vector3 |
| | A list of points. |
| | |
| | closed : bool, optional |
| | It defaults to `False`. If it is `True` it will create a closed |
| | Wire, closed BSpline, or a Face. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Shape` of type `'Edge'` or `'Face'`. |
| | Otherwise it tries to produce a `Draft Wire` or `Draft BSpline`. |
| | |
| | alwaysDiscretize : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | straight lines (Wires, Edges). |
| | Otherwise it will try to produce BSplines. |
| | |
| | Returns |
| | ------- |
| | Part::Feature or Part::TopoShape ('Edge', 'Face') |
| | The returned object is normally a `Draft Wire` or `Draft BSpline`, |
| | if the global variables `dxfCreateDraft` or `dxfCreateSketch` are set, |
| | and `forceShape` is `False`. |
| | It is a `Draft Wire` if the global variables |
| | `dxfDiscretizeCurves` or `alwaysDiscretize` are `True`, |
| | and a `Draft BSpline` otherwise. |
| | |
| | Otherwise it produces a `Part.Wire`. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if (dxfCreateDraft or dxfCreateSketch) and (not forceShape): |
| | if dxfDiscretizeCurves or alwaysDiscretize: |
| | ob = Draft.make_wire(verts, face=False) |
| | else: |
| | ob = Draft.make_bspline(verts, face=False) |
| | ob.Closed = closed |
| | return ob |
| | else: |
| | if dxfDiscretizeCurves or alwaysDiscretize: |
| | sh = Part.makePolygon(verts + [verts[0]]) |
| | else: |
| | sp = Part.BSplineCurve() |
| | |
| | sp.interpolate(verts) |
| | sh = Part.Wire(sp.toShape()) |
| | return sh |
| |
|
| |
|
| | def drawSplineOld(spline, forceShape=False): |
| | """Return a Part Shape from a DXF spline. DEPRECATED. |
| | |
| | It takes the vertices from the spline data, |
| | considers the value from code 70 to know if the spline |
| | is closed or not, and then calls |
| | `drawSplineIterpolation(verts, closed, forceShape)`. |
| | |
| | Parameters |
| | ---------- |
| | spline : drawing.entities |
| | The DXF object of type `'spline'`. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Shape` of type `'Edge'` or `'Face'`. |
| | Otherwise it tries to produce a `Draft Wire` or `Draft BSpline`. |
| | |
| | Returns |
| | ------- |
| | Part::Feature or Part::TopoShape ('Edge', 'Face') |
| | The returned object is normally a `Draft Wire` or `Draft BSpline` |
| | as returned from `drawSplineIterpolation()`. |
| | |
| | It returns `None` if it fails producing a shape. |
| | |
| | See also |
| | -------- |
| | drawSplineIterpolation |
| | """ |
| | flag = rawValue(spline, 70) |
| | if flag == 1: |
| | closed = True |
| | else: |
| | closed = False |
| | verts = [] |
| | knots = [] |
| | for dline in spline.data: |
| | if dline[0] == 10: |
| | cp = [dline[1]] |
| | elif dline[0] == 20: |
| | cp.append(dline[1]) |
| | elif dline[0] == 30: |
| | cp.append(dline[1]) |
| | pt = vec(cp) |
| | if verts: |
| | if pt != verts[-1]: |
| | verts.append(pt) |
| | else: |
| | verts.append(pt) |
| | elif dline[0] == 40: |
| | knots.append(dline[1]) |
| | try: |
| | return drawSplineIterpolation(verts, closed, forceShape) |
| | except Part.OCCError: |
| | warn(spline) |
| | return None |
| |
|
| |
|
| | def drawSpline(spline, forceShape=False): |
| | """Return a Part Shape (BSpline, Wire) from a DXF spline. |
| | |
| | A BSpline may be defined in several ways, by knots, |
| | control points, fit points, and weights. |
| | The function searches all values to determine the best way |
| | of building the BSpline with Draft or Part tools. |
| | |
| | Parameters |
| | ---------- |
| | spline : drawing.entities |
| | The DXF object of type `'spline'`. |
| | |
| | forceShape : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | a `Part.Shape` of type `'Wire'`. |
| | Otherwise it tries to produce a `Draft BSpline`. |
| | |
| | Returns |
| | ------- |
| | Part::Feature or Part::TopoShape ('Edge', 'Face') |
| | The returned object is normally a `Draft BezCurve` |
| | created with `Draft.make_bezcurve(controlpoints, degree=degree)`, |
| | if `forceShape` is `False` and there are no weights. |
| | |
| | Otherwise it tries to return a `Part.Shape` of type `'Wire'`, |
| | by first creating a Bezier curve with `Part.BezierCurve()`. |
| | |
| | If it's impossible to create the BSpline in this way, |
| | it will try to create an interpolated BSpline with |
| | `drawSplineIterpolation(controlpoints)`. |
| | |
| | If fit points exist and control points do not, |
| | it will try to create an interpolated BSpline with |
| | `drawSplineIterpolation(fitpoints)`. |
| | |
| | In other cases it will try to create a `Part.Shape` |
| | from a BSpline, using the available control points, |
| | multiplicity vector, the kot vector, the degree, |
| | the periodic data, and the weights. |
| | |
| | It returns `None` if it fails producing a shape. |
| | |
| | Raises |
| | ------ |
| | ValueError |
| | If there are wrong number of knots, wrong number of control points, |
| | wrong number of fit points, an inconsistent rational flag, or wrong |
| | number of weights. |
| | |
| | See also |
| | -------- |
| | drawBlock, Draft.make_bezcurve, Part.BezierCurve, drawSplineIterpolation, |
| | Part.BSplineCurve.buildFromPolesMultsKnots |
| | |
| | To do |
| | ---- |
| | As there is currently no Draft primitive to handle splines |
| | the result is a non-parametric curve. |
| | |
| | **2019:** There is a `Draft BSpline` now, but it's not used. |
| | """ |
| | flags = rawValue(spline, 70) |
| | closed = (flags & 1) != 0 |
| | periodic = (flags & 2) != 0 and False |
| | rational = (flags & 4) != 0 |
| | planar = (flags & 8) != 0 |
| | linear = (flags & 16) != 0 |
| | degree = rawValue(spline, 71) |
| | nbknots = rawValue(spline, 72) or 0 |
| | nbcontrolp = rawValue(spline, 73) or 0 |
| | nbfitp = rawValue(spline, 74) or 0 |
| | knots = [] |
| | weights = [] |
| | controlpoints = [] |
| | fitpoints = [] |
| | |
| | dataremain = spline.data[:] |
| | while len(dataremain) > 0: |
| | groupnumber = dataremain[0][0] |
| | if groupnumber == 40: |
| | knots.append(dataremain[0][1]) |
| | dataremain = dataremain[1:] |
| | elif groupnumber == 41: |
| | weights.append(dataremain[0][1]) |
| | dataremain = dataremain[1:] |
| | elif groupnumber in (10, 11): |
| | x = dataremain[0][1] |
| | if dataremain[1][0] in (20, 21): |
| | y = dataremain[1][1] |
| | if dataremain[2][0] in (30, 31): |
| | z = dataremain[2][1] |
| | dataremain = dataremain[3:] |
| | else: |
| | z = 0.0 |
| | dataremain = dataremain[2:] |
| | else: |
| | y = 0.0 |
| | dataremain = dataremain[1:] |
| | v = vec([x, y, z]) |
| | if groupnumber == 10: |
| | controlpoints.append(v) |
| | elif groupnumber == 11: |
| | fitpoints.append(v) |
| | else: |
| | dataremain = dataremain[1:] |
| | |
| |
|
| | if nbknots != len(knots): |
| | raise ValueError("Wrong number of knots") |
| | if nbcontrolp != len(controlpoints): |
| | raise ValueError("Wrong number of control points") |
| | if nbfitp != len(fitpoints): |
| | raise ValueError("Wrong number of fit points") |
| | if rational == all((w == 1.0 or w is None) for w in weights): |
| | raise ValueError("Inconsistent rational flag") |
| | if len(weights) == 0: |
| | weights = None |
| | elif len(weights) != len(controlpoints): |
| | raise ValueError("Wrong number of weights") |
| |
|
| | |
| | |
| | multvector = [] |
| | knotvector = [] |
| | mult = 0 |
| | previousknot = None |
| | for knotvalue in knots: |
| | if knotvalue == previousknot: |
| | mult += 1 |
| | else: |
| | if mult > 0: |
| | multvector.append(mult) |
| | mult = 1 |
| | previousknot = knotvalue |
| | knotvector.append(knotvalue) |
| | multvector.append(mult) |
| | |
| | innermults = multvector[:] if periodic else multvector[1:-1] |
| | if any(m > degree for m in innermults): |
| | if all(m == degree + 1 for m in multvector): |
| | if not forceShape and weights is None: |
| | points = controlpoints[:] |
| | del points[degree + 1 :: degree + 1] |
| | return Draft.make_bezcurve(points, degree=degree) |
| | else: |
| | poles = controlpoints[:] |
| | edges = [] |
| | while len(poles) >= degree + 1: |
| | |
| | bzseg = Part.BezierCurve() |
| | bzseg.increase(degree) |
| | bzseg.setPoles(poles[0 : degree + 1]) |
| | poles = poles[degree + 1 :] |
| | if weights is not None: |
| | bzseg.setWeights(weights[0 : degree + 1]) |
| | weights = weights[degree + 1 :] |
| | edges.append(bzseg.toShape()) |
| | return Part.Wire(edges) |
| | else: |
| | warn("polygon fallback on %s" % spline) |
| | return drawSplineIterpolation( |
| | controlpoints, closed=closed, forceShape=forceShape, alwaysDiscretize=True |
| | ) |
| | if fitpoints and not controlpoints: |
| | return drawSplineIterpolation(fitpoints, closed=closed, forceShape=forceShape) |
| | try: |
| | bspline = Part.BSplineCurve() |
| | bspline.buildFromPolesMultsKnots( |
| | poles=controlpoints, |
| | mults=multvector, |
| | knots=knotvector, |
| | degree=degree, |
| | periodic=periodic, |
| | weights=weights, |
| | ) |
| | return bspline.toShape() |
| | except Part.OCCError: |
| | warn(spline) |
| | return None |
| |
|
| |
|
| | def drawBlock(blockref, num=None, createObject=False): |
| | """Return a Part Shape (Compound) from a DXF block reference. |
| | |
| | It inspects the `blockref.entities` for objects of types `'line'`, |
| | `'polyline'`, `'lwpolyline'`, `'arc'`, `'circle'`, `'insert'`, |
| | `'solid'`, and `'spline'`. |
| | If they are found they create shapes with `drawLine`, |
| | `drawMesh` or `drawPolyline`, `drawArc`, `drawCircle`, `drawInsert`, |
| | `drawSolid`, `drawSpline`, and adds all shapes to a list. |
| | Then it makes a compound of all those shapes. |
| | |
| | In the case of entities of type `'text'` and `'mtext'` |
| | it will only process the entities if the global variable |
| | `dxfImportTexts` exist, and `dxfImportLayouts` exists |
| | or if the DXF code 67 doesn't indicate an empty space (empty text). |
| | Then it will use `addText` and add the found text to its proper |
| | layer. |
| | |
| | Parameters |
| | ---------- |
| | blockref : drawing.blocks.data |
| | The DXF block data. |
| | |
| | num : float, optional |
| | It defaults to `None`. A simple number that identifies |
| | the given `blockref`. |
| | |
| | createObject : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | and return a `'Part::Feature'` with the compound |
| | as its shape attribute. |
| | Otherwise, just return the `Part.Compound`. |
| | |
| | Returns |
| | ------- |
| | Part::TopoShape ('Compound') or Part::Feature |
| | The returned object is normally a `Part.Compound` |
| | created from the list of all `Part.Shapes` created from |
| | the `blockref` entities, if `createObject` is `False`. |
| | Otherwise, it will return a `'Part::Feature'` document object |
| | with the compound as its shape attribute. |
| | |
| | In the first case, it will add the compound shape |
| | to the global dictionary `blockshapes`. |
| | In the latter case, it will add the `'Part::Feature'` object |
| | to the global dictionary `blockobjects`. |
| | |
| | It returns `None` if the global variable `dxfStarBlocks` |
| | doesn't exist, if the `blockref.entities.data` is empty, |
| | or if it fails producing the compound shape. |
| | |
| | See also |
| | -------- |
| | `drawLine`, `drawMesh`, `drawPolyline`, `drawArc`, `drawCircle`, |
| | `drawInsert`, `drawSolid`, `drawSpline`, `addText`. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if not dxfStarBlocks: |
| | if blockref.name[0] == "*": |
| | return None |
| | if len(blockref.entities.data) == 0: |
| | print("skipping empty block ", blockref.name) |
| | return None |
| | |
| | |
| | shapes = [] |
| | for line in blockref.entities.get_type("line"): |
| | s = drawLine(line, forceShape=True) |
| | if s: |
| | shapes.append(s) |
| | for polyline in blockref.entities.get_type("polyline"): |
| | if hasattr(polyline, "flags") and polyline.flags in [16, 64]: |
| | s = drawMesh(polyline, forceShape=True) |
| | else: |
| | s = drawPolyline(polyline, forceShape=True) |
| | if s: |
| | shapes.append(s) |
| | for polyline in blockref.entities.get_type("lwpolyline"): |
| | s = drawPolyline(polyline, forceShape=True) |
| | if s: |
| | shapes.append(s) |
| | for arc in blockref.entities.get_type("arc"): |
| | s = drawArc(arc, forceShape=True) |
| | if s: |
| | shapes.append(s) |
| | for circle in blockref.entities.get_type("circle"): |
| | s = drawCircle(circle, forceShape=True) |
| | if s: |
| | shapes.append(s) |
| | for insert in blockref.entities.get_type("insert"): |
| | |
| | if dxfStarBlocks or insert.block[0] != "*": |
| | s = drawInsert(insert) |
| | if s: |
| | shapes.append(s) |
| | for solid in blockref.entities.get_type("solid"): |
| | s = drawSolid(solid) |
| | if s: |
| | shapes.append(s) |
| | for spline in blockref.entities.get_type("spline"): |
| | s = drawSpline(spline, forceShape=True) |
| | if s: |
| | shapes.append(s) |
| | for text in blockref.entities.get_type("text"): |
| | if dxfImportTexts: |
| | if dxfImportLayouts or (not rawValue(text, 67)): |
| | addText(text) |
| | for text in blockref.entities.get_type("mtext"): |
| | if dxfImportTexts: |
| | if dxfImportLayouts or (not rawValue(text, 67)): |
| | print("adding block text", text.value, " from ", blockref) |
| | addText(text) |
| | try: |
| | shape = Part.makeCompound(shapes) |
| | except Part.OCCError: |
| | warn(blockref) |
| | if shape: |
| | blockshapes[blockref.name] = shape |
| | if createObject: |
| | newob = doc.addObject("Part::Feature", blockref.name) |
| | newob.Shape = shape |
| | blockobjects[blockref.name] = newob |
| | return newob |
| | return shape |
| | return None |
| |
|
| |
|
| | def drawInsert(insert, num=None, clone=False): |
| | """Return a Part Shape (Compound, Clone) from a DXF insert. |
| | |
| | It searches for `insert.block` in `blockobjects` |
| | or `blockshapes`, and returns a clone or a copy of the compound, |
| | with transformations applied: rotation, translation (movement), |
| | and scaling. |
| | |
| | If the global variable `dxfImportTexts` is available |
| | it will check the attributes of `insert` and add those text attributes |
| | to their own layers with `addText`. |
| | |
| | Parameters |
| | ---------- |
| | insert : drawing.entities |
| | The DXF object of type `'insert'`. |
| | |
| | num : float, optional |
| | It defaults to `None`. A simple number that identifies |
| | the given block being drawn, if it is not a clone. |
| | |
| | clone : bool, optional |
| | It defaults to `False`. If it is `True` it will try to produce |
| | and return a `Draft Clone` of the `'insert.block'` contained |
| | in the global dictionary `blockobjects`. |
| | |
| | Otherwise, it will try to return a copy of the shape |
| | of the `'insert.block'` contained in the global dictionary |
| | `blockshapes`, or created from the `drawing.blocks.data` |
| | with `drawBlock()`. |
| | |
| | Returns |
| | ------- |
| | Part::TopoShape ('Compound') or |
| | Part::Part2DObject or Part::Feature (`Draft Clone`) |
| | The returned object is normally a copy of the `Part.Compound` |
| | extracted from `blockshapes` or created with `drawBlock()`. |
| | |
| | If `clone` is `True` then it will try returning |
| | a `Draft Clone` from the `'insert.block'` contained |
| | in the global dictionary `blockobjects`. |
| | It returns `None` if `insert.block` isn't in `blockobjects`. |
| | |
| | In any of these two cases, it will try to apply the |
| | insert transformations: rotation, translation (movement), |
| | and scaling. |
| | |
| | See also |
| | -------- |
| | drawBlock |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if dxfImportTexts: |
| | attrs = attribs(insert) |
| | for a in attrs: |
| | addText(a, attrib=True) |
| | if clone: |
| | if insert.block in blockobjects: |
| | newob = Draft.make_clone(blockobjects[insert.block]) |
| | tsf = FreeCAD.Matrix() |
| | rot = math.radians(insert.rotation) |
| | pos = vec(insert.loc) |
| | tsf.move(pos) |
| | tsf.rotateZ(rot) |
| | sc = insert.scale |
| | sc = vec([sc[0], sc[1], 0]) |
| | newob.Placement = FreeCAD.Placement(tsf) |
| | newob.Scale = sc |
| | return newob |
| | else: |
| | shape = None |
| | else: |
| | if insert in blockshapes: |
| | shape = blockshapes[insert.block].copy() |
| | else: |
| | shape = None |
| | for b in drawing.blocks.data: |
| | if b.name == insert.block: |
| | shape = drawBlock(b, num) |
| | if shape: |
| | pos = vec(insert.loc) |
| | rot = math.radians(insert.rotation) |
| | scale = insert.scale |
| | tsf = FreeCAD.Matrix() |
| | |
| | |
| | tsf.rotateZ(rot) |
| | try: |
| | shape = shape.transformGeometry(tsf) |
| | except Part.OCCError: |
| | tsf.scale(scale[0], scale[1], 0) |
| | try: |
| | shape = shape.transformGeometry(tsf) |
| | except Part.OCCError: |
| | print("importDXF: unable to apply insert transform:", tsf) |
| | shape.translate(pos) |
| | return shape |
| | return None |
| |
|
| |
|
| | def drawLayerBlock(objlist, name="LayerBlock"): |
| | """Return a Draft Block (compound) from the given object list. |
| | |
| | Parameters |
| | ---------- |
| | objlist : list |
| | A list of Draft objects or Part.shapes. |
| | |
| | Returns |
| | ------- |
| | Part::Feature or Part::TopoShape ('Compound') |
| | If the global variables `dxfCreateDraft` or `dxfCreateSketch` are set, |
| | and no element in `objlist` is a `Part.Shape`, |
| | it will try to return a `Draft Block`. |
| | Otherwise, it will try to return a `Part.Compound`. |
| | |
| | It returns `None` if it fails producing a shape. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | isObj = True |
| | for o in objlist: |
| | if isinstance(o, Part.Shape): |
| | isObj = False |
| | obj = None |
| | if (dxfCreateDraft or dxfCreateSketch) and isObj: |
| | try: |
| | |
| | obj = doc.addObject("Part::Compound", name) |
| | obj.Links = objlist |
| | except Part.OCCError: |
| | pass |
| | else: |
| | try: |
| | obj = Part.makeCompound(objlist) |
| | except Part.OCCError: |
| | pass |
| | return obj |
| |
|
| |
|
| | def attribs(insert): |
| | """Check if an insert has attributes, and return the values if positive. |
| | |
| | It checks the `drawing.entities.data` for the `insert`, |
| | and saves the index of the element. |
| | Then it iterates again looking for entities with an `'attrib'`, |
| | collecting the entities in a list. |
| | |
| | Parameters |
| | ---------- |
| | insert : drawing.entities |
| | The DXF object of type `'insert'`. |
| | |
| | Returns |
| | ------- |
| | list |
| | It returns a list with the entities that have `'attrib'` data, |
| | until `'seqend'` is found. |
| | |
| | It returns an empty list `[]`, if DXF code 66 ("Entities follow") |
| | is different from 1, or if the `insert` is not found |
| | in `drawing.entities.data`. |
| | """ |
| | atts = [] |
| | if rawValue(insert, 66) != 1: |
| | return [] |
| | index = None |
| | for i in range(len(drawing.entities.data)): |
| | if drawing.entities.data[i] == insert: |
| | index = i |
| | break |
| | if index is None: |
| | return [] |
| | j = index + 1 |
| | while True: |
| | ent = drawing.entities.data[j] |
| | if str(ent) == "seqend": |
| | return atts |
| | elif str(ent) == "attrib": |
| | atts.append(ent) |
| | j += 1 |
| |
|
| |
|
| | def addObject(shape, name="Shape", layer=None): |
| | """Adds a new object to the document, with the given name and layer. |
| | |
| | Parameters |
| | ---------- |
| | shape : Part.Shape or Part::Feature |
| | The simple Part.Shape or Draft object previously created |
| | from an entity in a DXF file. |
| | |
| | name : str, optional |
| | It defaults to "Shape". The name of the new document object. |
| | |
| | layer : App::FeaturePython or App::DocumentObjectGroup, optional |
| | It defaults to `None`. |
| | The `Draft Layer` (`App::FeaturePython`) |
| | or simple group (`App::DocumentObjectGroup`) |
| | to which the new object will be added. |
| | |
| | Returns |
| | ------- |
| | Part::Feature or Part::Part2DObject |
| | If the `shape` is a simple `Part.Shape`, it will be encapsulated |
| | inside a `Part::Feature` object and this will be returned. Otherwise, |
| | it is assumed it is already a Draft object which will just be returned. |
| | |
| | It applies the text and line color by calling `formatObject()` |
| | before returning the new object. |
| | """ |
| | if isinstance(shape, Part.Shape): |
| | newob = doc.addObject("Part::Feature", name) |
| | newob.Shape = shape |
| | else: |
| | newob = shape |
| | if layer: |
| | lay = locateLayer(layer) |
| | |
| | if hasattr(lay, "Group"): |
| | pass |
| | |
| | elif hasattr(lay, "Proxy") and hasattr(lay.Proxy, "Group"): |
| | lay = lay.Proxy |
| | else: |
| | lay = None |
| |
|
| | if lay != None: |
| | if lay not in layerObjects: |
| | l = [] |
| | layerObjects[lay] = l |
| | else: |
| | l = layerObjects[lay] |
| | l.append(newob) |
| |
|
| | formatObject(newob) |
| | return newob |
| |
|
| |
|
| | def addText(text, attrib=False): |
| | """Add a new Draft Text object to the document. |
| | |
| | It creates a `Draft Text` from the `text` entity, |
| | and adds the new object to its indicated layer, |
| | creating it if it doesn't exist. |
| | It also applies its rotation, position, justification |
| | ('center' or 'right'), and color. |
| | |
| | If the graphical interface is available, together with the Draft toolbar, |
| | as well as the global variable `dxfUseStandardSize`, it will |
| | use the toolbar's indicated font size. |
| | Otherwise, it will use the text's height scaled by the value of |
| | the global variable `TEXTSCALING`. |
| | |
| | Parameters |
| | ---------- |
| | text : drawing.entities |
| | The DXF object of type `'text'` or `'mtext'`. |
| | |
| | attrib : bool, optional |
| | It defaults to `False`. |
| | If `True` it determines from the DXF: |
| | - the text value - code 1, |
| | - the text colour - code 6, |
| | - the text font - code 7, |
| | - the layer name - code 8, |
| | - the position (X, Y, Z) - codes 10, 20, 30, |
| | - the text height - from code 40, |
| | - the rotation angle - from code 50, |
| | and assigns the name `'Attribute'`. |
| | Otherwise, it assumes these values from `text` |
| | and sets the name to `'Text'`. |
| | |
| | See also |
| | -------- |
| | locateLayer, drawBlock, Draft.make_text |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if attrib: |
| | lay = locateLayer(rawValue(text, 8)) |
| | val = rawValue(text, 1) |
| | pos = vec([rawValue(text, 10), rawValue(text, 20), rawValue(text, 30)]) |
| | hgt = vec(rawValue(text, 40)) |
| | else: |
| | lay = locateLayer(text.layer) |
| | val = text.value |
| | pos = vec(text.loc) |
| | hgt = vec(text.height) |
| | if val: |
| | if attrib: |
| | name = "Attribute" |
| | else: |
| | name = "Text" |
| | val = deformat(val) |
| | newob = Draft.make_text(val.split("\n")) |
| | if hasattr(lay, "addObject"): |
| | lay.addObject(newob) |
| | elif hasattr(lay, "Proxy") and hasattr(lay.Proxy, "addObject"): |
| | lay.Proxy.addObject(lay, newob) |
| | rx = rawValue(text, 11) |
| | ry = rawValue(text, 21) |
| | rz = rawValue(text, 31) |
| | xv = Vector(1, 0, 0) |
| | ax = Vector(0, 0, 1) |
| | if rx or ry or rz: |
| | xv = vec([rx, ry, rz]) |
| | if not DraftVecUtils.isNull(xv): |
| | ax = (xv.cross(Vector(1, 0, 0))).negative() |
| | if DraftVecUtils.isNull(ax): |
| | ax = Vector(0, 0, 1) |
| | ang = -math.degrees(DraftVecUtils.angle(xv, Vector(1, 0, 0), ax)) |
| | Draft.rotate(newob, ang, axis=ax) |
| | if ax == Vector(0, 0, -1): |
| | ax = Vector(0, 0, 1) |
| | elif hasattr(text, "rotation"): |
| | if text.rotation: |
| | Draft.rotate(newob, text.rotation) |
| | if attrib: |
| | attrot = rawValue(text, 50) |
| | if attrot: |
| | Draft.rotate(newob, attrot) |
| | if gui and draftui and dxfUseStandardSize: |
| | fsize = draftui.fontsize |
| | else: |
| | fsize = float(hgt) * TEXTSCALING |
| | if hasattr(text, "alignment"): |
| | yv = ax.cross(xv) |
| | if text.alignment in [1, 2, 3]: |
| | sup = DraftVecUtils.scaleTo(yv, fsize / TEXTSCALING).negative() |
| | |
| | pos = pos.add(sup) |
| | elif text.alignment in [4, 5, 6]: |
| | sup = DraftVecUtils.scaleTo(yv, fsize / (2 * TEXTSCALING)).negative() |
| | pos = pos.add(sup) |
| | newob.Placement.Base = pos |
| | if gui: |
| | newob.ViewObject.FontSize = fsize |
| | if hasattr(text, "alignment"): |
| | if text.alignment in [2, 5, 8]: |
| | newob.ViewObject.Justification = "Center" |
| | elif text.alignment in [3, 6, 9]: |
| | newob.ViewObject.Justification = "Right" |
| | |
| | formatObject(newob, text) |
| |
|
| |
|
| | def addToBlock(obj, layer): |
| | """Add the given object to the layer in the global dictionary. |
| | |
| | It searches for `layer` in the global dictionary `layerBlocks`. |
| | If found, it appends the `obj` to the `layer`; |
| | otherwise, it adds the `layer` to `layerBlocks` first, |
| | and then adds `obj`. |
| | |
| | Parameters |
| | ---------- |
| | obj : Part.Shape or App::DocumentObject |
| | Any shape or Draft object previously created from a DXF file. |
| | layer : str |
| | The name of a layer to which `obj` is added. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | if layer in layerBlocks: |
| | layerBlocks[layer].append(obj) |
| | else: |
| | layerBlocks[layer] = [obj] |
| |
|
| |
|
| | def getScaleFromDXF(header): |
| | """Get the scale from the header of a drawing object. |
| | |
| | Parameters |
| | ---------- |
| | header : header object |
| | """ |
| | data = header.data |
| | insunits = 0 |
| | if [9, "$INSUNITS"] in data: |
| | insunits = data[data.index([9, "$INSUNITS"]) + 1][1] |
| | if insunits == 0 and [9, "$MEASUREMENT"] in data: |
| | measurement = data[data.index([9, "$MEASUREMENT"]) + 1][1] |
| | insunits = 1 if measurement == 0 else 4 |
| |
|
| | if insunits == 0: |
| | |
| | return 1.0 |
| | if insunits == 1: |
| | |
| | return 25.4 |
| | if insunits == 2: |
| | |
| | return 25.4 * 12 |
| | if insunits == 3: |
| | |
| | return 1609344.0 |
| | if insunits == 4: |
| | |
| | return 1.0 |
| | if insunits == 5: |
| | |
| | return 10.0 |
| | if insunits == 6: |
| | |
| | return 1000.0 |
| | if insunits == 7: |
| | |
| | return 1000000.0 |
| | if insunits == 8: |
| | |
| | return 25.4 / 1000.0 |
| | if insunits == 9: |
| | |
| | return 25.4 / 1000.0 |
| | if insunits == 10: |
| | |
| | return 3 * 12 * 25.4 |
| | if insunits == 11: |
| | |
| | return 0.0000001 |
| | if insunits == 12: |
| | |
| | return 0.000001 |
| | if insunits == 13: |
| | |
| | return 0.001 |
| | if insunits == 14: |
| | |
| | return 100.0 |
| | if insunits == 15: |
| | |
| | return 10000.0 |
| | if insunits == 16: |
| | |
| | return 100000.0 |
| | if insunits == 17: |
| | |
| | return 1000000000000.0 |
| | if insunits == 18: |
| | |
| | return 149597870690000.0 |
| | if insunits == 19: |
| | |
| | return 9454254955500000000.0 |
| | if insunits == 20: |
| | |
| | return 30856774879000000000.0 |
| |
|
| | |
| | return 1.0 |
| |
|
| |
|
| | def processdxf(document, filename, getShapes=False, reComputeFlag=True): |
| | """Process the DXF file, creating Part objects in the document. |
| | |
| | If the `dxfReader` module is not available run `getDXFlibs()` |
| | to get the required libraries and `readPreferences()`. |
| | |
| | It defines the global variables `drawing`, `layers`, `doc`, |
| | `blockshapes`, `blockobjects`, `badobjects`, `layerBlocks`. |
| | The read data is placed in the object `drawing`. |
| | |
| | It iterates over `drawing.tables` to find tables of type `'layer'`, |
| | and adds them to the document considering its color and drawing style. |
| | Then it iterates over the `drawing.entities` processing the most common |
| | drawing types, that include `'line'`, `'lwpolyline'`, `'polyline'`, |
| | `'arc'`, `'circle'`, `'solid'`, `'spline'`, `'ellipse'`, `'mtext'`, |
| | `'text'`, and `'3dface'`. |
| | If `getShapes` is `False` it will additionally process the types |
| | `'dimension'`, `'point'`, `'leader'`, `'hatch'`, and `'insert'`. |
| | |
| | Parameters |
| | ---------- |
| | document : App::Document |
| | A document object opened in which to create the new Part shapes. |
| | |
| | filename : str |
| | The path to the DXF file to process. |
| | |
| | getShapes : bool, optional |
| | It defaults to `False`. If it is `True` it will try creating |
| | simple `Part Shapes` instead of Draft objects, |
| | and will immediately return the list of the most common shapes |
| | without processing the entities of types `'dimension'`, `'point'`, |
| | `'leader'`, `'hatch'`, and `'insert'`. |
| | |
| | reComputeFlag : bool, optional |
| | It defaults to `True`, in which case it recomputes the document |
| | after finishing processing of the entities. |
| | Otherwise, it skips the recompute. |
| | |
| | The recompute causes OpenSCAD import to loop, so this flag |
| | can be set to `False` to prevent this. |
| | |
| | Returns |
| | ------- |
| | list of `Part.Shapes` |
| | It returns `None` if the edges (lines, polylines, arcs) |
| | are above 100, and the user decides to interrupt (graphically) |
| | the process of joining them. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | |
| | |
| | global drawing |
| | if not dxfReader: |
| | getDXFlibs() |
| | readPreferences() |
| | FCC.PrintMessage("opening " + filename + "...\n") |
| | drawing = dxfReader.readDXF(filename) |
| | global resolvedScale |
| | resolvedScale = getScaleFromDXF(drawing.header) * dxfScaling |
| | global layers |
| | typ = "Layer" if dxfUseDraftVisGroups else "App::DocumentObjectGroup" |
| | layers = [o for o in FreeCAD.ActiveDocument.Objects if Draft.getType(o) == typ] |
| | global doc |
| | doc = document |
| | global blockshapes |
| | blockshapes = {} |
| | global blockobjects |
| | blockobjects = {} |
| | global badobjects |
| | badobjects = [] |
| | global layerBlocks |
| | layerBlocks = {} |
| | global layerObjects |
| | layerObjects = {} |
| | sketch = None |
| | shapes = [] |
| |
|
| | |
| | if hasattr(drawing, "tables"): |
| | for table in drawing.tables.get_type("table"): |
| | for layer in table.get_type("layer"): |
| | name = layer.name |
| | color = tuple(dxfColorMap.color_map[abs(layer.color)]) |
| | drawstyle = "Solid" |
| | lt = rawValue(layer, 6) |
| | if "DASHED" in lt.upper(): |
| | drawstyle = "Dashed" |
| | elif "HIDDEN" in lt.upper(): |
| | drawstyle = "Dotted" |
| | if ("DASHDOT" in lt.upper()) or ("CENTER" in lt.upper()): |
| | drawstyle = "Dashdot" |
| | locateLayer(name, color, drawstyle, layer.color > 0) |
| | else: |
| | locateLayer("0", (0.0, 0.0, 0.0), "Solid") |
| |
|
| | |
| | lines = drawing.entities.get_type("line") |
| | if lines: |
| | FCC.PrintMessage("drawing " + str(len(lines)) + " lines...\n") |
| | for line in lines: |
| | if dxfImportLayouts or (not rawValue(line, 67)): |
| | shape = drawLine(line) |
| | if shape: |
| | if dxfCreateSketch: |
| | FreeCAD.ActiveDocument.recompute() |
| | if dxfMakeBlocks or dxfJoin: |
| | if sketch: |
| | shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch) |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | sketch = shape |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | elif dxfJoin or getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | elif dxfMakeBlocks: |
| | addToBlock(shape, line.layer) |
| | else: |
| | newob = addObject(shape, "Line", line.layer) |
| | if gui: |
| | formatObject(newob, line) |
| |
|
| | |
| | pls = drawing.entities.get_type("lwpolyline") |
| | pls.extend(drawing.entities.get_type("polyline")) |
| | polylines = [] |
| | meshes = [] |
| | for p in pls: |
| | if hasattr(p, "flags"): |
| | if p.flags in [16, 64]: |
| | meshes.append(p) |
| | else: |
| | polylines.append(p) |
| | else: |
| | polylines.append(p) |
| | if polylines: |
| | FCC.PrintMessage("drawing " + str(len(polylines)) + " polylines...\n") |
| | num = 0 |
| | for polyline in polylines: |
| | if dxfImportLayouts or (not rawValue(polyline, 67)): |
| | shape = drawPolyline(polyline, num=num) |
| | if shape: |
| | if dxfCreateSketch: |
| | if isinstance(shape, Part.Shape): |
| | t = FreeCAD.ActiveDocument.addObject("Part::Feature", "Shape") |
| | t.Shape = shape |
| | shape = t |
| | FreeCAD.ActiveDocument.recompute() |
| | if dxfMakeBlocks or dxfJoin: |
| | if sketch: |
| | shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch) |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | sketch = shape |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | elif dxfJoin or getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | elif dxfMakeBlocks: |
| | addToBlock(shape, polyline.layer) |
| | else: |
| | newob = addObject(shape, "Polyline", polyline.layer) |
| | if gui: |
| | formatObject(newob, polyline) |
| | num += 1 |
| |
|
| | |
| | arcs = drawing.entities.get_type("arc") |
| | if arcs: |
| | FCC.PrintMessage("drawing " + str(len(arcs)) + " arcs...\n") |
| | for arc in arcs: |
| | if dxfImportLayouts or (not rawValue(arc, 67)): |
| | shape = drawArc(arc) |
| | if shape: |
| | if dxfCreateSketch: |
| | FreeCAD.ActiveDocument.recompute() |
| | if dxfMakeBlocks or dxfJoin: |
| | if sketch: |
| | shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch) |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | sketch = shape |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | elif dxfJoin or getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | elif dxfMakeBlocks: |
| | addToBlock(shape, arc.layer) |
| | else: |
| | newob = addObject(shape, "Arc", arc.layer) |
| | if gui: |
| | formatObject(newob, arc) |
| |
|
| | |
| | if dxfJoin and shapes: |
| | FCC.PrintMessage("Joining geometry...\n") |
| | edges = [] |
| | for s in shapes: |
| | edges.extend(s.Edges) |
| | if len(edges) > (100): |
| | FCC.PrintMessage(str(len(edges)) + " edges to join\n") |
| | if gui: |
| | d = QtWidgets.QMessageBox() |
| | d.setText("Warning: High number of entities to join (>100)") |
| | d.setInformativeText( |
| | "This might take a long time " |
| | "or even freeze your computer. " |
| | "Are you sure? You can also disable " |
| | "the 'join geometry' setting in DXF " |
| | "import preferences" |
| | ) |
| | d.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) |
| | d.setDefaultButton(QtWidgets.QMessageBox.Cancel) |
| | res = d.exec_() |
| | if res == QtWidgets.QMessageBox.Cancel: |
| | FCC.PrintMessage("Aborted\n") |
| | return |
| | shapes = DraftGeomUtils.findWires(edges) |
| | for s in shapes: |
| | newob = addObject(s) |
| |
|
| | |
| | circles = drawing.entities.get_type("circle") |
| | if circles: |
| | FCC.PrintMessage("drawing " + str(len(circles)) + " circles...\n") |
| | for circle in circles: |
| | if dxfImportLayouts or (not rawValue(circle, 67)): |
| | shape = drawCircle(circle) |
| | if shape: |
| | if dxfCreateSketch: |
| | FreeCAD.ActiveDocument.recompute() |
| | if dxfMakeBlocks or dxfJoin: |
| | if sketch: |
| | shape = Draft.make_sketch(shape, autoconstraints=True, addTo=sketch) |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | sketch = shape |
| | else: |
| | shape = Draft.make_sketch(shape, autoconstraints=True) |
| | elif dxfMakeBlocks: |
| | addToBlock(shape, circle.layer) |
| | elif getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | else: |
| | newob = addObject(shape, "Circle", circle.layer) |
| | if gui: |
| | formatObject(newob, circle) |
| |
|
| | |
| | solids = drawing.entities.get_type("solid") |
| | if solids: |
| | FCC.PrintMessage("drawing " + str(len(solids)) + " solids...\n") |
| | for solid in solids: |
| | lay = rawValue(solid, 8) |
| | if dxfImportLayouts or (not rawValue(solid, 67)): |
| | shape = drawSolid(solid) |
| | if shape: |
| | if dxfMakeBlocks: |
| | addToBlock(shape, lay) |
| | elif getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | else: |
| | newob = addObject(shape, "Solid", lay) |
| | if gui: |
| | formatObject(newob, solid) |
| |
|
| | |
| | splines = drawing.entities.get_type("spline") |
| | if splines: |
| | FCC.PrintMessage("drawing " + str(len(splines)) + " splines...\n") |
| | for spline in splines: |
| | lay = rawValue(spline, 8) |
| | if dxfImportLayouts or (not rawValue(spline, 67)): |
| | shape = drawSpline(spline) |
| | if shape: |
| | if dxfMakeBlocks: |
| | addToBlock(shape, lay) |
| | elif getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | else: |
| | newob = addObject(shape, "Spline", lay) |
| | if gui: |
| | formatObject(newob, spline) |
| |
|
| | |
| | ellipses = drawing.entities.get_type("ellipse") |
| | if ellipses: |
| | FCC.PrintMessage("drawing " + str(len(ellipses)) + " ellipses...\n") |
| | for ellipse in ellipses: |
| | lay = rawValue(ellipse, 8) |
| | if dxfImportLayouts or (not rawValue(ellipse, 67)): |
| | shape = drawEllipse(ellipse) |
| | if shape: |
| | if dxfMakeBlocks: |
| | addToBlock(shape, lay) |
| | elif getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | else: |
| | newob = addObject(shape, "Ellipse", lay) |
| | if gui: |
| | formatObject(newob, ellipse) |
| |
|
| | |
| | if dxfImportTexts: |
| | texts = drawing.entities.get_type("mtext") |
| | texts.extend(drawing.entities.get_type("text")) |
| | if texts: |
| | FCC.PrintMessage("drawing " + str(len(texts)) + " texts...\n") |
| | for text in texts: |
| | if dxfImportLayouts or (not rawValue(text, 67)): |
| | addText(text) |
| | else: |
| | FCC.PrintMessage("skipping texts...\n") |
| |
|
| | |
| | faces3d = drawing.entities.get_type("3dface") |
| | if faces3d: |
| | FCC.PrintMessage("drawing " + str(len(faces3d)) + " 3dfaces...\n") |
| | for face3d in faces3d: |
| | shape = drawFace(face3d) |
| | if shape: |
| | if getShapes: |
| | if isinstance(shape, Part.Shape): |
| | shapes.append(shape) |
| | else: |
| | shapes.append(shape.Shape) |
| | else: |
| | newob = addObject(shape, "Face", face3d.layer) |
| | if gui: |
| | formatObject(newob, face3d) |
| | if meshes: |
| | FCC.PrintMessage("drawing " + str(len(meshes)) + " 3dmeshes...\n") |
| | for mesh in meshes: |
| | me = drawMesh(mesh) |
| | if me: |
| | newob = doc.addObject("Mesh::Feature", "Mesh") |
| | lay = locateLayer(rawValue(mesh, 8)) |
| | lay.addObject(newob) |
| | newob.Mesh = me |
| | if gui: |
| | formatObject(newob, mesh) |
| |
|
| | |
| | if getShapes and shapes: |
| | return shapes |
| |
|
| | |
| | if dxfImportTexts: |
| | dims = drawing.entities.get_type("dimension") |
| | FCC.PrintMessage("drawing " + str(len(dims)) + " dimensions...\n") |
| | for dim in dims: |
| | if dxfImportLayouts or (not rawValue(dim, 67)): |
| | try: |
| | layer = rawValue(dim, 8) |
| | if rawValue(dim, 15) is not None: |
| | |
| | |
| | |
| | |
| | x2 = float(rawValue(dim, 10)) |
| | y2 = float(rawValue(dim, 20)) |
| | z2 = float(rawValue(dim, 30)) |
| | x3 = float(rawValue(dim, 15)) |
| | y3 = float(rawValue(dim, 25)) |
| | z3 = float(rawValue(dim, 35)) |
| | x1 = x2 |
| | y1 = y2 |
| | z1 = z2 |
| | else: |
| | x1 = float(rawValue(dim, 10)) |
| | y1 = float(rawValue(dim, 20)) |
| | z1 = float(rawValue(dim, 30)) |
| | x2 = float(rawValue(dim, 13)) |
| | y2 = float(rawValue(dim, 23)) |
| | z2 = float(rawValue(dim, 33)) |
| | x3 = float(rawValue(dim, 14)) |
| | y3 = float(rawValue(dim, 24)) |
| | z3 = float(rawValue(dim, 34)) |
| | d = rawValue(dim, 70) |
| | if d: |
| | align = int(d) |
| | else: |
| | align = 0 |
| | d = rawValue(dim, 50) |
| | if d: |
| | angle = float(d) |
| | else: |
| | angle = 0 |
| | except (ValueError, TypeError): |
| | warn(dim) |
| | else: |
| | lay = locateLayer(layer) |
| | pt = vec([x1, y1, z1]) |
| | p1 = vec([x2, y2, z2]) |
| | p2 = vec([x3, y3, z3]) |
| | if align >= 128: |
| | align -= 128 |
| | elif align >= 64: |
| | align -= 64 |
| | elif align >= 32: |
| | align -= 32 |
| | if align == 0: |
| | if angle in [0, 180]: |
| | p2 = vec([x3, y2, z2]) |
| | elif angle in [90, 270]: |
| | p2 = vec([x2, y3, z2]) |
| | newob = doc.addObject("App::FeaturePython", "Dimension") |
| | if hasattr(lay, "addObject"): |
| | lay.addObject(newob) |
| | elif hasattr(lay, "Proxy") and hasattr(lay.Proxy, "addObject"): |
| | lay.Proxy.addObject(lay, newob) |
| | _Dimension(newob) |
| | if gui: |
| | from Draft import _ViewProviderDimension |
| |
|
| | _ViewProviderDimension(newob.ViewObject) |
| | newob.Start = p1 |
| | newob.End = p2 |
| | newob.Dimline = pt |
| | if gui: |
| | dim.layer = layer |
| | dim.color_index = 256 |
| | formatObject(newob, dim) |
| | if dxfUseStandardSize and draftui: |
| | newob.ViewObject.FontSize = draftui.fontsize |
| | else: |
| | st = rawValue(dim, 3) |
| | size = getdimheight(st) or 1 |
| | newob.ViewObject.FontSize = float(size) * TEXTSCALING |
| | else: |
| | FCC.PrintMessage("skipping dimensions...\n") |
| |
|
| | |
| | if dxfImportPoints: |
| | points = drawing.entities.get_type("point") |
| | if points: |
| | FCC.PrintMessage("drawing " + str(len(points)) + " points...\n") |
| | for point in points: |
| | x = vec(rawValue(point, 10)) |
| | y = vec(rawValue(point, 20)) |
| | |
| | if rawValue(point, 30): |
| | z = vec(rawValue(point, 30)) |
| | else: |
| | z = 0 |
| | lay = rawValue(point, 8) |
| | if dxfImportLayouts or (not rawValue(point, 67)): |
| | if dxfMakeBlocks: |
| | shape = Part.Vertex(x, y, z) |
| | addToBlock(shape, lay) |
| | else: |
| | newob = Draft.make_point(x, y, z) |
| | lay = locateLayer(lay) |
| | lay.addObject(newob) |
| | if gui: |
| | formatObject(newob, point) |
| | else: |
| | FCC.PrintMessage("skipping points...\n") |
| |
|
| | |
| | if dxfImportTexts: |
| | leaders = drawing.entities.get_type("leader") |
| | if leaders: |
| | FCC.PrintMessage("drawing " + str(len(leaders)) + " leaders...\n") |
| | for leader in leaders: |
| | if dxfImportLayouts or (not rawValue(leader, 67)): |
| | points = getMultiplePoints(leader) |
| | newob = Draft.make_wire(points) |
| | lay = locateLayer(rawValue(leader, 8)) |
| | lay.addObject(newob) |
| | if gui: |
| | newob.ViewObject.EndArrow = True |
| | formatObject(newob, leader) |
| | else: |
| | FCC.PrintMessage("skipping leaders...\n") |
| |
|
| | |
| | if dxfImportHatches: |
| | hatches = drawing.entities.get_type("hatch") |
| | if hatches: |
| | FCC.PrintMessage("drawing " + str(len(hatches)) + " hatches...\n") |
| | for hatch in hatches: |
| | if dxfImportLayouts or (not rawValue(hatch, 67)): |
| | points = getMultiplePoints(hatch) |
| | if len(points) > 1: |
| | lay = rawValue(hatch, 8) |
| | points = points[:-1] |
| | newob = None |
| | if dxfCreatePart or dxfMakeBlocks: |
| | points.append(points[0]) |
| | s = Part.makePolygon(points) |
| | if dxfMakeBlocks: |
| | addToBlock(s, lay) |
| | else: |
| | newob = addObject(s, "Hatch", lay) |
| | if gui: |
| | formatObject(newob, hatch) |
| | else: |
| | newob = Draft.make_wire(points) |
| | locateLayer(lay).addObject(newob) |
| | if gui: |
| | formatObject(newob, hatch) |
| | else: |
| | FCC.PrintMessage("skipping hatches...\n") |
| |
|
| | |
| | inserts = drawing.entities.get_type("insert") |
| | if not dxfStarBlocks: |
| | FCC.PrintMessage("skipping *blocks...\n") |
| | newinserts = [] |
| | for i in inserts: |
| | if dxfImportLayouts or (not rawValue(i, 67)): |
| | if i.block[0] != "*": |
| | newinserts.append(i) |
| | inserts = newinserts |
| | if inserts: |
| | FCC.PrintMessage("drawing " + str(len(inserts)) + " blocks...\n") |
| | blockrefs = drawing.blocks.data |
| | for ref in blockrefs: |
| | if dxfCreateDraft or dxfCreateSketch: |
| | drawBlock(ref, createObject=True) |
| | else: |
| | drawBlock(ref, createObject=False) |
| | num = 0 |
| | for insert in inserts: |
| | if (dxfCreateDraft or dxfCreateSketch) and not dxfMakeBlocks: |
| | shape = drawInsert(insert, num, clone=True) |
| | else: |
| | shape = drawInsert(insert, num) |
| | if shape: |
| | if dxfMakeBlocks: |
| | addToBlock(shape, insert.layer) |
| | else: |
| | newob = addObject(shape, "Block." + insert.block, insert.layer) |
| | if gui: |
| | formatObject(newob, insert) |
| | num += 1 |
| |
|
| | |
| | if dxfMakeBlocks: |
| | print("creating layerblocks...") |
| | for k, l in layerBlocks.items(): |
| | shape = drawLayerBlock(l, "LayerBlock_" + k) |
| | if shape: |
| | newob = addObject(shape, "LayerBlock_" + k, k) |
| | del layerBlocks |
| |
|
| | |
| | for k, o in blockobjects.items(): |
| | if o.ViewObject: |
| | o.ViewObject.hide() |
| | del blockobjects |
| |
|
| | |
| | for l, contents in layerObjects.items(): |
| | l.Group += contents |
| |
|
| | |
| | print("done processing") |
| |
|
| | if reComputeFlag: |
| | doc.recompute() |
| | print("recompute done") |
| |
|
| | FCC.PrintMessage("successfully imported " + filename + "\n") |
| | if badobjects: |
| | print("dxf: ", len(badobjects), " objects were not imported") |
| | del doc |
| |
|
| |
|
| | def warn(dxfobject, num=None): |
| | """Print a warning that the DXF object couldn't be imported. |
| | |
| | Also add the object to the global list `badobjects`. |
| | |
| | Parameters |
| | ---------- |
| | dxfobject : drawing.entities |
| | The DXF object that couldn't be imported. |
| | |
| | num : float, optional |
| | It defaults to `None`. A simple number that identifies |
| | the given `dxfobject`. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | print("dxf: couldn't import ", dxfobject, " (", num, ")") |
| | badobjects.append(dxfobject) |
| |
|
| |
|
| | def _import_dxf_file(filename, doc_name=None): |
| | """ |
| | Internal helper to handle the core logic for both open and insert. |
| | """ |
| | hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") |
| | use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False) |
| | readPreferences() |
| |
|
| | |
| | try: |
| | if gui: |
| | FreeCADGui.suspendWaitCursor() |
| |
|
| | if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True): |
| | try: |
| | import ImportGui |
| |
|
| | entity_counts = ImportGui.preScanDxf(filename) |
| | except Exception: |
| | entity_counts = {} |
| |
|
| | from DxfImportDialog import DxfImportDialog |
| |
|
| | dlg = DxfImportDialog(entity_counts) |
| |
|
| | if dlg.exec_(): |
| | |
| | hGrp.SetInt("DxfImportMode", dlg.get_selected_mode()) |
| |
|
| | |
| | |
| | mode = dlg.get_selected_mode() |
| | params.set_param("dxfImportAsDraft", mode == 0) |
| | params.set_param("dxfImportAsPrimitives", mode == 1) |
| | params.set_param("dxfImportAsShapes", mode == 2) |
| | params.set_param("dxfImportAsFused", mode == 3) |
| | hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again()) |
| | else: |
| | return None, None, None, None |
| | finally: |
| | if gui: |
| | FreeCADGui.resumeWaitCursor() |
| |
|
| | import_mode = hGrp.GetInt("DxfImportMode", 2) |
| |
|
| | |
| | if doc_name: |
| | try: |
| | doc = FreeCAD.getDocument(doc_name) |
| | except NameError: |
| | doc = FreeCAD.newDocument(doc_name) |
| | FreeCAD.setActiveDocument(doc_name) |
| | else: |
| | docname = os.path.splitext(os.path.basename(filename))[0] |
| | doc = FreeCAD.newDocument(docname) |
| | doc.Label = docname |
| | FreeCAD.setActiveDocument(doc.Name) |
| |
|
| | |
| | processing_start_time = time.perf_counter() |
| |
|
| | |
| | objects_before = set(doc.Objects) |
| |
|
| | stats = None |
| | if use_legacy: |
| | getDXFlibs() |
| | if dxfReader: |
| | processdxf(doc, filename) |
| | else: |
| | errorDXFLib(gui) |
| | return None, None |
| | else: |
| | if gui: |
| | import ImportGui |
| |
|
| | stats = ImportGui.readDXF(filename) |
| | else: |
| | import Import |
| |
|
| | stats = Import.readDXF(filename) |
| |
|
| | |
| | objects_after = set(doc.Objects) |
| | newly_created_objects = objects_after - objects_before |
| |
|
| | |
| | if not use_legacy and newly_created_objects: |
| | draft_postprocessor = DxfDraftPostProcessor(doc, newly_created_objects, import_mode) |
| | draft_postprocessor.run() |
| |
|
| | Draft.convert_draft_texts() |
| | doc.recompute() |
| |
|
| | processing_end_time = time.perf_counter() |
| |
|
| | |
| | return doc, stats, processing_start_time, processing_end_time |
| |
|
| |
|
| | def open(filename): |
| | """Open a file and return a new document. |
| | |
| | This function handles the import of a DXF file into a new document. |
| | It shows an import dialog for the modern C++ importer if configured to do so. |
| | It manages the import workflow, including pre-processing, calling the |
| | correct backend (legacy or modern C++), and post-processing. |
| | |
| | Parameters |
| | ---------- |
| | filename : str |
| | The path to the file to open. |
| | |
| | Returns |
| | ------- |
| | App::Document or None |
| | The new document object with imported content, or None if the |
| | operation was cancelled or failed. |
| | """ |
| | doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=None) |
| |
|
| | if doc and stats: |
| | reporter = DxfImportReporter(filename, stats, end_time - start_time) |
| | reporter.report_to_console() |
| |
|
| | return doc |
| |
|
| |
|
| | def insert(filename, docname): |
| | """Import a file into the specified document. |
| | |
| | This function handles the import of a DXF file into a specified document. |
| | If the document does not exist, it will be created. It shows an import |
| | dialog for the modern C++ importer if configured to do so. |
| | |
| | Parameters |
| | ---------- |
| | filename : str |
| | The path to the file to import. |
| | docname : str |
| | The name of an App::Document instance to import the content into. |
| | """ |
| | doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=docname) |
| |
|
| | if doc and stats: |
| | reporter = DxfImportReporter(filename, stats, end_time - start_time) |
| | reporter.report_to_console() |
| |
|
| |
|
| | def getShapes(filename): |
| | """Read a DXF file, and return a list of shapes from its contents. |
| | |
| | This is an auxiliary function that processes the DXF file to list its |
| | contents but doesn't open or create a new document. |
| | |
| | Parameters |
| | ---------- |
| | filename : str |
| | The path to the file to read. |
| | |
| | Returns |
| | ------- |
| | list of `Part.Shapes` |
| | It returns `None` if the edges (lines, polylines, arcs) |
| | are above 100, and the user decides to interrupt (graphically) |
| | the process of joining them. |
| | |
| | See also |
| | -------- |
| | open, insert |
| | """ |
| | if dxfReader: |
| | return processdxf(None, filename, getShapes=True) |
| |
|
| |
|
| | |
| |
|
| |
|
| | def projectShape(shape, direction, tess=None): |
| | """Project shape in a given direction. |
| | |
| | It uses `TechDraw.projectEx(shape, direction)` |
| | to return a list with all the parts of the projection. |
| | The first five elements are added to a list of edges, |
| | which are then put in a `Part.Compound`. |
| | |
| | Parameters |
| | ---------- |
| | shape : Part.Shape |
| | Any shape previously created from a DXF file. |
| | |
| | direction : Base::Vector3 |
| | The direction of the projection. |
| | |
| | tess : list, optional |
| | It defaults to `None`. If it is available, it is a list with |
| | two elements, `[True, segment_length]` which are used by |
| | `DraftGeomUtils.cleanProjection(compound, tess[0], tess[1])` |
| | to create a valid compound of edges. |
| | |
| | Otherwise, a simple `Part.Compound` is produced. |
| | |
| | Returns |
| | ------- |
| | Part::TopoShape ('Compound') |
| | A `Part.Compound` of edges. |
| | |
| | It returns the original `shape` if it fails producing the projection |
| | in the given `direction`. |
| | |
| | See also |
| | -------- |
| | TechDraw.projectEx, DraftGeomUtils.cleanProjection |
| | """ |
| | import TechDraw |
| |
|
| | edges = [] |
| | try: |
| | groups = TechDraw.projectEx(shape, direction) |
| | except Part.OCCError: |
| | print("unable to project shape on direction ", direction) |
| | return shape |
| | else: |
| | for g in groups[0:5]: |
| | if g: |
| | edges.append(g) |
| | |
| | if tess: |
| | return DraftGeomUtils.cleanProjection(Part.makeCompound(edges), tess[0], tess[1]) |
| | else: |
| | return Part.makeCompound(edges) |
| | |
| |
|
| |
|
| | def getArcData(edge): |
| | """Return center, radius, start, and end angles of a circle-based edge. |
| | |
| | Parameters |
| | ---------- |
| | edge : Part::TopoShape ('Edge') |
| | An edge representing a circular arc, either open or closed. |
| | |
| | Returns |
| | ------- |
| | (tuple, float, float, float) |
| | It returns a tuple of four values; the first value is a tuple |
| | with the coordinates of the center `(x, y, z)`; |
| | the other three represent the magnitude of the radius, |
| | and the start and end angles in degrees that define the arc. |
| | |
| | (tuple, float, 0, 0) |
| | If the number of vertices in the `edge` is only one, only the center |
| | point exists, so it's a full circumference; in this case, both |
| | angles are zero. |
| | """ |
| | ce = edge.Curve.Center |
| | radius = edge.Curve.Radius |
| | if len(edge.Vertexes) == 1: |
| | |
| | return DraftVecUtils.tup(ce), radius, 0, 0 |
| | else: |
| | |
| | |
| | p1 = edge.Vertexes[0].Point |
| | p2 = edge.Vertexes[-1].Point |
| | v1 = p1.sub(ce) |
| | v2 = p2.sub(ce) |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if edge.Curve.Axis.z > 0: |
| | |
| | ang1 = -DraftVecUtils.angle(v1) |
| | ang2 = -DraftVecUtils.angle(v2) |
| | else: |
| | |
| | ang2 = -DraftVecUtils.angle(v1) |
| | ang1 = -DraftVecUtils.angle(v2) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | return (DraftVecUtils.tup(ce), radius, math.degrees(ang1), math.degrees(ang2)) |
| |
|
| |
|
| | def getSplineSegs(edge): |
| | """Return a list of points from an edge that is a spline or bezier curve. |
| | |
| | Parameters |
| | ---------- |
| | edge : Part::TopoShape ('Edge') |
| | An edge representing a spline or bezier curve. |
| | |
| | Returns |
| | ------- |
| | list of Base::Vector3 |
| | It returns a list with the points that form the curve. |
| | It returns the point in `edge.FirstParameter`, |
| | all the intermediate points, and the point in `edge.LastParameter`. |
| | |
| | If the `segmentlength` variable is zero in the parameters database, |
| | then it only returns the first and the last point of the `edge`. |
| | """ |
| | seglength = params.get_param("maxsegmentlength") |
| | points = [] |
| | if seglength == 0: |
| | points.append(edge.Vertexes[0].Point) |
| | points.append(edge.Vertexes[-1].Point) |
| | else: |
| | points.append(edge.valueAt(edge.FirstParameter)) |
| | if edge.Length > seglength: |
| | nbsegs = int(math.ceil(edge.Length / seglength)) |
| | step = (edge.LastParameter - edge.FirstParameter) / nbsegs |
| | for nv in range(1, nbsegs): |
| | |
| | v = edge.valueAt(edge.FirstParameter + (nv * step)) |
| | points.append(v) |
| | points.append(edge.valueAt(edge.LastParameter)) |
| | return points |
| |
|
| |
|
| | def getWire(wire, nospline=False, lw=True, asis=False): |
| | """Return a list of DXF ready points and bulges from a wire. |
| | |
| | It builds a list of points from the edges of a `wire`. |
| | If the edges are circular arcs, the "bulge" of that edge is calculated, |
| | for other cases, the bulge is considered zero. |
| | |
| | Parameters |
| | ---------- |
| | wire : Part::TopoShape ('Wire') |
| | A shape representing a wire. |
| | |
| | nospline : bool, optional |
| | It defaults to `False`. |
| | If it is `True`, the edges of the wire are not considered as |
| | being one of `'BSplineCurve'`, `'BezierCurve'`, or `'Ellipse'`, |
| | and a simple point is added to the list. |
| | Otherwise, `getSplineSegs(edge)` is used to extract |
| | the points and add them to the list. |
| | |
| | lw : bool, optional |
| | It defaults to `True`. If it is `True` it assumes the `wire` |
| | is a `'lwpolyline'`. |
| | Otherwise, it assumes it is a `'polyline'`. |
| | |
| | asis : bool, optional |
| | It defaults to `False`. If it is `True`, it just returns |
| | the points of the vertices of the `wire`, and considers the bulge |
| | is zero. |
| | |
| | Otherwise, it processes the edges of the `wire` and calculates |
| | the bulge of the edges if they are of type `'Circle'`. |
| | For types of edges that are `'BSplineCurve'`, `'BezierCurve'`, |
| | or `'Ellipse'`, the bulge is zero |
| | |
| | Returns |
| | ------- |
| | list of tuples |
| | It returns a list of tuples ``[(...), (...), ...]`` |
| | where each tuple indicates a point with additional information |
| | besides the coordinates. |
| | Two types of tuples may be returned. |
| | |
| | [(float, float, float, None, None, float), ...] |
| | When `lw` is `True` (`'lwpolyline'`) |
| | the first three values represent the coordinates of the point, |
| | the next two are `None`, and the last value is the bulge. |
| | |
| | [((float, float, float), None, [None, None], float), ...] |
| | When `lw` is `False` (`'polyline'`) |
| | the first element is a tuple of three values that indicate |
| | the coordinates of the point, the next element is `None`, |
| | the next element is a list of two `None` values, |
| | and the last element is the value of the bulge. |
| | |
| | See also |
| | -------- |
| | calcBulge |
| | """ |
| |
|
| | def fmt(v, b=0.0): |
| | if lw: |
| | |
| | return (v.x, v.y, v.z, None, None, b) |
| | else: |
| | |
| | return ((v.x, v.y, v.z), None, [None, None], b) |
| |
|
| | points = [] |
| | if asis: |
| | points = [fmt(v.Point) for v in wire.OrderedVertexes] |
| | else: |
| | edges = Part.__sortEdges__(wire.Edges) |
| | |
| | for edge in edges: |
| | v1 = edge.Vertexes[0].Point |
| | if DraftGeomUtils.geomType(edge) == "Circle": |
| | |
| | angle = edge.LastParameter - edge.FirstParameter |
| | bul = math.tan(angle / 4) |
| | |
| | |
| | |
| | if edge.Curve.Axis.dot(Vector(0, 0, 1)) < 0: |
| | bul = -bul |
| | points.append(fmt(v1, bul)) |
| | elif (DraftGeomUtils.geomType(edge) in ["BSplineCurve", "BezierCurve", "Ellipse"]) and ( |
| | not nospline |
| | ): |
| | spline = getSplineSegs(edge) |
| | spline.pop() |
| | for p in spline: |
| | points.append(fmt(p)) |
| | else: |
| | points.append(fmt(v1)) |
| | if not DraftGeomUtils.isReallyClosed(wire): |
| | v = edges[-1].Vertexes[-1].Point |
| | points.append(fmt(v)) |
| | |
| | return points |
| |
|
| |
|
| | def getBlock(sh, obj, lwPoly=False): |
| | """Return a DXF block with the contents of the object. |
| | |
| | It creates a `block` object using `dxfLibrary.Block`, |
| | and then writes the given shape with |
| | `writeShape(sh, obj, block, lwPoly)`. |
| | |
| | Parameters |
| | ---------- |
| | sh : Part::TopoShape |
| | Any shape in the document. |
| | |
| | obj : App::DocumentObject |
| | Any object in the document. |
| | |
| | lwPoly : bool, optional |
| | It defaults to `False`. If it is `True` it will write |
| | a `'lwpolyline'`. |
| | Otherwise, it will be a `'polyline'`. |
| | |
| | Returns |
| | ------- |
| | dxfLibrary.Block |
| | The block of data with the given `sh` shape and `obj` object. |
| | """ |
| | block = dxfLibrary.Block(name=obj.Name, layer=getStrGroup(obj)) |
| | writeShape(sh, obj, block, lwPoly) |
| | return block |
| |
|
| |
|
| | def writeShape(sh, ob, dxfobject, nospline=False, lwPoly=False, layer=None, color=None, asis=False): |
| | """Write the object's shape contents in the given DXF object. |
| | |
| | Iterates over the wires (polylines) and lone edges of `sh`. |
| | Then it creates DXF object depending of the type of wire, |
| | and adds those objects to the `dxfobject` list. |
| | |
| | If the wire only has one edge and it is of type `'Circle'` |
| | it will create an object of type `dxfLibrary.Circle` or `dxfLibrary.Arc`. |
| | In other cases, it will try creating objects of type |
| | `dxfLibrary.LwPolyLine` or `dxfLibrary.PolyLine`. |
| | |
| | When parsing lone edges it will approximate single closed edges of type |
| | `'BSplineCurve'` or `'BezierCurve'` with a `dxfLibrary.Circle`. |
| | In the case of edges of type `Ellipse`, it can approximate |
| | the edge as a `dxfLibrary.PolyLine`, depending on the value |
| | of `'DiscretizeEllipses'` in the parameter database. |
| | Otherwise it creates an object of type `dxfLibrary.Ellipse`. |
| | |
| | For other lone edges, they are treated as lines, |
| | so they create an object of type `linesdxfLibrary.Line`. |
| | |
| | Parameters |
| | ---------- |
| | sh : Part::TopoShape |
| | Any shape in the document. |
| | |
| | ob : App::DocumentObject |
| | Any object in the document. |
| | |
| | dxfobject : dxfLibrary.Drawing |
| | An object which will be populated with DXF objects created |
| | from `sh.Wires` and `sh.Edges`. |
| | |
| | nospline : bool, optional |
| | It defaults to `False`. |
| | If it is `True`, the edges of the wire are not considered as |
| | being one of `'BSplineCurve'`, `'BezierCurve'`, or `'Ellipse'`, |
| | and simple points are used to build the new object with |
| | `getWire(wire, nospline=True, asis=asis)`. |
| | |
| | lwPoly : bool, optional |
| | It defaults to `False`. If it is `True` it will try producing |
| | a `dxfLibrary.LwPolyLine`, instead of a `dxfLibrary.PolyLine`. |
| | |
| | layer : str, optional |
| | It defaults to `None`. It is the name of the layer or group where `ob` |
| | is contained. If it is `None`, `getStrGroup(ob)` is called to search |
| | for the layer's name that contains `ob`. |
| | The created object is placed in this layer. |
| | |
| | color : int, optional |
| | It defaults to `None`. It is the AutoCAD color index (ACI) |
| | closest to `ob`'s color obtained with `getACI(ob)`. |
| | The created object uses this color. |
| | |
| | asis : bool, optional |
| | It defaults to `False`. If it is `True`, it just extracts |
| | the edges of the wire as is, and creates the `'lwpolyline'` |
| | or `'polyline'` with the simple points returned by |
| | `getWire(wire, nospline, asis=True)`. |
| | |
| | Otherwise, the edges are sorted, and then creates |
| | more complex shapes with `getWire(wire, nospline, asis=False)`. |
| | |
| | See also |
| | -------- |
| | getWire, getStrGroup, getACI, dxfLibrary.Circle, dxfLibrary.Arc, |
| | dxfLibrary.LwPolyLine, dxfLibrary.PolyLine, dxfLibrary.Ellipse, |
| | dxfLibrary.Line |
| | """ |
| | processededges = [] |
| | if not layer: |
| | layer = getStrGroup(ob) |
| | if not color: |
| | color = getACI(ob) |
| | for wire in sh.Wires: |
| | if asis: |
| | edges = wire.Edges |
| | else: |
| | edges = Part.__sortEdges__(wire.Edges) |
| | for e in edges: |
| | processededges.append(e.hashCode()) |
| | if (len(wire.Edges) == 1) and (DraftGeomUtils.geomType(wire.Edges[0]) == "Circle"): |
| | center, radius, ang1, ang2 = getArcData(wire.Edges[0]) |
| | if center is not None: |
| | if len(wire.Edges[0].Vertexes) == 1: |
| | dxfobject.append(dxfLibrary.Circle(center, radius, color=color, layer=layer)) |
| | else: |
| | dxfobject.append( |
| | dxfLibrary.Arc(center, radius, ang1, ang2, color=color, layer=layer) |
| | ) |
| | else: |
| | if lwPoly: |
| | if hasattr(dxfLibrary, "LwPolyLine"): |
| | dxfobject.append( |
| | dxfLibrary.LwPolyLine( |
| | getWire(wire, nospline, asis=asis), |
| | [0.0, 0.0], |
| | int(DraftGeomUtils.isReallyClosed(wire)), |
| | color=color, |
| | layer=layer, |
| | ) |
| | ) |
| | else: |
| | FCC.PrintWarning( |
| | "LwPolyLine support not found. " |
| | "Please delete dxfLibrary.py " |
| | "from your FreeCAD user directory " |
| | "to force auto-update\n" |
| | ) |
| | else: |
| | dxfobject.append( |
| | dxfLibrary.PolyLine( |
| | getWire(wire, nospline, lw=False, asis=asis), |
| | [0.0, 0.0, 0.0], |
| | int(DraftGeomUtils.isReallyClosed(wire)), |
| | color=color, |
| | layer=layer, |
| | ) |
| | ) |
| | if len(processededges) < len(sh.Edges): |
| | loneedges = [] |
| | for e in sh.Edges: |
| | if e.hashCode() not in processededges: |
| | loneedges.append(e) |
| | |
| | for edge in loneedges: |
| | |
| | if DraftGeomUtils.geomType(edge) in ["BSplineCurve", "BezierCurve"]: |
| | if (len(edge.Vertexes) == 1) and (edge.Curve.isClosed()) and (edge.Area > 0): |
| | |
| | c = DraftGeomUtils.getCircleFromSpline(edge) |
| | if c: |
| | dxfobject.append( |
| | dxfLibrary.Circle( |
| | DraftVecUtils.tup(c.Curve.Center), |
| | c.Curve.Radius, |
| | color=color, |
| | layer=layer, |
| | ) |
| | ) |
| | else: |
| | points = [] |
| | spline = getSplineSegs(edge) |
| | for p in spline: |
| | points.append(((p.x, p.y, p.z), None, [None, None], 0.0)) |
| | dxfobject.append( |
| | dxfLibrary.PolyLine(points, [0.0, 0.0, 0.0], 0, color=color, layer=layer) |
| | ) |
| | elif DraftGeomUtils.geomType(edge) == "Circle": |
| | center, radius, ang1, ang2 = getArcData(edge) |
| | if center is not None: |
| | if not isinstance(center, tuple): |
| | center = DraftVecUtils.tup(center) |
| | if len(edge.Vertexes) == 1: |
| | dxfobject.append( |
| | dxfLibrary.Circle(center, radius, color=color, layer=layer) |
| | ) |
| | else: |
| | dxfobject.append( |
| | dxfLibrary.Arc( |
| | center, radius, ang1, ang2, color=getACI(ob), layer=layer |
| | ) |
| | ) |
| | elif DraftGeomUtils.geomType(edge) == "Ellipse": |
| | if params.get_param("DiscretizeEllipses"): |
| | points = [] |
| | spline = getSplineSegs(edge) |
| | for p in spline: |
| | points.append(((p.x, p.y, p.z), None, [None, None], 0.0)) |
| | dxfobject.append( |
| | dxfLibrary.PolyLine(points, [0.0, 0.0, 0.0], 0, color=color, layer=layer) |
| | ) |
| | else: |
| | if hasattr(dxfLibrary, "Ellipse"): |
| | center = DraftVecUtils.tup(edge.Curve.Center) |
| | norm = DraftVecUtils.tup(edge.Curve.Axis) |
| | start = edge.FirstParameter |
| | end = edge.LastParameter |
| | ax = edge.Curve.Focus1.sub(edge.Curve.Center) |
| | major = DraftVecUtils.tup(DraftVecUtils.scaleTo(ax, edge.Curve.MajorRadius)) |
| | minor = edge.Curve.MinorRadius / edge.Curve.MajorRadius |
| | |
| | |
| | dxfobject.append( |
| | dxfLibrary.Ellipse( |
| | center=center, |
| | majorAxis=major, |
| | normalAxis=norm, |
| | minorAxisRatio=minor, |
| | startParameter=start, |
| | endParameter=end, |
| | color=color, |
| | layer=layer, |
| | ) |
| | ) |
| | else: |
| | FCC.PrintWarning( |
| | "Ellipses support not found. " |
| | "Please delete dxfLibrary.py " |
| | "from your FreeCAD user directory " |
| | "to force auto-update\n" |
| | ) |
| | else: |
| | if len(edge.Vertexes) > 1: |
| | ve1 = edge.Vertexes[0].Point |
| | ve2 = edge.Vertexes[1].Point |
| | dxfobject.append( |
| | dxfLibrary.Line( |
| | [DraftVecUtils.tup(ve1), DraftVecUtils.tup(ve2)], |
| | color=color, |
| | layer=layer, |
| | ) |
| | ) |
| |
|
| |
|
| | def writeMesh(ob, dxf): |
| | """Write an object's shape as a polyface mesh in the given DXF list. |
| | |
| | It tessellates the `ob.Shape` with a tolerance of 0.5, |
| | to produce mesh data, that is, lists of vertices and face indices: |
| | ``([ point1, point2, ...], [(face1 indices), (face2 indices), ...])`` |
| | |
| | The points and faces are extracted, and used with |
| | `dxfLibrary.PolyLine` to produce a polyface mesh, that is added |
| | to the `dxf` object. |
| | |
| | Parameters |
| | ---------- |
| | ob : App::DocumentObject |
| | Any object in the document. |
| | |
| | dxf : dxfLibrary.Drawing |
| | An object which will be populated with a DXF polyface mesh |
| | created from `ob.Shape`. |
| | |
| | See also |
| | -------- |
| | dxfLibrary.Drawing, dxfLibrary.PolyLine, Part.Shape.tessellate |
| | """ |
| | meshdata = ob.Shape.tessellate(0.5) |
| | |
| | points = [] |
| | faces = [] |
| | for p in meshdata[0]: |
| | points.append([p.x, p.y, p.z]) |
| | for f in meshdata[1]: |
| | faces.append([f[0] + 1, f[1] + 1, f[2] + 1]) |
| | |
| | dxf.append( |
| | dxfLibrary.PolyLine( |
| | [points, faces], [0.0, 0.0, 0.0], 64, color=getACI(ob), layer=getGroup(ob) |
| | ) |
| | ) |
| |
|
| |
|
| | def writePanelCut(ob, dxf, nospline, lwPoly, parent=None): |
| | """Create an object's outline and add it to the given DXF list. |
| | |
| | Given an object `ob` that contains an outline in its proxy object, |
| | it tries obtaining the outline `outl`, the inline `inl`, and a `tag`. |
| | Then tries creating each shape using the `parent` object as base |
| | (or `ob` itself), and placing the result in the `dxf` list. |
| | |
| | For `outl` it places the result in an `'Outlines'` layer of color index 5 |
| | (blue). |
| | For `intl`, if it exists, it places the result in a `'Cuts'` layer |
| | of color index 4 (light blue). |
| | For `tag`, if it exists, it places the result in a `'Tags'` layer |
| | of color index 2 (yellow). |
| | :: |
| | writeShape(outl, parent, dxf, nospline, lwPoly, ...) |
| | writeShape(inl, parent, dxf, nospline, lwPoly, ...) |
| | writeShape(tag, parent, dxf, nospline, lwPoly, ...) |
| | |
| | Parameters |
| | ---------- |
| | ob : App::DocumentObject |
| | Any object in the document. |
| | |
| | dxf : dxfLibrary.Drawing |
| | An object which will be populated with a DXF object created |
| | from `writeShape()`. |
| | |
| | nospline : bool |
| | If it is `True`, the edges of the wire are not considered as |
| | being one of `'BSplineCurve'`, `'BezierCurve'`, or `'Ellipse'`, |
| | and simple points are used to build the new shape with |
| | `writeShape()`. |
| | |
| | lwPoly : bool |
| | If it is `True` it will try producing |
| | a `dxfLibrary.LwPolyLine`, instead of a `dxfLibrary.PolyLine`, |
| | by using `writeShape()`. |
| | |
| | parent : App::DocumentObject, optional |
| | It defaults to `None`. |
| | If it exists, its `Base::Placement` is used to modify the |
| | Placement of the output object and its tag. |
| | Otherwise, `ob` is also used as the `parent`. |
| | |
| | See also |
| | -------- |
| | writeShape |
| | """ |
| | if not hasattr(ob.Proxy, "outline"): |
| | ob.Proxy.execute(ob) |
| | if hasattr(ob.Proxy, "outline"): |
| | outl = ob.Proxy.outline.copy() |
| | tag = None |
| | if hasattr(ob.Proxy, "tag"): |
| | tag = ob.Proxy.tag |
| | if tag: |
| | tag = tag.copy() |
| | tag.Placement = ob.Placement.multiply(tag.Placement) |
| | if parent: |
| | tag.Placement = parent.Placement.multiply(tag.Placement) |
| | outl.Placement = ob.Placement.multiply(outl.Placement) |
| | if parent: |
| | outl.Placement = parent.Placement.multiply(outl.Placement) |
| | else: |
| | parent = ob |
| | if len(outl.Wires) > 1: |
| | |
| | d = 0 |
| | ow = None |
| | for w in outl.Wires: |
| | if w.BoundBox.DiagonalLength > d: |
| | d = w.BoundBox.DiagonalLength |
| | ow = w |
| | if ow: |
| | inl = Part.Compound([w for w in outl.Wires if w.hashCode() != ow.hashCode()]) |
| | outl = ow |
| | else: |
| | inl = None |
| | outl = outl.Wires[0] |
| |
|
| | writeShape(outl, parent, dxf, nospline, lwPoly, layer="Outlines", color=5) |
| | if inl: |
| | writeShape(inl, parent, dxf, nospline, lwPoly, layer="Cuts", color=4) |
| | if tag: |
| | writeShape(tag, parent, dxf, nospline, lwPoly, layer="Tags", color=2, asis=True) |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | def getStrGroup(ob): |
| | """Get a string version of the group or layer that contains the object. |
| | |
| | Parameters |
| | ---------- |
| | ob : App::DocumentObject |
| | Any object in the document. |
| | |
| | Returns |
| | ------- |
| | str |
| | The name of the layer in capital letters, |
| | as the DXF R12 format seems to favor this style. |
| | """ |
| | return getGroup(ob).upper() |
| |
|
| |
|
| | def export(objectslist, filename, nospline=False, lwPoly=False): |
| | """Export a DXF file into the specified filename. |
| | |
| | If will read the preferences. If the global variable |
| | `dxfUseLegacyExporter` exists, it will try using the `Import` module |
| | to write the DXF file. |
| | :: |
| | Import.writeDXFObject(objectslist, filename, version, lwPoly) |
| | |
| | Where `version` is 14, or 12 if `nospline` is `True`. |
| | |
| | Otherwise it will try to use the DXF export libraries |
| | by running `getDXFlibs()`. |
| | |
| | Iterating over all objects it writes shapes individually |
| | with `writeShape()`, looking for types `'PanelSheet'`, `'PanelCut'`, |
| | `'Axis'`, `'Annotation'`, `'DraftText'`, `'Dimension'`. |
| | For objects derived from `'Part::Feature'` it may use `writeMesh()` |
| | depending on the parameter `'dxfmesh'`, or it may project the object |
| | in the camera view, depending on the parameter `'dxfproject'`. |
| | |
| | Parameters |
| | ---------- |
| | objectslist : list of App::DocumentObject |
| | A list with all objects that will be exported. |
| | If any object of the given list is a group, its contents are appended |
| | to the export list. |
| | |
| | If the list only contains an `'ArchSectionView'` object |
| | it will use its `getDXF()` method to provide the DXF information |
| | to write into `filename`. |
| | |
| | If the list only contains a `'TechDraw::DrawPage'` object it will use |
| | `exportPage()` to produce the DXF file. |
| | |
| | filename : str |
| | The path of the new DXF file. |
| | |
| | nospline : bool, optional |
| | It defaults to `False`. |
| | If it is `True`, the BSplines are exported as straight segments, |
| | when passing the objects to `writeShape()`. |
| | |
| | lwPoly : bool, optional. |
| | It defaults to `False`. |
| | If it is `True` it will try producing |
| | a `dxfLibrary.LwPolyLine`, instead of a `dxfLibrary.PolyLine`, |
| | by using `writeShape()`. |
| | This is required to produce an OpenSCAD DXF. |
| | |
| | Returns |
| | ------- |
| | It returns `None` if the export is successful. |
| | |
| | See also |
| | -------- |
| | dxfLibrary.Drawing, readPreferences, getDXFlibs, errorDXFLib, |
| | writeShape, writeMesh, Import.writeDXFObject |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | readPreferences() |
| | if not dxfUseLegacyExporter: |
| | import Import |
| |
|
| | version = 14 |
| | if nospline: |
| | version = 12 |
| | Import.writeDXFObject(objectslist, filename, version, lwPoly) |
| | return |
| | getDXFlibs() |
| | if dxfLibrary: |
| | global exportList |
| | exportList = Draft.get_group_contents(objectslist, spaces=True) |
| |
|
| | nlist = [] |
| | exportLayers = [] |
| | for ob in exportList: |
| | t = Draft.getType(ob) |
| | if t == "AxisSystem": |
| | nlist.extend(ob.Axes) |
| | elif t == "Layer": |
| | exportLayers.append(ob) |
| | for child in ob.Group: |
| | if child not in nlist: |
| | nlist.append(child) |
| | else: |
| | if ob not in nlist: |
| | nlist.append(ob) |
| | exportList = nlist |
| |
|
| | if (len(exportList) == 1) and (Draft.getType(exportList[0]) == "ArchSectionView"): |
| | |
| | dxf = exportList[0].Proxy.getDXF() |
| | if dxf: |
| | f = pyopen(filename, "w") |
| | f.write(dxf) |
| | f.close() |
| |
|
| | elif (len(exportList) == 1) and (exportList[0].isDerivedFrom("TechDraw::DrawPage")): |
| | |
| | exportPage(exportList[0], filename) |
| |
|
| | else: |
| | |
| | dxf = dxfLibrary.Drawing() |
| | |
| | if hasattr(dxf, "header"): |
| | dxf.header.append( |
| | " 9\n$DIMTXT\n 40\n" + str(params.get_param("textheight")) + "\n" |
| | ) |
| | dxf.header.append(" 9\n$INSUNITS\n 70\n4\n") |
| | for ob in exportLayers: |
| | if ob.Label != "0": |
| | ltype = "continuous" |
| | if ob.ViewObject: |
| | if ob.ViewObject.DrawStyle == "Dashed": |
| | ltype = "DASHED" |
| | elif ob.ViewObject.DrawStyle == "Dotted": |
| | ltype = "HIDDEN" |
| | elif ob.ViewObject.DrawStyle == "Dashdot": |
| | ltype = "DASHDOT" |
| | |
| | |
| | dxf.layers.append( |
| | dxfLibrary.Layer(name=ob.Label, color=getACI(ob), lineType=ltype) |
| | ) |
| | base_sketch_pla = None |
| | for ob in exportList: |
| | obtype = Draft.getType(ob) |
| | |
| | if obtype == "PanelSheet": |
| | if not hasattr(ob.Proxy, "sheetborder"): |
| | ob.Proxy.execute(ob) |
| | sb = ob.Proxy.sheetborder |
| | if sb: |
| | sb.Placement = ob.Placement |
| | writeShape(sb, ob, dxf, nospline, lwPoly, layer="Sheets", color=1) |
| | ss = ob.Proxy.sheettag |
| | if ss: |
| | ss.Placement = ob.Placement.multiply(ss.Placement) |
| | writeShape(ss, ob, dxf, nospline, lwPoly, layer="SheetTags", color=1) |
| | for subob in ob.Group: |
| | if Draft.getType(subob) == "PanelCut": |
| | writePanelCut(subob, dxf, nospline, lwPoly, parent=ob) |
| | elif subob.isDerivedFrom("Part::Feature"): |
| | shp = subob.Shape.copy() |
| | shp.Placement = ob.Placement.multiply(shp.Placement) |
| | writeShape(shp, ob, dxf, nospline, lwPoly, layer="Outlines", color=5) |
| |
|
| | elif obtype == "PanelCut": |
| | writePanelCut(ob, dxf, nospline, lwPoly) |
| |
|
| | elif obtype == "Space" and gui: |
| | vobj = ob.ViewObject |
| | rotation = math.degrees(ob.Placement.Rotation.Angle) |
| | t1 = "".join(vobj.Proxy.text1.string.getValues()) |
| | t2 = "".join(vobj.Proxy.text2.string.getValues()) |
| | h1 = vobj.FirstLine.Value |
| | h2 = vobj.FontSize.Value |
| | _v = vobj.Proxy.coords.translation.getValue().getValue() |
| | _h = vobj.Proxy.header.translation.getValue().getValue() |
| | p2 = FreeCAD.Vector(_v) |
| | lspc = FreeCAD.Vector(_h) |
| | p1 = ob.Placement.multVec(p2 + lspc) |
| | justifyhor = ("Left", "Center", "Right").index(vobj.TextAlign) |
| | dxf.append( |
| | dxfLibrary.Text( |
| | t1, |
| | p1, |
| | alignment=p1 if justifyhor else None, |
| | height=h1 * 0.8, |
| | justifyhor=justifyhor, |
| | rotation=rotation, |
| | color=getACI(ob, text=True), |
| | style="STANDARD", |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| | if t2: |
| | ofs = FreeCAD.Vector(0, -lspc.Length, 0) |
| | if rotation: |
| | Z = FreeCAD.Vector(0, 0, 1) |
| | ofs = FreeCAD.Rotation(Z, rotation).multVec(ofs) |
| | dxf.append( |
| | dxfLibrary.Text( |
| | t2, |
| | p1.add(ofs), |
| | alignment=p1.add(ofs) if justifyhor else None, |
| | height=h2 * 0.8, |
| | justifyhor=justifyhor, |
| | rotation=rotation, |
| | color=getACI(ob, text=True), |
| | style="STANDARD", |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| |
|
| | elif obtype == "Axis": |
| | axes = ob.Proxy.getAxisData(ob) |
| | if not axes: |
| | continue |
| | for ax in axes: |
| | dxf.append( |
| | dxfLibrary.Line([ax[0], ax[1]], color=getACI(ob), layer=getStrGroup(ob)) |
| | ) |
| | h = 1 |
| | if gui: |
| | vobj = ob.ViewObject |
| | h = float(vobj.FontSize) |
| | for text in vobj.Proxy.getTextData(): |
| | pos = text[1].add(FreeCAD.Vector(0, -h / 2, 0)) |
| | dxf.append( |
| | dxfLibrary.Text( |
| | text[0], |
| | pos, |
| | alignment=pos, |
| | height=h, |
| | justifyhor=1, |
| | color=getACI(ob), |
| | style="STANDARD", |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| | for shape in vobj.Proxy.getShapeData(): |
| | if hasattr(shape, "Curve") and isinstance(shape.Curve, Part.Circle): |
| | dxf.append( |
| | dxfLibrary.Circle( |
| | shape.Curve.Center, |
| | shape.Curve.Radius, |
| | color=getACI(ob), |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| | else: |
| | if lwPoly: |
| | points = [ |
| | (v.Point.x, v.Point.y, v.Point.z, None, None, 0.0) |
| | for v in shape.Vertexes |
| | ] |
| | dxf.append( |
| | dxfLibrary.LwPolyLine( |
| | points, |
| | [0.0, 0.0], |
| | 1, |
| | color=getACI(ob), |
| | layer=getGroup(ob), |
| | ) |
| | ) |
| | else: |
| | points = [ |
| | ((v.Point.x, v.Point.y, v.Point.z), None, [None, None], 0.0) |
| | for v in shape.Vertexes |
| | ] |
| | dxf.append( |
| | dxfLibrary.PolyLine( |
| | points, |
| | [0.0, 0.0, 0.0], |
| | 1, |
| | color=getACI(ob), |
| | layer=getGroup(ob), |
| | ) |
| | ) |
| |
|
| | elif ob.isDerivedFrom("Part::Feature"): |
| | tess = None |
| | if getattr(ob, "Tessellation", False): |
| | tess = [ob.Tessellation, ob.SegmentLength] |
| | if ob.isDerivedFrom("Sketcher::SketchObject"): |
| | if base_sketch_pla is None: |
| | base_sketch_pla = ob.Placement |
| | sh = Part.Compound() |
| | sh.Placement = base_sketch_pla |
| | sh.add(ob.Shape.copy()) |
| | sh.transformShape(base_sketch_pla.inverse().Matrix) |
| | elif params.get_param("dxfmesh"): |
| | sh = None |
| | if not ob.Shape.isNull(): |
| | writeMesh(ob, dxf) |
| | elif gui and params.get_param("dxfproject"): |
| | _view = FreeCADGui.ActiveDocument.ActiveView |
| | direction = _view.getViewDirection().multiply(-1) |
| | sh = projectShape(ob.Shape, direction, tess) |
| | elif ob.Shape.Volume > 0: |
| | sh = projectShape(ob.Shape, Vector(0, 0, 1), tess) |
| | else: |
| | sh = ob.Shape |
| | if sh: |
| | if not sh.isNull(): |
| | if sh.ShapeType == "Compound": |
| | if len(sh.Wires) == 1: |
| | |
| | |
| | if len(sh.Wires[0].Edges) == len(sh.Edges): |
| | writeShape(sh, ob, dxf, nospline, lwPoly) |
| | else: |
| | |
| | block = getBlock(sh, ob, lwPoly) |
| | dxf.blocks.append(block) |
| | dxf.append( |
| | dxfLibrary.Insert( |
| | name=ob.Name.upper(), |
| | color=getACI(ob), |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| | else: |
| | |
| | block = getBlock(sh, ob, lwPoly) |
| | dxf.blocks.append(block) |
| | dxf.append( |
| | dxfLibrary.Insert( |
| | name=ob.Name.upper(), |
| | color=getACI(ob), |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| | else: |
| | writeShape(sh, ob, dxf, nospline, lwPoly) |
| |
|
| | elif obtype == "Annotation": |
| | |
| | |
| | |
| | |
| | |
| | for text in ob.LabelText: |
| | point = DraftVecUtils.tup( |
| | Vector( |
| | ob.Position.x, |
| | ob.Position.y - ob.LabelText.index(text), |
| | ob.Position.z, |
| | ) |
| | ) |
| | if gui: |
| | height = float(ob.ViewObject.FontSize) |
| | justifyhor = ("Left", "Center", "Right").index( |
| | ob.ViewObject.Justification |
| | ) |
| | else: |
| | height = 1 |
| | justifyhor = 0 |
| | dxf.append( |
| | dxfLibrary.Text( |
| | text, |
| | point, |
| | alignment=point if justifyhor else None, |
| | height=height, |
| | justifyhor=justifyhor, |
| | color=getACI(ob, text=True), |
| | style="STANDARD", |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| |
|
| | elif obtype in ("DraftText", "Text"): |
| | |
| | if gui: |
| | height = float(ob.ViewObject.FontSize) |
| | justifyhor = ("Left", "Center", "Right").index(ob.ViewObject.Justification) |
| | else: |
| | height = 1 |
| | justifyhor = 0 |
| | for idx, text in enumerate(ob.Text): |
| | point = DraftVecUtils.tup( |
| | Vector( |
| | ob.Placement.Base.x, |
| | ob.Placement.Base.y - (height * 1.2 * idx), |
| | ob.Placement.Base.z, |
| | ) |
| | ) |
| | rotation = math.degrees(ob.Placement.Rotation.Angle) |
| | dxf.append( |
| | dxfLibrary.Text( |
| | text, |
| | point, |
| | alignment=point if justifyhor else None, |
| | height=height * 0.8, |
| | justifyhor=justifyhor, |
| | rotation=rotation, |
| | color=getACI(ob, text=True), |
| | style="STANDARD", |
| | layer=getStrGroup(ob), |
| | ) |
| | ) |
| |
|
| | elif obtype in ["Dimension", "LinearDimension"]: |
| | p1 = DraftVecUtils.tup(ob.Start) |
| | p2 = DraftVecUtils.tup(ob.End) |
| | base = Part.LineSegment(ob.Start, ob.End).toShape() |
| | proj = DraftGeomUtils.findDistance(ob.Dimline, base) |
| | if not proj: |
| | pbase = DraftVecUtils.tup(ob.End) |
| | else: |
| | pbase = DraftVecUtils.tup(ob.End.add(proj.negative())) |
| | dxf.append( |
| | dxfLibrary.Dimension(pbase, p1, p2, color=getACI(ob), layer=getStrGroup(ob)) |
| | ) |
| |
|
| | dxf.saveas(filename) |
| |
|
| | FCC.PrintMessage("successfully exported" + " " + filename + "\n") |
| |
|
| | else: |
| | errorDXFLib(gui) |
| |
|
| |
|
| | class dxfcounter: |
| | """DXF counter class to count the number of entities.""" |
| |
|
| | def __init__(self): |
| | |
| | self.count = 10000 |
| |
|
| | def incr(self, matchobj): |
| | self.count += 1 |
| | |
| | return format(self.count, "02x") |
| |
|
| |
|
| | def exportPage(page, filename): |
| | """Export a page created with Drawing or TechDraw workbenches. |
| | |
| | The template is extracted from the page. |
| | If the template exists in the system, it will be searched |
| | for editable text fields, and replaced with their text values. |
| | If no template is found a dummy default DXF template is used. |
| | |
| | For TechDraw pages their templates are not supported currently, |
| | so the dummy template will be used. |
| | |
| | It considers all views or groups in the page, |
| | and tries to get the blocks and entities with `getViewDXF(view)`. |
| | It also increments the counter by using the `dxfcounter` class. |
| | |
| | The blocks and entities are added to the template, and finally |
| | this template is written into the `filename`. |
| | |
| | Parameters |
| | ---------- |
| | page : object derived from 'TechDraw::DrawPage' |
| | A TechDraw page to export. |
| | |
| | filename : str |
| | The path of the new DXF file. |
| | """ |
| | if hasattr(page.Template, "Template"): |
| | template = "" |
| | views = page.Views |
| | else: |
| | template = os.path.splitext(page.Template)[0] + ".dxf" |
| | views = page.Group |
| | if os.path.exists(template): |
| | f = pyopen(template, "U") |
| | template = f.read() |
| | f.close() |
| | |
| | f = pyopen(page.Template, "rb") |
| | svgtemplate = f.read() |
| | f.close() |
| | editables = re.findall(r"freecad:editable=\"(.*?)\"", svgtemplate) |
| | values = page.EditableTexts |
| | for i in range(len(editables)): |
| | if len(values) > i: |
| | template = template.replace(editables[i], values[i]) |
| | else: |
| | |
| | print("DXF version of the template not found. " "Creating a default empty template.") |
| | _v = FreeCAD.Version() |
| | _version = _v[0] + "." + _v[1] + "-" + _v[2] |
| | template = "999\nFreeCAD DXF exporter v" + _version + "\n" |
| | template += "0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1009\n0\nENDSEC\n" |
| | template += "0\nSECTION\n2\nBLOCKS\n999\n$blocks\n0\nENDSEC\n" |
| | template += "0\nSECTION\n2\nENTITIES\n999\n$entities\n0\nENDSEC\n" |
| | template += "0\nEOF" |
| | blocks = "" |
| | entities = "" |
| | r12 = False |
| | ver = re.findall(r"\\$ACADVER\n.*?\n(.*?)\n", template) |
| | if ver: |
| | |
| | |
| | if ver[0].upper() in ["AC1009", "AC1010", "AC1011", "AC1012", "AC1013"]: |
| | r12 = True |
| | for view in views: |
| | b, e = getViewDXF(view) |
| | blocks += b |
| | entities += e |
| | if blocks: |
| | template = template.replace("999\n$blocks", blocks[:-1]) |
| | if entities: |
| | template = template.replace("999\n$entities", entities[:-1]) |
| | c = dxfcounter() |
| | pat = re.compile(r"(_handle_)") |
| | template = pat.sub(c.incr, template) |
| | f = pyopen(filename, "w") |
| | f.write(template) |
| | f.close() |
| |
|
| |
|
| | def getViewBlock(geom, view, blockcount): |
| | """Get a view block. |
| | |
| | It iterates over all `geom` objects. |
| | If the global variable `dxfExportBlocks` exists, it will create |
| | the appropriate strings for `BLOCK` and `INSERT` sections, |
| | and increment the `blockcount`. |
| | Otherwise, it will just create an insert by changing the layer, |
| | and setting a handle. |
| | |
| | Parameters |
| | ---------- |
| | geom : list of str |
| | A list string objects or a single object, returned by |
| | the `getDXF()` method of the `view`. |
| | |
| | view : page view |
| | A TechDraw view which may be of different types |
| | depending on the objects being projected: |
| | `'TechDraw::DrawViewDraft'`, or `'TechDraw::DrawViewArch'`. |
| | |
| | blockcount : int |
| | A counter that increments by one each time an insert and block |
| | are added to the output strings, if the global variable |
| | `dxfExportBlocks` exists. |
| | |
| | Returns |
| | ------- |
| | str, str, int |
| | A tuple containing the strings for blocks, inserts, |
| | and the final value of `blockcount`. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | insert = "" |
| | block = "" |
| | r = view.Rotation |
| | if r != 0: |
| | r = -r |
| | if not isinstance(geom, list): |
| | geom = [geom] |
| | for g in geom: |
| | if dxfExportBlocks: |
| | |
| | g = g.replace("sheet_layer\n", "0\n6\nBYBLOCK\n62\n0\n5\n_handle_\n") |
| | block += "0\nBLOCK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockBegin\n2\n" |
| | block += view.Name + str(blockcount) |
| | block += "\n70\n0\n10\n0\n20\n0\n3\n" |
| | block += view.Name + str(blockcount) + "\n1\n\n" |
| | block += g |
| | block += "0\nENDBLK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockEnd\n" |
| | insert += "0\nINSERT\n5\n_handle_\n8\n0\n6\nBYLAYER\n62\n256\n2\n" |
| | insert += view.Name + str(blockcount) |
| | insert += "\n10\n" + str(view.X) + "\n20\n" + str(view.Y) |
| | insert += ( |
| | "\n30\n0\n41\n" |
| | + str(view.Scale) |
| | + "\n42\n" |
| | + str(view.Scale) |
| | + "\n43\n" |
| | + str(view.Scale) |
| | ) |
| | insert += "\n50\n" + str(r) + "\n" |
| | blockcount += 1 |
| | else: |
| | |
| | g = g.replace("sheet_layer\n", "0\n5\n_handle_\n") |
| | insert += g |
| | return block, insert, blockcount |
| |
|
| |
|
| | def getViewDXF(view): |
| | """Return a DXF fragment from a TechDraw view. |
| | |
| | Depending on the type of page view, it will try |
| | obtaining `geom`, the DXF representation of `view`, |
| | and then extract the block and insert strings |
| | with `getViewBlock(geom, view, blockcount)`, |
| | starting with a `blockcount` of 1. |
| | |
| | If the `view` is `'TechDraw::DrawViewPart'`, |
| | and if the global variable `dxfExportBlocks` exists, it will create |
| | the appropriate strings for `BLOCK` and `INSERT` sections, |
| | and increment the `blockcount`. |
| | Otherwise, it will just create an insert by changing the layer, |
| | and setting a handle |
| | |
| | Parameters |
| | ---------- |
| | view : App::DocumentObjectGroup or page view |
| | A TechDraw view which may be of different types |
| | depending on the objects being projected: |
| | `'TechDraw::DrawViewDraft'`, `'TechDraw::DrawViewArch'`, |
| | `'TechDraw::DrawViewPart'`, `'TechDraw::DrawViewAnnotation'` |
| | |
| | Returns |
| | ------- |
| | str, str |
| | It returns the two strings for DXF blocks and inserts. |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | block = "" |
| | insert = "" |
| | blockcount = 1 |
| |
|
| | if view.isDerivedFrom("TechDraw::DrawViewDraft"): |
| | geom = Draft.get_dxf(view) |
| | block, insert, blockcount = getViewBlock(geom, view, blockcount) |
| |
|
| | elif view.isDerivedFrom("TechDraw::DrawViewArch"): |
| | import ArchSectionPlane |
| |
|
| | geom = ArchSectionPlane.getDXF(view) |
| | block, insert, blockcount = getViewBlock(geom, view, blockcount) |
| |
|
| | elif view.isDerivedFrom("TechDraw::DrawViewPart"): |
| | import TechDraw |
| |
|
| | for obj in view.Source: |
| | proj = TechDraw.projectToDXF(obj.Shape, view.Direction) |
| | if dxfExportBlocks: |
| | |
| | proj = proj.replace("sheet_layer\n", "0\n6\nBYBLOCK\n62\n0\n5\n_handle_\n") |
| | block += "0\nBLOCK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockBegin\n2\n" |
| | block += view.Name + str(blockcount) |
| | block += "\n70\n0\n10\n0\n20\n0\n3\n" + view.Name + str(blockcount) |
| | block += "\n1\n\n" |
| | block += proj |
| | block += "0\nENDBLK\n5\n_handle_\n100\nAcDbEntity\n8\n0\n100\nAcDbBlockEnd\n" |
| | insert += "0\nINSERT\n5\n_handle_\n8\n0\n6\nBYLAYER\n62\n256\n2\n" |
| | insert += view.Name + str(blockcount) |
| | insert += "\n10\n" + str(view.X) + "\n20\n" + str(view.Y) |
| | insert += "\n30\n0\n41\n" + str(view.Scale) |
| | insert += "\n42\n" + str(view.Scale) + "\n43\n" + str(view.Scale) |
| | insert += "\n50\n" + str(view.Rotation) + "\n" |
| | blockcount += 1 |
| | else: |
| | proj = proj.replace("sheet_layer\n", "0\n5\n_handle_\n") |
| | insert += proj |
| |
|
| | elif view.isDerivedFrom("TechDraw::DrawViewAnnotation"): |
| | insert = "0\nTEXT\n5\n_handle_\n8\n0\n100\nAcDbEntity\n100\nAcDbText\n5\n_handle_" |
| | insert += "\n10\n" + str(view.X) + "\n20\n" + str(view.Y) |
| | insert += "\n30\n0\n40\n" + str(view.Scale / 2) |
| | insert += "\n50\n" + str(view.Rotation) |
| | insert += "\n1\n" + view.Text[0] + "\n" |
| |
|
| | else: |
| | print("Unable to get DXF representation from view: ", view.Label) |
| | return block, insert |
| |
|
| |
|
| | def readPreferences(): |
| | """Read the preferences of the this module from the parameter database. |
| | |
| | It creates and sets the global variables: |
| | `dxfCreatePart`, `dxfCreateDraft`, `dxfCreateSketch`, |
| | `dxfDiscretizeCurves`, `dxfStarBlocks`, `dxfMakeBlocks`, `dxfJoin`, |
| | `dxfRenderPolylineWidth`, `dxfImportTexts`, `dxfImportLayouts`, |
| | `dxfImportPoints`, `dxfImportHatches`, `dxfUseStandardSize`, |
| | `dxfGetColors`, `dxfUseDraftVisGroups`, |
| | `dxfBrightBackground`, `dxfDefaultColor`, `dxfUseLegacyImporter`, |
| | `dxfExportBlocks`, `dxfScaling`, `dxfUseLegacyExporter` |
| | |
| | The parameter path is ``User parameter:BaseApp/Preferences/Mod/Draft`` |
| | |
| | To do |
| | ----- |
| | Use local variables, not global variables. |
| | """ |
| | global dxfCreatePart, dxfCreateDraft, dxfCreateSketch |
| | global dxfDiscretizeCurves, dxfStarBlocks, dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth |
| | global dxfImportTexts, dxfImportLayouts, dxfImportPoints, dxfImportHatches, dxfUseStandardSize |
| | global dxfGetColors, dxfUseDraftVisGroups, dxfBrightBackground, dxfDefaultColor |
| | global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling, dxfUseLegacyExporter |
| |
|
| | |
| | hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") |
| |
|
| | dxfUseLegacyImporter = hGrp.GetBool("dxfUseLegacyImporter", False) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if hGrp.GetBool("dxfImportAsDraft", False): |
| | import_mode = 0 |
| | elif hGrp.GetBool("dxfImportAsPrimitives", False): |
| | import_mode = 1 |
| | elif hGrp.GetBool("dxfImportAsFused", False): |
| | import_mode = 3 |
| | else: |
| | import_mode = 2 |
| | hGrp.SetInt("DxfImportMode", import_mode) |
| |
|
| | |
| | |
| | if dxfUseLegacyImporter: |
| | |
| | dxfCreateSketch = hGrp.GetBool("dxfCreateSketch", False) |
| |
|
| | if dxfCreateSketch: |
| | dxfCreatePart = False |
| | dxfCreateDraft = False |
| | dxfMakeBlocks = False |
| | |
| | |
| | |
| | elif import_mode == 0: |
| | dxfMakeBlocks = False |
| | dxfCreatePart = False |
| | dxfCreateDraft = True |
| | elif import_mode == 3: |
| | dxfMakeBlocks = True |
| | dxfCreatePart = False |
| | dxfCreateDraft = False |
| | else: |
| | dxfMakeBlocks = False |
| | dxfCreatePart = True |
| | dxfCreateDraft = False |
| |
|
| | |
| | dxfDiscretizeCurves = hGrp.GetBool("DiscretizeEllipses", True) |
| | dxfStarBlocks = hGrp.GetBool("dxfstarblocks", False) |
| | dxfJoin = hGrp.GetBool("joingeometry", False) |
| | dxfRenderPolylineWidth = hGrp.GetBool("renderPolylineWidth", False) |
| | dxfImportTexts = hGrp.GetBool("dxftext", False) |
| | dxfImportLayouts = hGrp.GetBool("dxflayout", False) |
| | dxfImportPoints = hGrp.GetBool("dxfImportPoints", True) |
| | dxfImportHatches = hGrp.GetBool("importDxfHatches", False) |
| | dxfUseStandardSize = hGrp.GetBool("dxfStdSize", False) |
| | dxfGetColors = hGrp.GetBool("dxfGetOriginalColors", True) |
| | dxfUseDraftVisGroups = hGrp.GetBool("dxfUseDraftVisGroups", True) |
| | dxfUseLegacyExporter = hGrp.GetBool("dxfUseLegacyExporter", False) |
| | dxfExportBlocks = hGrp.GetBool("dxfExportBlocks", True) |
| | dxfScaling = hGrp.GetFloat("dxfScaling", 1.0) |
| |
|
| | dxfBrightBackground = isBrightBackground() |
| | dxfDefaultColor = getColor() |
| |
|
| |
|
| | class DxfImportReporter: |
| | """Formats and reports statistics from a DXF import process.""" |
| |
|
| | def __init__(self, filename, stats_dict, total_time=0.0): |
| | self.filename = filename |
| | self.stats = stats_dict |
| | self.total_time = total_time |
| |
|
| | def to_console_string(self): |
| | """ |
| | Formats the statistics into a human-readable string for console output. |
| | """ |
| | if not self.stats: |
| | return "DXF Import: no statistics were returned from the importer.\n" |
| |
|
| | lines = ["\n--- DXF import summary ---"] |
| | lines.append(f"Import of file: '{self.filename}'\n") |
| |
|
| | |
| | lines.append(f"DXF version: {self.stats.get('dxfVersion', 'Unknown')}") |
| | lines.append(f"File encoding: {self.stats.get('dxfEncoding', 'Unknown')}") |
| |
|
| | |
| | file_units = self.stats.get("fileUnits", "Not specified") |
| | source = self.stats.get("scalingSource", "") |
| | if source: |
| | lines.append(f"File units: {file_units} (from {source})") |
| | else: |
| | lines.append(f"File units: {file_units}") |
| |
|
| | manual_scaling = self.stats.get("importSettings", {}).get("Manual scaling factor", "1.0") |
| | lines.append(f"Manual scaling factor: {manual_scaling}") |
| |
|
| | final_scaling = self.stats.get("finalScalingFactor", 1.0) |
| | lines.append(f"Final scaling: 1 DXF unit = {final_scaling:.4f} mm") |
| | lines.append("") |
| |
|
| | |
| | lines.append("Performance:") |
| | cpp_time = self.stats.get("importTimeSeconds", 0.0) |
| | lines.append(f" - C++ import time: {cpp_time:.4f} seconds") |
| | lines.append(f" - Total import time: {self.total_time:.4f} seconds") |
| | lines.append("") |
| |
|
| | |
| | lines.append("Import settings:") |
| | settings = self.stats.get("importSettings", {}) |
| | if settings: |
| | for key, value in sorted(settings.items()): |
| | lines.append(f" - {key}: {value}") |
| | else: |
| | lines.append(" (No settings recorded)") |
| | lines.append("") |
| |
|
| | |
| | lines.append("Entity counts:") |
| | total_read = 0 |
| | unsupported_keys = self.stats.get("unsupportedFeatures", {}).keys() |
| | unsupported_entity_names = set() |
| | for key in unsupported_keys: |
| | |
| | entity_name_match = re.search(r"\'(.*?)\'", key) |
| | if entity_name_match: |
| | unsupported_entity_names.add(entity_name_match.group(1)) |
| |
|
| | has_unsupported_indicator = False |
| | entities = self.stats.get("entityCounts", {}) |
| | if entities: |
| | for key, value in sorted(entities.items()): |
| | indicator = "" |
| | if key in unsupported_entity_names: |
| | indicator = " (*)" |
| | has_unsupported_indicator = True |
| | lines.append(f" - {key}: {value}{indicator}") |
| | total_read += value |
| | lines.append("----------------------------") |
| | lines.append(f" Total entities read: {total_read}") |
| | else: |
| | lines.append(" (No entities recorded)") |
| | lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}") |
| |
|
| | lines.append("") |
| |
|
| | |
| | lines.append("System Blocks:") |
| | system_blocks = self.stats.get("systemBlockCounts", {}) |
| | if system_blocks: |
| | for key, value in sorted(system_blocks.items()): |
| | lines.append(f" - {key}: {value}") |
| | else: |
| | lines.append(" (None found or imported)") |
| |
|
| | lines.append("") |
| | if has_unsupported_indicator: |
| | lines.append("(*) Entity type not supported by importer.") |
| | lines.append("") |
| |
|
| | lines.append("Unsupported features:") |
| | unsupported = self.stats.get("unsupportedFeatures", {}) |
| | if unsupported: |
| | for key, occurrences in sorted(unsupported.items()): |
| | count = len(occurrences) |
| | max_details_to_show = 5 |
| |
|
| | details_list = [] |
| | for i, (line, handle) in enumerate(occurrences): |
| | if i >= max_details_to_show: |
| | break |
| | if handle: |
| | details_list.append(f"line {line} (handle {handle})") |
| | else: |
| | details_list.append(f"line {line} (no handle available)") |
| |
|
| | details_str = ", ".join(details_list) |
| | if count > max_details_to_show: |
| | lines.append(f" - {key}: {count} time(s). Examples: {details_str}, ...") |
| | else: |
| | lines.append(f" - {key}: {count} time(s) at {details_str}") |
| | else: |
| | lines.append(" (none)") |
| |
|
| | lines.append("--- End of summary ---\n") |
| | return "\n".join(lines) |
| |
|
| | def report_to_console(self): |
| | """ |
| | Prints the formatted statistics string to the FreeCAD console. |
| | """ |
| | output_string = self.to_console_string() |
| | FCC.PrintMessage(output_string) |
| |
|
| |
|
| | class DxfDraftPostProcessor: |
| | """ |
| | Handles the post-processing of DXF files imported as Part objects, |
| | converting them into fully parametric Draft objects while preserving |
| | the block and layer hierarchy. |
| | """ |
| |
|
| | def __init__(self, doc, new_objects, import_mode): |
| | self.doc = doc |
| | self.all_imported_objects = new_objects |
| | self.import_mode = import_mode |
| | self.all_originals_to_delete = set() |
| | self.newly_created_draft_objects = [] |
| |
|
| | def _categorize_objects(self): |
| | """ |
| | Scans newly created objects from the C++ importer and categorizes them. |
| | """ |
| | block_definitions = {} |
| | for group_name in ["_BlockDefinitions", "_UnreferencedBlocks"]: |
| | block_group = self.doc.getObject(group_name) |
| | if block_group: |
| | for block_def_obj in block_group.Group: |
| | if block_def_obj.isValid() and block_def_obj.isDerivedFrom("Part::Compound"): |
| | block_definitions[block_def_obj] = [ |
| | child for child in block_def_obj.Links if child.isValid() |
| | ] |
| |
|
| | all_block_internal_objects_set = set() |
| | for block_def, children in block_definitions.items(): |
| | all_block_internal_objects_set.add(block_def) |
| | all_block_internal_objects_set.update(children) |
| |
|
| | top_level_geometry = [] |
| | placeholders = [] |
| | for obj in self.all_imported_objects: |
| | if not obj.isValid() or obj in all_block_internal_objects_set: |
| | continue |
| |
|
| | if obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "DxfEntityType"): |
| | placeholders.append(obj) |
| | elif obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("App::Link"): |
| | top_level_geometry.append(obj) |
| |
|
| | return block_definitions, top_level_geometry, placeholders |
| |
|
| | def _create_draft_object_from_part(self, part_obj): |
| | """ |
| | Converts an intermediate Part object (from C++ importer) to a final Draft object, |
| | ensuring correct underlying C++ object typing and property management. |
| | Returns a tuple: (new_draft_object, type_string) or (None, None). |
| | """ |
| | if self.import_mode != 0: |
| | |
| | return part_obj, "KeptAsIs" |
| |
|
| | |
| | |
| | if not part_obj.isValid() or ( |
| | part_obj.isDerivedFrom("Part::Compound") and hasattr(part_obj, "Links") |
| | ): |
| | return None, None |
| |
|
| | new_obj = None |
| | obj_type_str = None |
| |
|
| | |
| | |
| | |
| | if part_obj.isDerivedFrom("Part::Line"): |
| | |
| | |
| | |
| | new_obj = self.doc.addObject( |
| | "Part::Part2DObjectPython", self.doc.getUniqueObjectName("Line") |
| | ) |
| | |
| | |
| | new_obj.Shape = part_obj.Shape |
| | Draft.Wire(new_obj) |
| |
|
| | |
| | |
| | start_point = FreeCAD.Vector(part_obj.X1.Value, part_obj.Y1.Value, part_obj.Z1.Value) |
| | end_point = FreeCAD.Vector(part_obj.X2.Value, part_obj.Y2.Value, part_obj.Z2.Value) |
| | new_obj.Points = [start_point, end_point] |
| |
|
| | new_obj.MakeFace = False |
| |
|
| | obj_type_str = "Line" |
| |
|
| | elif part_obj.isDerivedFrom("Part::Circle"): |
| | |
| | new_obj = self.doc.addObject( |
| | "Part::Part2DObjectPython", self.doc.getUniqueObjectName("Circle") |
| | ) |
| | |
| | |
| | new_obj.Shape = part_obj.Shape |
| |
|
| | |
| | |
| | Draft.Circle(new_obj) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if hasattr(part_obj, "Radius"): |
| | new_obj.Radius = FreeCAD.Units.Quantity(part_obj.Radius.Value, "mm") |
| |
|
| | |
| | if hasattr(part_obj, "Angle1") and hasattr(part_obj, "Angle2"): |
| | start_angle, end_angle = self._get_canonical_angles( |
| | part_obj.Angle1.Value, part_obj.Angle2.Value, part_obj.Radius.Value |
| | ) |
| |
|
| | new_obj.FirstAngle = FreeCAD.Units.Quantity(start_angle, "deg") |
| | new_obj.LastAngle = FreeCAD.Units.Quantity(end_angle, "deg") |
| |
|
| | |
| | is_full_circle = ( |
| | abs(new_obj.FirstAngle.Value - 0.0) < 1e-7 |
| | and abs(new_obj.LastAngle.Value - 360.0) < 1e-7 |
| | ) |
| |
|
| | new_obj.MakeFace = False |
| |
|
| | obj_type_str = "Circle" if is_full_circle else "Arc" |
| |
|
| | elif part_obj.isDerivedFrom( |
| | "Part::Vertex" |
| | ): |
| | |
| | new_obj = self.doc.addObject( |
| | "App::FeaturePython", self.doc.getUniqueObjectName("Point") |
| | ) |
| | new_obj.addExtension( |
| | "Part::AttachExtensionPython" |
| | ) |
| | |
| | if hasattr(part_obj, "Placement"): |
| | new_obj.Placement = part_obj.Placement |
| | else: |
| | new_obj.Placement = FreeCAD.Placement() |
| | Draft.Point(new_obj) |
| | obj_type_str = "Point" |
| |
|
| | elif part_obj.isDerivedFrom("Part::Ellipse"): |
| | |
| | |
| | span = abs(part_obj.Angle2.Value - part_obj.Angle1.Value) |
| | is_full_ellipse = abs(span % 360.0) < 1e-6 |
| |
|
| | if is_full_ellipse: |
| | |
| | new_obj = self.doc.addObject( |
| | "Part::Part2DObjectPython", self.doc.getUniqueObjectName("Ellipse") |
| | ) |
| |
|
| | |
| | Draft.Ellipse(new_obj) |
| |
|
| | |
| | |
| | new_obj.MajorRadius = part_obj.MajorRadius |
| | new_obj.MinorRadius = part_obj.MinorRadius |
| | new_obj.Placement = part_obj.Placement |
| |
|
| | obj_type_str = "Ellipse" |
| | else: |
| | |
| |
|
| | new_obj = self.doc.addObject( |
| | "Part::Part2DObjectPython", self.doc.getUniqueObjectName("EllipticalArc") |
| | ) |
| | Draft.Wire(new_obj) |
| |
|
| | |
| | |
| | center_at_origin = FreeCAD.Vector(0, 0, 0) |
| | geom = Part.Ellipse( |
| | center_at_origin, part_obj.MajorRadius.Value, part_obj.MinorRadius.Value |
| | ) |
| | shape_at_origin = geom.toShape( |
| | math.radians(part_obj.Angle1.Value), math.radians(part_obj.Angle2.Value) |
| | ) |
| |
|
| | |
| | new_obj.Shape = shape_at_origin |
| | new_obj.Placement = part_obj.Placement |
| |
|
| | new_obj.MakeFace = False |
| |
|
| | obj_type_str = "Shape" |
| |
|
| | |
| | elif part_obj.isDerivedFrom( |
| | "Part::Feature" |
| | ): |
| | shape = ( |
| | part_obj.Shape |
| | ) |
| | if not shape.isValid(): |
| | return None, None |
| |
|
| | |
| | if shape.ShapeType == "Wire": |
| | |
| | new_obj = self.doc.addObject( |
| | "Part::Part2DObjectPython", self.doc.getUniqueObjectName("Wire") |
| | ) |
| | new_obj.Shape = shape |
| | Draft.Wire(new_obj) |
| |
|
| | |
| | |
| | |
| | is_all_lines = True |
| |
|
| | for edge in shape.Edges: |
| | if edge.Curve.TypeId == "Part::GeomLine": |
| | continue |
| | else: |
| | is_all_lines = False |
| | break |
| |
|
| | if is_all_lines and shape.OrderedVertexes: |
| | |
| | points = [v.Point for v in shape.OrderedVertexes] |
| | new_obj.Points = points |
| |
|
| | new_obj.Closed = ( |
| | shape.isClosed() |
| | ) |
| |
|
| | new_obj.MakeFace = False |
| |
|
| | obj_type_str = "Wire" |
| |
|
| | |
| | else: |
| | |
| | new_obj = self.doc.addObject( |
| | "Part::FeaturePython", self.doc.getUniqueObjectName("Shape") |
| | ) |
| | new_obj.addExtension( |
| | "Part::AttachExtensionPython" |
| | ) |
| | new_obj.Shape = shape |
| | |
| | if hasattr(part_obj, "Placement"): |
| | new_obj.Placement = part_obj.Placement |
| | else: |
| | new_obj.Placement = FreeCAD.Placement() |
| | |
| | obj_type_str = "Shape" |
| |
|
| | |
| | elif part_obj.isDerivedFrom("App::Link"): |
| | |
| | |
| | new_obj = part_obj |
| | obj_type_str = "Link" |
| |
|
| | |
| | elif part_obj.isDerivedFrom( |
| | "App::FeaturePython" |
| | ): |
| | |
| | |
| | return None, None |
| |
|
| | |
| | if new_obj: |
| | new_obj.Label = part_obj.Label |
| |
|
| | |
| | |
| | |
| | |
| | if new_obj is not part_obj: |
| | if hasattr(part_obj, "Placement") and hasattr(new_obj, "Placement"): |
| | new_obj.Placement = part_obj.Placement |
| | elif not hasattr(new_obj, "Placement"): |
| | |
| | FCC.PrintWarning( |
| | f"Created object '{new_obj.Label}' of type '{obj_type_str}' does not have a 'Placement' property even after intended setup. This is unexpected.\n" |
| | ) |
| |
|
| | |
| | if new_obj is not part_obj: |
| | self.all_originals_to_delete.add(part_obj) |
| |
|
| | return new_obj, obj_type_str |
| |
|
| | |
| | |
| | self.all_originals_to_delete.add(part_obj) |
| | FCC.PrintWarning( |
| | f"DXF Post-Processor: Failed to convert object '{part_obj.Label}'. Discarding.\n" |
| | ) |
| | return None, None |
| |
|
| | def _parent_object_to_layer(self, new_obj, original_obj): |
| | """Finds the correct layer from the original object and parents the new object to it.""" |
| | if hasattr(original_obj, "OriginalLayer"): |
| | layer_name = original_obj.OriginalLayer |
| |
|
| | found_layers = self.doc.getObjectsByLabel(layer_name) |
| |
|
| | layer_obj = None |
| | if found_layers: |
| | for l_obj in found_layers: |
| | if Draft.get_type(l_obj) == "Layer": |
| | layer_obj = l_obj |
| | break |
| |
|
| | if layer_obj: |
| | layer_obj.Proxy.addObject(layer_obj, new_obj) |
| | else: |
| | FCC.PrintWarning( |
| | f"DXF Post-Processor: Could not find a valid Draft Layer with label '{layer_name}' for object '{new_obj.Label}'.\n" |
| | ) |
| |
|
| | def _create_and_parent_geometry(self, intermediate_obj): |
| | """High-level helper to convert, name, and parent a single geometric object.""" |
| | new_draft_obj, obj_type_str = self._create_draft_object_from_part(intermediate_obj) |
| | if new_draft_obj: |
| | label = intermediate_obj.Label |
| | if not label or "__Feature" in label: |
| | label = self.doc.getUniqueObjectName(obj_type_str) |
| | new_draft_obj.Label = label |
| | self._parent_object_to_layer(new_draft_obj, intermediate_obj) |
| | self.newly_created_draft_objects.append(new_draft_obj) |
| | else: |
| | FCC.PrintWarning( |
| | f"DXF Post-Processor: Failed to convert object '{intermediate_obj.Label}'. Discarding.\n" |
| | ) |
| | return new_draft_obj |
| |
|
| | def _create_from_placeholders(self, placeholders): |
| | """Creates final Draft objects from text/dimension placeholders.""" |
| | if not placeholders: |
| | return |
| |
|
| | for placeholder in placeholders: |
| | if not placeholder.isValid(): |
| | continue |
| | new_obj = None |
| | try: |
| | if placeholder.DxfEntityType == "DIMENSION": |
| | |
| | dim = self.doc.addObject("App::FeaturePython", "Dimension") |
| | _Dimension(dim) |
| |
|
| | if FreeCAD.GuiUp: |
| | ViewProviderLinearDimension(dim.ViewObject) |
| |
|
| | |
| | plc = placeholder.Placement |
| |
|
| | |
| | |
| | p_start = plc.multVec(placeholder.Start) |
| | p_end = plc.multVec(placeholder.End) |
| | p_dimline = plc.multVec(placeholder.Dimline) |
| |
|
| | |
| | dim.Start = p_start |
| | dim.End = p_end |
| | dim.Dimline = p_dimline |
| |
|
| | |
| |
|
| | new_obj = dim |
| |
|
| | |
| | |
| | if hasattr(placeholder, "DxfDimensionType"): |
| | |
| | |
| | |
| | |
| | dim_type = placeholder.DxfDimensionType & 0x0F |
| |
|
| | |
| | |
| | if dim_type == 0 and hasattr(placeholder, "DxfRotation"): |
| | angle = placeholder.DxfRotation.Value |
| |
|
| | |
| | |
| | |
| | direction_vector = FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) |
| | dim.Direction = direction_vector |
| |
|
| | elif placeholder.DxfEntityType == "TEXT": |
| | text_obj = Draft.make_text(placeholder.Text) |
| | text_obj.Placement = placeholder.Placement |
| | if FreeCAD.GuiUp: |
| | text_obj.addProperty("App::PropertyFloat", "DxfTextHeight", "Internal") |
| | text_obj.DxfTextHeight = placeholder.DxfTextHeight |
| | new_obj = text_obj |
| |
|
| | if new_obj: |
| | new_obj.Label = placeholder.Label |
| | self._parent_object_to_layer(new_obj, placeholder) |
| | self.newly_created_draft_objects.append(new_obj) |
| | except Exception as e: |
| | FCC.PrintWarning( |
| | f"Could not create Draft object from placeholder '{placeholder.Label}': {e}\n" |
| | ) |
| |
|
| | self.all_originals_to_delete.update(placeholders) |
| |
|
| | def _apply_gui_styles(self): |
| | """Attaches correct ViewProviders and styles to new Draft objects.""" |
| | if not FreeCAD.GuiUp: |
| | return |
| |
|
| | |
| | |
| | all_objects_to_style = self.newly_created_draft_objects |
| |
|
| | for obj in all_objects_to_style: |
| | if obj.isValid() and hasattr(obj, "ViewObject") and hasattr(obj, "Proxy"): |
| | try: |
| | proxy_name = obj.Proxy.__class__.__name__ |
| | if proxy_name in ("Wire", "Line"): |
| | if ViewProviderWire: |
| | ViewProviderWire(obj.ViewObject) |
| | elif proxy_name == "Circle": |
| | if ViewProviderDraft: |
| | ViewProviderDraft(obj.ViewObject) |
| | elif proxy_name == "Text": |
| | if hasattr(obj, "DxfTextHeight"): |
| | obj.ViewObject.FontSize = obj.DxfTextHeight * TEXTSCALING |
| | except Exception as e: |
| | FCC.PrintWarning(f"Failed to set ViewProvider for {obj.Name}: {e}\n") |
| |
|
| | def _delete_objects_in_batch(self): |
| | """Safely deletes all objects marked for removal.""" |
| | if not self.all_originals_to_delete: |
| | return |
| | for obj in self.all_originals_to_delete: |
| | if obj.isValid() and self.doc.getObject(obj.Name) is not None: |
| | try: |
| | if not obj.isDerivedFrom("App::DocumentObjectGroup") and not obj.isDerivedFrom( |
| | "App::Link" |
| | ): |
| | self.doc.removeObject(obj.Name) |
| | except Exception as e: |
| | FCC.PrintWarning( |
| | f"Failed to delete object '{getattr(obj, 'Label', obj.Name)}': {e}\n" |
| | ) |
| |
|
| | def _cleanup_organizational_groups(self): |
| | """Removes empty organizational groups after processing.""" |
| | for group_name in ["_BlockDefinitions", "_UnreferencedBlocks"]: |
| | group = self.doc.getObject(group_name) |
| | if group and not group.Group: |
| | try: |
| | self.doc.removeObject(group.Name) |
| | except Exception as e: |
| | FCC.PrintWarning( |
| | "DXF Post-Processor: Could not remove temporary group " |
| | f"'{group.Name}': {e}\n" |
| | ) |
| |
|
| | def _get_canonical_angles(self, start_angle_deg, end_angle_deg, radius_mm): |
| | """ |
| | Calculates canonical start and end angles for a Draft Arc/Circle that are |
| | both geometrically equivalent to the input and syntactically valid for |
| | FreeCAD's App::PropertyAngle, which constrains values to [-360, 360]. |
| | |
| | This is necessary because the C++ importer may provide angles outside this |
| | range (e.g., end_angle > 360) to unambiguously define an arc's span and |
| | distinguish between minor and major arcs. This function finds an |
| | equivalent angle pair that respects the C++ constraints while preserving |
| | the original geometry (span and direction). |
| | """ |
| | |
| | span = end_angle_deg - start_angle_deg |
| |
|
| | |
| | |
| | if abs(radius_mm) < 1e-9 and abs(span) < 1e-9: |
| | return 0.0, 0.0 |
| |
|
| | |
| | |
| | if abs(span % 360.0) < 1e-6 and abs(span) > 1e-7: |
| | |
| | return 0.0, 360.0 |
| |
|
| | |
| | canonical_start = start_angle_deg % 360.0 |
| | if canonical_start < 0: |
| | canonical_start += 360.0 |
| |
|
| | |
| | canonical_end = canonical_start + span |
| |
|
| | |
| | |
| | |
| | |
| | while canonical_start > 360.0 or canonical_end > 360.0: |
| | canonical_start -= 360.0 |
| | canonical_end -= 360.0 |
| |
|
| | while canonical_start < -360.0 or canonical_end < -360.0: |
| | canonical_start += 360.0 |
| | canonical_end += 360.0 |
| |
|
| | |
| | |
| | return canonical_start, canonical_end |
| |
|
| | def run(self): |
| | """Executes the entire post-processing workflow.""" |
| | FCC.PrintMessage("\n--- DXF DRAFT POST-PROCESSING ---\n") |
| | if not self.all_imported_objects: |
| | return |
| |
|
| | self.doc.openTransaction("DXF Post-processing") |
| | try: |
| | block_defs, top_geo, placeholders = self._categorize_objects() |
| |
|
| | |
| | for block_def_obj, original_children in block_defs.items(): |
| | new_draft_children = [ |
| | self._create_and_parent_geometry(child) for child in original_children |
| | ] |
| | block_def_obj.Links = [obj for obj in new_draft_children if obj] |
| | self.all_originals_to_delete.update( |
| | set(original_children) - set(new_draft_children) |
| | ) |
| |
|
| | |
| | converted_top_geo = [] |
| | for part_obj in top_geo: |
| | new_obj = self._create_and_parent_geometry(part_obj) |
| | if new_obj: |
| | converted_top_geo.append(new_obj) |
| | self.all_originals_to_delete.update(set(top_geo) - set(converted_top_geo)) |
| |
|
| | |
| | self._create_from_placeholders(placeholders) |
| |
|
| | |
| | self._delete_objects_in_batch() |
| |
|
| | except Exception as e: |
| | self.doc.abortTransaction() |
| | FCC.PrintError(f"Aborting DXF post-processing due to an error: {e}\n") |
| | import traceback |
| |
|
| | traceback.print_exc() |
| | return |
| | finally: |
| | self.doc.commitTransaction() |
| |
|
| | self._apply_gui_styles() |
| | self._cleanup_organizational_groups() |
| |
|
| | self.doc.recompute() |
| | FCC.PrintMessage("--- Draft post-processing finished. ---\n") |
| |
|