freecad-cam/Mod/Draft/draftfunctions/svgshapes.py
2026-02-01 01:59:24 +01:00

461 lines
16 KiB
Python

# -*- coding: utf8 -*-
# ***************************************************************************
# * Copyright (c) 2009 Yorik van Havre <yorik@uncreated.net> *
# * Copyright (c) 2018 George Shuklin (amarao) *
# * Copyright (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
# * *
# * 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 = '<circle '
svg += 'cx="{}" cy="{}" r="{}" '.format(cen.x, cen.y, rad)
else:
# Any other projection: ellipse
svg = '<path d="{}" '.format(get_discretized(edge, plane))
svg += 'stroke="{}" '.format(stroke)
# Editor: why is stroke-width repeated? Is this really necessary
# for the generated SVG?
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) + '"'
svg += '/>\n'
return svg
def getCircle(plane,
fill, stroke, linewidth, lstyle,
edge):
"""Get the SVG representation from a circular edge."""
utils.use_instead("get_circle")
return get_circle(plane, fill, stroke, linewidth, lstyle, edge)
def get_ellipse(plane,
fill, stroke, linewidth, lstyle,
edge):
"""Get the SVG representation from an elliptical edge."""
cen = get_proj(edge.Curve.Center, plane)
mir = edge.Curve.MinorRadius
mar = edge.Curve.MajorRadius
svg = '<ellipse '
svg += 'cx="{}" cy="{}" '.format(cen.x, cen.y)
svg += 'rx="{}" ry="{}" '.format(mar, mir)
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) + '"'
svg += '/>\n'
return svg
def getEllipse(plane,
fill, stroke, linewidth, lstyle,
edge):
"""Get the SVG representation from an elliptical edge. DEPRECATED."""
utils.use_instead("get_ellipse")
return get_ellipse(plane, fill, stroke, linewidth, lstyle, edge)
def get_path(obj, plane,
fill, pathdata, stroke, linewidth, lstyle,
fill_opacity=None,
edges=[], wires=[], pathname=None):
"""Get the SVG representation from an object's edges or wires.
TODO: the `edges` and `wires` must not default to empty list `[]`
but to `None`. Verify that the code doesn't break with this change.
`edges` and `wires` are mutually exclusive. If no `wires` are provided,
sort the `edges`, and use them. If `wires` are provided, sort the edges
in these `wires`, and use them.
"""
svg = "<path "
if pathname is None:
svg += 'id="{}" '.format(obj.Name)
elif pathname != "":
svg += 'id="{}" '.format(pathname)
svg += ' d="'
if not wires:
egroups = Part.sortEdges(edges)
else:
egroups = []
first = True
for w in wires:
wire = w.copy()
if first:
first = False
else:
# invert further wires to create holes
wire = DraftGeomUtils.invert(wire)
wire.fixWire()
egroups.append(Part.__sortEdges__(wire.Edges))
for _edges in egroups:
edata = ""
for edgeindex, edge in enumerate(_edges):
if edgeindex == 0:
verts = edge.Vertexes
if len(_edges) > 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)
## @}