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())