# SPDX-License-Identifier: LGPL-2.1-or-later # -*- coding: utf8 -*- # *************************************************************************** # * Copyright (c) 2009 Yorik van Havre * # * Copyright (c) 2018 George Shuklin (amarao) * # * Copyright (c) 2020 Eliud Cabrera Castillo * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** """Provides functions to return the SVG representation of some shapes.""" ## @package svgshapes # \ingroup draftfunctions # \brief Provides functions to return the SVG representation of some shapes. import math import lazy_loader.lazy_loader as lz import FreeCAD as App import DraftVecUtils import WorkingPlane from draftutils import params from draftutils import utils from draftutils.messages import _msg, _wrn # Delay import of module until first use because it is heavy Part = lz.LazyLoader("Part", globals(), "Part") DraftGeomUtils = lz.LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils") TechDraw = lz.LazyLoader("TechDraw", globals(), "TechDraw") ## \addtogroup draftfunctions # @{ def get_proj(vec, plane=None): """Get a projection of the vector in the plane's u and v directions. TODO: check if the same function for SVG and DXF projection can be used so that this function is not just duplicated code. This function may also be present elsewhere, like `WorkingPlane` or `DraftGeomUtils`, so we should avoid code duplication. Parameters ---------- vec: Base::Vector3 An arbitrary vector that will be projected on the U and V directions. plane: WorkingPlane.PlaneBase Working plane. """ if not plane: return vec nx = DraftVecUtils.project(vec, plane.u) lx = nx.Length if abs(nx.getAngle(plane.u)) > 0.1: lx = -lx ny = DraftVecUtils.project(vec, plane.v) ly = ny.Length if abs(ny.getAngle(plane.v)) > 0.1: ly = -ly # if techdraw: buggy - we now simply do it at the end # ly = -ly return App.Vector(lx, ly, 0) def getProj(vec, plane=None): """Get a projection of a vector. DEPRECATED.""" utils.use_instead("get_proj") return get_proj(vec, plane) def get_discretized(edge, plane): """Get a discretized edge on a plane.""" pieces = params.get_param("svgDiscretization") if pieces == 0: pieces = 10 d = int(edge.Length / pieces) if d == 0: d = 1 edata = "" for i in range(d + 1): _length = edge.LastParameter - edge.FirstParameter _point = edge.FirstParameter + float(i) / d * _length _vec = edge.valueAt(_point) v = get_proj(_vec, plane) if not edata: edata += "M " + str(v.x) + " " + str(v.y) + " " else: edata += "L " + str(v.x) + " " + str(v.y) + " " return edata def getDiscretized(edge, plane): """Get a discretized edge on a plane. DEPRECATED.""" utils.use_instead("get_discretized") return get_discretized(edge, plane) def _get_path_circ_ellipse( plane, edge, verts, edata, iscircle, isellipse, fill, stroke, linewidth, lstyle ): """Get the edge data from a path that is a circle or ellipse.""" if plane: drawing_plane_normal = plane.axis else: drawing_plane_normal = WorkingPlane.get_working_plane(update=False).axis center = edge.Curve ax = center.Axis # The angle between the curve axis and the plane is not 0 nor 180 degrees _angle = math.degrees(ax.getAngle(drawing_plane_normal)) if round(_angle, 2) not in (0, 180): edata += get_discretized(edge, plane) return "edata", edata # The angle is 0 or 180, coplanar occversion = Part.OCC_VERSION.split(".") done = False if int(occversion[0]) >= 7 and int(occversion[1]) >= 1: # if using occ >= 7.1, use HLR algorithm snip = TechDraw.projectToSVG(edge, drawing_plane_normal) if snip: try: _a = snip.split('path d="')[1] _a = _a.split('"')[0] _a = _a.split("A")[1] A = "A " + _a except IndexError: pass # TODO: trap only specific exception. # Check the problem. Split didn't produce a two element list? # _wrn("Circle or ellipse: " # "cannot split the projection snip " # "obtained by 'projectToSVG', " # "continue manually.") else: edata += A done = True if not done: if len(edge.Vertexes) == 1 and iscircle: # Complete circle not only arc svg = get_circle(plane, fill, stroke, linewidth, lstyle, edge) # If it's a circle we will return the final SVG string, # otherwise it will process the `edata` further return "svg", svg elif len(edge.Vertexes) == 1 and isellipse: # Complete ellipse not only arc # svg = get_ellipse(plane, # fill, stroke, linewidth, # lstyle, edge) # return svg # Difference in angles _diff = (center.LastParameter - center.FirstParameter) / 2.0 endpoints = [get_proj(center.value(_diff), plane), get_proj(verts[-1].Point, plane)] else: endpoints = [get_proj(verts[-1].Point, plane)] # Arc with more than one vertex if iscircle: rx = ry = center.Radius rot = 0 else: # ellipse rx = center.MajorRadius ry = center.MinorRadius _rot = center.AngleXU * center.Axis * App.Vector(0, 0, 1) rot = math.degrees(_rot) if rot > 90: rot -= 180 if rot < -90: rot += 180 # Be careful with the sweep flag _diff = edge.ParameterRange[1] - edge.ParameterRange[0] _diff = _diff / math.pi flag_large_arc = (_diff % 2) > 1 # flag_sweep = (center.Axis * drawing_plane_normal >= 0) \ # == (edge.LastParameter > edge.FirstParameter) # == (edge.Orientation == "Forward") # Another method: check the direction of the angle # between tangents _diff = edge.LastParameter - edge.FirstParameter t1 = edge.tangentAt(edge.FirstParameter) t2 = edge.tangentAt(edge.FirstParameter + _diff / 10) flag_sweep = DraftVecUtils.angle(t1, t2, drawing_plane_normal) < 0 for v in endpoints: edata += ( "A {} {} {} " "{} {} " "{} {} ".format(rx, ry, rot, int(flag_large_arc), int(flag_sweep), v.x, v.y) ) return "edata", edata def _get_path_bspline(plane, edge, edata): """Convert the edge to a BSpline and discretize it.""" bspline = edge.Curve.toBSpline(edge.FirstParameter, edge.LastParameter) if bspline.Degree > 3 or bspline.isRational(): try: bspline = bspline.approximateBSpline(0.05, 50, 3, "C0") except RuntimeError: _wrn("Debug: unable to approximate bspline from edge") if bspline.Degree <= 3 and not bspline.isRational(): for bezierseg in bspline.toBezier(): if bezierseg.Degree > 3: # should not happen _wrn("Bezier segment of degree > 3") raise AssertionError elif bezierseg.Degree == 1: edata += "L " elif bezierseg.Degree == 2: edata += "Q " elif bezierseg.Degree == 3: edata += "C " for pole in bezierseg.getPoles()[1:]: v = get_proj(pole, plane) edata += "{} {} ".format(v.x, v.y) else: _msg( "Debug: one edge (hash {}) " "has been discretized " "with parameter 0.1".format(edge.hashCode()) ) for linepoint in bspline.discretize(0.1)[1:]: v = get_proj(linepoint, plane) edata += "L {} {} ".format(v.x, v.y) return edata def get_circle(plane, fill, stroke, linewidth, lstyle, edge): """Get the SVG representation from a circular edge.""" cen = get_proj(edge.Curve.Center, plane) rad = edge.Curve.Radius if plane: drawing_plane_normal = plane.axis else: drawing_plane_normal = WorkingPlane.get_working_plane(update=False).axis if round(edge.Curve.Axis.getAngle(drawing_plane_normal), 2) in [0, 3.14]: # Perpendicular projection: circle svg = " 1: last_pt = verts[-1].Point nextverts = _edges[1].Vertexes if (last_pt - nextverts[0].Point).Length > 1e-6 and ( last_pt - nextverts[-1].Point ).Length > 1e-6: verts.reverse() v = get_proj(verts[0].Point, plane) edata += "M {} {} ".format(v.x, v.y) else: previousverts = verts verts = edge.Vertexes if (verts[0].Point - previousverts[-1].Point).Length > 1e-6: verts.reverse() if (verts[0].Point - previousverts[-1].Point).Length > 1e-6: raise ValueError("edges not ordered") iscircle = DraftGeomUtils.geomType(edge) == "Circle" isellipse = DraftGeomUtils.geomType(edge) == "Ellipse" if iscircle or isellipse: _type, data = _get_path_circ_ellipse( plane, edge, verts, edata, iscircle, isellipse, fill, stroke, linewidth, lstyle ) if _type == "svg": # final svg string already calculated, so just return it return data # else the `edata` was properly augmented, so re-assing it edata = data elif DraftGeomUtils.geomType(edge) == "Line": v = get_proj(verts[-1].Point, plane) edata += "L {} {} ".format(v.x, v.y) else: # If it's not a circle nor ellipse nor straight line # convert the curve to BSpline edata = _get_path_bspline(plane, edge, edata) if fill != "none": edata += "Z " if edata in pathdata: # do not draw a path on another identical path return "" else: svg += edata pathdata.append(edata) svg += '" ' svg += 'stroke="{}" '.format(stroke) svg += 'stroke-width="{} px" '.format(linewidth) svg += 'style="' svg += "stroke-width:{};".format(linewidth) svg += "stroke-miterlimit:4;" svg += "stroke-dasharray:{};".format(lstyle) svg += "stroke-linecap:square;" svg += "fill:{};".format(fill) # fill_opacity must be a number, but if it's `None` it is omitted if fill_opacity is not None: svg += "fill-opacity:{};".format(fill_opacity) svg += 'fill-rule: evenodd"' svg += "/>\n" return svg def getPath( obj, plane, fill, pathdata, stroke, linewidth, lstyle, fill_opacity, edges=[], wires=[], pathname=None, ): """Get the SVG representation from a path. DEPRECATED.""" utils.use_instead("get_path") return get_path( obj, plane, fill, pathdata, stroke, linewidth, lstyle, fill_opacity, edges=edges, wires=wires, pathname=pathname, ) ## @}