From 66923797cdc866d3ac530b8a0a98e518841126bd Mon Sep 17 00:00:00 2001 From: javl Date: Tue, 30 Aug 2016 12:19:17 +0200 Subject: [PATCH] Adds some extra options to eggbot_hatch. These are especially usefull when working with generated content that has a lot of elements you don't want to have to alter one-by-one. First is the option to delete the original object that was hatched. This allows you to only plot the newly made hatched fill, not the outline of the original object. Second is an option to use ranges for the hatchAngle and hatchSpacing variables. For each element it's brightness is calculated (0.1-1.0, where 0.0 is a black element and 1.0 is a white one) and this brightness is then mapped to the range. So if you select 0 to 30 for your angle range, black elements will use 0 degrees, white elements will use 30 and gray elements can use any value in between. This allows for the creation of some nice effects. --- inkscape_driver/eggbot_hatch.inx | 29 +- inkscape_driver/eggbot_hatch.py | 839 +++++++++++++++++-------------- 2 files changed, 483 insertions(+), 385 deletions(-) diff --git a/inkscape_driver/eggbot_hatch.inx b/inkscape_driver/eggbot_hatch.inx index 2177f312..b063882a 100755 --- a/inkscape_driver/eggbot_hatch.inx +++ b/inkscape_driver/eggbot_hatch.inx @@ -19,10 +19,11 @@ This extension fills each closed figure in your drawing with a path consisting of back and forth drawn "hatch" lines. If any objects are selected, then only those selected objects -will be filled. +will be filled. Hatched figures will be grouped with their fills. + 5.0 45 false @@ -33,10 +34,16 @@ Hatched figures will be grouped with their fills. 1.0 20.0 + false + false + 5.0 + 10.0 + 45 + 90 <_param name="aboutpage" type="description" xml:space="preserve"> -Hatch spacing is the distance between hatch lines, +Hatch spacing is the distance between hatch lines, measured in units of screen pixels (px). Angles are in degrees from horizontal; for example 90 is vertical. @@ -48,9 +55,9 @@ nearby line ends with a smoothly flowing curve, to improve the smoothness of plotting. The Range parameter sets the distance (in hatch widths) -over which that option searches for segments to join. +over which that option searches for segments to join. Large values may result in hatches where you don't want -them. Consider using a value in the range of 2-4. +them. Consider using a value in the range of 2-4. The Inset option allows you to hold back the edges of the fill somewhat from the edge of your original object. @@ -61,8 +68,18 @@ The hatches will be the same color and width as the original object. The Tolerance parameter affects how precisely -the hatches try to fill the input paths. - +the hatches try to fill the input paths. + +The Remove original element parameter lets the script delete +the element you choose to hatch, so only the new hatch will +remain (no outline around the object). + +When Use range is selected the hatch settings for each +element will depend on its brightness, with black elements +using the minimum value of the set range, white elements +using the maximum and gray elements using any +value in between. + diff --git a/inkscape_driver/eggbot_hatch.py b/inkscape_driver/eggbot_hatch.py index cb1672c1..c0bd9539 100755 --- a/inkscape_driver/eggbot_hatch.py +++ b/inkscape_driver/eggbot_hatch.py @@ -70,7 +70,7 @@ # Add min span/gap width # Updated by Windell H. Oskay, 1/8/2016 -# Added live preview and correct issue with nonzero min gap +# 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 @@ -79,7 +79,7 @@ # 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 -# +# # 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 @@ -319,7 +319,7 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps 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 ) @@ -355,19 +355,19 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps # 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 @@ -386,7 +386,7 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps 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 ): + # if ( s >= 0.0 ) and ( s <= 1.0 ): P3 = P4 # for P4 in subpath[1:]: # for subpath in paths[path]: @@ -397,14 +397,14 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps 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] @@ -431,7 +431,7 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps 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]] ) @@ -441,16 +441,16 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps # 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. @@ -462,7 +462,7 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps # 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. @@ -470,7 +470,7 @@ def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps # 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 ): @@ -488,13 +488,13 @@ def RelativeControlPointPosition( self, distance, fDeltaX, fDeltaY, deltaX, delt # 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 ): - + def inverseTransform ( tran ): ''' An SVG transform matrix looks like @@ -627,14 +627,38 @@ def __init__( self ): "--crossHatch", action="store", dest="crossHatch", type="inkbool", default=False, help="Generate a cross hatch pattern" ) + self.OptionParser.add_option( + "--removeOriginal", action="store", dest="removeOriginal", + type="inkbool", default=False, + help="Remove the original object" ) + self.OptionParser.add_option( + "--useRange", action="store", dest="useRange", + type="inkbool", default=False, + help="Use a range of values instead of fixed values" ) 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( + "--minHatchAngle", action="store", type="float", + dest="minHatchAngle", default=90.0, + help="Minimum angle of inclination for hatch lines" ) + self.OptionParser.add_option( + "--maxHatchAngle", action="store", type="float", + dest="maxHatchAngle", default=90.0, + help="Maximum 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( + "--minHatchSpacing", action="store", type="float", + dest="minHatchSpacing", default=10.0, + help="Minimum spacing between hatch lines" ) + self.OptionParser.add_option( + "--maxHatchSpacing", action="store", type="float", + dest="maxHatchSpacing", default=10.0, + help="Maximum spacing between hatch lines" ) self.OptionParser.add_option( "--tolerance", action="store", type="float", dest="tolerance", default=20.0, @@ -653,7 +677,7 @@ def getDocProps( self ): self.docHeight = plot_utils.getLength( self, 'height', N_PAGE_HEIGHT ) self.docWidth = plot_utils.getLength( self, 'width', N_PAGE_WIDTH ) - + if ( self.docHeight == None ) or ( self.docWidth == None ): return False else: @@ -790,7 +814,7 @@ def recursivelyTraverseSvg( self, aNodeList, # first apply the current matrix transform to this node's tranform matNew = simpletransform.composeTransform( matCurrent, simpletransform.parseTransform( node.get( "transform" ) ) ) - + if node.tag == inkex.addNS( 'g', 'svg' ) or node.tag == 'g': self.recursivelyTraverseSvg( node, matNew, parent_visibility=v ) @@ -998,7 +1022,7 @@ def recursivelyTraverseSvg( self, aNodeList, pass # for node in aNodeList: # def recursivelyTraverseSvg( self, aNodeList,... - + def joinFillsWithNode ( self, node, stroke_width, path ): ''' @@ -1024,7 +1048,7 @@ def joinFillsWithNode ( self, node, stroke_width, path ): # of the new element stroke_color = '#000000' # default assumption stroke_width = '1.0' # default value - + try: style = node.get('style') if style != None: @@ -1049,6 +1073,9 @@ def joinFillsWithNode ( self, node, stroke_width, path ): line_attribs['transform'] = tran inkex.etree.SubElement( g, inkex.addNS( 'path', 'svg' ), line_attribs ) + if self.options.removeOriginal == True: + node.getparent().remove(node) + def makeHatchGrid( self, angle, spacing, init=True ): # returns True if succeeds in making grid, else False ''' @@ -1073,12 +1100,12 @@ def makeHatchGrid( self, angle, spacing, init=True ): # returns True if succeeds 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 @@ -1129,360 +1156,414 @@ def makeHatchGrid( self, angle, spacing, init=True ): # returns True if succeeds # if bBoundingBoxExists: return retValue # def makeHatchGrid( self, angle, spacing, init=True ): - + + def map( self, value, inMin, inMax, outMin, outMax ): + # Map in range to 0.0 - 1.0 range + scaledValue = (float(value) - float(inMin)) / (float(inMax) - float(inMin)) + # Map 0.0 - 1.0 to the requested range + return outMin + (scaledValue * (float(outMax) - float(outMin))) + 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 ) + if self.options.useRange and not self.options.ids: + inkex.debug("At the moment the range function only works on selections. Using fixed values instead.") + self.options.useRange = False + + brightnessGroups = {} # Used to store groups of elements with the same brightness + if not self.options.useRange: # If we're not using the range setting... + brightnessGroups["1.0"] = [] # ...still add an empty group so we can use the for...in loop below else: - # Traverse the entire document - self.recursivelyTraverseSvg( self.document.getroot(), self.docTransform ) - - # Build a grid of possible hatch lines - bHaveGrid = self.makeHatchGrid( float( self.options.hatchAngle ), - float( self.options.hatchSpacing ), True ) - # makeHatchGrid returns false if could not make grid - probably because bounding box is non-existent - 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 ) - - # 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 self.transforms.has_key( key ): - 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] - simpletransform.applyTransformToPoint( transform, pt1 ) - simpletransform.applyTransformToPoint( transform, pt2 ) - dx = pt2[0] - pt1[0] - dy = pt2[1] - pt1[1] - stroke_width = math.sqrt( dx * dx + dy * dy ) + # Build a list of the vertices for the document's graphical elements + if self.options.ids: + for id in self.options.ids: + try: + fill = simplestyle.parseStyle(self.selected[id].get("style"))['fill'].strip('#') + if fill.lower() == "none": # to make sure + fill = "000000" + except: # If no fill, go for black + fill = "000000" + + # Calculate brightness + brightness = ((int(fill[:2], 16) + int(fill[2:4], 16) + int(fill[4:], 16) ) / 3.0) / 255.0 + if brightness in brightnessGroups: # we already have a group for this brightness + brightnessGroups[brightness].append(id) + else: # create a new group for this brightness + brightnessGroups[brightness] = [id] + else: + inkex.debug("Using range with no selection has not been implemented") + # Traverse the entire document + # self.recursivelyTraverseSvg( self.document.getroot(), self.docTransform ) + + for level in brightnessGroups: # Loop all of the groups we made based on brightness + + if not self.options.useRange: # If we don't use ranges, get the objects like before + # 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: - 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 element. Having it - # set in the path element seems a bit counterintuitive - # after the fact (i.e., what's this tranform here for?). - # So, we compute the inverse transform and apply it here. - if transform != None: - simpletransform.applyTransformToPoint( transform, pt1 ) - simpletransform.applyTransformToPoint( transform, pt2 ) - # Now generate the path data for the - 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 ): + # Traverse the entire document + self.recursivelyTraverseSvg( self.document.getroot(), self.docTransform ) + + else: # use ranges + # Set the right hatchAngle and hatchSpacing for this brightness group + self.options.hatchAngle = self.map( float(level), 0.0, 1.0, self.options.minHatchAngle, self.options.maxHatchAngle ) + self.options.hatchSpacing = self.map( float(level), 0.0, 1.0, self.options.minHatchSpacing, self.options.maxHatchSpacing ) + # Reset these values for each group + self.xmin, self.ymin = ( float( 0 ), float( 0 ) ) + self.xmax, self.ymax = ( float( 0 ), float( 0 ) ) + self.paths = {} + self.grid = [] + self.hatches = {} + self.transforms = {} + self.minGap = float( 0 ) + + for id in brightnessGroups[level]: + self.recursivelyTraverseSvg( [self.selected[id]], self.docTransform ) + + # Build a grid of possible hatch lines + bHaveGrid = self.makeHatchGrid( float( self.options.hatchAngle ), + float( self.options.hatchSpacing ), True ) + # makeHatchGrid returns false if could not make grid - probably because bounding box is non-existent + 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 ) + + # 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 self.transforms.has_key( key ): + 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] + simpletransform.applyTransformToPoint( transform, pt1 ) + simpletransform.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] - 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 element. Having it - # set in the path element seems a bit counterintuitive - # after the fact (i.e., what's this tranform here for?). - # So, we compute the inverse transform and apply it here. - if transform != None: - simpletransform.applyTransformToPoint( transform, pt1 ) - simpletransform.applyTransformToPoint( transform, pt2 ) - - # Now generate the path data for the - # 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 + # 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 element. Having it + # set in the path element seems a bit counterintuitive + # after the fact (i.e., what's this tranform here for?). + # So, we compute the inverse transform and apply it here. + if transform != None: + simpletransform.applyTransformToPoint( transform, pt1 ) + simpletransform.applyTransformToPoint( transform, pt2 ) + # Now generate the path data for the + if direction: + # Go this 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)) - ''' - else: # if bHaveGrid: - inkex.errormsg( ' Nothing to plot' ) - # if bHaveGrid: else: - # def effect( self ): - - def recursivelyAppendNearbySegmentIfAny( + ( 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 element. Having it + # set in the path element seems a bit counterintuitive + # after the fact (i.e., what's this tranform here for?). + # So, we compute the inverse transform and apply it here. + if transform != None: + simpletransform.applyTransformToPoint( transform, pt1 ) + simpletransform.applyTransformToPoint( transform, pt2 ) + + # Now generate the path data for the + # 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)) + ''' + else: # if bHaveGrid: + inkex.errormsg( ' Nothing to plot' ) + # if bHaveGrid: else: + # def effect( self ): + + def recursivelyAppendNearbySegmentIfAny( self, transformedHatchSpacing, nRecursionCount, - nReferenceSegmentCount, - nReferenceEndIndex, - nAbsoluteLineSegmentTotal, - absoluteLineSegments, - cumulativePath, + 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 @@ -1498,10 +1579,10 @@ def recursivelyAppendNearbySegmentIfAny( 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 @@ -1519,7 +1600,7 @@ def recursivelyAppendNearbySegmentIfAny( # 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, @@ -1561,14 +1642,14 @@ def recursivelyAppendNearbySegmentIfAny( else: # if ( not bFoundSegmentToAdd ): nNewSegmentInitialEndIndex = nNewSegmentInitialEndIndexAtClosest nNewSegmentFinalEndIndex = not nNewSegmentInitialEndIndex - # nNewSegmentInitialEndIndex is 0 for connecting to pt1, + # 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 @@ -1578,19 +1659,19 @@ def recursivelyAppendNearbySegmentIfAny( # 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, + # 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 @@ -1616,9 +1697,9 @@ def recursivelyAppendNearbySegmentIfAny( # 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, @@ -1636,7 +1717,7 @@ def recursivelyAppendNearbySegmentIfAny( 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] @@ -1657,7 +1738,7 @@ def recursivelyAppendNearbySegmentIfAny( 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 @@ -1665,17 +1746,17 @@ def recursivelyAppendNearbySegmentIfAny( 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 @@ -1700,9 +1781,9 @@ def WouldBeAnAlternatingDirection( self, fReferenceDirectionRadians, fNewSegment 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 )