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 )