2254 lines
103 KiB
Python
2254 lines
103 KiB
Python
|
#!/usr/bin/env python3
|
||
|
# coding=utf-8
|
||
|
# HatchFill.py
|
||
|
#
|
||
|
# Generate hatch fills for the closed paths (polygons) in the currently
|
||
|
# selected document elements. If no elements are selected, then all the
|
||
|
# polygons throughout the document are hatched. The fill rule is an odd/even
|
||
|
# rule: odd numbered intersections (1, 3, 5, etc.) are a hatch line entering
|
||
|
# a polygon while even numbered intersections (2, 4, 6, etc.) are the same
|
||
|
# hatch line exiting the polygon.
|
||
|
#
|
||
|
# This extension first decomposes the selected <path>, <rect>, <line>,
|
||
|
# <polyline>, <polygon>, <circle>, and <ellipse> elements into individual
|
||
|
# moveto and lineto coordinates using the same procedure that eggbot.py uses
|
||
|
# for plotting. These coordinates are then used to build vertex lists.
|
||
|
# Only the vertex lists corresponding to polygons (closed paths) are
|
||
|
# kept. Note that a single graphical element may be composed of several
|
||
|
# subpaths, each subpath potentially a polygon.
|
||
|
#
|
||
|
# Once the lists of all the vertices are built, potential hatch lines are
|
||
|
# "projected" through the bounding box containing all of the vertices.
|
||
|
# For each potential hatch line, all intersections with all the polygon
|
||
|
# edges are determined. These intersections are stored as decimal fractions
|
||
|
# indicating where along the length of the hatch line the intersection
|
||
|
# occurs. These values will always be in the range [0, 1]. A value of 0
|
||
|
# indicates that the intersection is at the start of the hatch line, a value
|
||
|
# of 0.5 midway, and a value of 1 at the end of the hatch line.
|
||
|
#
|
||
|
# For a given hatch line, all the fractional values are sorted and any
|
||
|
# duplicates removed. Duplicates occur, for instance, when the hatch
|
||
|
# line passes through a polygon vertex and thus intersects two edges
|
||
|
# segments of the polygon: the end of one edge and the start of
|
||
|
# another.
|
||
|
#
|
||
|
# Once sorted and duplicates removed, an odd/even rule is applied to
|
||
|
# determine which segments of the potential hatch line are within
|
||
|
# polygons. These segments found to be within polygons are then saved
|
||
|
# and become the hatch fill lines which will be drawn.
|
||
|
#
|
||
|
# With each saved hatch fill line, information about which SVG graphical
|
||
|
# element it is within is saved. This way, the hatch fill lines can
|
||
|
# later be grouped with the element they are associated with. This makes
|
||
|
# it possible to manipulate the two -- graphical element and hatch lines --
|
||
|
# as a single object within Inkscape.
|
||
|
#
|
||
|
# Note: we also save the transformation matrix for each graphical element.
|
||
|
# That way, when we group the hatch fills with the element they are
|
||
|
# filling, we can invert the transformation. That is, in order to compute
|
||
|
# the hatch fills, we first have had apply ALL applicable transforms to
|
||
|
# all the graphical elements. We need to do that so that we know where in
|
||
|
# the drawing each of the graphical elements are relative to one another.
|
||
|
# However, this means that the hatch lines have been computed in a setting
|
||
|
# where no further transforms are needed. If we then put these hatch lines
|
||
|
# into the same groups as the elements being hatched in the ORIGINAL
|
||
|
# drawing, then the hatch lines will have transforms applied again. So,
|
||
|
# once we compute the hatch lines, we need to invert the transforms of
|
||
|
# the group they will be placed in and apply this inverse transform to the
|
||
|
# hatch lines. Hence the need to save the transform matrix for every
|
||
|
# graphical element.
|
||
|
# Written by Daniel C. Newman for the Eggbot Project
|
||
|
# dan dot newman at mtbaldy dot us
|
||
|
# Updated by Windell H. Oskay, 6/14/2012
|
||
|
# Added tolerance parameter
|
||
|
# Update by Daniel C. Newman, 6/20/2012
|
||
|
# Add min span/gap width
|
||
|
# Updated by Windell H. Oskay, 1/8/2016
|
||
|
# Added live preview and correct issue with nonzero min gap
|
||
|
# https://github.com/evil-mad/EggBot/issues/32
|
||
|
# Updated by Sheldon B. Michaels, 1/11/2016 thru 3/15/2016
|
||
|
# shel at shel dot net
|
||
|
# Added feature: Option to inset the hatch segments from boundaries
|
||
|
# Added feature: Option to join hatch segments that are "nearby", to minimize pen lifts
|
||
|
# The joins are made using cubic Bezier segments.
|
||
|
# https://github.com/evil-mad/EggBot/issues/36
|
||
|
# Updated by Nathan Depew, 12/6/2017
|
||
|
# Modified hatch fill to create hatches as a relevant object it found on the SVG tree
|
||
|
# This prevents extremely complex plots from generating glitches
|
||
|
# Modifications are limited to recursivelyTraverseSvg and effect methods
|
||
|
#
|
||
|
# Forked July 2020
|
||
|
# Updated for Inkscape v1.0
|
||
|
# Updated code to remove deprecation warnings
|
||
|
# Added script to format xml & Python.
|
||
|
# Not tested on Python 2.
|
||
|
# Current software version:
|
||
|
# (v0.9.0b, July 2020) # forked from Evil-Mad EggBot (v2.3.2, September 29, 2019)
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License as published by
|
||
|
# the Free Software Foundation; either version 2 of the License, or
|
||
|
# (at your option) any later version.
|
||
|
# 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 General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU 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
|
||
|
import math
|
||
|
from lxml import etree
|
||
|
import inkex
|
||
|
from inkex import Transform
|
||
|
from inkex.paths import Path, CubicSuperPath
|
||
|
|
||
|
N_PAGE_WIDTH = 3200
|
||
|
N_PAGE_HEIGHT = 800
|
||
|
|
||
|
F_MINGAP_SMALL_VALUE = 0.0000000001
|
||
|
# Was 0.00001 in the original version which did not have joined lines.
|
||
|
# Reducing this by a factor of 10^5 decreased probability of occurrence of
|
||
|
# the bug in the original, which got confused when the path barely
|
||
|
# grazed a corner.
|
||
|
|
||
|
BEZIER_OVERSHOOT_MULTIPLIER = 0.75 # evaluation of cubic Bezier curve equation value,
|
||
|
# at x = 0, with
|
||
|
# endpoints at ( -0.5, 0 ), ( +0.5, 0 )
|
||
|
# and control points at ( -0.5, 1.0 ), ( +0.5, 1.0 )
|
||
|
|
||
|
RADIAN_TOLERANCE_FOR_COLINEAR = 0.1
|
||
|
# Pragmatically adjusted to allow adjacent segments from the same scan line, even short ones,
|
||
|
# to be classified as having the same angle
|
||
|
|
||
|
RADIAN_TOLERANCE_FOR_ALTERNATING_DIRECTION = 0.1
|
||
|
# Pragmatic adjustment again, as with colinearity tolerance
|
||
|
|
||
|
RECURSION_LIMIT = 500
|
||
|
# Pragmatic - if too high, risk runtime python error;
|
||
|
# if too low, miss some chances for reducing pen lifts
|
||
|
|
||
|
EXTREME_POS = 1.0e70 # Extremely large positive number
|
||
|
EXTREME_NEG = -1.0e70 # Extremely large negative number
|
||
|
|
||
|
MIN_HATCH_FRACTION = 0.25
|
||
|
# Minimum hatch length, as a fraction of the hatch spacing.
|
||
|
|
||
|
"""
|
||
|
Geometry 101: Determining if two lines intersect
|
||
|
|
||
|
A line L is defined by two points in space P1 and P2. Any point P on the
|
||
|
line L satisfies
|
||
|
|
||
|
P = P1 + s (P2 - P1)
|
||
|
|
||
|
for some value of the real number s in the range (-infinity, infinity).
|
||
|
If we confine s to the range [0, 1] then we've described the line segment
|
||
|
with end points P1 and P2.
|
||
|
|
||
|
Consider now the line La defined by the points P1 and P2, and the line Lb
|
||
|
defined by the points P3 and P4. Any points Pa and Pb on the lines La and
|
||
|
Lb therefore satisfy
|
||
|
|
||
|
Pa = P1 + sa (P2 - P1)
|
||
|
Pb = P3 + sb (P4 - P3)
|
||
|
|
||
|
for some values of the real numbers sa and sb. To see if these two lines
|
||
|
La and Lb intersect, we wish to see if there are finite values sa and sb
|
||
|
for which
|
||
|
|
||
|
Pa = Pb
|
||
|
|
||
|
Or, equivalently, we ask if there exists values of sa and sb for which
|
||
|
the equation
|
||
|
|
||
|
P1 + sa (P2 - P1) = P3 + sb (P4 - P3)
|
||
|
|
||
|
holds. If we confine ourselves to a two-dimensional plane, and take
|
||
|
|
||
|
P1 = (x1, y1)
|
||
|
P2 = (x2, y2)
|
||
|
P3 = (x3, y3)
|
||
|
P4 = (x4, y4)
|
||
|
|
||
|
we then find that we have two equations in two unknowns, sa and sb,
|
||
|
|
||
|
x1 + sa ( x2 - x1 ) = x3 + sb ( x4 - x3 )
|
||
|
y1 + sa ( y2 - y1 ) = y3 + sb ( y4 - y3 )
|
||
|
|
||
|
Solving these two equations for sa and sb yields,
|
||
|
|
||
|
sa = [ ( y1 - y3 ) ( x4 - x3 ) - ( y4 - y3 ) ( x1 - x3 ) ] / d
|
||
|
sb = [ ( y1 - y3 ) ( x2 - x1 ) - ( y2 - y1 ) ( x1 - x3 ) ] / d
|
||
|
|
||
|
where the denominator, d, is given by
|
||
|
|
||
|
d = ( y4 - y3 ) ( x2 - x1 ) - ( y2 - y1 ) ( x4 - x3 )
|
||
|
|
||
|
Substituting these back for the point (x, y) of intersection gives
|
||
|
|
||
|
x = x1 + sa ( x2 - x1 )
|
||
|
y = y1 + sa ( y2 - y1 )
|
||
|
|
||
|
Note that
|
||
|
|
||
|
1. The lines are parallel when d = 0
|
||
|
2. The lines are coincident d = 0 and the numerators for sa & sb are zero
|
||
|
3. For line segments, sa and sb are in the range [0, 1]; any value outside
|
||
|
that range indicates that the line segments do not intersect.
|
||
|
"""
|
||
|
|
||
|
|
||
|
def intersect(p1, p2, p3, p4):
|
||
|
"""
|
||
|
Determine if two line segments defined by the four points p1 & p2 and
|
||
|
p3 & p4 intersect. If they do intersect, then return the fractional
|
||
|
point of intersection "sa" along the first line at which the
|
||
|
intersection occurs.
|
||
|
"""
|
||
|
|
||
|
# Precompute these values -- note that we're basically shifting from
|
||
|
#
|
||
|
# p = p1 + s (p2 - p1)
|
||
|
#
|
||
|
# to
|
||
|
#
|
||
|
# p = p1 + s d
|
||
|
#
|
||
|
# where D is a direction vector. The solution remains the same of
|
||
|
# course. We'll just be computing D once for each line rather than
|
||
|
# computing it a couple of times.
|
||
|
|
||
|
d21x = p2[0] - p1[0]
|
||
|
d21y = p2[1] - p1[1]
|
||
|
d43x = p4[0] - p3[0]
|
||
|
d43y = p4[1] - p3[1]
|
||
|
|
||
|
# Denominator
|
||
|
d = d21x * d43y - d21y * d43x
|
||
|
|
||
|
# Return now if the denominator is zero
|
||
|
if d == 0:
|
||
|
return -1.0
|
||
|
|
||
|
# For our purposes, the first line segment given
|
||
|
# by p1 & p2 is the LONG hatch line running through
|
||
|
# the entire drawing. And, p3 & p4 describe the
|
||
|
# usually much shorter line segment from a polygon.
|
||
|
# As such, we compute sb first as it's more likely
|
||
|
# to indicate "no intersection". That is, sa is
|
||
|
# more likely to indicate an intersection with a
|
||
|
# much a long line containing p3 & p4.
|
||
|
|
||
|
nb = (p1[1] - p3[1]) * d21x - (p1[0] - p3[0]) * d21y
|
||
|
|
||
|
# Could first check if abs(nb) > abs(d) or if
|
||
|
# the signs differ.
|
||
|
sb = float(nb) / float(d)
|
||
|
if sb < 0 or sb > 1:
|
||
|
return -1.0
|
||
|
|
||
|
na = (p1[1] - p3[1]) * d43x - (p1[0] - p3[0]) * d43y
|
||
|
sa = float(na) / float(d)
|
||
|
if sa < 0 or sa > 1:
|
||
|
return -1.0
|
||
|
|
||
|
return sa
|
||
|
|
||
|
|
||
|
def interstices(self, p1, p2, paths, hatches, b_hold_back_hatches, f_hold_back_steps):
|
||
|
"""
|
||
|
For the line L defined by the points p1 & p2, determine the segments
|
||
|
of L which lie within the polygons described by the paths stored in
|
||
|
"paths"
|
||
|
|
||
|
p1 -- (x,y) coordinate [list]
|
||
|
p2 -- (x,y) coordinate [list]
|
||
|
paths -- Dictionary of all the paths to check for intersections
|
||
|
|
||
|
When an intersection of the line L is found with a polygon edge, then
|
||
|
the fractional distance along the line L is saved along with the
|
||
|
lxml.etree node which contained the intersecting polygon edge. This
|
||
|
fractional distance is always in the range [0, 1].
|
||
|
|
||
|
Once all polygons have been checked, the list of fractional distances
|
||
|
corresponding to intersections is sorted and any duplicates removed.
|
||
|
It is then assumed that the first intersection is the line L entering
|
||
|
a polygon; the second intersection the line leaving the polygon. This
|
||
|
line segment defined by the first and second intersection points is
|
||
|
thus a hatch fill line we sought to generate. In general, our hatch
|
||
|
fills become the line segments described by intersection i and i+1
|
||
|
with i an odd value (1, 3, 5, ...). Since we know the lxml.etree node
|
||
|
corresponding to each intersection, we can then correlate the hatch
|
||
|
fill lines to the graphical elements in the original SVG document.
|
||
|
This enables us to group hatch lines with the elements being hatched.
|
||
|
|
||
|
The hatch line segments are returned by populating a dictionary.
|
||
|
The dictionary is keyed off of the lxml.etree node pointer. Each
|
||
|
dictionary value is a list of 4-tuples,
|
||
|
|
||
|
(x1, y1, x2, y2)
|
||
|
|
||
|
where (x1, y1) and (x2, y2) are the (x,y) coordinates of the line
|
||
|
segment's starting and ending points.
|
||
|
"""
|
||
|
|
||
|
d_and_a = []
|
||
|
# p1 & p2 is the hatch line
|
||
|
# p3 & p4 is the polygon edge to check
|
||
|
for path in paths:
|
||
|
for subpath in paths[path]:
|
||
|
p3 = subpath[0]
|
||
|
for p4 in subpath[1:]:
|
||
|
s = intersect(p1, p2, p3, p4)
|
||
|
if 0.0 <= s <= 1.0:
|
||
|
# Save this intersection point along the hatch line
|
||
|
if b_hold_back_hatches:
|
||
|
# We will need to know how the hatch meets the polygon segment, so that we can
|
||
|
# calculate the end of a shorter line that stops short
|
||
|
# of the polygon segment.
|
||
|
# We compute the angle now while we have the information required,
|
||
|
# but do _not_ apply it now, as we need the real,original, intersects
|
||
|
# for the odd/even inside/outside operations yet to come.
|
||
|
# Note that though the intersect() routine _could_ compute the join angle,
|
||
|
# we do it here because we go thru here much less often than we go thru intersect().
|
||
|
angle_hatch_radians = math.atan2(
|
||
|
-(p2[1] - p1[1]), (p2[0] - p1[0])
|
||
|
) # from p1 toward p2, cartesian coordinates
|
||
|
angle_segment_radians = math.atan2(
|
||
|
-(p4[1] - p3[1]), (p4[0] - p3[0])
|
||
|
) # from p3 toward p4, cartesian coordinates
|
||
|
angle_difference_radians = (
|
||
|
angle_hatch_radians - angle_segment_radians
|
||
|
)
|
||
|
# coerce to range -pi to +pi
|
||
|
if angle_difference_radians > math.pi:
|
||
|
angle_difference_radians -= 2 * math.pi
|
||
|
elif angle_difference_radians < -math.pi:
|
||
|
angle_difference_radians += 2 * math.pi
|
||
|
f_sin_of_join_angle = math.sin(angle_difference_radians)
|
||
|
f_abs_sin_of_join_angle = abs(f_sin_of_join_angle)
|
||
|
if (
|
||
|
f_abs_sin_of_join_angle != 0.0
|
||
|
): # Worrying about case of intersecting a segment parallel to the hatch
|
||
|
prelim_length_to_be_removed = (
|
||
|
f_hold_back_steps / f_abs_sin_of_join_angle
|
||
|
)
|
||
|
b_unconditionally_excise_hatch = False
|
||
|
else:
|
||
|
b_unconditionally_excise_hatch = True
|
||
|
|
||
|
if not b_unconditionally_excise_hatch:
|
||
|
# The relevant end of the segment is the end from which the hatch approaches at an acute angle.
|
||
|
intersection = [0, 0]
|
||
|
intersection[0] = p1[0] + s * (
|
||
|
p2[0] - p1[0]
|
||
|
) # compute intersection point of hatch with segment
|
||
|
intersection[1] = p1[1] + s * (
|
||
|
p2[1] - p1[1]
|
||
|
) # intersecting hatch line starts at p1, vectored toward p2,
|
||
|
# but terminates at intersection
|
||
|
# Note that atan2 returns answer in range -pi to pi
|
||
|
# Which end is the approach end of the hatch to the segment?
|
||
|
# The dot product tells the answer:
|
||
|
# if dot product is positive, p2 is at the p4 end,
|
||
|
# else p2 is at the p3 end
|
||
|
# We really don't need to take the time to actually take
|
||
|
# the cosine of the angle, we are just interested in
|
||
|
# the quadrant within which the angle lies.
|
||
|
# I'm sure there is an elegant way to do this, but I'll settle for results just now.
|
||
|
# If the angle is in quadrants I or IV then p4 is the relevant end, otherwise p3 is
|
||
|
# nb: Y increases down, rather than up
|
||
|
# nb: difference angle has been forced to the range -pi to +pi
|
||
|
if abs(angle_difference_radians) < math.pi / 2:
|
||
|
# It's near the p3 the relevant end from which the hatch departs
|
||
|
dist_intersection_to_relevant_end = math.hypot(
|
||
|
p3[0] - intersection[0], p3[1] - intersection[1]
|
||
|
)
|
||
|
dist_intersection_to_irrelevant_end = math.hypot(
|
||
|
p4[0] - intersection[0], p4[1] - intersection[1]
|
||
|
)
|
||
|
else:
|
||
|
# It's near the p4 end from which the hatch departs
|
||
|
dist_intersection_to_relevant_end = math.hypot(
|
||
|
p4[0] - intersection[0], p4[1] - intersection[1]
|
||
|
)
|
||
|
dist_intersection_to_irrelevant_end = math.hypot(
|
||
|
p3[0] - intersection[0], p3[1] - intersection[1]
|
||
|
)
|
||
|
|
||
|
# Now, the problem defined in issue 22 is that we may not need to remove the
|
||
|
# entire preliminary length we've calculated. This problem occurs because
|
||
|
# we have so far been considering the polygon segment as a line of infinite extent.
|
||
|
# Thus, we may be holding back at a point where no holdback is required, when
|
||
|
# calculated holdback is well beyond the position of the segment end.
|
||
|
|
||
|
# To make matters worse, we do not currently know whether we're
|
||
|
# starting a hatch or terminating a hatch, because the duplicates have
|
||
|
# yet to be removed. All we can do then, is calculate the required
|
||
|
# line shortening for both possibilities - and then choose the correct
|
||
|
# one after duplicate-removal, when actually finalizing the hatches.
|
||
|
|
||
|
# Let's see if either end, or perhaps both ends, has a case of excessive holdback
|
||
|
|
||
|
# First, default assumption is that neither end has excessive holdback
|
||
|
length_remove_starting_hatch = prelim_length_to_be_removed
|
||
|
length_remove_ending_hatch = prelim_length_to_be_removed
|
||
|
|
||
|
# Now check each of the two ends
|
||
|
if prelim_length_to_be_removed > (
|
||
|
dist_intersection_to_relevant_end + f_hold_back_steps
|
||
|
):
|
||
|
# Yes, would be excessive holdback approaching from this direction
|
||
|
length_remove_starting_hatch = (
|
||
|
dist_intersection_to_relevant_end
|
||
|
+ f_hold_back_steps
|
||
|
)
|
||
|
if prelim_length_to_be_removed > (
|
||
|
dist_intersection_to_irrelevant_end + f_hold_back_steps
|
||
|
):
|
||
|
# Yes, would be excessive holdback approaching from other direction
|
||
|
length_remove_ending_hatch = (
|
||
|
dist_intersection_to_irrelevant_end
|
||
|
+ f_hold_back_steps
|
||
|
)
|
||
|
|
||
|
d_and_a.append(
|
||
|
(
|
||
|
s,
|
||
|
path,
|
||
|
length_remove_starting_hatch,
|
||
|
length_remove_ending_hatch,
|
||
|
)
|
||
|
)
|
||
|
else:
|
||
|
d_and_a.append(
|
||
|
(s, path, 123456.0, 123456.0)
|
||
|
) # Mark for complete hatch excision, hatch is parallel to segment
|
||
|
# Just a random number guaranteed large enough to be longer than any hatch length
|
||
|
else:
|
||
|
d_and_a.append(
|
||
|
(s, path, 0, 0)
|
||
|
) # zero length to be removed from hatch
|
||
|
|
||
|
p3 = p4
|
||
|
|
||
|
# Return now if there were no intersections
|
||
|
if len(d_and_a) == 0:
|
||
|
return None
|
||
|
|
||
|
d_and_a.sort()
|
||
|
|
||
|
# Remove duplicate intersections. A common case where these arise
|
||
|
# is when the hatch line passes through a vertex where one line segment
|
||
|
# ends and the next one begins.
|
||
|
|
||
|
# Having sorted the data, it's trivial to just scan through
|
||
|
# removing duplicates as we go and then truncating the array
|
||
|
|
||
|
n = len(d_and_a)
|
||
|
i_last = 1
|
||
|
i = 1
|
||
|
last = d_and_a[0]
|
||
|
while i < n:
|
||
|
if (abs(d_and_a[i][0] - last[0])) > F_MINGAP_SMALL_VALUE:
|
||
|
d_and_a[i_last] = last = d_and_a[i]
|
||
|
i_last += 1
|
||
|
i += 1
|
||
|
d_and_a = d_and_a[:i_last]
|
||
|
if len(d_and_a) < 2:
|
||
|
return
|
||
|
|
||
|
# Now, entries with even valued indices into sa[] are where we start
|
||
|
# a hatch line and odd valued indices where we end the hatch line.
|
||
|
|
||
|
i = 0
|
||
|
while i < (len(d_and_a) - 1):
|
||
|
if d_and_a[i][1] not in hatches:
|
||
|
hatches[d_and_a[i][1]] = []
|
||
|
|
||
|
x1 = p1[0] + d_and_a[i][0] * (p2[0] - p1[0])
|
||
|
y1 = p1[1] + d_and_a[i][0] * (p2[1] - p1[1])
|
||
|
x2 = p1[0] + d_and_a[i + 1][0] * (p2[0] - p1[0])
|
||
|
y2 = p1[1] + d_and_a[i + 1][0] * (p2[1] - p1[1])
|
||
|
|
||
|
# These are the hatch ends if we are _not_ holding off from the boundary.
|
||
|
if not b_hold_back_hatches:
|
||
|
hatches[d_and_a[i][1]].append([[x1, y1], [x2, y2]])
|
||
|
else:
|
||
|
# User wants us to perform a pseudo inset operation.
|
||
|
# We will accomplish this by trimming back the ends of the hatches.
|
||
|
# The amount by which to trim back depends on the angle between the
|
||
|
# intersecting hatch line with the intersecting polygon segment, and
|
||
|
# may well be different at the two different ends of the hatch line.
|
||
|
|
||
|
# To visualize this, imagine a hatch intersecting a segment that is
|
||
|
# close to parallel with it. The length of the hatch would have to be
|
||
|
# drastically reduced in order that its closest approach to the
|
||
|
# segment be reduced to the desired distance.
|
||
|
|
||
|
# Imagine a Cartesian coordinate system, with the X axis representing the
|
||
|
# polygon segment, and a line running through the origin with a small
|
||
|
# positive slope being the intersecting hatch line.
|
||
|
|
||
|
# We see that we want a Y value of the specified hatch width, and that
|
||
|
# at that Y, the distance from the origin to that point is the
|
||
|
# hypotenuse of the triangle.
|
||
|
# Y / cutlength = sin(angle)
|
||
|
# therefore:
|
||
|
# cutlength = Y / sin(angle)
|
||
|
# Fortunately, we have already stored this angle for exactly this purpose.
|
||
|
# For each end, trim back the hatch line by the amount required by
|
||
|
# its own angle. If the resultant diminished hatch is too short,
|
||
|
# remove it from consideration by marking it as already drawn - a
|
||
|
# fiction, but is much quicker than actually removing the hatch from the list.
|
||
|
|
||
|
f_min_allowed_hatch_length = self.options.hatchSpacing * MIN_HATCH_FRACTION
|
||
|
f_initial_hatch_length = math.hypot(x2 - x1, y2 - y1)
|
||
|
# We did as much as possible of the inset operation back when we were finding intersections.
|
||
|
# We did it back then because at that point we knew more about the geometry than we know now.
|
||
|
# Now we don't know where the ends of the segments are, so we can't address issue 22 here.
|
||
|
f_length_to_be_removed_from_pt1 = d_and_a[i][3]
|
||
|
f_length_to_be_removed_from_pt2 = d_and_a[i + 1][2]
|
||
|
|
||
|
if (
|
||
|
f_initial_hatch_length
|
||
|
- (f_length_to_be_removed_from_pt1 + f_length_to_be_removed_from_pt2)
|
||
|
) <= f_min_allowed_hatch_length:
|
||
|
pass # Just don't insert it into the hatch list
|
||
|
else:
|
||
|
"""
|
||
|
Use:
|
||
|
def RelativeControlPointPosition( self, distance, fDeltaX, fDeltaY, deltaX, deltaY ):
|
||
|
# returns the point, relative to 0, 0 offset by deltaX, deltaY,
|
||
|
# which extends a distance of "distance" at a slope defined by fDeltaX and fDeltaY
|
||
|
"""
|
||
|
pt1 = self.RelativeControlPointPosition(
|
||
|
f_length_to_be_removed_from_pt1, x2 - x1, y2 - y1, x1, y1
|
||
|
)
|
||
|
pt2 = self.RelativeControlPointPosition(
|
||
|
f_length_to_be_removed_from_pt2, x1 - x2, y1 - y2, x2, y2
|
||
|
)
|
||
|
hatches[d_and_a[i][1]].append([[pt1[0], pt1[1]], [pt2[0], pt2[1]]])
|
||
|
|
||
|
# Remember the relative start and end of this hatch segment
|
||
|
last_d_and_a = [d_and_a[i], d_and_a[i + 1]]
|
||
|
|
||
|
i += 2
|
||
|
|
||
|
|
||
|
def inverseTransform(tran):
|
||
|
"""
|
||
|
An SVG transform matrix looks like
|
||
|
|
||
|
[ a c e ]
|
||
|
[ b d f ]
|
||
|
[ 0 0 1 ]
|
||
|
|
||
|
And it's inverse is
|
||
|
|
||
|
[ d -c cf - de ]
|
||
|
[ -b a be - af ] * ( ad - bc ) ** -1
|
||
|
[ 0 0 1 ]
|
||
|
|
||
|
And, no reasonable 2d coordinate transform will have
|
||
|
the products ad and bc equal.
|
||
|
|
||
|
SVG represents the transform matrix column by column as
|
||
|
matrix(a b c d e f) while Inkscape extensions store the
|
||
|
transform matrix as
|
||
|
|
||
|
[[a, c, e], [b, d, f]]
|
||
|
|
||
|
To invert the transform stored Inkscape style, we wish to
|
||
|
produce
|
||
|
|
||
|
[[d/D, -c/D, (cf - de)/D], [-b/D, a/D, (be-af)/D]]
|
||
|
|
||
|
where
|
||
|
|
||
|
D = 1 / (ad - bc)
|
||
|
"""
|
||
|
D = tran.a * tran.d - tran.b * tran.c
|
||
|
if D == 0:
|
||
|
return None
|
||
|
|
||
|
return [
|
||
|
[tran.d / D, -tran.c / D, (tran.c * tran.f - tran.d * tran.e) / D],
|
||
|
[-tran.b / D, tran.a / D, (tran.b * tran.e - tran.a * tran.f) / D],
|
||
|
]
|
||
|
|
||
|
|
||
|
import inkex.bezier
|
||
|
|
||
|
|
||
|
def subdivideCubicPath(sp, flat, i=1):
|
||
|
"""
|
||
|
Break up a bezier curve into smaller curves, each of which
|
||
|
is approximately a straight line within a given tolerance
|
||
|
(the "smoothness" defined by [flat]).
|
||
|
|
||
|
to avoid recurrence.
|
||
|
"""
|
||
|
|
||
|
while True:
|
||
|
while True:
|
||
|
if i >= len(sp):
|
||
|
return
|
||
|
|
||
|
p0 = sp[i - 1][1]
|
||
|
p1 = sp[i - 1][2]
|
||
|
p2 = sp[i][0]
|
||
|
p3 = sp[i][1]
|
||
|
|
||
|
b = (p0, p1, p2, p3)
|
||
|
|
||
|
if inkex.bezier.maxdist(b) > flat:
|
||
|
break
|
||
|
|
||
|
i += 1
|
||
|
|
||
|
one, two = inkex.bezier.beziersplitatt(b, 0.5)
|
||
|
sp[i - 1][2] = one[1]
|
||
|
sp[i][0] = two[2]
|
||
|
p = [one[2], one[3], two[1]]
|
||
|
sp[i:1] = [p]
|
||
|
|
||
|
|
||
|
def distanceSquared(p1, p2):
|
||
|
"""
|
||
|
Pythagorean distance formula WITHOUT the square root. Since
|
||
|
we just want to know if the distance is less than some fixed
|
||
|
fudge factor, we can just square the fudge factor once and run
|
||
|
with it rather than compute square roots over and over.
|
||
|
"""
|
||
|
|
||
|
dx = p2[0] - p1[0]
|
||
|
dy = p2[1] - p1[1]
|
||
|
|
||
|
return dx * dx + dy * dy
|
||
|
|
||
|
|
||
|
class HatchFill(inkex.Effect):
|
||
|
def __init__(self):
|
||
|
|
||
|
inkex.Effect.__init__(self)
|
||
|
|
||
|
self.xmin, self.ymin = (0.0, 0.0)
|
||
|
self.xmax, self.ymax = (0.0, 0.0)
|
||
|
self.paths = {}
|
||
|
self.grid = []
|
||
|
self.hatches = {}
|
||
|
self.transforms = {}
|
||
|
|
||
|
# For handling an SVG viewbox attribute, we will need to know the
|
||
|
# values of the document's <svg> width and height attributes as well
|
||
|
# as establishing a transform from the viewbox to the display.
|
||
|
self.docWidth = float(N_PAGE_WIDTH)
|
||
|
self.docHeight = float(N_PAGE_HEIGHT)
|
||
|
self.docTransform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||
|
|
||
|
self.arg_parser.add_argument( "--holdBackSteps", type=float, default=3.0, help="How far hatch strokes stay from boundary (steps)", )
|
||
|
self.arg_parser.add_argument( "--hatchScope", type=float, default=3.0, help="Radius searched for segments to join (units of hatch width)", )
|
||
|
self.arg_parser.add_argument( "--holdBackHatchFromEdges", type=inkex.Boolean, default=True, help="Stay away from edges, so no need for inset", )
|
||
|
self.arg_parser.add_argument( "--reducePenLifts", type=inkex.Boolean, default=True, help="Reduce plotting time by joining some hatches", )
|
||
|
self.arg_parser.add_argument( "--crossHatch", type=inkex.Boolean, default=False, help="Generate a cross hatch pattern", )
|
||
|
self.arg_parser.add_argument( "--hatchAngle", type=float, default=90.0, help="Angle of inclination for hatch lines", )
|
||
|
self.arg_parser.add_argument( "--hatchSpacing", type=float, default=10.0, help="Spacing between hatch lines", )
|
||
|
self.arg_parser.add_argument( "--tolerance", type=float, default=20.0, help="Allowed deviation from original paths", )
|
||
|
self.arg_parser.add_argument( "--tab", default="splash")
|
||
|
|
||
|
def getDocProps(self):
|
||
|
|
||
|
"""
|
||
|
Get the document's height and width attributes from the <svg> tag.
|
||
|
Use a default value in case the property is not present or is
|
||
|
expressed in units of percentages.
|
||
|
"""
|
||
|
|
||
|
self.docHeight = getLength(self, "height", N_PAGE_HEIGHT)
|
||
|
self.docWidth = getLength(self, "width", N_PAGE_WIDTH)
|
||
|
|
||
|
if self.docHeight is None or self.docWidth is None:
|
||
|
return False
|
||
|
else:
|
||
|
return True
|
||
|
|
||
|
def handleViewBox(self):
|
||
|
|
||
|
"""
|
||
|
Set up the document-wide transform in the event that the document has an SVG viewbox
|
||
|
"""
|
||
|
|
||
|
if self.getDocProps():
|
||
|
viewbox = self.document.getroot().get("viewBox")
|
||
|
if viewbox:
|
||
|
vinfo = viewbox.strip().replace(",", " ").split(" ")
|
||
|
if vinfo[2] != 0 and vinfo[3] != 0:
|
||
|
sx = self.docWidth / float(vinfo[2])
|
||
|
sy = self.docHeight / float(vinfo[3])
|
||
|
# self.docTransform = Transform('scale({0:f},{1:f})'.format(sx, sy))
|
||
|
self.docTransform = Transform(
|
||
|
f"scale({sx}, {sy})"
|
||
|
).matrix
|
||
|
|
||
|
def addPathVertices(self, path, node=None, transform=None):
|
||
|
|
||
|
"""
|
||
|
Decompose the path data from an SVG element into individual
|
||
|
subpaths, each starting with an absolute move-to (x, y)
|
||
|
coordinate followed by one or more absolute line-to (x, y)
|
||
|
coordinates. Each subpath is stored as a list of (x, y)
|
||
|
coordinates, with the first entry understood to be a
|
||
|
move-to coordinate and the rest line-to coordinates. A list
|
||
|
is then made of all the subpath lists and then stored in the
|
||
|
self.paths dictionary using the path's lxml.etree node pointer
|
||
|
as the dictionary key.
|
||
|
"""
|
||
|
|
||
|
if not path or len(path) == 0:
|
||
|
return
|
||
|
|
||
|
# parsePath() may raise an exception. This is okay
|
||
|
sp = inkex.paths.Path(path).to_arrays()
|
||
|
if not sp or len(sp) == 0:
|
||
|
return
|
||
|
|
||
|
# Get a cubic super duper path
|
||
|
p = CubicSuperPath(sp)
|
||
|
if not p or len(p) == 0:
|
||
|
return
|
||
|
|
||
|
# Apply any transformation
|
||
|
if transform is not None:
|
||
|
Path(p).transform(transform)
|
||
|
|
||
|
# Now traverse the simplified path
|
||
|
subpaths = []
|
||
|
subpath_vertices = []
|
||
|
for sp in p:
|
||
|
# We've started a new subpath
|
||
|
# See if there is a prior subpath and whether we should keep it
|
||
|
if len(subpath_vertices):
|
||
|
if distanceSquared(subpath_vertices[0], subpath_vertices[-1]) < 1:
|
||
|
# Keep the prior subpath: it appears to be a closed path
|
||
|
subpaths.append(subpath_vertices)
|
||
|
subpath_vertices = []
|
||
|
subdivideCubicPath(sp, float(self.options.tolerance / 100))
|
||
|
for csp in sp:
|
||
|
# Add this vertex to the list of vertices
|
||
|
subpath_vertices.append(csp[1])
|
||
|
|
||
|
# Handle final subpath
|
||
|
if len(subpath_vertices):
|
||
|
if distanceSquared(subpath_vertices[0], subpath_vertices[-1]) < 1:
|
||
|
# Path appears to be closed so let's keep it
|
||
|
subpaths.append(subpath_vertices)
|
||
|
|
||
|
# Empty path?
|
||
|
if len(subpaths) == 0:
|
||
|
return
|
||
|
|
||
|
# And add this path to our dictionary of paths
|
||
|
self.paths[node] = subpaths
|
||
|
|
||
|
# And save the transform for this element in a dictionary keyed
|
||
|
# by the element's lxml node pointer
|
||
|
self.transforms[node] = transform
|
||
|
|
||
|
def getBoundingBox(self):
|
||
|
|
||
|
"""
|
||
|
Determine the bounding box for our collection of polygons
|
||
|
"""
|
||
|
|
||
|
self.xmin, self.xmax = EXTREME_POS, EXTREME_NEG
|
||
|
self.ymin, self.ymax = EXTREME_POS, EXTREME_NEG
|
||
|
for path in self.paths:
|
||
|
for subpath in self.paths[path]:
|
||
|
for vertex in subpath:
|
||
|
if vertex[0] < self.xmin:
|
||
|
self.xmin = vertex[0]
|
||
|
elif vertex[0] > self.xmax:
|
||
|
self.xmax = vertex[0]
|
||
|
if vertex[1] < self.ymin:
|
||
|
self.ymin = vertex[1]
|
||
|
elif vertex[1] > self.ymax:
|
||
|
self.ymax = vertex[1]
|
||
|
|
||
|
def recursivelyTraverseSvg(
|
||
|
self, a_node_list, mat_current=None, parent_visibility="visible"
|
||
|
):
|
||
|
"""
|
||
|
Recursively walk the SVG document, building polygon vertex lists
|
||
|
for each graphical element we support.
|
||
|
|
||
|
Rendered SVG elements:
|
||
|
<circle>, <ellipse>, <line>, <path>, <polygon>, <polyline>, <rect>
|
||
|
|
||
|
Supported SVG elements:
|
||
|
<group>, <use>
|
||
|
|
||
|
Ignored SVG elements:
|
||
|
<defs>, <eggbot>, <metadata>, <namedview>, <pattern>
|
||
|
|
||
|
All other SVG elements trigger an error (including <text>)
|
||
|
|
||
|
Once a supported graphical element is found, we call functions to
|
||
|
create a hatchfill specific to this element. These hatches and their
|
||
|
corresponding transforms are stored in self.hatches and self.transforms
|
||
|
These two dictionaries are used when we return to the effect method
|
||
|
in joinFillsWithNode()
|
||
|
|
||
|
"""
|
||
|
if mat_current is None:
|
||
|
mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
|
||
|
for node in a_node_list:
|
||
|
|
||
|
"""
|
||
|
Initialize dictionary for each new node
|
||
|
This allows us to create hatch fills as if each
|
||
|
object to be hatched has been selected individually
|
||
|
|
||
|
"""
|
||
|
self.xmin, self.ymin = (0.0, 0.0)
|
||
|
self.xmax, self.ymax = (0.0, 0.0)
|
||
|
self.paths = {}
|
||
|
self.grid = []
|
||
|
|
||
|
# Ignore invisible nodes
|
||
|
v = node.get("visibility", parent_visibility)
|
||
|
if v == "inherit":
|
||
|
v = parent_visibility
|
||
|
if v == "hidden" or v == "collapse":
|
||
|
pass
|
||
|
|
||
|
# first apply the current matrix transform to this node's transform
|
||
|
mat_new = Transform(mat_current) * Transform(Transform(node.get("transform")).matrix)
|
||
|
|
||
|
if node.tag in [inkex.addNS("g", "svg"), "g"]:
|
||
|
self.recursivelyTraverseSvg(node, mat_new, parent_visibility=v)
|
||
|
|
||
|
elif node.tag in [inkex.addNS("use", "svg"), "use"]:
|
||
|
|
||
|
# A <use> element refers to another SVG element via an xlink:href="#blah"
|
||
|
# attribute. We will handle the element by doing an XPath search through
|
||
|
# the document, looking for the element with the matching id="blah"
|
||
|
# attribute. We then recursively process that element after applying
|
||
|
# any necessary (x,y) translation.
|
||
|
#
|
||
|
# Notes:
|
||
|
# 1. We ignore the height and width attributes as they do not apply to
|
||
|
# path-like elements, and
|
||
|
# 2. Even if the use element has visibility="hidden", SVG still calls
|
||
|
# for processing the referenced element. The referenced element is
|
||
|
# hidden only if its visibility is "inherit" or "hidden".
|
||
|
|
||
|
refid = node.get(inkex.addNS("href", "xlink"))
|
||
|
|
||
|
# [1:] to ignore leading '#' in reference
|
||
|
path = '//*[@id="{0}"]'.format(refid[1:])
|
||
|
refnode = node.xpath(path)
|
||
|
if refnode:
|
||
|
x = float(node.get("x", "0"))
|
||
|
y = float(node.get("y", "0"))
|
||
|
# Note: the transform has already been applied
|
||
|
if x != 0 or y != 0:
|
||
|
mat_new2 = Transform(mat_new) * Transform(Transform("translate({0:f},{1:f})".format(x, y)))
|
||
|
else:
|
||
|
mat_new2 = mat_new
|
||
|
v = node.get("visibility", v)
|
||
|
self.recursivelyTraverseSvg(refnode, mat_new2, parent_visibility=v)
|
||
|
|
||
|
elif node.tag == inkex.addNS("path", "svg"):
|
||
|
|
||
|
path_data = node.get("d")
|
||
|
if path_data:
|
||
|
self.addPathVertices(path_data, node, mat_new)
|
||
|
# We now have a path we want to apply a (cross)hatch to
|
||
|
# Apply appropriate functions
|
||
|
b_have_grid = self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle),
|
||
|
float(self.options.hatchSpacing),
|
||
|
True,
|
||
|
)
|
||
|
if b_have_grid:
|
||
|
if self.options.crossHatch:
|
||
|
self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle + 90.0),
|
||
|
float(self.options.hatchSpacing),
|
||
|
False,
|
||
|
)
|
||
|
# Now loop over our hatch lines looking for intersections
|
||
|
for h in self.grid:
|
||
|
interstices(
|
||
|
self,
|
||
|
(h[0], h[1]),
|
||
|
(h[2], h[3]),
|
||
|
self.paths,
|
||
|
self.hatches,
|
||
|
self.options.holdBackHatchFromEdges,
|
||
|
self.options.holdBackSteps,
|
||
|
)
|
||
|
|
||
|
elif node.tag in [inkex.addNS("rect", "svg"), "rect"]:
|
||
|
|
||
|
# Manually transform
|
||
|
#
|
||
|
# <rect x="X" y="Y" width="W" height="H"/>
|
||
|
#
|
||
|
# into
|
||
|
#
|
||
|
# <path d="MX,Y lW,0 l0,H l-W,0 z"/>
|
||
|
#
|
||
|
# I.e., explicitly draw three sides of the rectangle and the
|
||
|
# fourth side implicitly
|
||
|
|
||
|
# Create a path with the outline of the rectangle
|
||
|
x = float(node.get("x"))
|
||
|
y = float(node.get("y"))
|
||
|
|
||
|
w = float(node.get("width", "0"))
|
||
|
h = float(node.get("height", "0"))
|
||
|
a = [
|
||
|
["M", [x, y]],
|
||
|
["l", [w, 0]],
|
||
|
["l", [0, h]],
|
||
|
["l", [-w, 0]],
|
||
|
["Z", []],
|
||
|
]
|
||
|
ret = Path(a)
|
||
|
self.addPathVertices(ret, node, mat_new)
|
||
|
# We now have a path we want to apply a (cross)hatch to
|
||
|
# Apply appropriate functions
|
||
|
b_have_grid = self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle),
|
||
|
float(self.options.hatchSpacing),
|
||
|
True,
|
||
|
)
|
||
|
if b_have_grid:
|
||
|
if self.options.crossHatch:
|
||
|
self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle + 90.0),
|
||
|
float(self.options.hatchSpacing),
|
||
|
False,
|
||
|
)
|
||
|
# Now loop over our hatch lines looking for intersections
|
||
|
for h in self.grid:
|
||
|
interstices(
|
||
|
self,
|
||
|
(h[0], h[1]),
|
||
|
(h[2], h[3]),
|
||
|
self.paths,
|
||
|
self.hatches,
|
||
|
self.options.holdBackHatchFromEdges,
|
||
|
self.options.holdBackSteps,
|
||
|
)
|
||
|
|
||
|
elif node.tag in [inkex.addNS("line", "svg"), "line"]:
|
||
|
|
||
|
# Convert
|
||
|
#
|
||
|
# <line x1="X1" y1="Y1" x2="X2" y2="Y2/>
|
||
|
#
|
||
|
# to
|
||
|
#
|
||
|
# <path d="MX1,Y1 LX2,Y2"/>
|
||
|
|
||
|
x1 = float(node.get("x1"))
|
||
|
y1 = float(node.get("y1"))
|
||
|
x2 = float(node.get("x2"))
|
||
|
y2 = float(node.get("y2"))
|
||
|
|
||
|
a = [
|
||
|
["M ", [x1, y1]],
|
||
|
[" L ", [x2, y2]],
|
||
|
]
|
||
|
self.addPathVertices(Path(a), node, mat_new)
|
||
|
# We now have a path we want to apply a (cross)hatch to
|
||
|
# Apply appropriate functions
|
||
|
b_have_grid = self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle),
|
||
|
float(self.options.hatchSpacing),
|
||
|
True,
|
||
|
)
|
||
|
if b_have_grid:
|
||
|
if self.options.crossHatch:
|
||
|
self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle + 90.0),
|
||
|
float(self.options.hatchSpacing),
|
||
|
False,
|
||
|
)
|
||
|
# Now loop over our hatch lines looking for intersections
|
||
|
for h in self.grid:
|
||
|
interstices(
|
||
|
self,
|
||
|
(h[0], h[1]),
|
||
|
(h[2], h[3]),
|
||
|
self.paths,
|
||
|
self.hatches,
|
||
|
self.options.holdBackHatchFromEdges,
|
||
|
self.options.holdBackSteps,
|
||
|
)
|
||
|
|
||
|
elif node.tag in [inkex.addNS("polyline", "svg"), "polyline"]:
|
||
|
|
||
|
# Convert
|
||
|
#
|
||
|
# <polyline points="x1,y1 x2,y2 x3,y3 [...]"/>
|
||
|
#
|
||
|
# to
|
||
|
#
|
||
|
# <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...]"/>
|
||
|
#
|
||
|
# Note: we ignore polylines with no points
|
||
|
|
||
|
pl = node.get("points", "").strip()
|
||
|
if pl == "":
|
||
|
continue
|
||
|
pa = pl.split()
|
||
|
if not pa:
|
||
|
continue
|
||
|
pathLength = len(pa)
|
||
|
if pathLength < 4: # Minimum of x1,y1 x2,y2 required.
|
||
|
continue
|
||
|
|
||
|
d = "M " + pa[0] + " " + pa[1]
|
||
|
i = 2
|
||
|
while i < (pathLength - 1):
|
||
|
d += " L " + pa[i] + " " + pa[i + 1]
|
||
|
i += 2
|
||
|
|
||
|
if d:
|
||
|
self.addPathVertices(d, node, mat_new)
|
||
|
|
||
|
# We now have a path we want to apply a (cross)hatch to
|
||
|
# Apply appropriate functions
|
||
|
b_have_grid = self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle),
|
||
|
float(self.options.hatchSpacing),
|
||
|
True,
|
||
|
)
|
||
|
if b_have_grid:
|
||
|
if self.options.crossHatch:
|
||
|
self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle + 90.0),
|
||
|
float(self.options.hatchSpacing),
|
||
|
False,
|
||
|
)
|
||
|
# Now loop over our hatch lines looking for intersections
|
||
|
for h in self.grid:
|
||
|
interstices(
|
||
|
self,
|
||
|
(h[0], h[1]),
|
||
|
(h[2], h[3]),
|
||
|
self.paths,
|
||
|
self.hatches,
|
||
|
self.options.holdBackHatchFromEdges,
|
||
|
self.options.holdBackSteps,
|
||
|
)
|
||
|
|
||
|
elif node.tag in [inkex.addNS("polygon", "svg"), "polygon"]:
|
||
|
# Convert
|
||
|
#
|
||
|
# <polygon points="x1,y1 x2,y2 x3,y3 [...]"/>
|
||
|
#
|
||
|
# to
|
||
|
#
|
||
|
# <path d="Mx1,y1 Lx2,y2 Lx3,y3 [...] Z"/>
|
||
|
#
|
||
|
# Note: we ignore polygons with no points
|
||
|
|
||
|
pl = node.get("points", "").strip()
|
||
|
|
||
|
pa = pl.split()
|
||
|
d = "".join(
|
||
|
[
|
||
|
"M " + pa[i] if i == 0 else " L " + pa[i]
|
||
|
for i in range(0, len(pa))
|
||
|
]
|
||
|
)
|
||
|
d += " Z"
|
||
|
self.addPathVertices(d, node, mat_new)
|
||
|
# We now have a path we want to apply a (cross)hatch to
|
||
|
# Apply appropriate functions
|
||
|
b_have_grid = self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle),
|
||
|
float(self.options.hatchSpacing),
|
||
|
True,
|
||
|
)
|
||
|
if b_have_grid:
|
||
|
if self.options.crossHatch:
|
||
|
self.makeHatchGrid(
|
||
|
float(self.options.hatchAngle + 90.0),
|
||
|
float(self.options.hatchSpacing),
|
||
|
False,
|
||
|
)
|
||
|
# Now loop over our hatch lines looking for intersections
|
||
|
for h in self.grid:
|
||
|
interstices(
|
||
|
self,
|
||
|
(h[0], h[1]),
|
||
|
(h[2], h[3]),
|
||
|
self.paths,
|
||
|
self.hatches,
|
||
|
self.options.holdBackHatchFromEdges,
|
||
|
self.options.holdBackSteps,
|
||
|
)
|
||
|
|
||
|
elif node.tag in [
|
||
|
inkex.addNS("ellipse", "svg"),
|
||
|
"ellipse",
|
||
|
inkex.addNS("circle", "svg"),
|
||
|
"circle",
|
||
|
]:
|
||
|
|
||
|
# Convert circles and ellipses to a path with two 180 degree arcs.
|
||
|
# In general (an ellipse), we convert
|
||
|
#
|
||
|
# <ellipse rx="RX" ry="RY" cx="X" cy="Y"/>
|
||
|
#
|
||
|
# to
|
||
|
#
|
||
|
# <path d="MX1,CY A RX,RY 0 1 0 X2,CY A RX,RY 0 1 0 X1,CY"/>
|
||
|
#
|
||
|