Download src/Mod/Draft/SVGPath.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 28.9 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Draft/SVGPath.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/Draft/SVGPath.py
-
curl -L -o SVGPath.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/Draft/SVGPath.py
28.9 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| 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 endpoints are all on a line | |
| if equals(last_v, next_v, self.precision): | |
| # in this case we don't accept (nearly) zero | |
| # distance between 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 | |