#!/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 , , ,
# , , , and 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