diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt
index 8cd5bcef7b..9f2a65e6b7 100644
--- a/src/Mod/Draft/CMakeLists.txt
+++ b/src/Mod/Draft/CMakeLists.txt
@@ -23,6 +23,7 @@ SET(Draft_import
importDWG.py
importOCA.py
importSVG.py
+ SVGPath.py
)
SET (Draft_geoutils
diff --git a/src/Mod/Draft/Resources/ui/preferences-svg.ui b/src/Mod/Draft/Resources/ui/preferences-svg.ui
index dd5466c1b5..44683fd14c 100644
--- a/src/Mod/Draft/Resources/ui/preferences-svg.ui
+++ b/src/Mod/Draft/Resources/ui/preferences-svg.ui
@@ -44,7 +44,7 @@
-
- Method chosen for importing SVG object color to FreeCAD
+ Method for importing SVG object colors into FreeCAD
0
@@ -56,18 +56,13 @@
Mod/Draft
-
-
- None (fastest)
-
+
+ Use default style from Part/PartDesign
+
-
- Use default color and linewidth
-
-
- -
-
- Original color and linewidth
+ Use original SVG style
@@ -79,11 +74,11 @@
-
- If checked, no units conversion will occur.
-One unit in the SVG file will translate as one millimeter.
+ If checked, no unit conversion will occur.
+One unit in the SVG file will be interpreted as one millimeter.
- Disable units scaling
+ Disable unit scaling
false
@@ -96,6 +91,95 @@ One unit in the SVG file will translate as one millimeter.
+ -
+
+
+ If face generation results in a degenerated face,
+a raw Wire from the original Shape is added.
+
+
+ Add wires for invalid faces
+
+
+ false
+
+
+ svgAddWireForInvalidFace
+
+
+ Mod/Draft
+
+
+
+ -
+
+
+ Check to cut shapes according to the even/odd SVG fill rule.
+
+
+ Apply Cuts
+
+
+ true
+
+
+ svgMakeCuts
+
+
+ Mod/Draft
+
+
+
+
+
+ -
+
+
-
+
+
+ Coordinate precision (crucial for detecting closed paths)
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 140
+ 0
+
+
+
+ The number of decimal places used in internal coordinate operations (for example 3 = 0.001).
+ The optimal value depends on the absolute size of the import. Typical values are between 1 and 5.
+
+
+ 10
+
+
+ 3
+
+
+ svgPrecision
+
+
+ Mod/Draft
+
+
+
diff --git a/src/Mod/Draft/SVGPath.py b/src/Mod/Draft/SVGPath.py
new file mode 100644
index 0000000000..c3a01515bd
--- /dev/null
+++ b/src/Mod/Draft/SVGPath.py
@@ -0,0 +1,788 @@
+import re
+import math
+from FreeCAD import Vector, Matrix
+from DraftVecUtils import equals, isNull, angle
+from draftutils.utils import svg_precision
+from draftutils.messages import _err, _msg, _wrn
+
+from Part import (
+ Arc,
+ BezierCurve,
+ BSplineCurve,
+ Ellipse,
+ Face,
+ LineSegment,
+ Shape,
+ Edge,
+ Wire,
+ Compound,
+ OCCError
+)
+
+def _tolerance(precision):
+ return 10**(-precision)
+
+def _arc_end_to_center(lastvec, currentvec, rx, ry,
+ x_rotation=0.0, correction=False):
+ '''Calculate the possible centers for an arc in endpoint parameterization.
+
+ Calculate (positive and negative) possible centers for an arc given in
+ ``endpoint parametrization``.
+ See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
+
+ the sweepflag is interpreted as: sweepflag <==> arc is travelled clockwise
+
+ Parameters
+ ----------
+ lastvec : Base::Vector3
+ First point of the arc.
+ currentvec : Base::Vector3
+ End point (current) of the arc.
+ rx : float
+ Radius of the ellipse, semi-major axis in the X direction.
+ ry : float
+ Radius of the ellipse, semi-minor axis in the Y direction.
+ x_rotation : float
+ Default is 0. Rotation around the Z axis, in radians (CCW).
+ correction : bool, optional
+ Default is `False`. If it is `True`, the radii will be scaled
+ by a factor.
+
+ Returns
+ -------
+ list, (float, float)
+ A tuple that consists of one list, and a tuple of radii.
+ [(positive), (negative)], (rx, ry)
+ The first element of the list is the positive tuple,
+ the second is the negative tuple.
+ [(Base::Vector3, float, float),
+ (Base::Vector3, float, float)], (float, float)
+ Types
+ [(vcenter+, angle1+, angledelta+),
+ (vcenter-, angle1-, angledelta-)], (rx, ry)
+ The first element of the list is the positive tuple,
+ consisting of center, angle, and angle increment;
+ the second element is the negative tuple.
+ '''
+ # scalefacsign = 1 if (largeflag != sweepflag) else -1
+ rx = float(rx)
+ ry = float(ry)
+ v0 = lastvec.sub(currentvec)
+ v0.multiply(0.5)
+ m1 = Matrix()
+ m1.rotateZ(-x_rotation) # eq. 5.1
+ v1 = m1.multiply(v0)
+ if correction:
+ eparam = v1.x**2 / rx**2 + v1.y**2 / ry**2
+ if eparam > 1:
+ eproot = math.sqrt(eparam)
+ rx = eproot * rx
+ ry = eproot * ry
+ denom = rx**2 * v1.y**2 + ry**2 * v1.x**2
+ numer = rx**2 * ry**2 - denom
+ results = []
+
+ # If the division is very small, set the scaling factor to zero,
+ # otherwise try to calculate it by taking the square root
+ if abs(numer/denom) < 1.0e-7:
+ scalefacpos = 0
+ else:
+ try:
+ scalefacpos = math.sqrt(numer/denom)
+ except ValueError:
+ _msg("sqrt({0}/{1})".format(numer, denom))
+ scalefacpos = 0
+
+ # Calculate two values because the square root may be positive or negative
+ for scalefacsign in (1, -1):
+ scalefac = scalefacpos * scalefacsign
+ # Step2 eq. 5.2
+ vcx1 = Vector(v1.y * rx/ry, -v1.x * ry/rx, 0).multiply(scalefac)
+ m2 = Matrix()
+ m2.rotateZ(x_rotation)
+ centeroff = currentvec.add(lastvec)
+ centeroff.multiply(0.5)
+ vcenter = m2.multiply(vcx1).add(centeroff) # Step3 eq. 5.3
+ # angle1 = Vector(1, 0, 0).getAngle(Vector((v1.x - vcx1.x)/rx,
+ # (v1.y - vcx1.y)/ry,
+ # 0)) # eq. 5.5
+ # angledelta = Vector((v1.x - vcx1.x)/rx,
+ # (v1.y - vcx1.y)/ry,
+ # 0).getAngle(Vector((-v1.x - vcx1.x)/rx,
+ # (-v1.y - vcx1.y)/ry,
+ # 0)) # eq. 5.6
+ # we need the right sign for the angle
+ angle1 = angle(Vector(1, 0, 0),
+ Vector((v1.x - vcx1.x)/rx,
+ (v1.y - vcx1.y)/ry,
+ 0)) # eq. 5.5
+ angledelta = angle(Vector((v1.x - vcx1.x)/rx,
+ (v1.y - vcx1.y)/ry,
+ 0),
+ Vector((-v1.x - vcx1.x)/rx,
+ (-v1.y - vcx1.y)/ry,
+ 0)) # eq. 5.6
+ results.append((vcenter, angle1, angledelta))
+
+ if rx < 0 or ry < 0:
+ _wrn("Warning: 'rx' or 'ry' is negative, check the SVG file")
+
+ return results, (rx, ry)
+
+
+def _arc_center_to_end(center, rx, ry, angle1, angledelta, xrotation=0.0):
+ '''Calculate start and end points, and flags of an arc.
+
+ Calculate start and end points, and flags of an arc given in
+ ``center parametrization``.
+ See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
+
+ Parameters
+ ----------
+ center : Base::Vector3
+ Coordinates of the center of the ellipse.
+ rx : float
+ Radius of the ellipse, semi-major axis in the X direction
+ ry : float
+ Radius of the ellipse, semi-minor axis in the Y direction
+ angle1 : float
+ Initial angle in radians
+ angledelta : float
+ Additional angle in radians
+ xrotation : float, optional
+ Default 0. Rotation around the Z axis
+
+ Returns
+ -------
+ v1, v2, largerc, sweep
+ Tuple indicating the end points of the arc, and two boolean values
+ indicating whether the arc is less than 180 degrees or not,
+ and whether the angledelta is negative.
+ '''
+ vr1 = Vector(rx * math.cos(angle1), ry * math.sin(angle1), 0)
+ vr2 = Vector(rx * math.cos(angle1 + angledelta),
+ ry * math.sin(angle1 + angledelta),
+ 0)
+ mxrot = Matrix()
+ mxrot.rotateZ(xrotation)
+ v1 = mxrot.multiply(vr1).add(center)
+ v2 = mxrot.multiply(vr2).add(center)
+ fa = ((abs(angledelta) / math.pi) % 2) > 1 # < 180 deg
+ fs = angledelta < 0
+ return v1, v2, fa, fs
+
+
+def _approx_bspline(
+ curve: BezierCurve,
+ num: int = 10,
+ tol: float = 1e-7,
+) -> BSplineCurve | BezierCurve:
+ _p0, d0 = curve.getD1(curve.FirstParameter)
+ _p1, d1 = curve.getD1(curve.LastParameter)
+ if (d0.Length < tol) or (d1.Length < tol):
+ tan1 = curve.tangent(curve.FirstParameter)[0]
+ tan2 = curve.tangent(curve.LastParameter)[0]
+ pts = curve.discretize(num)
+ bs = BSplineCurve()
+ try:
+ bs.interpolate(Points=pts, InitialTangent=tan1, FinalTangent=tan2)
+ return bs
+ except OCCError:
+ pass
+ return curve
+
+
+def _make_wire(path : list[Edge], precision : int, checkclosed : bool=False, donttry : bool=False):
+ '''Try to make a wire out of the list of edges.
+
+ If the wire functions fail or the wire is not closed,
+ if required the TopoShapeCompoundPy::connectEdgesToWires()
+ function is used.
+
+ Parameters
+ ----------
+ path : list[Edge]
+ A collection of edges
+ checkclosed : bool, optional
+ Default is `False`.
+ donttry : bool, optional
+ Default is `False`. If it's `True` it won't try to check
+ for a closed path.
+
+ Returns
+ -------
+ Part::Wire
+ A wire created from the ordered edges.
+ Part::Compound
+ A compound made of the edges, but unable to form a wire.
+ '''
+ if not donttry:
+ try:
+ sh = Wire(path)
+ # sh = Wire(path)
+ isok = (not checkclosed) or sh.isClosed()
+ if len(sh.Edges) != len(path):
+ isok = False
+ # BRep_API: command not done
+ except OCCError:
+ isok = False
+ if donttry or not isok:
+ # Code from wmayer forum p15549 to fix the tolerance problem
+ # original tolerance = 0.00001
+ comp = Compound(path)
+ _sh = comp.connectEdgesToWires(False, _tolerance(precision))
+ sh = _sh.Wires[0]
+ if len(sh.Edges) != len(path):
+ _wrn("Unable to form a wire. Resort to a Compound of Edges.")
+ sh = comp
+ return sh
+
+
+class FaceTreeNode:
+ '''
+ Building Block of a tree structure holding one-closed-wire faces
+ sorted after their enclosure of each other.
+ This class only works with faces that have exactly one closed wire
+ '''
+ face : Face
+ children : list
+ name : str
+
+
+ def __init__(self, face=None, name="root"):
+ super().__init__()
+ self.face = face
+ self.name = name
+ self.children = []
+
+
+ def insert (self, face, name):
+ '''
+ takes a single-wire named face, and inserts it into the tree
+ depending on its enclosure in/of already added faces.
+
+ Parameters
+ ----------
+ face : Face
+ single closed wire face to be added to the tree
+ name : str
+ face identifier
+ '''
+ for node in self.children:
+ if node.face.Area > face.Area:
+ # new face could be encompassed
+ if (face.distToShape(node.face)[0] == 0.0 and
+ face.Wires[0].distToShape(node.face.Wires[0])[0] != 0.0):
+ # it is encompassed - enter next tree layer
+ node.insert(face, name)
+ return
+ else:
+ # new face could encompass
+ if (node.face.distToShape(face)[0] == 0.0 and
+ node.face.Wires[0].distToShape(face.Wires[0])[0] != 0.0):
+ # it does encompass the current child nodes face
+ # create new node from face
+ new = FaceTreeNode(face, name)
+ # swap the new one with the child node
+ self.children.remove(node)
+ self.children.append(new)
+ # add former child node as child to the new node
+ new.children.append(node)
+ return
+ # the face is not encompassing and is not encompassed (from) any
+ # other face, we add it as new child
+ new = FaceTreeNode(face, name)
+ self.children.append(new)
+
+
+ def makeCuts(self):
+ '''
+ recursively traverse the tree and cuts all faces in even
+ numbered tree levels with their direct childrens faces.
+ Additionally the tree is shrunk by removing the odd numbered
+ tree levels.
+ '''
+ result = self.face
+ if not result:
+ for node in self.children:
+ node.makeCuts()
+ else:
+ new_children = []
+ for node in self.children:
+ result = result.cut(node.face)
+ for subnode in node.children:
+ subnode.makeCuts()
+ new_children.append(subnode)
+ self.children = new_children
+ self.face = result
+
+
+ def flatten(self):
+ ''' creates a flattened list of face-name tuples from the facetree
+ content
+ '''
+ result = []
+ result.append((self.name, self.face))
+ for node in self.children:
+ result.extend(node.flatten())
+ return result
+
+
+
+class SvgPathElement:
+
+ path : list[dict]
+
+ def __init__(self, precision : int, interpol_pts : int, origin : Vector = Vector(0, 0, 0)):
+ self.precision = precision
+ self.interpol_pts = interpol_pts
+ self.path = [{"type": "start", "last_v": origin }]
+
+ def add_move(self, x : float, y : float, relative : bool) -> None:
+ if relative:
+ last_v = self.path[-1]["last_v"].add(Vector(x, -y, 0))
+ else:
+ last_v = Vector(x, -y, 0)
+ # if we're at the beginning of a wire we overwrite the start vector
+ if self.path[-1]["type"] == "start":
+ self.path[-1]["last_v"] = last_v
+ else:
+ self.path.append({"type": "start", "last_v": last_v})
+
+ def add_lines(self, coords: list[float], relative: bool) -> None:
+ last_v = self.path[-1]["last_v"]
+ for x, y in zip(coords[0::2], coords[1::2]):
+ if relative:
+ last_v = last_v.add(Vector(x, -y, 0))
+ else:
+ last_v = Vector(x, -y, 0)
+ self.path.append({"type": "line", "last_v": last_v})
+
+ def add_horizontals(self, x_coords: list[float], relative: bool) -> None:
+ last_v = self.path[-1]["last_v"]
+ for x in x_coords:
+ if relative:
+ last_v = Vector(x + last_v.x, last_v.y, 0)
+ else:
+ last_v = Vector(x, last_v.y, 0)
+ self.path.append({"type": "line", "last_v": last_v})
+
+ def add_verticals(self, y_coords: list[float], relative: bool) -> None:
+ last_v = self.path[-1]["last_v"]
+ if relative:
+ for y in y_coords:
+ last_v = Vector(last_v.x, last_v.y - y, 0)
+ self.path.append({"type": "line", "last_v": last_v})
+ else:
+ for y in y_coords:
+ last_v = Vector(last_v.x, -y, 0)
+ self.path.append({"type": "line", "last_v": last_v})
+
+ def add_arcs(self, args: list[float], relative: bool) -> None:
+ p_iter = zip(
+ args[0::7], args[1::7], args[2::7], args[3::7],
+ args[4::7], args[5::7], args[6::7], strict=False,
+ )
+ for rx, ry, x_rotation, large_flag, sweep_flag, x, y in p_iter:
+ # support for large-arc and x-rotation is missing
+ if relative:
+ last_v = self.path[-1]["last_v"].add(Vector(x, -y, 0))
+ else:
+ last_v = Vector(x, -y, 0)
+ self.path.append({
+ "type": "arc",
+ "rx": rx,
+ "ry": ry,
+ "x_rotation": x_rotation,
+ "large_flag": large_flag != 0,
+ "sweep_flag": sweep_flag != 0,
+ "last_v": last_v
+ })
+
+ def add_cubic_beziers(self, args: list[float], relative: bool, smooth: bool) -> None:
+ last_v = self.path[-1]["last_v"]
+ if smooth:
+ p_iter = list(
+ zip(
+ args[2::4], args[3::4],
+ args[0::4], args[1::4],
+ args[2::4], args[3::4], strict=False )
+ )
+ else:
+ p_iter = list(
+ zip(
+ args[0::6], args[1::6],
+ args[2::6], args[3::6],
+ args[4::6], args[5::6], strict=False )
+ )
+ for p1x, p1y, p2x, p2y, x, y in p_iter:
+ if smooth:
+ if self.path[-1]["type"] == "cbezier":
+ pole1 = last_v.sub(self.path[-1]["pole2"]).add(last_v)
+ else:
+ pole1 = last_v
+ else:
+ if relative:
+ pole1 = last_v.add(Vector(p1x, -p1y, 0))
+ else:
+ pole1 = Vector(p1x, -p1y, 0)
+ if relative:
+ pole2 = last_v.add(Vector(p2x, -p2y, 0))
+ last_v = last_v.add(Vector(x, -y, 0))
+ else:
+ pole2 = Vector(p2x, -p2y, 0)
+ last_v = Vector(x, -y, 0)
+
+ self.path.append({
+ "type": "cbezier",
+ "pole1": pole1,
+ "pole2": pole2,
+ "last_v": last_v
+ })
+
+ def add_quadratic_beziers(self, args: list[float], relative: bool, smooth: bool):
+ last_v = self.path[-1]["last_v"]
+ if smooth:
+ p_iter = list( zip( args[1::2], args[1::2],
+ args[0::2], args[1::2], strict=False ) )
+ else:
+ p_iter = list( zip( args[0::4], args[1::4],
+ args[2::4], args[3::4], strict=False ) )
+ for px, py, x, y in p_iter:
+ if smooth:
+ if self.path[-1]["type"] == "qbezier":
+ pole = last_v.sub(self.path[-1]["pole"]).add(last_v)
+ else:
+ pole = last_v
+ else:
+ if relative:
+ pole = last_v.add(Vector(px, -py, 0))
+ else:
+ pole = Vector(px, -py, 0)
+ if relative:
+ last_v = last_v.add(Vector(x, -y, 0))
+ else:
+ last_v = Vector(x, -y, 0)
+
+ self.path.append({
+ "type": "qbezier",
+ "pole": pole,
+ "last_v": last_v
+ })
+
+ def add_close(self):
+ last_v = self.path[-1]["last_v"]
+ first_v = self.__get_last_start()
+ if not equals(last_v, first_v, self.precision):
+ self.path.append({"type": "line", "last_v": first_v})
+ # assume that a close command finalizes a subpath
+ self.path.append({"type": "start", "last_v": first_v})
+
+ def __get_last_start(self) -> Vector:
+ """
+ Return the startpoint of the last SubPath.
+ """
+ for pds in reversed(self.path):
+ if pds["type"] == "start":
+ return pds["last_v"]
+ return Vector(0, 0, 0)
+
+ def __correct_last_v(self, pds: dict, last_v: Vector) -> None:
+ """
+ Correct the endpoint of the given path dataset to the
+ given vector and move possibly associated members accordingly.
+ """
+ delta = last_v.sub(pds["last_v"])
+ # we won't move last_v if it's already correct or if the delta
+ # is substantially greater than what rounding errors could accumulate,
+ # so we assume the path is intended to be open.
+ if (delta.x == 0 and delta.y == 0 and delta.z == 0 or
+ not isNull(delta, self.precision)):
+ return
+
+ # for cbeziers we also relocate the second pole
+ if pds["type"] == "cbezier":
+ pds["pole2"] = pds["pole2"].add(delta)
+ # for qbeziers we also relocate the pole by half of the delta
+ elif pds["type"] == "qbezier":
+ pds["pole"] = pds["pole"].add(delta.scale(0.5, 0.5, 0))
+ # all data types have last_v
+ pds["last_v"] = last_v
+
+
+ def correct_endpoints(self):
+ """
+ Correct the endpoints of all subpaths and move possibly
+ associated members accordingly.
+ """
+ start = None
+ last = None
+ for pds in self.path:
+ if pds["type"] == "start":
+ if start:
+ # there is already a start
+ if last:
+ # and there are edges behind us.
+ # we correct the last to the start vector
+ self.__correct_last_v(last, start["last_v"])
+ last = None
+ start = pds
+ continue
+ last = pds
+ if start and last and start != last:
+ self.__correct_last_v(last, start["last_v"])
+
+
+ def create_edges(self) -> list[list[Edge]]:
+ """
+ Creates shapes from prepared path datasets and returns them in an
+ ordered list of lists of edges, where each 1st order list entry
+ represents a single continuous (and probably closed) sub-path.
+ """
+ result = []
+ edges = None
+ last_v = Vector(0, 0, 0)
+ for pds in self.path:
+ next_v = pds["last_v"]
+ match pds["type"]:
+ case "start":
+ if edges and len(edges) > 0 :
+ result.append(edges)
+ edges = []
+ case "line":
+ if equals(last_v, next_v, self.precision):
+ # line segment too short, skip it
+ next_v = last_v
+ else:
+ edges.append(LineSegment(last_v, next_v).toShape())
+ case "arc":
+ rx = pds["rx"]
+ ry = pds["ry"]
+ x_rotation = pds["x_rotation"]
+ large_flag = pds["large_flag"]
+ sweep_flag = pds["sweep_flag"]
+ # Calculate the possible centers for an arc
+ # in 'endpoint parameterization'.
+ _x_rot = math.radians(-x_rotation)
+ (solution, (rx, ry)) = _arc_end_to_center(
+ last_v, next_v,
+ rx, ry,
+ _x_rot,
+ correction=True
+ )
+ # Choose one of the two solutions
+ neg_sol = large_flag != sweep_flag
+ v_center, angle1, angle_delta = solution[neg_sol]
+ if ry > rx:
+ rx, ry = ry, rx
+ swap_axis = True
+ else:
+ swap_axis = False
+ e1 = Ellipse(v_center, rx, ry)
+ if sweep_flag:
+ angle1 = angle1 + angle_delta
+ angle_delta = -angle_delta
+
+ d90 = math.radians(90)
+ e1a = Arc(e1, angle1 - swap_axis * d90, angle1 + angle_delta - swap_axis * d90)
+ seg = e1a.toShape()
+ if swap_axis:
+ seg.rotate(v_center, Vector(0, 0, 1), 90)
+ _tol = _tolerance(self.precision)
+ if abs(x_rotation) > _tol:
+ seg.rotate(v_center, Vector(0, 0, 1), -x_rotation)
+ if sweep_flag:
+ seg.reverse()
+ edges.append(seg)
+
+ case "cbezier":
+ pole1 = pds["pole1"]
+ pole2 = pds["pole2"]
+ _tol = _tolerance(self.precision + 2)
+ _d1 = pole1.distanceToLine(last_v, next_v)
+ _d2 = pole2.distanceToLine(last_v, next_v)
+ if _d1 < _tol and _d2 < _tol:
+ # poles and endpints are all on a line
+ if equals(last_v, next_v, self.precision):
+ # in this case we don't accept (nearly) zero
+ # distance betwen start and end (skip it).
+ next_v = last_v
+ else:
+ seg = LineSegment(last_v, next_v).toShape()
+ edges.append(seg)
+ else:
+ b = BezierCurve()
+ b.setPoles([last_v, pole1, pole2, next_v])
+ seg = _approx_bspline(b, self.interpol_pts).toShape()
+ edges.append(seg)
+ case "qbezier":
+ if equals(last_v, next_v, self.precision):
+ # segment too small - skipping.
+ next_v = last_v
+ else:
+ pole = pds["pole"]
+ _tol = _tolerance(self.precision + 2)
+ _distance = pole.distanceToLine(last_v, next_v)
+ if _distance < _tol:
+ # pole is on the line
+ _seg = LineSegment(last_v, next_v)
+ seg = _seg.toShape()
+ else:
+ b = BezierCurve()
+ b.setPoles([last_v, pole, next_v])
+ seg = _approx_bspline(b, self.interpol_pts).toShape()
+ edges.append(seg)
+ case _:
+ _msg("Illegal path_data type. {}".format(pds['type']))
+ return []
+ last_v = next_v
+ if not edges is None and len(edges) > 0 :
+ result.append(edges)
+ return result
+
+
+
+class SvgPathParser:
+ """Parse SVG path data and create FreeCAD Shapes."""
+
+ commands : list[tuple]
+ pointsre : re.Pattern
+ data : dict
+ shapes : list[list[Shape]]
+ faces : FaceTreeNode
+ name : str
+
+ def __init__(self, data, name):
+ super().__init__()
+ """Evaluate path data and initialize."""
+ _op = '([mMlLhHvVaAcCqQsStTzZ])'
+ _op2 = '([^mMlLhHvVaAcCqQsStTzZ]*)'
+ _command = '\\s*?' + _op + '\\s*?' + _op2 + '\\s*?'
+ pathcommandsre = re.compile(_command, re.DOTALL)
+
+ _num = '[-+]?[0-9]*\\.?[0-9]+'
+ _exp = '([eE][-+]?[0-9]+)?'
+ _arg = '(' + _num + _exp + ')'
+ self.commands = pathcommandsre.findall(' '.join(data['d']))
+ self.argsre = re.compile(_arg, re.DOTALL)
+ self.data = data
+ self.paths = []
+ self.shapes = []
+ self.faces = None
+ self.name = name
+
+
+ def parse(self):
+ '''
+ Creates lists of SvgPathElements from raw svg path
+ data. It's supposed to be called direct after SvgPath Object
+ creation.
+ '''
+ path = SvgPathElement(svg_precision(), 10)
+ self.paths = []
+ for d, argsstr in self.commands:
+ relative = d.islower()
+
+ _args = self.argsre.findall(argsstr.replace(',', ' '))
+ args = [float(number) for number, exponent in _args]
+
+ if d in "Mm":
+ path.add_move(args.pop(0), args.pop(0), relative)
+ if d in "LlMm":
+ path.add_lines(args, relative)
+ elif d in "Hh":
+ path.add_horizontals(args, relative)
+ elif d in "Vv":
+ path.add_verticals(args, relative)
+ elif d in "Aa":
+ path.add_arcs(args, relative)
+ elif d in "Cc":
+ path.add_cubic_beziers(args, relative, False)
+ elif d in "Ss":
+ path.add_cubic_beziers(args, relative, True)
+ elif d in "Qq":
+ path.add_quadratic_beziers(args, relative, False)
+ elif d in "Tt":
+ path.add_quadratic_beziers(args, relative, True)
+ elif d in "Zz":
+ path.add_close()
+
+ path.correct_endpoints();
+ self.shapes = path.create_edges()
+
+
+ def create_faces(self, fill=True, add_wire_for_invalid_face=False):
+ '''
+ Generate Faces from lists of Shapes.
+ If shapes form a closed wire and the fill Attribute is set, we
+ generate a closed Face. Otherwise we treat the shape as pure wire.
+
+ Parameters
+ ----------
+ fill : Object/bool
+ if True or not None Faces are generated from closed shapes.
+ '''
+ precision = svg_precision()
+ cnt = -1;
+ openShapes = []
+ self.faces = FaceTreeNode()
+ for sh in self.shapes:
+ cnt += 1
+ add_wire = True
+ wr = _make_wire(sh, precision, checkclosed=True)
+ wrcpy = wr.copy();
+ wire_reason = ""
+ if cnt > 0:
+ face_name = self.name + "_" + str(cnt)
+ else:
+ face_name = self.name
+
+
+ if not fill:
+ wire_reason = " no-fill"
+ if not wr.Wires[0].isClosed():
+ wire_reason += " open Wire"
+ if fill and wr.Wires[0].isClosed():
+ try:
+ face = Face(wr)
+ if not face.isValid():
+ add_wire = add_wire_for_invalid_face
+ wire_reason = " invalid Face"
+ if face.fix(1e-6, 0, 1):
+ res = "succeed"
+ else:
+ res = "fail"
+ _wrn("Invalid Face '{}' created. Attempt to fix - {}ed."
+ .format(face_name, res))
+ else:
+ add_wire = False
+ if not (face.Area < 10 * (_tolerance(precision) ** 2)):
+ self.faces.insert(face, face_name)
+ except:
+ _wrn("Failed to make a shape from '{}'. ".format(face_name)
+ + "This Path will be discarded.")
+ if add_wire:
+ if wrcpy.Length > _tolerance(precision):
+ _msg("Adding wire for '{}' - reason: {}."
+ .format(face_name, wire_reason))
+ openShapes.append((face_name + "_w", wrcpy))
+
+ self.shapes = openShapes
+
+
+ def doCuts(self):
+ ''' Exposes the FaceTreeNode.makeCuts function of the tree containing
+ closed wire faces.
+ This function is called after creating closed Faces with
+ 'createFaces' in order to hollow faces encompassing others.
+ '''
+ self.faces.makeCuts()
+
+
+ def getShapeList(self):
+ ''' Returns the resulting list of tuples containing name and face of
+ each created element.
+ '''
+ result = self.faces.flatten()
+ result.extend(self.shapes)
+ return result
diff --git a/src/Mod/Draft/draftutils/utils.py b/src/Mod/Draft/draftutils/utils.py
index cb97c82441..a0e2c8fab9 100644
--- a/src/Mod/Draft/draftutils/utils.py
+++ b/src/Mod/Draft/draftutils/utils.py
@@ -206,6 +206,29 @@ def precision():
return params.get_param("precision")
+def svg_precision():
+ """Return the precision value for SVG import from the parameter database.
+
+ It is the number of decimal places that a float will have.
+ Example
+ ::
+ precision=5, 0.12345
+ precision=4, 0.1234
+ precision=3, 0.123
+
+ Due to floating point operations there may be rounding errors.
+ Therefore, this precision number is used to round up values
+ so that all operations are consistent.
+ By default the precision is 3 decimal places.
+
+ Returns
+ -------
+ int
+ params.get_param("svgPrecision")
+ """
+ return params.get_param("svgPrecision")
+
+
def tolerance():
"""Return a tolerance based on the precision() value
diff --git a/src/Mod/Draft/importSVG.py b/src/Mod/Draft/importSVG.py
index eb0749aaa4..51f824b7ba 100644
--- a/src/Mod/Draft/importSVG.py
+++ b/src/Mod/Draft/importSVG.py
@@ -54,14 +54,17 @@ import re
import xml.sax
import FreeCAD
+import Part
import Draft
-import DraftVecUtils
+from DraftVecUtils import equals
from FreeCAD import Vector
from draftutils import params
from draftutils import utils
+from draftutils.utils import svg_precision
from draftutils.translate import translate
from draftutils.messages import _err, _msg, _wrn
from draftutils.utils import pyopen
+from SVGPath import SvgPathParser
if FreeCAD.GuiUp:
from PySide import QtWidgets
@@ -76,7 +79,6 @@ else:
draftui = None
-
svgcolors = {
'Pink': (255, 192, 203),
'Blue': (0, 0, 255),
@@ -291,7 +293,7 @@ def transformCopyShape(shape, m):
"""Apply transformation matrix m on given shape.
Since OCCT 6.8.0 transformShape can be used to apply certain
- non-orthogonal transformations on shapes. This way a conversion
+ similarity transformations on shapes. This way a conversion
to BSplines in transformGeometry can be avoided.
@sa: Part::TopoShape::transformGeometry(), TopoShapePy::transformGeometry()
@@ -309,18 +311,12 @@ def transformCopyShape(shape, m):
shape : Part::TopoShape
The shape transformed by the matrix
"""
- # If there is no shear, these matrix operations will be very small
- _s1 = abs(m.A11**2 + m.A12**2 - m.A21**2 - m.A22**2)
- _s2 = abs(m.A11 * m.A21 + m.A12 * m.A22)
- if _s1 < 1e-8 and _s2 < 1e-8:
- try:
- newshape = shape.copy()
- newshape.transformShape(m)
- return newshape
+ try:
+ return shape.transformShape(m, True, True)
# Older versions of OCCT will refuse to work on
# non-orthogonal matrices
- except Part.OCCError:
- pass
+ except Part.OCCError:
+ pass
return shape.transformGeometry(m)
@@ -433,204 +429,6 @@ def getsize(length, mode='discard', base=1):
return float(number) * base
-def makewire(path, checkclosed=False, donttry=False):
- '''Try to make a wire out of the list of edges.
-
- If the wire functions fail or the wire is not closed,
- if required the TopoShapeCompoundPy::connectEdgesToWires()
- function is used.
-
- Parameters
- ----------
- path : Part.Edge
- A collection of edges
- checkclosed : bool, optional
- Default is `False`.
- donttry : bool, optional
- Default is `False`. If it's `True` it won't try to check
- for a closed path.
-
- Returns
- -------
- Part::Wire
- A wire created from the ordered edges.
- Part::Compound
- A compound made of the edges, but unable to form a wire.
- '''
- if not donttry:
- try:
- import Part
- sh = Part.Wire(Part.__sortEdges__(path))
- # sh = Part.Wire(path)
- isok = (not checkclosed) or sh.isClosed()
- if len(sh.Edges) != len(path):
- isok = False
- # BRep_API: command not done
- except Part.OCCError:
- isok = False
- if donttry or not isok:
- # Code from wmayer forum p15549 to fix the tolerance problem
- # original tolerance = 0.00001
- comp = Part.Compound(path)
- _sh = comp.connectEdgesToWires(False,
- 10**(-1 * (Draft.precision() - 2)))
- sh = _sh.Wires[0]
- if len(sh.Edges) != len(path):
- _wrn("Unable to form a wire")
- sh = comp
- return sh
-
-
-def arccenter2end(center, rx, ry, angle1, angledelta, xrotation=0.0):
- '''Calculate start and end points, and flags of an arc.
-
- Calculate start and end points, and flags of an arc given in
- ``center parametrization``.
- See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
-
- Parameters
- ----------
- center : Base::Vector3
- Coordinates of the center of the ellipse.
- rx : float
- Radius of the ellipse, semi-major axis in the X direction
- ry : float
- Radius of the ellipse, semi-minor axis in the Y direction
- angle1 : float
- Initial angle in radians
- angledelta : float
- Additional angle in radians
- xrotation : float, optional
- Default 0. Rotation around the Z axis
-
- Returns
- -------
- v1, v2, largerc, sweep
- Tuple indicating the end points of the arc, and two boolean values
- indicating whether the arc is less than 180 degrees or not,
- and whether the angledelta is negative.
- '''
- vr1 = Vector(rx * math.cos(angle1), ry * math.sin(angle1), 0)
- vr2 = Vector(rx * math.cos(angle1 + angledelta),
- ry * math.sin(angle1 + angledelta),
- 0)
- mxrot = FreeCAD.Matrix()
- mxrot.rotateZ(xrotation)
- v1 = mxrot.multiply(vr1).add(center)
- v2 = mxrot.multiply(vr2).add(center)
- fa = ((abs(angledelta) / math.pi) % 2) > 1 # < 180 deg
- fs = angledelta < 0
- return v1, v2, fa, fs
-
-
-def arcend2center(lastvec, currentvec, rx, ry,
- xrotation=0.0, correction=False):
- '''Calculate the possible centers for an arc in endpoint parameterization.
-
- Calculate (positive and negative) possible centers for an arc given in
- ``endpoint parametrization``.
- See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
-
- the sweepflag is interpreted as: sweepflag <==> arc is travelled clockwise
-
- Parameters
- ----------
- lastvec : Base::Vector3
- First point of the arc.
- currentvec : Base::Vector3
- End point (current) of the arc.
- rx : float
- Radius of the ellipse, semi-major axis in the X direction.
- ry : float
- Radius of the ellipse, semi-minor axis in the Y direction.
- xrotation : float, optional
- Default is 0. Rotation around the Z axis, in radians (CCW).
- correction : bool, optional
- Default is `False`. If it is `True`, the radii will be scaled
- by a factor.
-
- Returns
- -------
- list, (float, float)
- A tuple that consists of one list, and a tuple of radii.
- [(positive), (negative)], (rx, ry)
- The first element of the list is the positive tuple,
- the second is the negative tuple.
- [(Base::Vector3, float, float),
- (Base::Vector3, float, float)], (float, float)
- Types
- [(vcenter+, angle1+, angledelta+),
- (vcenter-, angle1-, angledelta-)], (rx, ry)
- The first element of the list is the positive tuple,
- consisting of center, angle, and angle increment;
- the second element is the negative tuple.
- '''
- # scalefacsign = 1 if (largeflag != sweepflag) else -1
- rx = float(rx)
- ry = float(ry)
- v0 = lastvec.sub(currentvec)
- v0.multiply(0.5)
- m1 = FreeCAD.Matrix()
- m1.rotateZ(-xrotation) # eq. 5.1
- v1 = m1.multiply(v0)
- if correction:
- eparam = v1.x**2 / rx**2 + v1.y**2 / ry**2
- if eparam > 1:
- eproot = math.sqrt(eparam)
- rx = eproot * rx
- ry = eproot * ry
- denom = rx**2 * v1.y**2 + ry**2 * v1.x**2
- numer = rx**2 * ry**2 - denom
- results = []
-
- # If the division is very small, set the scaling factor to zero,
- # otherwise try to calculate it by taking the square root
- if abs(numer/denom) < 10**(-1 * (Draft.precision())):
- scalefacpos = 0
- else:
- try:
- scalefacpos = math.sqrt(numer/denom)
- except ValueError:
- _msg("sqrt({0}/{1})".format(numer, denom))
- scalefacpos = 0
-
- # Calculate two values because the square root may be positive or negative
- for scalefacsign in (1, -1):
- scalefac = scalefacpos * scalefacsign
- # Step2 eq. 5.2
- vcx1 = Vector(v1.y * rx/ry, -v1.x * ry/rx, 0).multiply(scalefac)
- m2 = FreeCAD.Matrix()
- m2.rotateZ(xrotation)
- centeroff = currentvec.add(lastvec)
- centeroff.multiply(0.5)
- vcenter = m2.multiply(vcx1).add(centeroff) # Step3 eq. 5.3
- # angle1 = Vector(1, 0, 0).getAngle(Vector((v1.x - vcx1.x)/rx,
- # (v1.y - vcx1.y)/ry,
- # 0)) # eq. 5.5
- # angledelta = Vector((v1.x - vcx1.x)/rx,
- # (v1.y - vcx1.y)/ry,
- # 0).getAngle(Vector((-v1.x - vcx1.x)/rx,
- # (-v1.y - vcx1.y)/ry,
- # 0)) # eq. 5.6
- # we need the right sign for the angle
- angle1 = DraftVecUtils.angle(Vector(1, 0, 0),
- Vector((v1.x - vcx1.x)/rx,
- (v1.y - vcx1.y)/ry,
- 0)) # eq. 5.5
- angledelta = DraftVecUtils.angle(Vector((v1.x - vcx1.x)/rx,
- (v1.y - vcx1.y)/ry,
- 0),
- Vector((-v1.x - vcx1.x)/rx,
- (-v1.y - vcx1.y)/ry,
- 0)) # eq. 5.6
- results.append((vcenter, angle1, angledelta))
-
- if rx < 0 or ry < 0:
- _wrn("Warning: 'rx' or 'ry' is negative, check the SVG file")
-
- return results, (rx, ry)
-
-
def getrgb(color):
"""Return an RGB hexadecimal string '#00aaff' from a FreeCAD color.
@@ -650,14 +448,18 @@ def getrgb(color):
return "#" + r + g + b
+
+
class svgHandler(xml.sax.ContentHandler):
"""Parse SVG files and create FreeCAD objects."""
-
+
def __init__(self):
super().__init__()
"""Retrieve Draft parameters and initialize."""
self.style = params.get_param("svgstyle")
self.disableUnitScaling = params.get_param("svgDisableUnitScaling")
+ self.make_cuts = params.get_param("svgMakeCuts")
+ self.add_wire_for_invalid_face = params.get_param("svgAddWireForInvalidFace")
self.count = 0
self.transform = None
self.grouptransform = []
@@ -669,17 +471,21 @@ class svgHandler(xml.sax.ContentHandler):
self.svgdpi = 1.0
global Part
- import Part
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)
- self.lw = float(draftui.linewidth)
+ rf = float(draftui.facecolor.red() / 255.0)
+ gf = float(draftui.facecolor.green() / 255.0)
+ bf = float(draftui.facecolor.blue() / 255.0)
+ self.width_default = float(draftui.linewidth)
else:
- self.lw = float(params.get_param_view("DefaultShapeLineWidth"))
+ self.width_default = float(params.get_param_view("DefaultShapeLineWidth"))
r, g, b, _ = utils.get_rgba_tuple(params.get_param_view("DefaultShapeLineColor"))
- self.col = (r, g, b, 0.0)
+ rf, gf, bf, _ = utils.get_rgba_tuple(params.get_param_view("DefaultShapeColor"))
+ self.fill_default = (rf, gf, bf, 0.0)
+ self.color_default = (r, g, b, 0.0)
def format(self, obj):
"""Apply styles to the object if the graphical interface is up."""
@@ -691,6 +497,27 @@ class svgHandler(xml.sax.ContentHandler):
v.LineWidth = self.width
if self.fill:
v.ShapeColor = self.fill
+
+
+ def __addFaceToDoc(self, named_face):
+ """Create a named document object from a name/face tuple
+
+ Parameters
+ ----------
+ named_face : name : str, face : Part.Face
+ The Face/Wire to add, and its name
+ """
+ name, face = named_face
+ if not face:
+ return
+
+ face = self.applyTrans(face)
+ obj = self.doc.addObject("Part::Feature", name)
+ obj.Shape = face
+ self.format(obj)
+ if self.currentsymbol:
+ self.symbols[self.currentsymbol].append(obj)
+
def startElement(self, name, attrs):
"""Re-organize data into a nice clean dictionary.
@@ -704,6 +531,8 @@ class svgHandler(xml.sax.ContentHandler):
Dictionary of content of the elements
"""
self.count += 1
+ precision = svg_precision()
+
_msg('processing element {0}: {1}'.format(self.count, name))
_msg('existing group transform: {}'.format(self.grouptransform))
_msg('existing group style: {}'.format(self.groupstyles))
@@ -820,29 +649,27 @@ class svgHandler(xml.sax.ContentHandler):
else:
# nested svg element
unitmode = 'css' + str(self.svgdpi)
- vbw = getsize(data['viewBox'][2], 'discard')
- vbh = getsize(data['viewBox'][3], 'discard')
- abw = getsize(attrs.getValue('width'), unitmode)
- abh = getsize(attrs.getValue('height'), unitmode)
+ vbw = round(getsize(data['viewBox'][2], 'discard'),precision)
+ vbh = round(getsize(data['viewBox'][3], 'discard'), precision)
+ abw = round(getsize(attrs.getValue('width'), unitmode), precision)
+ abh = round(getsize(attrs.getValue('height'), unitmode), precision)
self.viewbox = (vbw, vbh)
sx = abw / vbw
sy = abh / vbh
- _data = data.get('preserveAspectRatio', [])
- preservearstr = ' '.join(_data).lower()
- uniformscaling = round(sx/sy, 5) == 1
- if uniformscaling:
+ preserve_ar = ' '.join(data.get('preserveAspectRatio', [])).lower()
+ if preserve_ar.startswith('none'):
m.scale(Vector(sx, sy, 1))
+ if sx != sy:
+ _wrn('Non-uniform scaling with probably degenerating '
+ + 'effects on Edges. ({} vs. {}).'.format(sx, sy))
+
else:
- _wrn('Scaling factors do not match!')
- if preservearstr.startswith('none'):
- m.scale(Vector(sx, sy, 1))
+ # preserve aspect ratio - svg default is 'x/y-mid meet'
+ if preserve_ar.endswith('slice'):
+ sxy = max(sx, sy)
else:
- # preserve the aspect ratio
- if preservearstr.endswith('slice'):
- sxy = max(sx, sy)
- else:
- sxy = min(sx, sy)
- m.scale(Vector(sxy, sxy, 1))
+ sxy = min(sx, sy)
+ m.scale(Vector(sxy, sxy, 1))
elif len(self.grouptransform) == 0:
# fallback to current dpi
m.scale(Vector(25.4/self.svgdpi, 25.4/self.svgdpi, 1))
@@ -867,20 +694,25 @@ class svgHandler(xml.sax.ContentHandler):
if name == "g":
self.grouptransform.append(FreeCAD.Matrix())
- if self.style == 1:
- self.color = self.col
- self.width = self.lw
+ if self.style == 0:
+ if self.fill is not None:
+ self.fill = self.fill_default
+ self.color = self.color_default
+ self.width = self.width_default
# apply group styles
if name == "g":
self.groupstyles.append([self.fill, self.color, self.width])
if self.fill is None:
- if "fill" not in data or data['fill'] != 'none':
+ if "fill" not in data:
# do not override fill if this item has specifically set a none fill
for groupstyle in reversed(self.groupstyles):
if groupstyle[0] is not None:
self.fill = groupstyle[0]
break
+ if self.fill is None:
+ # svg fill default is Black
+ self.fill = getcolor('Black')
if self.color is None:
for groupstyle in reversed(self.groupstyles):
if groupstyle[1] is not None:
@@ -899,18 +731,9 @@ class svgHandler(xml.sax.ContentHandler):
# Process paths
if name == "path":
- _msg('data: {}'.format(data))
-
if not pathname:
- pathname = 'Path'
-
- path = []
- point = []
- lastvec = Vector(0, 0, 0)
- lastpole = None
- # command = None
- relative = False
- firstvec = None
+ pathname = "Path"
+ _msg('data: {}'.format(data))
if "freecad:basepoint1" in data:
p1 = data["freecad:basepoint1"]
@@ -924,333 +747,17 @@ class svgHandler(xml.sax.ContentHandler):
self.format(obj)
self.lastdim = obj
data['d'] = []
-
- _op = '([mMlLhHvVaAcCqQsStTzZ])'
- _op2 = '([^mMlLhHvVaAcCqQsStTzZ]*)'
- _command = '\\s*?' + _op + '\\s*?' + _op2 + '\\s*?'
- pathcommandsre = re.compile(_command, re.DOTALL)
-
- _num = '[-+]?[0-9]*\\.?[0-9]+'
- _exp = '([eE][-+]?[0-9]+)?'
- _point = '(' + _num + _exp + ')'
- pointsre = re.compile(_point, re.DOTALL)
- _commands = pathcommandsre.findall(' '.join(data['d']))
- for d, pointsstr in _commands:
- relative = d.islower()
- _points = pointsre.findall(pointsstr.replace(',', ' '))
- pointlist = [float(number) for number, exponent in _points]
-
- if (d == "M" or d == "m"):
- x = pointlist.pop(0)
- y = pointlist.pop(0)
- if path:
- # sh = Part.Wire(path)
- sh = makewire(path)
- if self.fill and sh.isClosed():
- sh = Part.Face(sh)
- if sh.isValid() is False:
- sh.fix(1e-6, 0, 1)
- sh = self.applyTrans(sh)
- obj = self.doc.addObject("Part::Feature", pathname)
- obj.Shape = sh
- self.format(obj)
- if self.currentsymbol:
- self.symbols[self.currentsymbol].append(obj)
- path = []
- # if firstvec:
- # Move relative to last move command
- # not last draw command
- # lastvec = firstvec
- if relative:
- lastvec = lastvec.add(Vector(x, -y, 0))
- else:
- lastvec = Vector(x, -y, 0)
- firstvec = lastvec
- _msg('move {}'.format(lastvec))
- lastpole = None
-
- if (d == "L" or d == "l") \
- or ((d == 'm' or d == 'M') and pointlist):
- for x, y in zip(pointlist[0::2], pointlist[1::2]):
- if relative:
- currentvec = lastvec.add(Vector(x, -y, 0))
- else:
- currentvec = Vector(x, -y, 0)
- if not DraftVecUtils.equals(lastvec, currentvec):
- _seg = Part.LineSegment(lastvec, currentvec)
- seg = _seg.toShape()
- _msg("line {} {}".format(lastvec, currentvec))
- lastvec = currentvec
- path.append(seg)
- lastpole = None
- elif (d == "H" or d == "h"):
- for x in pointlist:
- if relative:
- currentvec = lastvec.add(Vector(x, 0, 0))
- else:
- currentvec = Vector(x, lastvec.y, 0)
- seg = Part.LineSegment(lastvec, currentvec).toShape()
- lastvec = currentvec
- lastpole = None
- path.append(seg)
- elif (d == "V" or d == "v"):
- for y in pointlist:
- if relative:
- currentvec = lastvec.add(Vector(0, -y, 0))
- else:
- currentvec = Vector(lastvec.x, -y, 0)
- if lastvec != currentvec:
- _seg = Part.LineSegment(lastvec, currentvec)
- seg = _seg.toShape()
- lastvec = currentvec
- lastpole = None
- path.append(seg)
- elif (d == "A" or d == "a"):
- piter = zip(pointlist[0::7], pointlist[1::7],
- pointlist[2::7], pointlist[3::7],
- pointlist[4::7], pointlist[5::7],
- pointlist[6::7])
- for (rx, ry, xrotation,
- largeflag, sweepflag,
- x, y) in piter:
- # support for large-arc and x-rotation is missing
- if relative:
- currentvec = lastvec.add(Vector(x, -y, 0))
- else:
- currentvec = Vector(x, -y, 0)
- chord = currentvec.sub(lastvec)
- # small circular arc
- _precision = 10**(-1*Draft.precision())
- if (not largeflag) and abs(rx - ry) < _precision:
- # perp = chord.cross(Vector(0, 0, -1))
- # here is a better way to find the perpendicular
- if sweepflag == 1:
- # clockwise
- perp = DraftVecUtils.rotate2D(chord,
- -math.pi/2)
- else:
- # anticlockwise
- perp = DraftVecUtils.rotate2D(chord, math.pi/2)
- chord.multiply(0.5)
- if chord.Length > rx:
- a = 0
- else:
- a = math.sqrt(rx**2 - chord.Length**2)
- s = rx - a
- perp.multiply(s/perp.Length)
- midpoint = lastvec.add(chord.add(perp))
- _seg = Part.Arc(lastvec, midpoint, currentvec)
- seg = _seg.toShape()
- # big arc or elliptical arc
- else:
- # Calculate the possible centers for an arc
- # in 'endpoint parameterization'.
- _xrot = math.radians(-xrotation)
- (solution,
- (rx, ry)) = arcend2center(lastvec,
- currentvec,
- rx, ry,
- xrotation=_xrot,
- correction=True)
- # Chose one of the two solutions
- negsol = (largeflag != sweepflag)
- vcenter, angle1, angledelta = solution[negsol]
- # print(angle1)
- # print(angledelta)
- if ry > rx:
- rx, ry = ry, rx
- swapaxis = True
- else:
- swapaxis = False
- # print('Elliptical arc %s rx=%f ry=%f'
- # % (vcenter, rx, ry))
- e1 = Part.Ellipse(vcenter, rx, ry)
- if sweepflag:
- # Step4
- # angledelta = -(-angledelta % (2*math.pi))
- # angledelta = (-angledelta % (2*math.pi))
- angle1 = angle1 + angledelta
- angledelta = -angledelta
- # angle1 = math.pi - angle1
-
- d90 = math.radians(90)
- e1a = Part.Arc(e1,
- angle1 - swapaxis * d90,
- angle1 + angledelta
- - swapaxis * d90)
- # e1a = Part.Arc(e1,
- # angle1 - 0 * swapaxis * d90,
- # angle1 + angledelta
- # - 0 * swapaxis * d90)
- seg = e1a.toShape()
- if swapaxis:
- seg.rotate(vcenter, Vector(0, 0, 1), 90)
- _precision = 10**(-1*Draft.precision())
- if abs(xrotation) > _precision:
- seg.rotate(vcenter, Vector(0, 0, 1), -xrotation)
- if sweepflag:
- seg.reverse()
- # DEBUG
- # obj = self.doc.addObject("Part::Feature",
- # 'DEBUG %s' % pathname)
- # obj.Shape = seg
- # _seg = Part.LineSegment(lastvec, currentvec)
- # seg = _seg.toShape()
- lastvec = currentvec
- lastpole = None
- path.append(seg)
- elif (d == "C" or d == "c") or (d == "S" or d == "s"):
- smooth = (d == 'S' or d == 's')
- if smooth:
- piter = list(zip(pointlist[2::4],
- pointlist[3::4],
- pointlist[0::4],
- pointlist[1::4],
- pointlist[2::4],
- pointlist[3::4]))
- else:
- piter = list(zip(pointlist[0::6],
- pointlist[1::6],
- pointlist[2::6],
- pointlist[3::6],
- pointlist[4::6],
- pointlist[5::6]))
- for p1x, p1y, p2x, p2y, x, y in piter:
- if smooth:
- if lastpole is not None and lastpole[0] == 'cubic':
- pole1 = lastvec.sub(lastpole[1]).add(lastvec)
- else:
- pole1 = lastvec
- else:
- if relative:
- pole1 = lastvec.add(Vector(p1x, -p1y, 0))
- else:
- pole1 = Vector(p1x, -p1y, 0)
- if relative:
- currentvec = lastvec.add(Vector(x, -y, 0))
- pole2 = lastvec.add(Vector(p2x, -p2y, 0))
- else:
- currentvec = Vector(x, -y, 0)
- pole2 = Vector(p2x, -p2y, 0)
-
- if not DraftVecUtils.equals(currentvec, lastvec):
- # mainv = currentvec.sub(lastvec)
- # pole1v = lastvec.add(pole1)
- # pole2v = currentvec.add(pole2)
- # print("cubic curve data:",
- # mainv.normalize(),
- # pole1v.normalize(),
- # pole2v.normalize())
- _precision = 10**(-1*(2+Draft.precision()))
- _d1 = pole1.distanceToLine(lastvec, currentvec)
- _d2 = pole2.distanceToLine(lastvec, currentvec)
- if True and \
- _d1 < _precision and \
- _d2 < _precision:
- # print("straight segment")
- _seg = Part.LineSegment(lastvec, currentvec)
- seg = _seg.toShape()
- else:
- # print("cubic bezier segment")
- b = Part.BezierCurve()
- b.setPoles([lastvec, pole1, pole2, currentvec])
- seg = b.toShape()
- # print("connect ", lastvec, currentvec)
- lastvec = currentvec
- lastpole = ('cubic', pole2)
- path.append(seg)
- elif (d == "Q" or d == "q") or (d == "T" or d == "t"):
- smooth = (d == 'T' or d == 't')
- if smooth:
- piter = list(zip(pointlist[1::2],
- pointlist[1::2],
- pointlist[0::2],
- pointlist[1::2]))
- else:
- piter = list(zip(pointlist[0::4],
- pointlist[1::4],
- pointlist[2::4],
- pointlist[3::4]))
- for px, py, x, y in piter:
- if smooth:
- if (lastpole is not None
- and lastpole[0] == 'quadratic'):
- pole = lastvec.sub(lastpole[1]).add(lastvec)
- else:
- pole = lastvec
- else:
- if relative:
- pole = lastvec.add(Vector(px, -py, 0))
- else:
- pole = Vector(px, -py, 0)
- if relative:
- currentvec = lastvec.add(Vector(x, -y, 0))
- else:
- currentvec = Vector(x, -y, 0)
-
- if not DraftVecUtils.equals(currentvec, lastvec):
- _precision = 20**(-1*(2+Draft.precision()))
- _distance = pole.distanceToLine(lastvec,
- currentvec)
- if True and \
- _distance < _precision:
- # print("straight segment")
- _seg = Part.LineSegment(lastvec, currentvec)
- seg = _seg.toShape()
- else:
- # print("quadratic bezier segment")
- b = Part.BezierCurve()
- b.setPoles([lastvec, pole, currentvec])
- seg = b.toShape()
- # print("connect ", lastvec, currentvec)
- lastvec = currentvec
- lastpole = ('quadratic', pole)
- path.append(seg)
- elif (d == "Z") or (d == "z"):
- if not DraftVecUtils.equals(lastvec, firstvec):
- try:
- seg = Part.LineSegment(lastvec, firstvec).toShape()
- except Part.OCCError:
- pass
- else:
- path.append(seg)
- if path:
- # The path should be closed by now
- # sh = makewire(path, True)
- sh = makewire(path, donttry=False)
- if self.fill \
- and len(sh.Wires) == 1 \
- and sh.Wires[0].isClosed():
- sh = Part.Face(sh)
- if sh.isValid() is False:
- sh.fix(1e-6, 0, 1)
- sh = self.applyTrans(sh)
- obj = self.doc.addObject("Part::Feature", pathname)
- obj.Shape = sh
- self.format(obj)
- path = []
- if firstvec:
- # Move relative to recent draw command
- lastvec = firstvec
- point = []
- # command = None
- if self.currentsymbol:
- self.symbols[self.currentsymbol].append(obj)
- if path:
- sh = makewire(path, checkclosed=False)
- # sh = Part.Wire(path)
- if self.fill and sh.isClosed():
- sh = Part.Face(sh)
- if sh.isValid() is False:
- sh.fix(1e-6, 0, 1)
- sh = self.applyTrans(sh)
- obj = self.doc.addObject("Part::Feature", pathname)
- obj.Shape = sh
- self.format(obj)
- if self.currentsymbol:
- self.symbols[self.currentsymbol].append(obj)
- # end process paths
-
+
+ if "d" in data:
+ svgPath = SvgPathParser(data, pathname)
+ svgPath.parse()
+ svgPath.create_faces(self.fill, self.add_wire_for_invalid_face)
+ if self.make_cuts:
+ svgPath.doCuts()
+ shapes = svgPath.getShapeList()
+ for named_shape in shapes:
+ self.__addFaceToDoc(named_shape)
+
# Process rects
if name == "rect":
if not pathname:
@@ -1261,7 +768,7 @@ class svgHandler(xml.sax.ContentHandler):
if "y" not in data:
data["y"] = 0
# Negative values are invalid
- _precision = 10**(-1*Draft.precision())
+ _precision = 10**(-precision)
if ('rx' not in data or data['rx'] < _precision) \
and ('ry' not in data or data['ry'] < _precision):
# if True:
@@ -1333,7 +840,7 @@ class svgHandler(xml.sax.ContentHandler):
for esh1, esh2 in zip(esh[-1:] + esh[:-1], esh):
p1 = esh1.Vertexes[-1].Point
p2 = esh2.Vertexes[0].Point
- if not DraftVecUtils.equals(p1, p2):
+ if not equals(p1, p2, precision):
# straight segments
_sh = Part.LineSegment(p1, p2).toShape()
edges.append(_sh)
@@ -1376,7 +883,6 @@ class svgHandler(xml.sax.ContentHandler):
if not pathname:
pathname = 'Polyline'
points = [float(d) for d in data['points']]
- _msg('points {}'.format(points))
lenpoints = len(points)
if lenpoints >= 4 and lenpoints % 2 == 0:
lastvec = Vector(points[0], -points[1], 0)
@@ -1385,7 +891,7 @@ class svgHandler(xml.sax.ContentHandler):
points = points + points[:2] # emulate closepath
for svgx, svgy in zip(points[2::2], points[3::2]):
currentvec = Vector(svgx, -svgy, 0)
- if not DraftVecUtils.equals(lastvec, currentvec):
+ if not equals(lastvec, currentvec, precision):
seg = Part.LineSegment(lastvec, currentvec).toShape()
# print("polyline seg ", lastvec, currentvec)
lastvec = currentvec
@@ -1555,28 +1061,19 @@ class svgHandler(xml.sax.ContentHandler):
sh : Part.Shape or Draft.Dimension
Object to be transformed
"""
- if isinstance(sh, Part.Shape):
+ if isinstance(sh, Part.Shape) or isinstance(sh, Part.Wire):
if self.transform:
- _msg("applying object transform: {}".format(self.transform))
- # sh = transformCopyShape(sh, self.transform)
- # see issue #2062
- sh = sh.transformGeometry(self.transform)
+ sh = transformCopyShape(sh, self.transform)
for transform in self.grouptransform[::-1]:
- _msg("applying group transform: {}".format(transform))
- # sh = transformCopyShape(sh, transform)
- # see issue #2062
- sh = sh.transformGeometry(transform)
+ sh = transformCopyShape(sh, transform)
return sh
elif Draft.getType(sh) in ["Dimension","LinearDimension"]:
pts = []
for p in [sh.Start, sh.End, sh.Dimline]:
cp = Vector(p)
if self.transform:
- _msg("applying object transform: "
- "{}".format(self.transform))
cp = self.transform.multiply(cp)
for transform in self.grouptransform[::-1]:
- _msg("applying group transform: {}".format(transform))
cp = transform.multiply(cp)
pts.append(cp)
sh.Start = pts[0]
@@ -1821,7 +1318,6 @@ def export(exportList, filename):
if hidden_doc is None:
hidden_doc = FreeCAD.newDocument(name="hidden", hidden=True, temp=True)
base_sketch_pla = obj.Placement
- import Part
sh = Part.Compound()
sh.Placement = base_sketch_pla
sh.add(obj.Shape.copy())