2010-11-27 20:44:32 +00:00
#!/usr/bin/env python
2018-03-29 21:39:43 +00:00
# coding=utf-8
2010-11-27 20:44:32 +00:00
# eggbot_hatch.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
2010-11-29 02:34:30 +00:00
# Last updated 28 November 2010
2010-11-27 20:44:32 +00:00
# 15 October 2010
2012-06-29 03:36:07 +00:00
# Updated by Windell H. Oskay, 6/14/2012
# Added tolerance parameter
# Update by Daniel C. Newman, 6/20/2012
# Add min span/gap width
2016-01-08 17:45:51 +00:00
# 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
2016-03-18 06:46:01 +00:00
# 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.
2017-12-07 22:39:02 +00:00
# https://github.com/evil-mad/EggBot/issues/36
2017-12-07 22:06:32 +00:00
# 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
2017-12-07 22:39:02 +00:00
# Modifications are limited to recursivelyTraverseSvg and effect methods
2018-03-29 21:39:43 +00:00
2017-12-07 22:06:32 +00:00
#
# Current software version:
2017-12-07 22:39:02 +00:00
# (v2.1.0, December 6, 2017)
#
2010-11-27 20:44:32 +00:00
# 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.
2017-12-07 22:39:02 +00:00
2010-11-27 20:44:32 +00:00
# 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
2018-03-29 21:39:43 +00:00
import math
import bezmisc
import cspsubdiv
import cubicsuperpath
2010-11-27 20:44:32 +00:00
import inkex
2018-03-29 21:39:43 +00:00
import plot_utils # https://github.com/evil-mad/plotink
2010-11-27 20:44:32 +00:00
import simplepath
import simplestyle
2018-03-29 21:39:43 +00:00
from simpletransform import applyTransformToPath , composeTransform , parseTransform , applyTransformToPoint
2010-11-28 16:33:02 +00:00
N_PAGE_WIDTH = 3200
N_PAGE_HEIGHT = 800
2010-11-27 20:44:32 +00:00
2018-03-29 21:39:43 +00:00
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_POSITIVE_NUMBER = float ( 1.0E70 )
EXTREME_NEGATIVE_NUMBER = float ( - 1.0E70 )
MIN_HATCH_LENGTH_AS_FRACTION_OF_HATCH_SPACING = 0.25 # set minimum hatch length to some function
# (e.g. this multiplier) of the hatch spacing
"""
2016-03-18 06:46:01 +00:00
Geometry 101 : Determining if two lines intersect
2010-11-27 20:44:32 +00:00
A line L is defined by two points in space P1 and P2 . Any point P on the
line L satisfies
2018-03-30 04:45:11 +00:00
P = P1 + s ( P2 - P1 )
2010-11-27 20:44:32 +00:00
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
2018-03-30 04:45:11 +00:00
Pa = P1 + sa ( P2 - P1 )
Pb = P3 + sb ( P4 - P3 )
2010-11-27 20:44:32 +00:00
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
2018-03-30 04:45:11 +00:00
Pa = Pb
2010-11-27 20:44:32 +00:00
Or , equivalently , we ask if there exists values of sa and sb for which
the equation
2018-03-30 04:45:11 +00:00
P1 + sa ( P2 - P1 ) = P3 + sb ( P4 - P3 )
2010-11-27 20:44:32 +00:00
holds . If we confine ourselves to a two - dimensional plane , and take
2018-03-30 04:45:11 +00:00
P1 = ( x1 , y1 )
P2 = ( x2 , y2 )
P3 = ( x3 , y3 )
P4 = ( x4 , y4 )
2010-11-27 20:44:32 +00:00
we then find that we have two equations in two unknowns , sa and sb ,
2018-03-30 04:45:11 +00:00
x1 + sa ( x2 - x1 ) = x3 + sb ( x4 - x3 )
y1 + sa ( y2 - y1 ) = y3 + sb ( y4 - y3 )
2010-11-27 20:44:32 +00:00
Solving these two equations for sa and sb yields ,
2018-03-30 04:45:11 +00:00
sa = [ ( y1 - y3 ) ( x4 - x3 ) - ( y4 - y3 ) ( x1 - x3 ) ] / d
sb = [ ( y1 - y3 ) ( x2 - x1 ) - ( y2 - y1 ) ( x1 - x3 ) ] / d
2010-11-27 20:44:32 +00:00
where the denominator , d , is given by
2018-03-30 04:45:11 +00:00
d = ( y4 - y3 ) ( x2 - x1 ) - ( y2 - y1 ) ( x4 - x3 )
2010-11-27 20:44:32 +00:00
Substituting these back for the point ( x , y ) of intersection gives
2018-03-30 04:45:11 +00:00
x = x1 + sa ( x2 - x1 )
y = y1 + sa ( y2 - y1 )
2010-11-27 20:44:32 +00:00
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 .
2018-03-29 21:39:43 +00:00
"""
2010-11-27 20:44:32 +00:00
2018-03-29 21:39:43 +00:00
def intersect ( P1 , P2 , P3 , P4 ) :
2018-03-30 04:45:11 +00:00
"""
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 float ( - 1 )
# 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 float ( - 1 )
na = ( P1 [ 1 ] - P3 [ 1 ] ) * D43x - ( P1 [ 0 ] - P3 [ 0 ] ) * D43y
sa = float ( na ) / float ( d )
if ( sa < 0 ) or ( sa > 1 ) :
return float ( - 1 )
return sa
2010-11-27 20:44:32 +00:00
2018-03-29 21:39:43 +00:00
def interstices ( self , P1 , P2 , paths , hatches , bHoldBackHatches , fHoldBackSteps ) :
2018-03-30 04:45:11 +00:00
"""
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.
"""
dAndA = [ ]
# 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 ( s > = 0.0 ) and ( s < = 1.0 ) :
# Save this intersection point along the hatch line
if bHoldBackHatches :
# 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().
angleHatchRadians = math . atan2 ( - ( P2 [ 1 ] - P1 [ 1 ] ) , ( P2 [ 0 ] - P1 [ 0 ] ) ) # from P1 toward P2, cartesian coordinates
angleSegmentRadians = math . atan2 ( - ( P4 [ 1 ] - P3 [ 1 ] ) , ( P4 [ 0 ] - P3 [ 0 ] ) ) # from P3 toward P4, cartesian coordinates
angleDifferenceRadians = angleHatchRadians - angleSegmentRadians
# coerce to range -pi to +pi
if angleDifferenceRadians > math . pi :
angleDifferenceRadians - = 2 * math . pi
elif angleDifferenceRadians < - math . pi :
angleDifferenceRadians + = 2 * math . pi
fSinOfJoinAngle = math . sin ( angleDifferenceRadians )
fAbsSinOfJoinAngle = abs ( fSinOfJoinAngle )
if fAbsSinOfJoinAngle != 0.0 : # Worrying about case of intersecting a segment parallel to the hatch
fPreliminaryLengthToBeRemovedFromPt = fHoldBackSteps / fAbsSinOfJoinAngle
bUnconditionallyExciseHatch = False
else : # if (fSinOfJoinAngle != 0.0):
bUnconditionallyExciseHatch = True
# if (fAbsSinOfJoinAngle != 0.0): else:
if not bUnconditionallyExciseHatch :
# if ( fPreliminaryLengthToBeRemovedFromPt > ( distance from intersection to relevant end + fHoldbackSteps ) ):
# fFinalLengthToBeRemovedFromPt = ( distance from intersection to relevant end + fHoldbackSteps )
# The relevant end of the segment is the end from which the hatch approaches at an acute angle.
I = [ 0 , 0 ]
I [ 0 ] = P1 [ 0 ] + s * ( P2 [ 0 ] - P1 [ 0 ] ) # compute intersection point of hatch with segment
I [ 1 ] = P1 [ 1 ] + s * ( P2 [ 1 ] - P1 [ 1 ] ) # intersecting hatch line starts at P1, vectored toward P2,
# but terminates at intersection I
# 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 ( angleDifferenceRadians ) < math . pi / 2 :
# It's near the P3 the relevant end from which the hatch departs
fDistanceFromIntersectionToRelevantEnd = math . hypot ( P3 [ 0 ] - I [ 0 ] , P3 [ 1 ] - I [ 1 ] )
fDistanceFromIntersectionToIrrelevantEnd = math . hypot ( P4 [ 0 ] - I [ 0 ] , P4 [ 1 ] - I [ 1 ] )
else : # if ( abs(angleDifferenceRadians) < math.pi / 2 )
# It's near the P4 end from which the hatch departs
fDistanceFromIntersectionToRelevantEnd = math . hypot ( P4 [ 0 ] - I [ 0 ] , P4 [ 1 ] - I [ 1 ] )
fDistanceFromIntersectionToIrrelevantEnd = math . hypot ( P3 [ 0 ] - I [ 0 ] , P3 [ 1 ] - I [ 1 ] )
# if ( abs(angleDifferenceRadians) < math.pi / 2 ): else:
# 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
fFinalLengthToBeRemovedFromPtWhenStartingHatch = fPreliminaryLengthToBeRemovedFromPt
fFinalLengthToBeRemovedFromPtWhenEndingHatch = fPreliminaryLengthToBeRemovedFromPt
# Now check each of the two ends
if fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToRelevantEnd + fHoldBackSteps ) :
# Yes, would be excessive holdback approaching from this direction
fFinalLengthToBeRemovedFromPtWhenStartingHatch = fDistanceFromIntersectionToRelevantEnd + fHoldBackSteps
# if ( fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToRelevantEnd + fHoldBackSteps ) ):
if fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToIrrelevantEnd + fHoldBackSteps ) :
# Yes, would be excessive holdback approaching from other direction
fFinalLengthToBeRemovedFromPtWhenEndingHatch = fDistanceFromIntersectionToIrrelevantEnd + fHoldBackSteps
# if ( fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToIrrelevantEnd + fHoldBackSteps ) ):
dAndA . append ( ( s , path , fFinalLengthToBeRemovedFromPtWhenStartingHatch , fFinalLengthToBeRemovedFromPtWhenEndingHatch ) )
else : # if ( not bUnconditionallyExciseHatch):
dAndA . 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
# if ( not bUnconditionallyExciseHatch): else :
else : # if bHoldBackHatches:
dAndA . append ( ( s , path , 0 , 0 ) ) # zero length to be removed from hatch
# if bHoldBackHatches: else:
# if ( s >= 0.0 ) and ( s <= 1.0 ):
P3 = P4
# for P4 in subpath[1:]:
# for subpath in paths[path]:
# for path in paths:
# Return now if there were no intersections
if len ( dAndA ) == 0 :
return None
dAndA . 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 ( dAndA )
ilast = i = 1
last = dAndA [ 0 ]
while i < n :
if ( abs ( dAndA [ i ] [ 0 ] - last [ 0 ] ) ) > F_MINGAP_SMALL_VALUE :
dAndA [ ilast ] = last = dAndA [ i ]
ilast + = 1
i + = 1
dAndA = dAndA [ : ilast ]
if len ( dAndA ) < 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.
last_dAndA = None
i = 0
while i < ( len ( dAndA ) - 1 ) :
# for i in range( 0, len( sa ) - 1, 2 ):
if dAndA [ i ] [ 1 ] not in hatches :
hatches [ dAndA [ i ] [ 1 ] ] = [ ]
x1 = P1 [ 0 ] + dAndA [ i ] [ 0 ] * ( P2 [ 0 ] - P1 [ 0 ] )
y1 = P1 [ 1 ] + dAndA [ i ] [ 0 ] * ( P2 [ 1 ] - P1 [ 1 ] )
x2 = P1 [ 0 ] + dAndA [ i + 1 ] [ 0 ] * ( P2 [ 0 ] - P1 [ 0 ] )
y2 = P1 [ 1 ] + dAndA [ i + 1 ] [ 0 ] * ( P2 [ 1 ] - P1 [ 1 ] )
# These are the hatch ends if we are _not_ holding off from the boundary.
if not bHoldBackHatches :
hatches [ dAndA [ i ] [ 1 ] ] . append ( [ [ x1 , y1 ] , [ x2 , y2 ] ] )
else : # if not bHoldBackHatches:
# 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.
fMinAllowedHatchLength = self . options . hatchSpacing * MIN_HATCH_LENGTH_AS_FRACTION_OF_HATCH_SPACING
fInitialHatchLength = 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.
fLengthToBeRemovedFromPt1 = dAndA [ i ] [ 3 ]
fLengthToBeRemovedFromPt2 = dAndA [ i + 1 ] [ 2 ]
if ( fInitialHatchLength - ( fLengthToBeRemovedFromPt1 + fLengthToBeRemovedFromPt2 ) ) < = fMinAllowedHatchLength :
pass # Just don't insert it into the hatch list
else : # if (...too short...):
"""
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 ( fLengthToBeRemovedFromPt1 , x2 - x1 , y2 - y1 , x1 , y1 )
pt2 = self . RelativeControlPointPosition ( fLengthToBeRemovedFromPt2 , x1 - x2 , y1 - y2 , x2 , y2 )
hatches [ dAndA [ i ] [ 1 ] ] . append ( [ [ pt1 [ 0 ] , pt1 [ 1 ] ] , [ pt2 [ 0 ] , pt2 [ 1 ] ] ] )
# if (...too short...): else:
# if not bHoldBackHatches: else:
# Remember the relative start and end of this hatch segment
last_dAndA = [ dAndA [ i ] , dAndA [ i + 1 ] ]
i = i + 2
# while i < ( len( dAndA ) - 1 ):
2018-03-29 21:39:43 +00:00
def inverseTransform ( tran ) :
2018-03-30 04:45:11 +00:00
"""
An SVG transform matrix looks like
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
[ a c e ]
[ b d f ]
[ 0 0 1 ]
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
And it ' s inverse is
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
[ d - c cf - de ]
[ - b a be - af ] * ( ad - bc ) * * - 1
[ 0 0 1 ]
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
And , no reasonable 2 d coordinate transform will have
the products ad and bc equal .
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
SVG represents the transform matrix column by column as
matrix ( a b c d e f ) while Inkscape extensions store the
transform matrix as
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
[ [ a , c , e ] , [ b , d , f ] ]
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
To invert the transform stored Inkscape style , we wish to
produce
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
[ [ d / D , - c / D , ( cf - de ) / D ] , [ - b / D , a / D , ( be - af ) / D ] ]
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
where
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
D = 1 / ( ad - bc )
"""
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
D = tran [ 0 ] [ 0 ] * tran [ 1 ] [ 1 ] - tran [ 1 ] [ 0 ] * tran [ 0 ] [ 1 ]
if D == 0 :
return None
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
return [ [ tran [ 1 ] [ 1 ] / D , - tran [ 0 ] [ 1 ] / D ,
( tran [ 0 ] [ 1 ] * tran [ 1 ] [ 2 ] - tran [ 1 ] [ 1 ] * tran [ 0 ] [ 2 ] ) / D ] ,
[ - tran [ 1 ] [ 0 ] / D , tran [ 0 ] [ 0 ] / D ,
( tran [ 1 ] [ 0 ] * tran [ 0 ] [ 2 ] - tran [ 0 ] [ 0 ] * tran [ 1 ] [ 2 ] ) / D ] ]
2010-11-27 20:44:32 +00:00
2018-03-29 21:39:43 +00:00
def subdivideCubicPath ( sp , flat , i = 1 ) :
2018-03-30 04:45:11 +00:00
"""
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 ] ) .
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
This is a modified version of cspsubdiv . cspsubdiv ( ) rewritten
to avoid recurrence .
"""
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
while True :
while True :
if i > = len ( sp ) :
return
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
p0 = sp [ i - 1 ] [ 1 ]
p1 = sp [ i - 1 ] [ 2 ]
p2 = sp [ i ] [ 0 ]
p3 = sp [ i ] [ 1 ]
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
b = ( p0 , p1 , p2 , p3 )
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
if cspsubdiv . maxdist ( b ) > flat :
break
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
i + = 1
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
one , two = bezmisc . 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 ]
2010-11-27 20:44:32 +00:00
2018-03-29 21:39:43 +00:00
def distanceSquared ( P1 , P2 ) :
2018-03-30 04:45:11 +00:00
"""
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 .
"""
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
dx = P2 [ 0 ] - P1 [ 0 ]
dy = P2 [ 1 ] - P1 [ 1 ]
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
return dx * dx + dy * dy
2010-11-27 20:44:32 +00:00
2018-03-29 21:39:43 +00:00
class Eggbot_Hatch ( inkex . Effect ) :
2010-11-27 20:44:32 +00:00
2018-03-30 04:45:11 +00:00
def __init__ ( self ) :
inkex . Effect . __init__ ( self )
self . xmin , self . ymin = ( float ( 0 ) , float ( 0 ) )
self . xmax , self . ymax = ( float ( 0 ) , float ( 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 . OptionParser . add_option (
" --holdBackSteps " , action = " store " , type = " float " ,
dest = " holdBackSteps " , default = 3.0 ,
help = " How far hatch strokes stay from boundary (steps) " )
self . OptionParser . add_option (
" --hatchScope " , action = " store " , type = " float " ,
dest = " hatchScope " , default = 3.0 ,
help = " Radius searched for segments to join (units of hatch width) " )
self . OptionParser . add_option (
" --holdBackHatchFromEdges " , action = " store " , dest = " holdBackHatchFromEdges " ,
type = " inkbool " , default = True ,
help = " Stay away from edges, so no need for inset " )
self . OptionParser . add_option (
" --reducePenLifts " , action = " store " , dest = " reducePenLifts " ,
type = " inkbool " , default = True ,
help = " Reduce plotting time by joining some hatches " )
self . OptionParser . add_option (
" --crossHatch " , action = " store " , dest = " crossHatch " ,
type = " inkbool " , default = False ,
help = " Generate a cross hatch pattern " )
self . OptionParser . add_option (
" --hatchAngle " , action = " store " , type = " float " ,
dest = " hatchAngle " , default = 90.0 ,
help = " Angle of inclination for hatch lines " )
self . OptionParser . add_option (
" --hatchSpacing " , action = " store " , type = " float " ,
dest = " hatchSpacing " , default = 10.0 ,
help = " Spacing between hatch lines " )
self . OptionParser . add_option (
" --tolerance " , action = " store " , type = " float " ,
dest = " tolerance " , default = 20.0 ,
help = " Allowed deviation from original paths " )
self . OptionParser . add_option ( " --tab " , # NOTE: value is not used.
action = " store " , type = " string " , dest = " tab " , default = " splash " ,
help = " The active tab when Apply was pressed " )
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 = plot_utils . getLength ( self , ' height ' , N_PAGE_HEIGHT )
self . docWidth = plot_utils . 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 = parseTransform ( ' scale( %f , %f ) ' % ( sx , sy ) )
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 = simplepath . parsePath ( path )
if ( not sp ) or ( len ( sp ) == 0 ) :
return
# Get a cubic super duper path
p = cubicsuperpath . CubicSuperPath ( sp )
if ( not p ) or ( len ( p ) == 0 ) :
return
# Apply any transformation
if transform is not None :
applyTransformToPath ( transform , p )
# 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_POSITIVE_NUMBER , EXTREME_NEGATIVE_NUMBER
self . ymin , self . ymax = EXTREME_POSITIVE_NUMBER , EXTREME_NEGATIVE_NUMBER
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 ]
# for vertex in subpath:
# for subpath in self.paths[path]:
# for path in self.paths:
def recursivelyTraverseSvg ( self , aNodeList ,
matCurrent = [ [ 1.0 , 0.0 , 0.0 ] , [ 0.0 , 1.0 , 0.0 ] ] ,
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 ( )
"""
for node in aNodeList :
"""
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 = ( float ( 0 ) , float ( 0 ) )
self . xmax , self . ymax = ( float ( 0 ) , float ( 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
matNew = composeTransform ( matCurrent , parseTransform ( node . get ( " transform " ) ) )
if node . tag == inkex . addNS ( ' g ' , ' svg ' ) or node . tag == ' g ' :
self . recursivelyTraverseSvg ( node , matNew , parent_visibility = v )
elif node . tag == inkex . addNS ( ' use ' , ' svg ' ) or node . tag == ' 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 ' ) )
if not refid :
pass
# [1:] to ignore leading '#' in reference
path = ' //*[@id= " %s " ] ' % 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 ) :
matNew2 = composeTransform ( matNew , parseTransform ( ' translate( %f , %f ) ' % ( x , y ) ) )
else :
matNew2 = matNew
v = node . get ( ' visibility ' , v )
self . recursivelyTraverseSvg ( refnode , matNew2 , parent_visibility = v )
elif node . tag == inkex . addNS ( ' path ' , ' svg ' ) :
path_data = node . get ( ' d ' )
if path_data :
self . addPathVertices ( path_data , node , matNew )
# We now have a path we want to apply a (cross)hatch to
# Apply appropriate functions
bHaveGrid = self . makeHatchGrid ( float ( self . options . hatchAngle ) , float ( self . options . hatchSpacing ) , True )
if bHaveGrid :
if self . options . crossHatch :
self . makeHatchGrid ( float ( self . options . hatchAngle + 90.0 ) , float ( self . options . hatchSpacing ) , False )
# if self.options.crossHatch:
# 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 )
# if bHaveGrid
else :
inkex . errormsg ( ' Nothing to plot ' )
elif node . tag == inkex . addNS ( ' rect ' , ' svg ' ) or node . tag == ' 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 ' ) )
if ( not x ) or ( not y ) :
pass
w = float ( node . get ( ' width ' , ' 0 ' ) )
h = float ( node . get ( ' height ' , ' 0 ' ) )
a = [ ]
a . append ( [ ' M ' , [ x , y ] ] )
a . append ( [ ' l ' , [ w , 0 ] ] )
a . append ( [ ' l ' , [ 0 , h ] ] )
a . append ( [ ' l ' , [ - w , 0 ] ] )
a . append ( [ ' Z ' , [ ] ] )
self . addPathVertices ( simplepath . formatPath ( a ) , node , matNew )
# We now have a path we want to apply a (cross)hatch to
# Apply appropriate functions
bHaveGrid = self . makeHatchGrid ( float ( self . options . hatchAngle ) , float ( self . options . hatchSpacing ) , True )
if bHaveGrid :
if self . options . crossHatch :
self . makeHatchGrid ( float ( self . options . hatchAngle + 90.0 ) , float ( self . options . hatchSpacing ) , False )
# if self.options.crossHatch:
# 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 )
# if bHaveGrid
else :
inkex . errormsg ( ' Nothing to plot ' )
elif node . tag == inkex . addNS ( ' line ' , ' svg ' ) or node . tag == ' 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 ' ) )
if ( not x1 ) or ( not y1 ) or ( not x2 ) or ( not y2 ) :
pass
a = [ ]
a . append ( [ ' M ' , [ x1 , y1 ] ] )
a . append ( [ ' L ' , [ x2 , y2 ] ] )
self . addPathVertices ( simplepath . formatPath ( a ) , node , matNew )
# We now have a path we want to apply a (cross)hatch to
# Apply appropriate functions
bHaveGrid = self . makeHatchGrid ( float ( self . options . hatchAngle ) , float ( self . options . hatchSpacing ) , True )
if bHaveGrid :
if self . options . crossHatch :
self . makeHatchGrid ( float ( self . options . hatchAngle + 90.0 ) , float ( self . options . hatchSpacing ) , False )
# if self.options.crossHatch:
# 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 )
# if bHaveGrid
else :
inkex . errormsg ( ' Nothing to plot ' )
elif node . tag == inkex . addNS ( ' polyline ' , ' svg ' ) or node . tag == ' 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 == ' ' :
pass
pa = pl . split ( )
d = " " . join ( [ " M " + pa [ i ] if i == 0 else " L " + pa [ i ] for i in range ( 0 , len ( pa ) ) ] )
self . addPathVertices ( d , node , matNew )
# We now have a path we want to apply a (cross)hatch to
# Apply appropriate functions
bHaveGrid = self . makeHatchGrid ( float ( self . options . hatchAngle ) , float ( self . options . hatchSpacing ) , True )
if bHaveGrid :
if self . options . crossHatch :
self . makeHatchGrid ( float ( self . options . hatchAngle + 90.0 ) , float ( self . options . hatchSpacing ) , False )
# if self.options.crossHatch:
# 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 )
# if bHaveGrid
else :
inkex . errormsg ( ' Nothing to plot ' )
elif node . tag == inkex . addNS ( ' polygon ' , ' svg ' ) or node . tag == ' 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 ( )
if pl == ' ' :
pass
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 , matNew )
# We now have a path we want to apply a (cross)hatch to
# Apply appropriate functions
bHaveGrid = self . makeHatchGrid ( float ( self . options . hatchAngle ) , float ( self . options . hatchSpacing ) , True )
if bHaveGrid :
if self . options . crossHatch :
self . makeHatchGrid ( float ( self . options . hatchAngle + 90.0 ) , float ( self . options . hatchSpacing ) , False )
# if self.options.crossHatch:
# 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 )
else :
inkex . errormsg ( ' Nothing to plot ' )
elif node . tag == inkex . addNS ( ' ellipse ' , ' svg ' ) or \
node . tag == ' ellipse ' or \
node . tag == inkex . addNS ( ' circle ' , ' svg ' ) or \
node . tag == ' 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"/>
#
# where
#
# X1 = CX - RX
# X2 = CX + RX
#
# Note: ellipses or circles with a radius attribute of value 0 are ignored
if node . tag == inkex . addNS ( ' ellipse ' , ' svg ' ) or node . tag == ' ellipse ' :
rx = float ( node . get ( ' rx ' , ' 0 ' ) )
ry = float ( node . get ( ' ry ' , ' 0 ' ) )
else :
rx = float ( node . get ( ' r ' , ' 0 ' ) )
ry = rx
if rx == 0 or ry == 0 :
pass
cx = float ( node . get ( ' cx ' , ' 0 ' ) )
cy = float ( node . get ( ' cy ' , ' 0 ' ) )
x1 = cx - rx
x2 = cx + rx
d = ' M %f , %f ' % ( x1 , cy ) + \
' A %f , %f ' % ( rx , ry ) + \
' 0 1 0 %f , %f ' % ( x2 , cy ) + \
' A %f , %f ' % ( rx , ry ) + \
' 0 1 0 %f , %f ' % ( x1 , cy )
self . addPathVertices ( d , node , matNew )
# We now have a path we want to apply a (cross)hatch to
# Apply appropriate functions
bHaveGrid = self . makeHatchGrid ( float ( self . options . hatchAngle ) , float ( self . options . hatchSpacing ) , True )
if bHaveGrid :
if self . options . crossHatch :
self . makeHatchGrid ( float ( self . options . hatchAngle + 90.0 ) , float ( self . options . hatchSpacing ) , False )
# if self.options.crossHatch:
# 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 )
else :
inkex . errormsg ( ' Nothing to plot ' )
elif node . tag == inkex . addNS ( ' pattern ' , ' svg ' ) or node . tag == ' pattern ' :
pass
elif node . tag == inkex . addNS ( ' metadata ' , ' svg ' ) or node . tag == ' metadata ' :
pass
elif node . tag == inkex . addNS ( ' defs ' , ' svg ' ) or node . tag == ' defs ' :
pass
elif node . tag == inkex . addNS ( ' namedview ' , ' sodipodi ' ) or node . tag == ' namedview ' :
pass
elif node . tag == inkex . addNS ( ' eggbot ' , ' svg ' ) or node . tag == ' eggbot ' :
pass
elif node . tag == inkex . addNS ( ' WCB ' , ' svg ' ) or node . tag == ' WCB ' :
pass
elif node . tag == inkex . addNS ( ' text ' , ' svg ' ) or node . tag == ' text ' :
inkex . errormsg ( ' Warning: unable to draw text, please convert it to a path first. ' )
pass
elif not isinstance ( node . tag , basestring ) :
pass
else :
inkex . errormsg ( ' Warning: unable to hatch object < %s >, please convert it to a path first. ' % node . tag )
pass
def joinFillsWithNode ( self , node , stroke_width , path ) :
"""
Generate a SVG < path > element containing the path data " path " .
Then put this new < path > element into a < group > with the supplied
node . This means making a new < group > element and moving node
under it with the new < path > as a sibling element .
"""
if ( not path ) or ( len ( path ) == 0 ) :
return
# Make a new SVG <group> element whose parent is the parent of node
parent = node . getparent ( )
# was: if not parent:
if parent is None :
parent = self . document . getroot ( )
g = inkex . etree . SubElement ( parent , inkex . addNS ( ' g ' , ' svg ' ) )
# Move node to be a child of this new <g> element
g . append ( node )
# Now make a <path> element which contains the hatches & is a child
# of the new <g> element
stroke_color = ' #000000 ' # default assumption
stroke_width = ' 1.0 ' # default value
try :
style = node . get ( ' style ' )
if style is not None :
declarations = style . split ( ' ; ' )
for i , declaration in enumerate ( declarations ) :
parts = declaration . split ( ' : ' , 2 )
if len ( parts ) == 2 :
( prop , val ) = parts
prop = prop . strip ( ) . lower ( )
if prop == ' stroke-width ' :
stroke_width = val . strip ( )
elif prop == ' stroke ' :
val = val . strip ( )
stroke_color = val
# for i,declaration in enumerate(declarations):
# if style != 'none':
finally :
style = { ' stroke ' : ' %s ' % stroke_color , ' fill ' : ' none ' , ' stroke-width ' : ' %s ' % stroke_width }
line_attribs = { ' style ' : simplestyle . formatStyle ( style ) , ' d ' : path }
tran = node . get ( ' transform ' )
if ( tran is not None ) and ( tran != ' ' ) :
line_attribs [ ' transform ' ] = tran
inkex . etree . SubElement ( g , inkex . addNS ( ' path ' , ' svg ' ) , line_attribs )
def makeHatchGrid ( self , angle , spacing , init = True ) : # returns True if succeeds in making grid, else False
"""
Build a grid of hatch lines which encompasses the entire bounding
box of the graphical elements we are to hatch .
1. Figure out the bounding box for all of the graphical elements
2. Pick a rectangle larger than that bounding box so that we can
later rotate the rectangle and still have it cover the bounding
box of the graphical elements .
3. Center the rectangle of 2 on the origin ( 0 , 0 ) .
4. Build the hatch line grid in this rectangle .
5. Rotate the rectangle by the hatch angle .
6. Translate the center of the rotated rectangle , ( 0 , 0 ) , to be
the center of the bounding box for the graphical elements .
7. We now have a grid of hatch lines which overlay the graphical
elements and can now be intersected with those graphical elements .
"""
# If this is the first call, do some one time initializations
# When generating cross hatches, we may be called more than once
if init :
self . getBoundingBox ( )
self . grid = [ ]
# Determine the width and height of the bounding box containing
# all the polygons to be hatched
w = self . xmax - self . xmin
h = self . ymax - self . ymin
bBoundingBoxExists = ( ( w != ( EXTREME_NEGATIVE_NUMBER - EXTREME_POSITIVE_NUMBER ) ) and ( h != ( EXTREME_NEGATIVE_NUMBER - EXTREME_POSITIVE_NUMBER ) ) )
retValue = bBoundingBoxExists
if bBoundingBoxExists :
# Nice thing about rectangles is that the diameter of the circle
# encompassing them is the length the rectangle's diagonal...
r = math . sqrt ( w * w + h * h ) / 2.0
# Length of a hatch line will be 2r
# Now generate hatch lines within the square
# centered at (0, 0) and with side length at least d
# While we could generate these lines running back and forth,
# that makes for weird behavior later when applying odd/even
# rules AND there are nested polygons. Instead, when we
# generate the SVG <path> elements with the hatch line
# segments, we can do the back and forth weaving.
# Rotation information
ca = math . cos ( math . radians ( 90 - angle ) )
sa = math . sin ( math . radians ( 90 - angle ) )
# Translation information
cx = self . xmin + ( w / 2 )
cy = self . ymin + ( h / 2 )
# Since the spacing may be fractional (e.g., 6.5), we
# don't try to use range() or other integer iterator
spacing = float ( abs ( spacing ) )
i = - r
while i < = r :
# Line starts at (i, -r) and goes to (i, +r)
x1 = cx + ( i * ca ) + ( r * sa ) # i * ca - (-r) * sa
y1 = cy + ( i * sa ) - ( r * ca ) # i * sa + (-r) * ca
x2 = cx + ( i * ca ) - ( r * sa ) # i * ca - (+r) * sa
y2 = cy + ( i * sa ) + ( r * ca ) # i * sa + (+r) * ca
i + = spacing
# Remove any potential hatch lines which are entirely
# outside of the bounding box
if ( ( x1 < self . xmin ) and ( x2 < self . xmin ) ) or \
( ( x1 > self . xmax ) and ( x2 > self . xmax ) ) :
continue
if ( ( y1 < self . ymin ) and ( y2 < self . ymin ) ) or \
( ( y1 > self . ymax ) and ( y2 > self . ymax ) ) :
continue
self . grid . append ( ( x1 , y1 , x2 , y2 ) )
# while i <= r:
# if bBoundingBoxExists:
return retValue
# def makeHatchGrid( self, angle, spacing, init=True ):
def effect ( self ) :
global referenceCount
global ptLastPositionAbsolute
# Viewbox handling
self . handleViewBox ( )
referenceCount = 0
ptLastPositionAbsolute = [ 0 , 0 ]
# Build a list of the vertices for the document's graphical elements
if self . options . ids :
# Traverse the selected objects
for id in self . options . ids :
self . recursivelyTraverseSvg ( [ self . selected [ id ] ] , self . docTransform )
else :
# Traverse the entire document
self . recursivelyTraverseSvg ( self . document . getroot ( ) , self . docTransform )
# After recursively traversing the svg, we will have a dictionary of transforms and hatches
# Target stroke width will be (doc width + doc height) / 2 / 1000
# stroke_width_target = ( self.docHeight + self.docWidth ) / 2000
# stroke_width_target = 1
stroke_width_target = 1
# Each hatch line stroke will be within an SVG object which may
# be subject to transforms. So, on an object by object basis,
# we need to transform our target width to a width suitable
# for that object (so that after the object and its hatches are
# transformed, the result has the desired width).
# To aid in the process, we use a diagonal line segment of length
# stroke_width_target. We then run this segment through an object's
# inverse transform and see what the resulting length of the inversely
# transformed segment is. We could, alternatively, look at the
# x and y scaling factors in the transform and average them.
s = stroke_width_target / math . sqrt ( 2 )
# Now, dump the hatch fills sorted by which document element
# they correspond to. This is made easy by the fact that we
# saved the information and used each element's lxml.etree node
# pointer as the dictionary key under which to save the hatch
# fills for that node.
absoluteLineSegments = { }
nAbsoluteLineSegmentTotal = 0
nPenLifts = 0
# To implement
for key in self . hatches :
direction = True
if key in self . transforms :
transform = inverseTransform ( self . transforms [ key ] )
# Determine the scaled stroke width for a hatch line
# We produce a line segment of unit length, transform
# its endpoints and then determine the length of the
# resulting line segment.
pt1 = [ 0 , 0 ]
pt2 = [ s , s ]
applyTransformToPoint ( transform , pt1 )
applyTransformToPoint ( transform , pt2 )
dx = pt2 [ 0 ] - pt1 [ 0 ]
dy = pt2 [ 1 ] - pt1 [ 1 ]
stroke_width = math . sqrt ( dx * dx + dy * dy )
else :
transform = None
stroke_width = float ( 1.0 )
# The transform also applies to the hatch spacing we use when searching for end connections
transformedHatchSpacing = stroke_width * self . options . hatchSpacing
path = ' ' # regardless of whether or not we're reducing pen lifts
ptLastPositionAbsolute = [ 0 , 0 ]
ptLastPositionAbsolute [ 0 ] = 0
ptLastPositionAbsolute [ 1 ] = 0
fDistanceMovedWithPenUp = 0
if not self . options . reducePenLifts :
for segment in self . hatches [ key ] :
if len ( segment ) < 2 :
continue
pt1 = segment [ 0 ]
pt2 = segment [ 1 ]
# Okay, we're going to put these hatch lines into the same
# group as the element they hatch. That element is down
# some chain of SVG elements, some of which may have
# transforms attached. But, our hatch lines have been
# computed assuming that those transforms have already
# been applied (since we had to apply them so as to know
# where this element is on the page relative to other
# elements and their transforms). So, we need to invert
# the transforms for this element and then either apply
# that inverse transform here and now or set it in a
# transform attribute of the <path> element. Having it
# set in the path element seems a bit counterintuitive
# after the fact (i.e., what's this transform here for?).
# So, we compute the inverse transform and apply it here.
if transform is not None :
applyTransformToPoint ( transform , pt1 )
applyTransformToPoint ( transform , pt2 )
# Now generate the path data for the <path>
if direction :
# Go this direction
path + = ( ' M %f , %f l %f , %f ' %
( pt1 [ 0 ] , pt1 [ 1 ] , pt2 [ 0 ] - pt1 [ 0 ] , pt2 [ 1 ] - pt1 [ 1 ] ) )
else :
# Or go this direction
path + = ( ' M %f , %f l %f , %f ' %
( pt2 [ 0 ] , pt2 [ 1 ] , pt1 [ 0 ] - pt2 [ 0 ] , pt1 [ 1 ] - pt2 [ 1 ] ) )
direction = not direction
# for segment in self.hatches[key]:
self . joinFillsWithNode ( key , stroke_width , path [ : - 1 ] )
else : # if not self.options.reducePenLifts:
for segment in self . hatches [ key ] :
if len ( segment ) < 2 : # Copied from original, no idea why this is needed [sbm]
continue
if direction :
pt1 = segment [ 0 ]
pt2 = segment [ 1 ]
else :
pt1 = segment [ 1 ]
pt2 = segment [ 0 ]
# Okay, we're going to put these hatch lines into the same
# group as the element they hatch. That element is down
# some chain of SVG elements, some of which may have
# transforms attached. But, our hatch lines have been
# computed assuming that those transforms have already
# been applied (since we had to apply them so as to know
# where this element is on the page relative to other
# elements and their transforms). So, we need to invert
# the transforms for this element and then either apply
# that inverse transform here and now or set it in a
# transform attribute of the <path> element. Having it
# set in the path element seems a bit counterintuitive
# after the fact (i.e., what's this transform here for?).
# So, we compute the inverse transform and apply it here.
if transform is not None :
applyTransformToPoint ( transform , pt1 )
applyTransformToPoint ( transform , pt2 )
# Now generate the path data for the <path>
# BUT we want to combine as many paths as possible to reduce pen lifts.
# In order to combine paths, we need to know all of the path segments.
# The solution to this conundrum is to generate all path segments,
# but instead of drawing them into the path right away, we put them in
# an array where they'll be available for random access
# by our anti-pen-lift algorithm
absoluteLineSegments [ nAbsoluteLineSegmentTotal ] = [ pt1 , pt2 , False ] # False indicates that segment has not yet been drawn
nAbsoluteLineSegmentTotal + = 1
direction = not direction
# for segment in self.hatches[key]:
# Now have a nice juicy buffer full of line segments with absolute coordinates
fProposedNeighborhoodRadiusSquared = self . ProposeNeighborhoodRadiusSquared ( transformedHatchSpacing ) # Just fixed and simple for now - may make function of neighborhood later
for referenceCount in range ( nAbsoluteLineSegmentTotal ) : # This is the entire range of segments,
# Sets global referenceCount to segment which has an end closest to current pen position.
# Doesn't need to select which end is closest, as that will happen below, with nReferenceEndIndex.
# When we have gone thru this whole range, we will be completely done.
# We only get here again, after all _connected_ segments have been "drawn".
if not absoluteLineSegments [ referenceCount ] [ 2 ] : # Test whether this segment has been drawn
# Has not been drawn yet
# Before we do any irrevocable changes to path, let's see if we are going to be able to append any segments.
# The below solution is inelegant, but has the virtue of being relatively simple to implement.
# Pre-qualify this segment on the issue of whether it has any connecting segments.
# If it does not, then just add the path for this one segment, and go on to the next.
# If it does have connecting segments, we need to go through the recursive logic.
# Lazily, again, select the desired direction of line ahead of time.
bFoundSegmentToAdd = False # default assumption
nReferenceEndIndexAtClosest = 0
nInnerCountAtClosest = - 1
fClosestDistanceSquared = 123456 # just a random large number
for nReferenceEndIndex in range ( 2 ) :
ptReference = absoluteLineSegments [ referenceCount ] [ nReferenceEndIndex ]
ptReferenceOtherEnd = absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndex ]
fReferenceDirectionRadians = math . atan2 ( ptReferenceOtherEnd [ 1 ] - ptReference [ 1 ] , ptReferenceOtherEnd [ 0 ] - ptReference [ 0 ] ) # from other end to this end
# The following is just a simple copy from the routine in recursivelyAppendNearbySegmentIfAny procedure
# Look through all possibilities to choose the closest that fulfills all requirements e.g. direction and colinearity
for innerCount in range ( nAbsoluteLineSegmentTotal ) : # investigate all segments
if not absoluteLineSegments [ innerCount ] [ 2 ] :
# This segment currently undrawn, so it is a candidate for a path extension
# Need to check both ends of each and every proposed segment so we can find the most appropriate one
# Define pt2 in the reference as the end which we want to extend
for nNewSegmentInitialEndIndex in range ( 2 ) :
# First try initial end of test segment (aka pt1) vs final end (aka pt2) of reference segment
if innerCount != referenceCount : # don't investigate self ends
deltaX = absoluteLineSegments [ innerCount ] [ nNewSegmentInitialEndIndex ] [ 0 ] - ptReference [ 0 ] # proposed initial pt1 X minus existing final pt1 X
deltaY = absoluteLineSegments [ innerCount ] [ nNewSegmentInitialEndIndex ] [ 1 ] - ptReference [ 1 ] # proposed initial pt1 Y minus existing final pt1 Y
if ( deltaX * deltaX + deltaY * deltaY ) < fProposedNeighborhoodRadiusSquared :
fThisDistanceSquared = deltaX * deltaX + deltaY * deltaY
ptNewSegmentThisEnd = absoluteLineSegments [ innerCount ] [ nNewSegmentInitialEndIndex ]
ptNewSegmentOtherEnd = absoluteLineSegments [ innerCount ] [ not nNewSegmentInitialEndIndex ]
fNewSegmentDirectionRadians = math . atan2 ( ptNewSegmentThisEnd [ 1 ] - ptNewSegmentOtherEnd [ 1 ] , ptNewSegmentThisEnd [ 0 ] - ptNewSegmentOtherEnd [ 0 ] ) # from other end to this end
# If this end would cause an alternating direction,
# then exclude it
if not self . WouldBeAnAlternatingDirection ( fReferenceDirectionRadians , fNewSegmentDirectionRadians ) :
pass
# break # out of for nNewSegmentInitialEndIndex in range( 2 ):
# if ( not self.WouldBeAnAlternatingDirection( fReferenceDirectionRadians, fNewSegmentDirectionRadians ) ):
elif fThisDistanceSquared < fClosestDistanceSquared :
# One other thing could rule out choosing this segment end:
# Want to screen and remove two segments that, while close enough,
# should be disqualified because they are colinear. The reason for this is that
# if they are colinear, they arose from the same global grid line, which means
# that the gap between them arises from intersections with the boundary.
# The idea here is that, all things being more-or-less equal,
# we would like to give preference to connecting to a segment
# which is the reverse of our current direction. This makes for better
# bezier curve join.
# The criterion for being colinear is that the reference segment angle is effectively
# the same as the line connecting the reference segment to the end of the new segment.
fJoinerDirectionRadians = math . atan2 ( ptNewSegmentThisEnd [ 1 ] - ptReference [ 1 ] , ptNewSegmentThisEnd [ 0 ] - ptReference [ 0 ] )
if not self . AreCoLinear ( fReferenceDirectionRadians , fJoinerDirectionRadians ) :
# not colinear
fClosestDistanceSquared = fThisDistanceSquared
bFoundSegmentToAdd = True
nReferenceEndIndexAtClosest = nReferenceEndIndex
nInnerCountAtClosest = innerCount
deltaXAtClosest = deltaX
deltaYAtClosest = deltaY
# if ( not self.AreCoLinear( fReferenceDirectionRadians, fJoinerDirectionRadians) ):
# if ( fThisDistanceSquared < fClosestDistanceSquared ):
# if ( ( deltaX * deltaX + deltaY * deltaY ) < fProposedNeighborhoodRadiusSquared ):
# if ( innerCount != referenceCount ):
# for nNewSegmentInitialEndIndex in range( 2 ):
# if ( not absoluteLineSegments[2] ):
# for innerCount in range( nAbsoluteLineSegmentTotal ):
# for nReferenceEndIndex in range( 2 ):
# At last we've looked at all the candidate segment ends, as related to all the reference ends
if not bFoundSegmentToAdd :
# This segment is solitary.
# Must start a new line, not joined to any previous paths
deltaX = absoluteLineSegments [ referenceCount ] [ 1 ] [ 0 ] - absoluteLineSegments [ referenceCount ] [ 0 ] [ 0 ] # end minus start, in original direction
deltaY = absoluteLineSegments [ referenceCount ] [ 1 ] [ 1 ] - absoluteLineSegments [ referenceCount ] [ 0 ] [ 1 ] # end minus start, in original direction
path + = ( ' M %f , %f l %f , %f ' %
( absoluteLineSegments [ referenceCount ] [ 0 ] [ 0 ] , absoluteLineSegments [ referenceCount ] [ 0 ] [ 1 ] ,
deltaX , deltaY ) ) # delta is from initial point
fDistanceMovedWithPenUp + = math . hypot (
absoluteLineSegments [ referenceCount ] [ 0 ] [ 0 ] - ptLastPositionAbsolute [ 0 ] ,
absoluteLineSegments [ referenceCount ] [ 0 ] [ 1 ] - ptLastPositionAbsolute [ 1 ] )
ptLastPositionAbsolute [ 0 ] = absoluteLineSegments [ referenceCount ] [ 0 ] [ 0 ] + deltaX
ptLastPositionAbsolute [ 1 ] = absoluteLineSegments [ referenceCount ] [ 0 ] [ 1 ] + deltaY
absoluteLineSegments [ referenceCount ] [ 2 ] = True # True flags that this line segment has been
# added to the path to be drawn, so should
# no longer be a candidate for any kind of move.
nPenLifts + = 1
else : # if ( not bFoundSegmentToAdd ):
# Found segment to add, and we must get to it in absolute terms
deltaX = ( absoluteLineSegments [ referenceCount ] [ nReferenceEndIndexAtClosest ] [ 0 ] -
absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 0 ] )
# final point (which was closer to the closest continuation segment) minus initial point = deltaX
deltaY = ( absoluteLineSegments [ referenceCount ] [ nReferenceEndIndexAtClosest ] [ 1 ] -
absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 1 ] )
# final point (which was closer to the closest continuation segment) minus initial point = deltaY
path + = ( ' M %f , %f l ' % (
absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 0 ] ,
absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 1 ] ) )
fDistanceMovedWithPenUp + = math . hypot (
absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 0 ] - ptLastPositionAbsolute [ 0 ] ,
absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 1 ] - ptLastPositionAbsolute [ 1 ] )
ptLastPositionAbsolute [ 0 ] = absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 0 ]
ptLastPositionAbsolute [ 1 ] = absoluteLineSegments [ referenceCount ] [ not nReferenceEndIndexAtClosest ] [ 1 ]
# Note that this does not complete the line, as the completion (the deltaX, deltaY part) is being held in abeyance
# We are coming up on a problem:
# If we add a curve to the end of the line, we have made the curve extend beyond the end of the line,
# and thus beyond the boundaries we should be respecting.
# The solution is to hold in abeyance the actual plotting of the line,
# holding it available for shrinking if a curve is to be added.
# That is
relativePositionOfLastPlottedLineWasHeldInAbeyance = { }
relativePositionOfLastPlottedLineWasHeldInAbeyance [ 0 ] = deltaX # delta is from initial point
relativePositionOfLastPlottedLineWasHeldInAbeyance [ 1 ] = deltaY # Will be printed after we know if it must be modified
# to keep the ending join within bounds
ptLastPositionAbsolute [ 0 ] + = deltaX
ptLastPositionAbsolute [ 1 ] + = deltaY
absoluteLineSegments [ referenceCount ] [ 2 ] = True # True flags that this line segment has been
# added to the path to be drawn, so should
# no longer be a candidate for any kind of move.
nPenLifts + = 1
# Now comes the speedup logic:
# We've just drawn a segment starting at an absolute, not relative, position.
# It was drawn from pt1 to pt2.
# Look for an as-yet-not-drawn segment which has a beginning or ending
# point "near" the end point of this absolute draw, and leave the pen down
# while moving to and then drawing this found line.
# Do this recursively, marking each segment True to show that
# it has been "drawn" already.
# pt2 is the reference point, ie. the point from which the next segment will start
path = self . recursivelyAppendNearbySegmentIfAny (
transformedHatchSpacing ,
0 ,
referenceCount ,
nReferenceEndIndexAtClosest ,
nAbsoluteLineSegmentTotal ,
absoluteLineSegments ,
path ,
relativePositionOfLastPlottedLineWasHeldInAbeyance )
# if ( not bFoundSegmentToAdd ): else:
# if ( not absoluteLineSegments[referenceCount][2] ):
# while ( self.IndexOfNearestSegmentToLastPosition() ):
self . joinFillsWithNode ( key , stroke_width , path [ : - 1 ] )
# if not self.options.reducePenLifts: else:
# for key in self.hatches:
"""
if self . options . reducePenLifts :
if ( nAbsoluteLineSegmentTotal != 0 ) :
inkex . errormsg ( ' Saved %i %% of %i pen lifts. ' % ( 100 * ( nAbsoluteLineSegmentTotal - nPenLifts ) / nAbsoluteLineSegmentTotal , nAbsoluteLineSegmentTotal ) )
inkex . errormsg ( ' pen lifts= %i , line segments= %i ' % ( nPenLifts , nAbsoluteLineSegmentTotal ) )
else :
inkex . errormsg ( ' No lines were plotted ' )
inkex . errormsg ( ' Press OK ' )
# if self.options.reducePenLifts:
#inkex.errormsg("Elapsed CPU time was %f" % (time.clock()-self.t0))
"""
# def effect( self ):
def recursivelyAppendNearbySegmentIfAny (
self ,
transformedHatchSpacing ,
nRecursionCount ,
nReferenceSegmentCount ,
nReferenceEndIndex ,
nAbsoluteLineSegmentTotal ,
absoluteLineSegments ,
cumulativePath ,
relativePositionOfLastPlottedLineWasHeldInAbeyance ) :
global ptLastPositionAbsolute
fProposedNeighborhoodRadiusSquared = self . ProposeNeighborhoodRadiusSquared ( transformedHatchSpacing )
# Look through all possibilities to choose the closest
bFoundSegmentToAdd = False # default assumption
nNewSegmentInitialEndIndexAtClosest = 0
nOuterCountAtClosest = - 1
fClosestDistanceSquared = 123456789.0 # just a random large number
ptReference = absoluteLineSegments [ nReferenceSegmentCount ] [ nReferenceEndIndex ]
ptReferenceOtherEnd = absoluteLineSegments [ nReferenceSegmentCount ] [ not nReferenceEndIndex ]
fReferenceDeltaX = ptReferenceOtherEnd [ 0 ] - ptReference [ 0 ]
fReferenceDeltaY = ptReferenceOtherEnd [ 1 ] - ptReference [ 1 ]
fReferenceDirectionRadians = math . atan2 ( fReferenceDeltaY , fReferenceDeltaX ) # from other end to this end
for outerCount in range ( nAbsoluteLineSegmentTotal ) : # investigate all segments
if not absoluteLineSegments [ outerCount ] [ 2 ] :
# This segment currently undrawn, so it is a candidate for a path extension
# Need to check both ends of each and every proposed segment until we find one in the neighborhood
# Defines pt2 in the reference as the end which we want to extend
for nNewSegmentInitialEndIndex in range ( 2 ) :
# First try initial end of test segment (aka pt1) vs final end (aka pt2) of reference segment
if outerCount != nReferenceSegmentCount : # don't investigate self ends
deltaX = absoluteLineSegments [ outerCount ] [ nNewSegmentInitialEndIndex ] [ 0 ] - ptReference [ 0 ] # proposed initial pt1 X minus existing final pt1 X
deltaY = absoluteLineSegments [ outerCount ] [ nNewSegmentInitialEndIndex ] [ 1 ] - ptReference [ 1 ] # proposed initial pt1 Y minus existing final pt1 Y
if ( deltaX * deltaX + deltaY * deltaY ) < fProposedNeighborhoodRadiusSquared :
fThisDistanceSquared = deltaX * deltaX + deltaY * deltaY
ptNewSegmentThisEnd = absoluteLineSegments [ outerCount ] [ nNewSegmentInitialEndIndex ]
ptNewSegmentOtherEnd = absoluteLineSegments [ outerCount ] [ not nNewSegmentInitialEndIndex ]
fNewSegmentDeltaX = ptNewSegmentThisEnd [ 0 ] - ptNewSegmentOtherEnd [ 0 ]
fNewSegmentDeltaY = ptNewSegmentThisEnd [ 1 ] - ptNewSegmentOtherEnd [ 1 ]
fNewSegmentDirectionRadians = math . atan2 ( fNewSegmentDeltaY , fNewSegmentDeltaX ) # from other end to this end
if not self . WouldBeAnAlternatingDirection ( fReferenceDirectionRadians , fNewSegmentDirectionRadians ) :
# If this end would cause an alternating direction,
# then exclude it regardless of how close it is
pass
# if ( not self.WouldBeAnAlternatingDirection( fReferenceDirectionRadians, fNewSegmentDirectionRadians ) ):
elif fThisDistanceSquared < fClosestDistanceSquared :
# One other thing could rule out choosing this segment end:
# Want to screen and remove two segments that, while close enough,
# should be disqualified because they are colinear. The reason for this is that
# if they are colinear, they arose from the same global grid line, which means
# that the gap between them arises from intersections with the boundary.
# The idea here is that, all things being more-or-less equal,
# we would like to give preference to connecting to a segment
# which is the reverse of our current direction. This makes for better
# bezier curve join.
# The criterion for being colinear is that the reference segment angle is effectively
# the same as the line connecting the reference segment to the end of the new segment.
fJoinerDirectionRadians = math . atan2 ( ptNewSegmentThisEnd [ 1 ] - ptReference [ 1 ] , ptNewSegmentThisEnd [ 0 ] - ptReference [ 0 ] )
if not self . AreCoLinear ( fReferenceDirectionRadians , fJoinerDirectionRadians ) :
# not colinear
fClosestDistanceSquared = fThisDistanceSquared
bFoundSegmentToAdd = True
nNewSegmentInitialEndIndexAtClosest = nNewSegmentInitialEndIndex
nOuterCountAtClosest = outerCount
deltaXAtClosest = deltaX
deltaYAtClosest = deltaY
# if ( not self.AreCoLinear( fReferenceDirectionRadians, fJoinerDirectionRadians) ):
# if ( not self.WouldBeAnAlternatingDirection( fReferenceDirectionRadians, fNewSegmentDirectionRadians ) ): elif ( fThisDistanceSquared < fClosestDistanceSquared ):
# if ( ( deltaX * deltaX + deltaY * deltaY ) < fProposedNeighborhoodRadiusSquared ):
# if ( outerCount != nReferenceSegmentCount ):
# for nNewSegmentInitialEndIndex in range( 2 ):
# if ( not absoluteLineSegments[2] ):
# for outerCount in range( nAbsoluteLineSegmentTotal ):
# At last we've looked at all the candidate segment ends
nRecursionCount + = 1
if ( not bFoundSegmentToAdd ) or ( nRecursionCount > = RECURSION_LIMIT ) :
cumulativePath + = ' %f , %f ' % ( relativePositionOfLastPlottedLineWasHeldInAbeyance [ 0 ] , relativePositionOfLastPlottedLineWasHeldInAbeyance [ 1 ] ) # close out this segment
ptLastPositionAbsolute [ 0 ] + = relativePositionOfLastPlottedLineWasHeldInAbeyance [ 0 ]
ptLastPositionAbsolute [ 1 ] + = relativePositionOfLastPlottedLineWasHeldInAbeyance [ 1 ]
return cumulativePath # No undrawn segments were suitable for appending,
# or there were so many that we worry about python recursion limit
else : # if ( not bFoundSegmentToAdd ):
nNewSegmentInitialEndIndex = nNewSegmentInitialEndIndexAtClosest
nNewSegmentFinalEndIndex = not nNewSegmentInitialEndIndex
# nNewSegmentInitialEndIndex is 0 for connecting to pt1,
# and is 1 for connecting to pt2
count = nOuterCountAtClosest # count is the index of the segment to be appended.
deltaX = deltaXAtClosest # delta from final end of incoming segment to initial end of outgoing segment
deltaY = deltaYAtClosest
# First, move pen to initial end (may be either its pt1 or its pt2) of new segment
# Insert a bezier curve for this transition element
# To accomplish this, we need information on the incoming and outgoing segments.
# Specifically, we need to know the lengths and angles of the segments in
# order to decide on control points.
fIncomingDeltaX = absoluteLineSegments [ nReferenceSegmentCount ] [ nReferenceEndIndex ] [ 0 ] - absoluteLineSegments [ nReferenceSegmentCount ] [ not nReferenceEndIndex ] [ 0 ]
fIncomingDeltaY = absoluteLineSegments [ nReferenceSegmentCount ] [ nReferenceEndIndex ] [ 1 ] - absoluteLineSegments [ nReferenceSegmentCount ] [ not nReferenceEndIndex ] [ 1 ]
# The outgoing deltas are based on the reverse direction of the segment, i.e. the segment pointing back to the joiner bezier curve
fOutgoingDeltaX = absoluteLineSegments [ count ] [ nNewSegmentInitialEndIndex ] [ 0 ] - absoluteLineSegments [ count ] [ nNewSegmentFinalEndIndex ] [ 0 ] # index is [count][start point = 0, final point = 1][0=x, 1=y]
fOutgoingDeltaY = absoluteLineSegments [ count ] [ nNewSegmentInitialEndIndex ] [ 1 ] - absoluteLineSegments [ count ] [ nNewSegmentFinalEndIndex ] [ 1 ]
lengthOfIncoming = math . hypot ( fIncomingDeltaX , fIncomingDeltaY )
lengthOfOutgoing = math . hypot ( fOutgoingDeltaX , fOutgoingDeltaY )
# We are going to trim-up the ends of the incoming and outgoing segments,
# in order to get a curve which reliably does not extend beyond the boundary.
# Crude readings from inkscape on bezier curve overshoot, using control points extended hatch-spacing distance parallel to segment:
# when end points are in line, overshoot 12/16 in direction of segment
# when at 45 degrees, overshoot 12/16 in direction of segment
# when at 60 degrees, overshoot 12/16 in direction of segment
# Conclusion, at any angle, remove 0.75 * hatch spacing from the length of both lines,
# where 0.75 is, by no coincidence, BEZIER_OVERSHOOT_MULTIPLIER
# If hatches are getting quite short, we can use a smaller Bezier loop at
# the end to squeeze into smaller spaces. We'll use a normal nice smooth
# curve for non-short hatches
fDesiredShortenForSmoothestJoin = transformedHatchSpacing * BEZIER_OVERSHOOT_MULTIPLIER # This is what we really want to use for smooth curves
# Separately check incoming vs outgoing lengths to see if bezier distances must be reduced,
# then choose greatest reduction to apply to both - lest we go off-course
# Finally, clip reduction to be no less than 1.0
fControlPointDividerIncoming = 2.0 * fDesiredShortenForSmoothestJoin / lengthOfIncoming
fControlPointDividerOutgoing = 2.0 * fDesiredShortenForSmoothestJoin / lengthOfOutgoing
if fControlPointDividerIncoming > fControlPointDividerOutgoing :
fLargestDesiredControlPointDivider = fControlPointDividerIncoming
else : # if ( fControlPointDividerIncoming > fControlPointDividerOutgoing ):
fLargestDesiredControlPointDivider = fControlPointDividerOutgoing
# if ( fControlPointDividerIncoming > fControlPointDividerOutgoing ): else:
if fLargestDesiredControlPointDivider < 1.0 :
fControlPointDivider = 1.0
else : # if (fLargestDesiredControlPointDivider < 1.0):
fControlPointDivider = fLargestDesiredControlPointDivider
# if (fLargestDesiredControlPointDivider < 1.0): else:
fDesiredShorten = fDesiredShortenForSmoothestJoin / fControlPointDivider
ptDeltaToSubtractFromIncomingEnd = self . RelativeControlPointPosition ( fDesiredShorten , fIncomingDeltaX , fIncomingDeltaY , 0 , 0 )
# Note that this will be subtracted from the _point held in abeyance_.
relativePositionOfLastPlottedLineWasHeldInAbeyance [ 0 ] - = ptDeltaToSubtractFromIncomingEnd [ 0 ]
relativePositionOfLastPlottedLineWasHeldInAbeyance [ 1 ] - = ptDeltaToSubtractFromIncomingEnd [ 1 ]
ptDeltaToAddToOutgoingStart = self . RelativeControlPointPosition ( fDesiredShorten , fOutgoingDeltaX , fOutgoingDeltaY , 0 , 0 )
# We know that when we tack on a curve, we must chop some off the end of the incoming segment,
# and also chop some off the start of the outgoing segment.
# Now, we know we want the control points to be on a projection of each segment,
# in order that there be no abrupt change of plotting angle. The question is, how
# far beyond the endpoint should we place the control point.
ptRelativeControlPointIncoming = self . RelativeControlPointPosition (
transformedHatchSpacing / fControlPointDivider ,
fIncomingDeltaX ,
fIncomingDeltaY ,
0 ,
0 )
ptRelativeControlPointOutgoing = self . RelativeControlPointPosition (
transformedHatchSpacing / fControlPointDivider ,
fOutgoingDeltaX ,
fOutgoingDeltaY ,
deltaX ,
deltaY )
cumulativePath + = ' %f , %f ' % ( relativePositionOfLastPlottedLineWasHeldInAbeyance [ 0 ] , relativePositionOfLastPlottedLineWasHeldInAbeyance [ 1 ] ) # close out this segment, which has been modified
ptLastPositionAbsolute [ 0 ] + = relativePositionOfLastPlottedLineWasHeldInAbeyance [ 0 ]
ptLastPositionAbsolute [ 1 ] + = relativePositionOfLastPlottedLineWasHeldInAbeyance [ 1 ]
# add bezier cubic curve
cumulativePath + = ( ' c %f , %f %f , %f %f , %f l ' %
( ptRelativeControlPointIncoming [ 0 ] ,
ptRelativeControlPointIncoming [ 1 ] ,
ptRelativeControlPointOutgoing [ 0 ] ,
ptRelativeControlPointOutgoing [ 1 ] ,
deltaX ,
deltaY ) )
ptLastPositionAbsolute [ 0 ] + = deltaX
ptLastPositionAbsolute [ 1 ] + = deltaY
# Next, move pen in appropriate direction to draw the new segment, given that
# we have just moved to the initial end of the new segment.
# This needs special treatment, as we just did some length changing.
deltaX = absoluteLineSegments [ count ] [ nNewSegmentFinalEndIndex ] [ 0 ] - absoluteLineSegments [ count ] [ nNewSegmentInitialEndIndex ] [ 0 ] + ptDeltaToAddToOutgoingStart [ 0 ]
deltaY = absoluteLineSegments [ count ] [ nNewSegmentFinalEndIndex ] [ 1 ] - absoluteLineSegments [ count ] [ nNewSegmentInitialEndIndex ] [ 1 ] + ptDeltaToAddToOutgoingStart [ 1 ]
relativePositionOfLastPlottedLineWasHeldInAbeyance [ 0 ] = deltaX # delta is from initial point
relativePositionOfLastPlottedLineWasHeldInAbeyance [ 1 ] = deltaY # Will be printed after we know if it must be modified
# Mark this segment as drawn
absoluteLineSegments [ count ] [ 2 ] = True
cumulativePath = self . recursivelyAppendNearbySegmentIfAny ( transformedHatchSpacing , nRecursionCount , count , nNewSegmentFinalEndIndex , nAbsoluteLineSegmentTotal , absoluteLineSegments , cumulativePath , relativePositionOfLastPlottedLineWasHeldInAbeyance )
return cumulativePath
# if ( not bFoundSegmentToAdd ): else:
# def recursivelyAppendNearbySegmentIfAny( ... ):
def ProposeNeighborhoodRadiusSquared ( self , transformedHatchSpacing ) :
return transformedHatchSpacing * transformedHatchSpacing * self . options . hatchScope * self . options . hatchScope
# The multiplier of x generates a radius of x^0.5 times the hatch spacing.
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
ptReturn = [ 0 , 0 ]
if fDeltaX == 0 :
ptReturn [ 0 ] = deltaX
ptReturn [ 1 ] = math . copysign ( distance , fDeltaY ) + deltaY
elif fDeltaY == 0 :
ptReturn [ 0 ] = math . copysign ( distance , fDeltaX ) + deltaX
ptReturn [ 1 ] = deltaY
else :
fSlope = math . atan2 ( fDeltaY , fDeltaX )
ptReturn [ 0 ] = distance * math . cos ( fSlope ) + deltaX
ptReturn [ 1 ] = distance * math . sin ( fSlope ) + deltaY
return ptReturn
def WouldBeAnAlternatingDirection ( self , fReferenceDirectionRadians , fNewSegmentDirectionRadians ) :
# atan2 returns values in the range -pi to +pi, so we must evaluate difference values
# in the range of -2*pi to +2*pi
fDirectionDifferenceRadians = fReferenceDirectionRadians - fNewSegmentDirectionRadians
if fDirectionDifferenceRadians < 0 :
fDirectionDifferenceRadians + = 2 * math . pi
# Without having changed the vector direction of the difference, we have
# now reduced the range to 0 to 2*pi
fDirectionDifferenceRadians - = math . pi # flip opposite direction to coincide with same direction
# Of course they may not be _exactly_ pi different due to osmosis, so allow a tolerance
bRetVal = ( abs ( fDirectionDifferenceRadians ) < RADIAN_TOLERANCE_FOR_ALTERNATING_DIRECTION )
return bRetVal
def AreCoLinear ( self , fDirection1Radians , fDirection2Radians ) :
# allow slight difference in angles, for floating-point indeterminacy
fAbsDeltaRadians = abs ( fDirection1Radians - fDirection2Radians )
if fAbsDeltaRadians < RADIAN_TOLERANCE_FOR_COLINEAR :
return True
elif abs ( fAbsDeltaRadians - math . pi ) < RADIAN_TOLERANCE_FOR_COLINEAR :
return True
else :
return False
2010-11-27 20:44:32 +00:00
2018-03-29 21:39:43 +00:00
2010-11-27 20:44:32 +00:00
if __name__ == ' __main__ ' :
2018-03-30 04:45:11 +00:00
e = Eggbot_Hatch ( )
e . affect ( )