* Import: DXF importer, block and inserts overhaul Implement support for blocks as definitions and inserts as instances. A BLOCK definition becomes a hidden master object in a _BlockDefinitions group (currently a Part::Compound). An INSERT becomes a visible App::Link that points to that master definition. * Import: DXF parser/importer add debug print statements * Import: DXF importer, various improvements - Add more code comments - Correctly increase object count - Improve readability of anonymous block checks * Import: DXF (all) report anonymous blocks separately * Import: DXF importer (App + GUI) add nested inserts support * Import: DXF importer (GUI) fix preserving colors for inserts * Import: DXF importer, make primitives naming more specific * Import: DXF parser/importer remove debug print statements * Import: DXF parser, work around DXF color index 7 * Import: DXF importer GUI, temporary debug print to show layer color data * Import: DXF importer, fix nested inserts hierarchy * Import: DXF importer, prefix and suffix underscores to names To work around mangling from FreeCAD's unique name generator. * Import: DXF importer, implement flattening blocks on import * Revert "Import: DXF importer GUI, temporary debug print to show layer color data" This reverts commit b6ece395c9d2b8a1e0796d6f7b0d58842a2f3686. * Import: DXF importer, fix imported text orientation The DXF file provides the text rotation angle in degrees. The used rotZ function expects the angle in radians, thus the conversion needs to be made. Fixes: #21548 * Import: DXF importer, honour the preference to not import layouts * Import: DXF importer, move unreferenced blocks to separate group This provides better classification, but it has a performance hit: we import all blocks as before, we sort them and we reparent them to the correct group. The reparentipart with Grou.setValues() appears to be an expensive call. Probably because internally the FreeCAD dependency graph is modified. * Import: DXF importer, count compound children in the report * Import: DXF importer, cleanup defined blocks group if empty
4349 lines
158 KiB
Python
4349 lines
158 KiB
Python
# -*- coding: utf8 -*-
|
|
# Check code with
|
|
# flake8 --ignore=E226,E266,E401,W503
|
|
|
|
# ***************************************************************************
|
|
# * Copyright (c) 2009 Yorik van Havre <yorik@uncreated.net> *
|
|
# * *
|
|
# * This program is free software; you can redistribute it and/or modify *
|
|
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
|
# * as published by the Free Software Foundation; either version 2 of *
|
|
# * the License, or (at your option) any later version. *
|
|
# * for detail see the LICENCE text file. *
|
|
# * *
|
|
# * This program is distributed in the hope that it will be useful, *
|
|
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
# * GNU Library General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Library General Public *
|
|
# * License along with this program; if not, write to the Free Software *
|
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
# * USA *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
__title__ = "FreeCAD Draft Workbench - DXF importer/exporter"
|
|
__author__ = "Yorik van Havre <yorik@uncreated.net>"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
## @package importDXF
|
|
# \ingroup DRAFT
|
|
# \brief DXF file importer & exporter
|
|
#
|
|
# This module provides support for importing and exporting Autodesk DXF files
|
|
|
|
"""
|
|
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)
|
|
"""
|
|
# scaling factor between autocad font sizes and coin font sizes
|
|
TEXTSCALING = 1.35
|
|
# the minimum version of the dxfLibrary needed to run
|
|
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
|
|
|
|
gui = FreeCAD.GuiUp
|
|
draftui = None
|
|
if gui:
|
|
import FreeCADGui
|
|
try:
|
|
draftui = FreeCADGui.draftToolBar
|
|
except (AttributeError, NameError):
|
|
draftui = 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.
|
|
"""
|
|
# remove ACAD string formatation
|
|
# t = re.sub(r'{([^!}]([^}]|\n)*)}', '', text)
|
|
# print("input text: ",text)
|
|
t = text.strip("{}")
|
|
t = re.sub(r"\\\\.*?;", "", t)
|
|
# replace UTF codes by utf chars
|
|
sts = re.split("\\\\(U\\+....)", t)
|
|
t = u"".join(sts)
|
|
# replace degrees, diameters chars
|
|
t = re.sub(r'%%d', u'°', t)
|
|
t = re.sub(r'%%c', u'Ø', t)
|
|
t = re.sub(r'%%D', u'°', t)
|
|
t = re.sub(r'%%C', u'Ø', t)
|
|
# print("output text: ", 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.
|
|
"""
|
|
# layers is a global variable.
|
|
# It should probably be passed as an argument.
|
|
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:
|
|
# detect if we need to set "BYLAYER"
|
|
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 # BYLAYER
|
|
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*.3 + cv.y*.59 + cv.z*.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)
|
|
# Object Coordinate Systems (OCS)
|
|
# http://docs.autodesk.com/ACD/2011/ENU/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-7941.htm
|
|
# Arbitrary Axis Algorithm
|
|
# http://docs.autodesk.com/ACD/2011/ENU/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-793d.htm#WSc30cd3d5faa8f6d81cb25f1ffb755717d-7ff5
|
|
# Riferimenti dell'algoritmo dell'asse arbitrario in italiano
|
|
# http://docs.autodesk.com/ACD/2011/ITA/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-7941.htm
|
|
# http://docs.autodesk.com/ACD/2011/ITA/filesDXF/WS1a9193826455f5ff18cb41610ec0a2e719-793d.htm#WSc30cd3d5faa8f6d81cb25f1ffb755717d-7ff5
|
|
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])
|
|
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`.
|
|
|
|
If the polyline is closed, and the global variable `dxfMakeFaceMode`
|
|
is set, it will return a `Part.Face`, 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)
|
|
ob.Closed = polyline.closed
|
|
ob.Placement = placementFromDXFOCS(polyline)
|
|
return ob
|
|
else:
|
|
if polyline.closed and dxfMakeFaceMode:
|
|
w = Part.Wire(edges)
|
|
w.Placement = placementFromDXFOCS(polyline)
|
|
return Part.Face(w)
|
|
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)
|
|
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)
|
|
# print("Creating polyface with", len(pts),
|
|
# "points and", len(fcs), "facets")
|
|
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 tries producing a `Part.Edge`
|
|
(`dxfDiscretizeCurves` or `alwaysDiscretize` are `True`)
|
|
or `Part.Face`
|
|
if `closed` and the global variable `dxfMakeFaceMode` are `True`.
|
|
|
|
To do
|
|
-----
|
|
Use local variables, not global variables.
|
|
"""
|
|
if (dxfCreateDraft or dxfCreateSketch) and (not forceShape):
|
|
if dxfDiscretizeCurves or alwaysDiscretize:
|
|
ob = Draft.make_wire(verts)
|
|
else:
|
|
ob = Draft.make_bspline(verts)
|
|
ob.Closed = closed
|
|
return ob
|
|
else:
|
|
if dxfDiscretizeCurves or alwaysDiscretize:
|
|
sh = Part.makePolygon(verts+[verts[0]])
|
|
else:
|
|
sp = Part.BSplineCurve()
|
|
# print(knots)
|
|
sp.interpolate(verts)
|
|
sh = Part.Wire(sp.toShape())
|
|
if closed and dxfMakeFaceMode:
|
|
return Part.Face(sh)
|
|
else:
|
|
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 # workaround
|
|
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 = []
|
|
# parse the knots and points
|
|
dataremain = spline.data[:]
|
|
while len(dataremain) > 0:
|
|
groupnumber = dataremain[0][0]
|
|
if groupnumber == 40: # knot
|
|
knots.append(dataremain[0][1])
|
|
dataremain = dataremain[1:]
|
|
elif groupnumber == 41: # weight
|
|
weights.append(dataremain[0][1])
|
|
dataremain = dataremain[1:]
|
|
elif groupnumber in (10, 11): # control or fit point
|
|
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:]
|
|
# print(groupnumber)
|
|
|
|
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('inconsistant rational flag')
|
|
if len(weights) == 0:
|
|
weights = None
|
|
elif len(weights) != len(controlpoints):
|
|
raise ValueError('Wrong number of weights')
|
|
|
|
# build knotvector and multvector
|
|
# this means to remove duplicate knots
|
|
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)
|
|
# check if the multiplicities are valid
|
|
innermults = multvector[:] if periodic else multvector[1:-1]
|
|
if any(m > degree for m in innermults): # invalid
|
|
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:
|
|
# bezier segments
|
|
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
|
|
# print("creating block ", blockref.name,
|
|
# " containing ", len(blockref.entities.data), " entities")
|
|
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'):
|
|
# print("insert ",insert," in block ",insert.block[0])
|
|
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()
|
|
# for some reason z must be 0 to work
|
|
# tsf.scale(scale[0], scale[1], 0)
|
|
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 = Draft.make_block(objlist)
|
|
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)
|
|
# For old style layers, which are just groups
|
|
if hasattr(lay, "Group"):
|
|
pass
|
|
# For new Draft Layers
|
|
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
|
|
the layer name from the DXF code 8, the text value from code 1,
|
|
the position from codes 10, 20, 30, the height from code 40,
|
|
the rotation 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()
|
|
# print(ax, sup)
|
|
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"
|
|
# newob.ViewObject.DisplayMode = "World"
|
|
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:
|
|
# Unspecified
|
|
return 1.0
|
|
if insunits == 1:
|
|
# Inches
|
|
return 25.4
|
|
if insunits == 2:
|
|
# Feet
|
|
return 25.4 * 12
|
|
if insunits == 3:
|
|
# Miles
|
|
return 1609344.0
|
|
if insunits == 4:
|
|
# Millimeters
|
|
return 1.0
|
|
if insunits == 5:
|
|
# Centimeters
|
|
return 10.0
|
|
if insunits == 6:
|
|
# Meters
|
|
return 1000.0
|
|
if insunits == 7:
|
|
# Kilometers
|
|
return 1000000.0
|
|
if insunits == 8:
|
|
# Microinches
|
|
return 25.4 / 1000.0
|
|
if insunits == 9:
|
|
# Mils
|
|
return 25.4 / 1000.0
|
|
if insunits == 10:
|
|
# Yards
|
|
return 3 * 12 * 25.4
|
|
if insunits == 11:
|
|
# Angstroms
|
|
return 0.0000001
|
|
if insunits == 12:
|
|
# Nanometers
|
|
return 0.000001
|
|
if insunits == 13:
|
|
# Microns
|
|
return 0.001
|
|
if insunits == 14:
|
|
# Decimeters
|
|
return 100.0
|
|
if insunits == 15:
|
|
# Decameters
|
|
return 10000.0
|
|
if insunits == 16:
|
|
# Hectometers
|
|
return 100000.0
|
|
if insunits == 17:
|
|
# Gigameters
|
|
return 1000000000000.0
|
|
if insunits == 18:
|
|
# AstronomicalUnits
|
|
return 149597870690000.0
|
|
if insunits == 19:
|
|
# LightYears
|
|
return 9454254955500000000.0
|
|
if insunits == 20:
|
|
# Parsecs
|
|
return 30856774879000000000.0
|
|
|
|
# Unsupported
|
|
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.
|
|
"""
|
|
# for debugging the drawing variable is global so it is still accessible
|
|
# after running the script
|
|
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 = []
|
|
|
|
# Create layers
|
|
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")
|
|
|
|
# Draw lines
|
|
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)
|
|
|
|
# Draw polylines
|
|
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)
|
|
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
|
|
|
|
# Draw arcs
|
|
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)
|
|
|
|
# Join lines, polylines and arcs if needed
|
|
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)
|
|
|
|
# Draw circles
|
|
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)
|
|
|
|
# Draw solids
|
|
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)
|
|
|
|
# Draw splines
|
|
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)
|
|
|
|
# Draw ellipses
|
|
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)
|
|
|
|
# Draw texts
|
|
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")
|
|
|
|
# Draw 3D objects
|
|
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)
|
|
|
|
# End of shape-based objects, return if we are just getting shapes
|
|
if getShapes and shapes:
|
|
return shapes
|
|
|
|
# Draw dimensions
|
|
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:
|
|
# this is a radial or diameter dimension
|
|
# x1 = float(rawValue(dim,11))
|
|
# y1 = float(rawValue(dim,21))
|
|
# z1 = float(rawValue(dim,31))
|
|
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")
|
|
|
|
# Draw points
|
|
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))
|
|
z = vec(rawValue(point, 30))
|
|
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")
|
|
|
|
# Draw leaders
|
|
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")
|
|
|
|
# Draw hatches
|
|
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")
|
|
|
|
# Draw blocks
|
|
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
|
|
|
|
# Make blocks, if any
|
|
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
|
|
|
|
# Hide block objects, if any
|
|
for k, o in blockobjects.items():
|
|
if o.ViewObject:
|
|
o.ViewObject.hide()
|
|
del blockobjects
|
|
|
|
# Move layer contents to layers
|
|
for (l, contents) in layerObjects.items():
|
|
l.Group += contents
|
|
|
|
# Finishing
|
|
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 open(filename):
|
|
"""Open a file and return a new document.
|
|
|
|
If the global variable `dxfUseLegacyImporter` exists,
|
|
it will process `filename` with `processdxf`.
|
|
Otherwise, it will use the `Import` module, `Import.readDXF(filename)`.
|
|
|
|
Parameters
|
|
----------
|
|
filename : str
|
|
The path to the file to open.
|
|
|
|
Returns
|
|
-------
|
|
App::Document
|
|
The new document object with objects and shapes built from `filename`.
|
|
|
|
To do
|
|
-----
|
|
Use local variables, not global variables.
|
|
"""
|
|
readPreferences()
|
|
total_start_time = time.perf_counter()
|
|
|
|
if dxfUseLegacyImporter:
|
|
getDXFlibs()
|
|
if dxfReader:
|
|
docname = os.path.splitext(os.path.basename(filename))[0]
|
|
doc = FreeCAD.newDocument(docname)
|
|
doc.Label = docname
|
|
processdxf(doc, filename)
|
|
return doc
|
|
else:
|
|
errorDXFLib(gui)
|
|
else:
|
|
docname = os.path.splitext(os.path.basename(filename))[0]
|
|
doc = FreeCAD.newDocument(docname)
|
|
doc.Label = docname
|
|
FreeCAD.setActiveDocument(doc.Name)
|
|
stats = None
|
|
if gui:
|
|
import ImportGui
|
|
stats = ImportGui.readDXF(filename)
|
|
else:
|
|
import Import
|
|
stats = Import.readDXF(filename)
|
|
|
|
total_end_time = time.perf_counter()
|
|
if stats:
|
|
reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
|
|
reporter.report_to_console()
|
|
|
|
Draft.convert_draft_texts() # convert annotations to Draft texts
|
|
doc.recompute()
|
|
|
|
|
|
def insert(filename, docname):
|
|
"""Import a file into the specified document.
|
|
|
|
Parameters
|
|
----------
|
|
filename : str
|
|
The path to the file to import.
|
|
|
|
docname : str
|
|
The name of an `App::Document` instance into which
|
|
the objects and shapes from `filename` will be imported.
|
|
|
|
If the document doesn't exist, it is created
|
|
and set as the active document.
|
|
|
|
To do
|
|
-----
|
|
Use local variables, not global variables.
|
|
"""
|
|
readPreferences()
|
|
total_start_time = time.perf_counter()
|
|
try:
|
|
doc = FreeCAD.getDocument(docname)
|
|
except NameError:
|
|
doc = FreeCAD.newDocument(docname)
|
|
FreeCAD.setActiveDocument(docname)
|
|
if dxfUseLegacyImporter:
|
|
getDXFlibs()
|
|
if dxfReader:
|
|
processdxf(doc, filename)
|
|
else:
|
|
errorDXFLib(gui)
|
|
else:
|
|
stats = None
|
|
if gui:
|
|
import ImportGui
|
|
stats = ImportGui.readDXF(filename)
|
|
else:
|
|
import Import
|
|
stats = Import.readDXF(filename)
|
|
|
|
total_end_time = time.perf_counter()
|
|
if stats:
|
|
reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
|
|
reporter.report_to_console()
|
|
|
|
Draft.convert_draft_texts() # convert annotations to Draft texts
|
|
doc.recompute()
|
|
|
|
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)
|
|
|
|
|
|
# EXPORT ######################################################################
|
|
|
|
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)
|
|
# return DraftGeomUtils.cleanProjection(Part.makeCompound(edges))
|
|
if tess:
|
|
return DraftGeomUtils.cleanProjection(Part.makeCompound(edges),
|
|
tess[0], tess[1])
|
|
else:
|
|
return Part.makeCompound(edges)
|
|
# return DraftGeomUtils.cleanProjection(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:
|
|
# closed circle
|
|
return DraftVecUtils.tup(ce), radius, 0, 0
|
|
else:
|
|
# new method: recalculate ourselves as we cannot trust edge.Curve.Axis
|
|
# or XAxis
|
|
p1 = edge.Vertexes[0].Point
|
|
p2 = edge.Vertexes[-1].Point
|
|
v1 = p1.sub(ce)
|
|
v2 = p2.sub(ce)
|
|
# print(v1.cross(v2))
|
|
# print(edge.Curve.Axis)
|
|
# print(p1)
|
|
# print(p2)
|
|
# we can use Z check since arcs getting here will ALWAYS be in XY plane
|
|
# Z can be 0 if the arc is 180 deg
|
|
# if (v1.cross(v2).z >= 0) or (edge.Curve.Axis.z > 0):
|
|
# Calculates the angles of the first and last points
|
|
# in the circular arc, with respect to the global X axis.
|
|
if edge.Curve.Axis.z > 0:
|
|
# clockwise
|
|
ang1 = -DraftVecUtils.angle(v1)
|
|
ang2 = -DraftVecUtils.angle(v2)
|
|
else:
|
|
# counterclockwise
|
|
ang2 = -DraftVecUtils.angle(v1)
|
|
ang1 = -DraftVecUtils.angle(v2)
|
|
|
|
# obsolete method - fails a lot
|
|
# if round(edge.Curve.Axis.dot(Vector(0, 0, 1))) == 1:
|
|
# ang1, ang2 = edge.ParameterRange
|
|
# else:
|
|
# ang2, ang1 = edge.ParameterRange
|
|
# if edge.Curve.XAxis != Vector(1, 0, 0):
|
|
# ang1 -= DraftVecUtils.angle(edge.Curve.XAxis)
|
|
# ang2 -= DraftVecUtils.angle(edge.Curve.XAxis)
|
|
|
|
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):
|
|
# print("value at", nv*step, "=", edge.valueAt(nv*step))
|
|
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:
|
|
# LWpolyline format
|
|
return (v.x, v.y, v.z, None, None, b)
|
|
else:
|
|
# Polyline format
|
|
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)
|
|
# print("processing wire ",wire.Edges)
|
|
for edge in edges:
|
|
v1 = edge.Vertexes[0].Point
|
|
if DraftGeomUtils.geomType(edge) == "Circle":
|
|
# polyline bulge -> negative makes the arc go clockwise
|
|
angle = edge.LastParameter-edge.FirstParameter
|
|
bul = math.tan(angle/4)
|
|
# if cross1[2] < 0:
|
|
# # polyline bulge -> negative makes the arc go clockwise
|
|
# bul = -bul
|
|
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))
|
|
# print("wire verts: ",points)
|
|
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: # polylines
|
|
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: # circle
|
|
dxfobject.append(dxfLibrary.Circle(center, radius,
|
|
color=color,
|
|
layer=layer))
|
|
else: # arc
|
|
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): # lone edges
|
|
loneedges = []
|
|
for e in sh.Edges:
|
|
if e.hashCode() not in processededges:
|
|
loneedges.append(e)
|
|
# print("lone edges ", loneedges)
|
|
for edge in loneedges:
|
|
# splines
|
|
if (DraftGeomUtils.geomType(edge) in ["BSplineCurve",
|
|
"BezierCurve"]):
|
|
if (len(edge.Vertexes) == 1) and (edge.Curve.isClosed()) and (edge.Area > 0):
|
|
# special case: 1-vert closed spline, approximate as a circle
|
|
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": # curves
|
|
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: # circles
|
|
dxfobject.append(dxfLibrary.Circle(center,
|
|
radius,
|
|
color=color,
|
|
layer=layer))
|
|
else: # arcs
|
|
dxfobject.append(dxfLibrary.Arc(center,
|
|
radius,
|
|
ang1, ang2,
|
|
color=getACI(ob),
|
|
layer=layer))
|
|
elif DraftGeomUtils.geomType(edge) == "Ellipse": # ellipses:
|
|
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
|
|
# print("exporting ellipse: ", center, norm,
|
|
# start, end, major, minor)
|
|
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: # anything else is treated as lines
|
|
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)
|
|
# print(meshdata)
|
|
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])
|
|
# print(len(points),len(faces))
|
|
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:
|
|
# separate outline
|
|
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)
|
|
# sticky fonts can render very odd wires...
|
|
# for w in tag.Edges:
|
|
# pts = [(v.X, v.Y, v.Z) for v in w.Vertexes]
|
|
# dxf.append(dxfLibrary.Line(pts, color=getACI(ob),
|
|
# layer="Tags"))
|
|
|
|
|
|
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"):
|
|
# arch view: export it "as is"
|
|
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")):
|
|
# page: special hack-export! (see below)
|
|
exportPage(exportList[0], filename)
|
|
|
|
else:
|
|
# other cases, treat objects one by one
|
|
dxf = dxfLibrary.Drawing()
|
|
# add global variables
|
|
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": # dxflibrary already creates it
|
|
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'
|
|
# print("exporting layer:", ob.Label,
|
|
# getACI(ob), ltype)
|
|
dxf.layers.append(dxfLibrary.Layer(name=ob.Label,
|
|
color=getACI(ob),
|
|
lineType=ltype))
|
|
base_sketch_pla = None # Placement of the 1st sketch.
|
|
for ob in exportList:
|
|
obtype = Draft.getType(ob)
|
|
# print("processing " + str(ob.Name))
|
|
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:
|
|
# only one wire in this compound,
|
|
# no lone edge -> polyline
|
|
if len(sh.Wires[0].Edges) == len(sh.Edges):
|
|
writeShape(sh, ob, dxf,
|
|
nospline, lwPoly)
|
|
else:
|
|
# 1 wire + lone edges -> block
|
|
block = getBlock(sh, ob, lwPoly)
|
|
dxf.blocks.append(block)
|
|
dxf.append(dxfLibrary.Insert(name=ob.Name.upper(),
|
|
color=getACI(ob),
|
|
layer=getStrGroup(ob)))
|
|
else:
|
|
# all other cases: block
|
|
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":
|
|
# old-style texts
|
|
# temporary - as dxfLibrary doesn't support mtexts well,
|
|
# we use several single-line texts
|
|
# well, anyway, at the moment, Draft only writes
|
|
# single-line texts, so...
|
|
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"):
|
|
# texts
|
|
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):
|
|
# this leaves 10000 entities for the template
|
|
self.count = 10000
|
|
|
|
def incr(self, matchobj):
|
|
self.count += 1
|
|
# print(format(self.count, '02x'))
|
|
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"): # techdraw
|
|
template = "" # not supported for now...
|
|
views = page.Views
|
|
else: # drawing
|
|
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()
|
|
# find & replace editable texts
|
|
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:
|
|
# dummy default template
|
|
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:
|
|
# at the moment this is not used.
|
|
# TODO: if r12, do not print ellipses or splines
|
|
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 # fix rotation direction
|
|
if not isinstance(geom, list):
|
|
geom = [geom]
|
|
for g in geom: # getDXF returns a list of entities
|
|
if dxfExportBlocks:
|
|
# change layer and set color and ltype to BYBLOCK (0)
|
|
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:
|
|
# change layer, add handle
|
|
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:
|
|
# change layer and set color and ltype to BYBLOCK (0)
|
|
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 # view.Rotation is ignored
|
|
|
|
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`, `dxfMakeFaceMode`,
|
|
`dxfBrightBackground`, `dxfDefaultColor`, `dxfUseLegacyImporter`,
|
|
`dxfExportBlocks`, `dxfScaling`, `dxfUseLegacyExporter`
|
|
|
|
The parameter path is ``User parameter:BaseApp/Preferences/Mod/Draft``
|
|
|
|
To do
|
|
-----
|
|
Use local variables, not global variables.
|
|
"""
|
|
# reading parameters
|
|
if gui and params.get_param("dxfShowDialog"):
|
|
FreeCADGui.showPreferencesByName("Import-Export", ":/ui/preferences-dxf.ui")
|
|
global dxfCreatePart, dxfCreateDraft, dxfCreateSketch
|
|
global dxfDiscretizeCurves, dxfStarBlocks
|
|
global dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth
|
|
global dxfImportTexts, dxfImportLayouts
|
|
global dxfImportPoints, dxfImportHatches, dxfUseStandardSize
|
|
global dxfGetColors, dxfUseDraftVisGroups
|
|
global dxfMakeFaceMode, dxfBrightBackground, dxfDefaultColor
|
|
global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling
|
|
global dxfUseLegacyExporter
|
|
dxfCreatePart = params.get_param("dxfCreatePart")
|
|
dxfCreateDraft = params.get_param("dxfCreateDraft")
|
|
dxfCreateSketch = params.get_param("dxfCreateSketch")
|
|
dxfDiscretizeCurves = params.get_param("DiscretizeEllipses")
|
|
dxfStarBlocks = params.get_param("dxfstarblocks")
|
|
dxfMakeBlocks = params.get_param("groupLayers")
|
|
dxfJoin = params.get_param("joingeometry")
|
|
dxfRenderPolylineWidth = params.get_param("renderPolylineWidth")
|
|
dxfImportTexts = params.get_param("dxftext")
|
|
dxfImportLayouts = params.get_param("dxflayout")
|
|
dxfImportPoints = params.get_param("dxfImportPoints")
|
|
dxfImportHatches = params.get_param("importDxfHatches")
|
|
dxfUseStandardSize = params.get_param("dxfStdSize")
|
|
dxfGetColors = params.get_param("dxfGetOriginalColors")
|
|
dxfUseDraftVisGroups = params.get_param("dxfUseDraftVisGroups")
|
|
dxfMakeFaceMode = params.get_param("MakeFaceMode")
|
|
dxfUseLegacyImporter = params.get_param("dxfUseLegacyImporter")
|
|
dxfUseLegacyExporter = params.get_param("dxfUseLegacyExporter")
|
|
dxfBrightBackground = isBrightBackground()
|
|
dxfDefaultColor = getColor()
|
|
dxfExportBlocks = params.get_param("dxfExportBlocks")
|
|
dxfScaling = params.get_param("dxfScaling")
|
|
|
|
|
|
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")
|
|
|
|
# General info
|
|
lines.append(f"DXF version: {self.stats.get('dxfVersion', 'Unknown')}")
|
|
lines.append(f"File encoding: {self.stats.get('dxfEncoding', 'Unknown')}")
|
|
|
|
# Scaling info
|
|
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("")
|
|
|
|
# Timing
|
|
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("")
|
|
|
|
# Settings
|
|
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("")
|
|
|
|
# Counts
|
|
lines.append("Entity counts:")
|
|
total_read = 0
|
|
unsupported_keys = self.stats.get('unsupportedFeatures', {}).keys()
|
|
unsupported_entity_names = set()
|
|
for key in unsupported_keys:
|
|
# Extract the entity name from the key string, e.g., 'HATCH' from "Entity type 'HATCH'"
|
|
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("")
|
|
|
|
# System Blocks
|
|
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)
|