From 069fda9a4589f6e05096b5cc7d549d34c9997a86 Mon Sep 17 00:00:00 2001 From: "Mario Voigt (Leyghis)" Date: Fri, 1 Mar 2019 23:14:59 +0100 Subject: [PATCH] Initial Commit --- eggbot_hatch.py | 1719 +++++++++++++++++++++++ plaster.inx | 143 ++ plaster.py | 3396 +++++++++++++++++++++++++++++++++++++++++++++ plaster_hatch.inx | 77 + plot_utils.py | 226 +++ 5 files changed, 5561 insertions(+) create mode 100644 eggbot_hatch.py create mode 100644 plaster.inx create mode 100644 plaster.py create mode 100644 plaster_hatch.inx create mode 100644 plot_utils.py diff --git a/eggbot_hatch.py b/eggbot_hatch.py new file mode 100644 index 0000000..cb1672c --- /dev/null +++ b/eggbot_hatch.py @@ -0,0 +1,1719 @@ +#!/usr/bin/env python + +# eggbot_hatch.py +# +# Generate hatch fills for the closed paths (polygons) in the currently +# selected document elements. If no elements are selected, then all the +# polygons throughout the document are hatched. The fill rule is an odd/even +# rule: odd numbered intersections (1, 3, 5, etc.) are a hatch line entering +# a polygon while even numbered intersections (2, 4, 6, etc.) are the same +# hatch line exiting the polygon. +# +# This extension first decomposes the selected , , , +# , , , and elements into individual +# moveto and lineto coordinates using the same procedure that eggbot.py uses +# for plotting. These coordinates are then used to build vertex lists. +# Only the vertex lists corresponding to polygons (closed paths) are +# kept. Note that a single graphical element may be composed of several +# subpaths, each subpath potentially a polygon. +# +# Once the lists of all the vertices are built, potential hatch lines are +# "projected" through the bounding box containing all of the vertices. +# For each potential hatch line, all intersections with all the polygon +# edges are determined. These intersections are stored as decimal fractions +# indicating where along the length of the hatch line the intersection +# occurs. These values will always be in the range [0, 1]. A value of 0 +# indicates that the intersection is at the start of the hatch line, a value +# of 0.5 midway, and a value of 1 at the end of the hatch line. +# +# For a given hatch line, all the fractional values are sorted and any +# duplicates removed. Duplicates occur, for instance, when the hatch +# line passes through a polygon vertex and thus intersects two edges +# segments of the polygon: the end of one edge and the start of +# another. +# +# Once sorted and duplicates removed, an odd/even rule is applied to +# determine which segments of the potential hatch line are within +# polygons. These segments found to be within polygons are then saved +# and become the hatch fill lines which will be drawn. +# +# With each saved hatch fill line, information about which SVG graphical +# element it is within is saved. This way, the hatch fill lines can +# later be grouped with the element they are associated with. This makes +# it possible to manipulate the two -- graphical element and hatch lines -- +# as a single object within Inkscape. +# +# Note: we also save the transformation matrix for each graphical element. +# That way, when we group the hatch fills with the element they are +# filling, we can invert the transformation. That is, in order to compute +# the hatch fills, we first have had apply ALL applicable transforms to +# all the graphical elements. We need to do that so that we know where in +# the drawing each of the graphical elements are relative to one another. +# However, this means that the hatch lines have been computed in a setting +# where no further transforms are needed. If we then put these hatch lines +# into the same groups as the elements being hatched in the ORIGINAL +# drawing, then the hatch lines will have transforms applied again. So, +# once we compute the hatch lines, we need to invert the transforms of +# the group they will be placed in and apply this inverse transform to the +# hatch lines. Hence the need to save the transform matrix for every +# graphical element. + +# Written by Daniel C. Newman for the Eggbot Project +# dan dot newman at mtbaldy dot us +# Last updated 28 November 2010 +# 15 October 2010 + +# Updated by Windell H. Oskay, 6/14/2012 +# Added tolerance parameter + +# Update by Daniel C. Newman, 6/20/2012 +# Add min span/gap width + +# Updated by Windell H. Oskay, 1/8/2016 +# Added live preview and correct issue with nonzero min gap +# https://github.com/evil-mad/EggBot/issues/32 + +# Updated by Sheldon B. Michaels, 1/11/2016 thru 3/15/2016 +# shel at shel dot net +# Added feature: Option to inset the hatch segments from boundaries +# Added feature: Option to join hatch segments that are "nearby", to minimize pen lifts +# The joins are made using cubic Bezier segments. +# https://github.com/evil-mad/EggBot/issues/36 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import inkex +import simplepath +import simpletransform +import simplestyle +import cubicsuperpath +import cspsubdiv +import bezmisc +import math +import plot_utils # https://github.com/evil-mad/plotink + + +N_PAGE_WIDTH = 3200 +N_PAGE_HEIGHT = 800 + +F_MINGAP_SMALL_VALUE = 0.0000000001 # Was 0.00001 in the original version which did not have joined lines. + # Reducing this by a factor of 10^5 decreased probability of occurrence of + # the bug in the original, which got confused when the path barely + # grazed a corner. +BEZIER_OVERSHOOT_MULTIPLIER = 0.75 # evaluation of cubic Bezier curve equation value, + # at x = 0, with + # endpoints at ( -0.5, 0 ), ( +0.5, 0 ) + # and control points at ( -0.5, 1.0 ), ( +0.5, 1.0 ) + +RADIAN_TOLERANCE_FOR_COLINEAR = 0.1 # Pragmatically adjusted to allow adjacent segments from the same scan line, even short ones, + # to be classified as having the same angle +RADIAN_TOLERANCE_FOR_ALTERNATING_DIRECTION = 0.1 # Pragmatic adjustment again, as with colinearity tolerance +RECURSION_LIMIT = 500 # Pragmatic - if too high, risk runtime python error; if too low, miss some chances for reducing pen lifts +EXTREME_POSITIVE_NUMBER = float( 1.0E70 ) +EXTREME_NEGATIVE_NUMBER = float( -1.0E70 ) +MIN_HATCH_LENGTH_AS_FRACTION_OF_HATCH_SPACING = 0.25 # set minimum hatch length to some function + # (e.g. this multiplier) of the hatch spacing +''' +Geometry 101: Determining if two lines intersect + +A line L is defined by two points in space P1 and P2. Any point P on the +line L satisfies + + P = P1 + s (P2 - P1) + +for some value of the real number s in the range (-infinity, infinity). +If we confine s to the range [0, 1] then we've described the line segment +with end points P1 and P2. + +Consider now the line La defined by the points P1 and P2, and the line Lb +defined by the points P3 and P4. Any points Pa and Pb on the lines La and +Lb therefore satisfy + + Pa = P1 + sa (P2 - P1) + Pb = P3 + sb (P4 - P3) + +for some values of the real numbers sa and sb. To see if these two lines +La and Lb intersect, we wish to see if there are finite values sa and sb +for which + + Pa = Pb + +Or, equivalently, we ask if there exists values of sa and sb for which +the equation + + P1 + sa (P2 - P1) = P3 + sb (P4 - P3) + +holds. If we confine ourselves to a two-dimensional plane, and take + + P1 = (x1, y1) + P2 = (x2, y2) + P3 = (x3, y3) + P4 = (x4, y4) + +we then find that we have two equations in two unknowns, sa and sb, + + x1 + sa ( x2 - x1 ) = x3 + sb ( x4 - x3 ) + y1 + sa ( y2 - y1 ) = y3 + sb ( y4 - y3 ) + +Solving these two equations for sa and sb yields, + + sa = [ ( y1 - y3 ) ( x4 - x3 ) - ( y4 - y3 ) ( x1 - x3 ) ] / d + sb = [ ( y1 - y3 ) ( x2 - x1 ) - ( y2 - y1 ) ( x1 - x3 ) ] / d + +where the denominator, d, is given by + + d = ( y4 - y3 ) ( x2 - x1 ) - ( y2 - y1 ) ( x4 - x3 ) + +Substituting these back for the point (x, y) of intersection gives + + x = x1 + sa ( x2 - x1 ) + y = y1 + sa ( y2 - y1 ) + +Note that + +1. The lines are parallel when d = 0 +2. The lines are coincident d = 0 and the numerators for sa & sb are zero +3. For line segments, sa and sb are in the range [0, 1]; any value outside + that range indicates that the line segments do not intersect. +''' + +def intersect( P1, P2, P3, P4 ): + + ''' + Determine if two line segments defined by the four points P1 & P2 and + P3 & P4 intersect. If they do intersect, then return the fractional + point of intersection "sa" along the first line at which the + intersection occurs. + ''' + + # Precompute these values -- note that we're basically shifting from + # + # P = P1 + s (P2 - P1) + # + # to + # + # P = P1 + s D + # + # where D is a direction vector. The solution remains the same of + # course. We'll just be computing D once for each line rather than + # computing it a couple of times. + + D21x = P2[0] - P1[0] + D21y = P2[1] - P1[1] + D43x = P4[0] - P3[0] + D43y = P4[1] - P3[1] + + # Denominator + d = D21x * D43y - D21y * D43x + + # Return now if the denominator is zero + if d == 0: + return float( -1 ) + + # For our purposes, the first line segment given + # by P1 & P2 is the LONG hatch line running through + # the entire drawing. And, P3 & P4 describe the + # usually much shorter line segment from a polygon. + # As such, we compute sb first as it's more likely + # to indicate "no intersection". That is, sa is + # more likely to indicate an intersection with a + # much a long line containing P3 & P4. + + nb = ( P1[1] - P3[1] ) * D21x - ( P1[0] - P3[0] ) * D21y + + # Could first check if abs(nb) > abs(d) or if + # the signs differ. + sb = float( nb ) / float( d ) + if ( sb < 0 ) or ( sb > 1 ): + return float( -1 ) + + na = ( P1[1] - P3[1] ) * D43x - ( P1[0] - P3[0] ) * D43y + sa = float( na ) / float( d ) + if ( sa < 0 ) or ( sa > 1 ): + return float( -1 ) + + return sa + +def interstices( self, P1, P2, paths, hatches, bHoldBackHatches, fHoldBackSteps ): + + ''' + For the line L defined by the points P1 & P2, determine the segments + of L which lie within the polygons described by the paths stored in + "paths" + + P1 -- (x,y) coordinate [list] + P2 -- (x,y) coordinate [list] + paths -- Dictionary of all the paths to check for intersections + + When an intersection of the line L is found with a polygon edge, then + the fractional distance along the line L is saved along with the + lxml.etree node which contained the intersecting polygon edge. This + fractional distance is always in the range [0, 1]. + + Once all polygons have been checked, the list of fractional distances + corresponding to intersections is sorted and any duplicates removed. + It is then assumed that the first intersection is the line L entering + a polygon; the second intersection the line leaving the polygon. This + line segment defined by the first and second intersection points is + thus a hatch fill line we sought to generate. In general, our hatch + fills become the line segments described by intersection i and i+1 + with i an odd value (1, 3, 5, ...). Since we know the lxml.etree node + corresponding to each intersection, we can then correlate the hatch + fill lines to the graphical elements in the original SVG document. + This enables us to group hatch lines with the elements being hatched. + + The hatch line segments are returned by populating a dictionary. + The dictionary is keyed off of the lxml.etree node pointer. Each + dictionary value is a list of 4-tuples, + + (x1, y1, x2, y2) + + where (x1, y1) and (x2, y2) are the (x,y) coordinates of the line + segment's starting and ending points. + ''' + + dAndA = [] + # P1 & P2 is the hatch line + # P3 & P4 is the polygon edge to check + for path in paths: + for subpath in paths[path]: + P3 = subpath[0] + for P4 in subpath[1:]: + s = intersect( P1, P2, P3, P4 ) + if ( s >= 0.0 ) and ( s <= 1.0 ): + # Save this intersection point along the hatch line + if bHoldBackHatches: + # We will need to know how the hatch meets the polygon segment, so that we can + # calculate the end of a shorter line that stops short + # of the polygon segment. + # We compute the angle now while we have the information required, + # but do _not_ apply it now, as we need the real,original, intersects + # for the odd/even inside/outside operations yet to come. + # Note that though the intersect() routine _could_ compute the join angle, + # we do it here because we go thru here much less often than we go thru intersect(). + angleHatchRadians = math.atan2( -( P2[1] - P1[1] ) ,( P2[0] - P1[0] ) ) # from P1 toward P2, cartesian coordinates + angleSegmentRadians = math.atan2( -( P4[1] - P3[1] ) ,( P4[0] - P3[0] ) ) # from P3 toward P4, cartesian coordinates + angleDifferenceRadians = angleHatchRadians - angleSegmentRadians + # coerce to range -pi to +pi + if ( angleDifferenceRadians > math.pi ): + angleDifferenceRadians -= 2 * math.pi + elif (angleDifferenceRadians < -math.pi ): + angleDifferenceRadians += 2 * math.pi + fSinOfJoinAngle = math.sin( angleDifferenceRadians ) + fAbsSinOfJoinAngle = abs( fSinOfJoinAngle ) + if (fAbsSinOfJoinAngle != 0.0): # Worrying about case of intersecting a segment parallel to the hatch + fPreliminaryLengthToBeRemovedFromPt = fHoldBackSteps / fAbsSinOfJoinAngle + bUnconditionallyExciseHatch = False + else: # if (fSinOfJoinAngle != 0.0): + bUnconditionallyExciseHatch = True + # if (fAbsSinOfJoinAngle != 0.0): else: + + if ( not bUnconditionallyExciseHatch): + # if ( fPreliminaryLengthToBeRemovedFromPt > ( distance from intersection to relevant end + fHoldbackSteps ) ): + # fFinalLengthToBeRemovedFromPt = ( distance from intersection to relevant end + fHoldbackSteps ) + # The relevant end of the segment is the end from which the hatch approaches at an acute angle. + I = [0,0] + I[0] = P1[0] + s * ( P2[0] - P1[0] ) # compute intersection point of hatch with segment + I[1] = P1[1] + s * ( P2[1] - P1[1] ) # intersecting hatch line starts at P1, vectored toward P2, + # but terminates at intersection I + # Note that atan2 returns answer in range -pi to pi + # Which end is the approach end of the hatch to the segment? + # The dot product tells the answer: + # if dot product is positive, P2 is at the P4 end, + # else P2 is at the P3 end + # We really don't need to take the time to actually take + # the cosine of the angle, we are just interested in + # the quadrant within which the angle lies. + # I'm sure there is an elegant way to do this, but I'll settle for results just now. + # If the angle is in quadrants I or IV then P4 is the relevant end, otherwise P3 is + # nb: Y increases down, rather than up + # nb: difference angle has been forced to the range -pi to +pi + if ( abs(angleDifferenceRadians) < math.pi / 2 ): + # It's near the P3 the relevant end from which the hatch departs + fDistanceFromIntersectionToRelevantEnd = math.hypot( P3[0] - I[0], P3[1] - I[1] ) + fDistanceFromIntersectionToIrrelevantEnd = math.hypot( P4[0] - I[0], P4[1] - I[1] ) + else: # if ( abs(angleDifferenceRadians) < math.pi / 2 ) + # It's near the P4 end from which the hatch departs + fDistanceFromIntersectionToRelevantEnd = math.hypot( P4[0] - I[0], P4[1] - I[1] ) + fDistanceFromIntersectionToIrrelevantEnd = math.hypot( P3[0] - I[0], P3[1] - I[1] ) + # if ( abs(angleDifferenceRadians) < math.pi / 2 ): else: + + # Now, the problem defined in issue 22 is that we may not need to remove the + # entire preliminary length we've calculated. This problem occurs because + # we have so far been considering the polygon segment as a line of infinite extent. + # Thus, we may be holding back at a point where no holdback is required, when + # calculated holdback is well beyond the position of the segment end. + + # To make matters worse, we do not currently know whether we're + # starting a hatch or terminating a hatch, because the duplicates have + # yet to be removed. All we can do then, is calculate the required + # line shortening for both possibilities - and then choose the correct + # one after duplicate-removal, when actually finalizing the hatches. + + # Let's see if either end, or perhaps both ends, has a case of excessive holdback + + # First, default assumption is that neither end has excessive holdback + fFinalLengthToBeRemovedFromPtWhenStartingHatch = fPreliminaryLengthToBeRemovedFromPt + fFinalLengthToBeRemovedFromPtWhenEndingHatch = fPreliminaryLengthToBeRemovedFromPt + + # Now check each of the two ends + if ( fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToRelevantEnd + fHoldBackSteps ) ): + # Yes, would be excessive holdback approaching from this direction + fFinalLengthToBeRemovedFromPtWhenStartingHatch = fDistanceFromIntersectionToRelevantEnd + fHoldBackSteps + # if ( fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToRelevantEnd + fHoldBackSteps ) ): + if ( fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToIrrelevantEnd + fHoldBackSteps ) ): + # Yes, would be excessive holdback approaching from other direction + fFinalLengthToBeRemovedFromPtWhenEndingHatch = fDistanceFromIntersectionToIrrelevantEnd + fHoldBackSteps + # if ( fPreliminaryLengthToBeRemovedFromPt > ( fDistanceFromIntersectionToIrrelevantEnd + fHoldBackSteps ) ): + + dAndA.append( ( s, path, fFinalLengthToBeRemovedFromPtWhenStartingHatch, fFinalLengthToBeRemovedFromPtWhenEndingHatch ) ) + else: # if ( not bUnconditionallyExciseHatch): + dAndA.append( ( s, path, 123456.0, 123456.0 ) ) # Mark for complete hatch excision, hatch is parallel to segment + # Just a random number guaranteed large enough to be longer than any hatch length + # if ( not bUnconditionallyExciseHatch): else : + else: # if bHoldBackHatches: + dAndA.append( ( s, path, 0, 0 ) ) # zero length to be removed from hatch + # if bHoldBackHatches: else: + # if ( s >= 0.0 ) and ( s <= 1.0 ): + P3 = P4 + # for P4 in subpath[1:]: + # for subpath in paths[path]: + # for path in paths: + + # Return now if there were no intersections + if len( dAndA ) == 0: + return None + + dAndA.sort() + + # Remove duplicate intersections. A common case where these arise + # is when the hatch line passes through a vertex where one line segment + # ends and the next one begins. + + # Having sorted the data, it's trivial to just scan through + # removing duplicates as we go and then truncating the array + + n = len( dAndA ) + ilast = i = 1 + last = dAndA[0] + while i < n: + if ( ( abs( dAndA[i][0] - last[0] ) ) > F_MINGAP_SMALL_VALUE ): + dAndA[ilast] = last = dAndA[i] + ilast += 1 + i += 1 + dAndA = dAndA[:ilast] + if len( dAndA ) < 2: + return + + # Now, entries with even valued indices into sa[] are where we start + # a hatch line and odd valued indices where we end the hatch line. + + last_dAndA = None + i = 0 + while i < ( len( dAndA ) - 1 ): + #for i in range( 0, len( sa ) - 1, 2 ): + if not hatches.has_key( dAndA[i][1] ): + hatches[dAndA[i][1]] = [] + + x1 = P1[0] + dAndA[i][0] * ( P2[0] - P1[0] ) + y1 = P1[1] + dAndA[i][0] * ( P2[1] - P1[1] ) + x2 = P1[0] + dAndA[i+1][0] * ( P2[0] - P1[0] ) + y2 = P1[1] + dAndA[i+1][0] * ( P2[1] - P1[1] ) + + # These are the hatch ends if we are _not_ holding off from the boundary. + if not bHoldBackHatches: + hatches[dAndA[i][1]].append( [[x1, y1], [x2, y2]] ) + else: # if not bHoldBackHatches: + # User wants us to perform a pseudo inset operation. + # We will accomplish this by trimming back the ends of the hatches. + # The amount by which to trim back depends on the angle between the + # intersecting hatch line with the intersecting polygon segment, and + # may well be different at the two different ends of the hatch line. + + # To visualize this, imagine a hatch intersecting a segment that is + # close to parallel with it. The length of the hatch would have to be + # drastically reduced in order that its closest approach to the + # segment be reduced to the desired distance. + + # Imagine a Cartesian coordinate system, with the X axis representing the + # polygon segment, and a line running through the origin with a small + # positive slope being the intersecting hatch line. + + # We see that we want a Y value of the specified hatch width, and that + # at that Y, the distance from the origin to that point is the + # hypotenuse of the triangle. + # Y / cutlength = sin(angle) + # therefore: + # cutlength = Y / sin(angle) + # Fortunately, we have already stored this angle for exactly this purpose. + # For each end, trim back the hatch line by the amount required by + # its own angle. If the resultant diminished hatch is too short, + # remove it from consideration by marking it as already drawn - a + # fiction, but is much quicker than actually removing the hatch from the list. + + fMinAllowedHatchLength = self.options.hatchSpacing * MIN_HATCH_LENGTH_AS_FRACTION_OF_HATCH_SPACING + fInitialHatchLength = math.hypot( x2 - x1, y2 - y1 ) + # We did as much as possible of the inset operation back when we were finding intersections. + # We did it back then because at that point we knew more about the geometry than we know now. + # Now we don't know where the ends of the segments are, so we can't address issue 22 here. + fLengthToBeRemovedFromPt1 = dAndA[i][3] + fLengthToBeRemovedFromPt2 = dAndA[i+1][2] + + if ( ( fInitialHatchLength - ( fLengthToBeRemovedFromPt1 + fLengthToBeRemovedFromPt2 ) ) \ + <= \ + fMinAllowedHatchLength ): + pass # Just don't insert it into the hatch list + else: # if (...too short...): + ''' + Use: + def RelativeControlPointPosition( self, distance, fDeltaX, fDeltaY, deltaX, deltaY ): + # returns the point, relative to 0, 0 offset by deltaX, deltaY, + # which extends a distance of "distance" at a slope defined by fDeltaX and fDeltaY + ''' + pt1 = self.RelativeControlPointPosition( fLengthToBeRemovedFromPt1, x2 - x1, y2 - y1, x1, y1 ) + pt2 = self.RelativeControlPointPosition( fLengthToBeRemovedFromPt2, x1 - x2, y1 - y2, x2, y2 ) + hatches[dAndA[i][1]].append( [[pt1[0], pt1[1]], [pt2[0], pt2[1]]] ) + + # if (...too short...): else: + # if not bHoldBackHatches: else: + + # Remember the relative start and end of this hatch segment + last_dAndA = [ dAndA[i], dAndA[i+1] ] + + i = i + 2 + # while i < ( len( dAndA ) - 1 ): + +def inverseTransform ( tran ): + ''' + An SVG transform matrix looks like + + [ a c e ] + [ b d f ] + [ 0 0 1 ] + + And it's inverse is + + [ d -c cf - de ] + [ -b a be - af ] * ( ad - bc ) ** -1 + [ 0 0 1 ] + + And, no reasonable 2d coordinate transform will have + the products ad and bc equal. + + SVG represents the transform matrix column by column as + matrix(a b c d e f) while Inkscape extensions store the + transform matrix as + + [[a, c, e], [b, d, f]] + + To invert the transform stored Inskcape style, we wish to + produce + + [[d/D, -c/D, (cf - de)/D], [-b/D, a/D, (be-af)/D]] + + where + + D = 1 / (ad - bc) + ''' + + D = tran[0][0] * tran[1][1] - tran[1][0] * tran[0][1] + if D == 0: + return None + + return [[tran[1][1]/D, -tran[0][1]/D, + (tran[0][1]*tran[1][2] - tran[1][1]*tran[0][2])/D], + [-tran[1][0]/D, tran[0][0]/D, + (tran[1][0]*tran[0][2] - tran[0][0]*tran[1][2])/D]] + +def subdivideCubicPath( sp, flat, i=1 ): + + """ + Break up a bezier curve into smaller curves, each of which + is approximately a straight line within a given tolerance + (the "smoothness" defined by [flat]). + + This is a modified version of cspsubdiv.cspsubdiv() rewritten + to avoid recurrence. + """ + + while True: + while True: + if i >= len( sp ): + return + + p0 = sp[i - 1][1] + p1 = sp[i - 1][2] + p2 = sp[i][0] + p3 = sp[i][1] + + b = ( p0, p1, p2, p3 ) + + if cspsubdiv.maxdist( b ) > flat: + break + + i += 1 + + one, two = bezmisc.beziersplitatt( b, 0.5 ) + sp[i - 1][2] = one[1] + sp[i][0] = two[2] + p = [one[2], one[3], two[1]] + sp[i:1] = [p] + + +def distanceSquared( P1, P2 ): + + ''' + Pythagorean distance formula WITHOUT the square root. Since + we just want to know if the distance is less than some fixed + fudge factor, we can just square the fudge factor once and run + with it rather than compute square roots over and over. + ''' + + dx = P2[0] - P1[0] + dy = P2[1] - P1[1] + + return ( dx * dx + dy * dy ) + +class Eggbot_Hatch( inkex.Effect ): + + def __init__( self ): + + inkex.Effect.__init__( self ) + + self.xmin, self.ymin = ( float( 0 ), float( 0 ) ) + self.xmax, self.ymax = ( float( 0 ), float( 0 ) ) + self.paths = {} + self.grid = [] + self.hatches = {} + self.transforms = {} + + # For handling an SVG viewbox attribute, we will need to know the + # values of the document's width and height attributes as well + # as establishing a transform from the viewbox to the display. + self.docWidth = float( N_PAGE_WIDTH ) + self.docHeight = float( N_PAGE_HEIGHT ) + self.docTransform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + + + self.OptionParser.add_option( + "--holdBackSteps", action="store", type="float", + dest="holdBackSteps", default=3.0, + help="How far hatch strokes stay from boundary (steps)" ) + self.OptionParser.add_option( + "--hatchScope", action="store", type="float", + dest="hatchScope", default=3.0, + help="Radius searched for segments to join (units of hatch width)" ) + self.OptionParser.add_option( + "--holdBackHatchFromEdges", action="store", dest="holdBackHatchFromEdges", + type="inkbool", default=True, + help="Stay away from edges, so no need for inset" ) + self.OptionParser.add_option( + "--reducePenLifts", action="store", dest="reducePenLifts", + type="inkbool", default=True, + help="Reduce plotting time by joining some hatches" ) + self.OptionParser.add_option( + "--crossHatch", action="store", dest="crossHatch", + type="inkbool", default=False, + help="Generate a cross hatch pattern" ) + self.OptionParser.add_option( + "--hatchAngle", action="store", type="float", + dest="hatchAngle", default=90.0, + help="Angle of inclination for hatch lines" ) + self.OptionParser.add_option( + "--hatchSpacing", action="store", type="float", + dest="hatchSpacing", default=10.0, + help="Spacing between hatch lines" ) + self.OptionParser.add_option( + "--tolerance", action="store", type="float", + dest="tolerance", default=20.0, + help="Allowed deviation from original paths" ) + self.OptionParser.add_option( "--tab", #NOTE: value is not used. + action="store", type="string", dest="tab", default="splash", + help="The active tab when Apply was pressed" ) + + def getDocProps( self ): + + ''' + Get the document's height and width attributes from the tag. + Use a default value in case the property is not present or is + expressed in units of percentages. + ''' + + self.docHeight = plot_utils.getLength( self, 'height', N_PAGE_HEIGHT ) + self.docWidth = plot_utils.getLength( self, 'width', N_PAGE_WIDTH ) + + if ( self.docHeight == None ) or ( self.docWidth == None ): + return False + else: + return True + + def handleViewBox( self ): + + ''' + Set up the document-wide transform in the event that the document has an SVG viewbox + ''' + + if self.getDocProps(): + viewbox = self.document.getroot().get( 'viewBox' ) + if viewbox: + vinfo = viewbox.strip().replace( ',', ' ' ).split( ' ' ) + if ( vinfo[2] != 0 ) and ( vinfo[3] != 0 ): + sx = self.docWidth / float( vinfo[2] ) + sy = self.docHeight / float( vinfo[3] ) + self.docTransform = simpletransform.parseTransform( 'scale(%f,%f)' % (sx, sy) ) + + def addPathVertices( self, path, node=None, transform=None ): + + ''' + Decompose the path data from an SVG element into individual + subpaths, each starting with an absolute move-to (x, y) + coordinate followed by one or more absolute line-to (x, y) + coordinates. Each subpath is stored as a list of (x, y) + coordinates, with the first entry understood to be a + move-to coordinate and the rest line-to coordinates. A list + is then made of all the subpath lists and then stored in the + self.paths dictionary using the path's lxml.etree node pointer + as the dictionary key. + ''' + + if ( not path ) or ( len( path ) == 0 ): + return + + # parsePath() may raise an exception. This is okay + sp = simplepath.parsePath( path ) + if ( not sp ) or ( len( sp ) == 0 ): + return + + # Get a cubic super duper path + p = cubicsuperpath.CubicSuperPath( sp ) + if ( not p ) or ( len( p ) == 0 ): + return + + # Apply any transformation + if transform != None: + simpletransform.applyTransformToPath( transform, p ) + + # Now traverse the simplified path + subpaths = [] + subpath_vertices = [] + for sp in p: + # We've started a new subpath + # See if there is a prior subpath and whether we should keep it + if len( subpath_vertices ): + if distanceSquared( subpath_vertices[0], subpath_vertices[-1] ) < 1: + # Keep the prior subpath: it appears to be a closed path + subpaths.append( subpath_vertices ) + subpath_vertices = [] + subdivideCubicPath( sp, float( self.options.tolerance / 100 ) ) + for csp in sp: + # Add this vertex to the list of vertices + subpath_vertices.append( csp[1] ) + + # Handle final subpath + if len( subpath_vertices ): + if distanceSquared( subpath_vertices[0], subpath_vertices[-1] ) < 1: + # Path appears to be closed so let's keep it + subpaths.append( subpath_vertices ) + + # Empty path? + if len( subpaths ) == 0: + return + + # And add this path to our dictionary of paths + self.paths[node] = subpaths + + # And save the transform for this element in a dictionary keyed + # by the element's lxml node pointer + self.transforms[node] = transform + + def getBoundingBox( self ): + + ''' + Determine the bounding box for our collection of polygons + ''' + + self.xmin, self.xmax = EXTREME_POSITIVE_NUMBER, EXTREME_NEGATIVE_NUMBER + self.ymin, self.ymax = EXTREME_POSITIVE_NUMBER, EXTREME_NEGATIVE_NUMBER + for path in self.paths: + for subpath in self.paths[path]: + for vertex in subpath: + if vertex[0] < self.xmin: + self.xmin = vertex[0] + elif vertex[0] > self.xmax: + self.xmax = vertex[0] + if vertex[1] < self.ymin: + self.ymin = vertex[1] + elif vertex[1] > self.ymax: + self.ymax = vertex[1] + # for vertex in subpath: + # for subpath in self.paths[path]: + # for path in self.paths: + + def recursivelyTraverseSvg( self, aNodeList, + matCurrent=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + parent_visibility='visible' ): + ''' + Recursively walk the SVG document, building polygon vertex lists + for each graphical element we support. + + Rendered SVG elements: + , , , , , , + + Supported SVG elements: + , + + Ignored SVG elements: + , , , , + + All other SVG elements trigger an error (including ) + ''' + for node in aNodeList: + # Ignore invisible nodes + v = node.get( 'visibility', parent_visibility ) + if v == 'inherit': + v = parent_visibility + if v == 'hidden' or v == 'collapse': + pass + + # first apply the current matrix transform to this node's 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 ) + + elif node.tag == inkex.addNS( 'use', 'svg' ) or node.tag == 'use': + + # A element refers to another SVG element via an xlink:href="#blah" + # attribute. We will handle the element by doing an XPath search through + # the document, looking for the element with the matching id="blah" + # attribute. We then recursively process that element after applying + # any necessary (x,y) translation. + # + # Notes: + # 1. We ignore the height and width attributes as they do not apply to + # path-like elements, and + # 2. Even if the use element has visibility="hidden", SVG still calls + # for processing the referenced element. The referenced element is + # hidden only if its visibility is "inherit" or "hidden". + + refid = node.get( inkex.addNS( 'href', 'xlink' ) ) + if not refid: + pass + + # [1:] to ignore leading '#' in reference + path = '//*[@id="%s"]' % refid[1:] + refnode = node.xpath( path ) + if refnode: + x = float( node.get( 'x', '0' ) ) + y = float( node.get( 'y', '0' ) ) + # Note: the transform has already been applied + if ( x != 0 ) or ( y != 0 ): + matNew2 = composeTransform( matNew, parseTransform( 'translate(%f,%f)' % (x,y) ) ) + else: + matNew2 = matNew + v = node.get( 'visibility', v ) + self.recursivelyTraverseSvg( refnode, matNew2, parent_visibility=v ) + + elif node.tag == inkex.addNS( 'path', 'svg' ): + + path_data = node.get( 'd') + if path_data: + self.addPathVertices( path_data, node, matNew ) + + elif node.tag == inkex.addNS( 'rect', 'svg' ) or node.tag == 'rect': + + # Manually transform + # + # + # + # into + # + # + # + # I.e., explicitly draw three sides of the rectangle and the + # fourth side implicitly + + # Create a path with the outline of the rectangle + x = float( node.get( 'x' ) ) + y = float( node.get( 'y' ) ) + if ( not x ) or ( not y ): + pass + w = float( node.get( 'width', '0' ) ) + h = float( node.get( 'height', '0' ) ) + a = [] + a.append( ['M ', [x, y]] ) + a.append( [' l ', [w, 0]] ) + a.append( [' l ', [0, h]] ) + a.append( [' l ', [-w, 0]] ) + a.append( [' Z', []] ) + self.addPathVertices( simplepath.formatPath( a ), node, matNew ) + + elif node.tag == inkex.addNS( 'line', 'svg' ) or node.tag == 'line': + + # Convert + # + # + + x1 = float( node.get( 'x1' ) ) + y1 = float( node.get( 'y1' ) ) + x2 = float( node.get( 'x2' ) ) + y2 = float( node.get( 'y2' ) ) + if ( not x1 ) or ( not y1 ) or ( not x2 ) or ( not y2 ): + pass + a = [] + a.append( ['M ', [x1, y1]] ) + a.append( [' L ', [x2, y2]] ) + self.addPathVertices( simplepath.formatPath( a ), node, matNew ) + + elif node.tag == inkex.addNS( 'polyline', 'svg' ) or node.tag == 'polyline': + + # Convert + # + # + # + # to + # + # + # + # Note: we ignore polylines with no points + + pl = node.get( 'points', '' ).strip() + if pl == '': + pass + + pa = pl.split() + d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] ) + self.addPathVertices( d, node, matNew ) + + elif node.tag == inkex.addNS( 'polygon', 'svg' ) or node.tag == 'polygon': + # Convert + # + # + # + # to + # + # + # + # Note: we ignore polygons with no points + + pl = node.get( 'points', '' ).strip() + if pl == '': + pass + + pa = pl.split() + d = "".join( ["M " + pa[i] if i == 0 else " L " + pa[i] for i in range( 0, len( pa ) )] ) + d += " Z" + self.addPathVertices( d, node, matNew ) + + elif node.tag == inkex.addNS( 'ellipse', 'svg' ) or \ + node.tag == 'ellipse' or \ + node.tag == inkex.addNS( 'circle', 'svg' ) or \ + node.tag == 'circle': + + # Convert circles and ellipses to a path with two 180 degree arcs. + # In general (an ellipse), we convert + # + # + # + # to + # + # + # + # where + # + # X1 = CX - RX + # X2 = CX + RX + # + # Note: ellipses or circles with a radius attribute of value 0 are ignored + + if node.tag == inkex.addNS( 'ellipse', 'svg' ) or node.tag == 'ellipse': + rx = float( node.get( 'rx', '0' ) ) + ry = float( node.get( 'ry', '0' ) ) + else: + rx = float( node.get( 'r', '0' ) ) + ry = rx + if rx == 0 or ry == 0: + pass + + cx = float( node.get( 'cx', '0' ) ) + cy = float( node.get( 'cy', '0' ) ) + x1 = cx - rx + x2 = cx + rx + d = 'M %f,%f ' % ( x1, cy ) + \ + 'A %f,%f ' % ( rx, ry ) + \ + '0 1 0 %f,%f ' % ( x2, cy ) + \ + 'A %f,%f ' % ( rx, ry ) + \ + '0 1 0 %f,%f' % ( x1, cy ) + self.addPathVertices( d, node, matNew ) + + elif node.tag == inkex.addNS( 'pattern', 'svg' ) or node.tag == 'pattern': + + pass + + elif node.tag == inkex.addNS( 'metadata', 'svg' ) or node.tag == 'metadata': + + pass + + elif node.tag == inkex.addNS( 'defs', 'svg' ) or node.tag == 'defs': + pass + + elif node.tag == inkex.addNS( 'namedview', 'sodipodi' ) or node.tag == 'namedview': + + pass + + elif node.tag == inkex.addNS( 'eggbot', 'svg' ) or node.tag == 'eggbot': + + pass + + elif node.tag == inkex.addNS( 'text', 'svg' ) or node.tag == 'text': + + inkex.errormsg( 'Warning: unable to draw text, please convert it to a path first.' ) + + pass + + elif not isinstance( node.tag, basestring ): + + pass + + else: + + inkex.errormsg( 'Warning: unable to draw object <%s>, please convert it to a path first.' % node.tag ) + pass + # for node in aNodeList: + # def recursivelyTraverseSvg( self, aNodeList,... + + def joinFillsWithNode ( self, node, stroke_width, path ): + + ''' + Generate a SVG element containing the path data "path". + Then put this new element into a with the supplied + node. This means making a new element and moving node + under it with the new as a sibling element. + ''' + + if ( not path ) or ( len( path ) == 0 ): + return + + # Make a new SVG element whose parent is the parent of node + parent = node.getparent() + #was: if not parent: + if parent is None: + parent = self.document.getroot() + g = inkex.etree.SubElement( parent, inkex.addNS( 'g', 'svg' ) ) + # Move node to be a child of this new element + g.append( node ) + + # Now make a element which contains the hatches & is a child + # of the new element + stroke_color = '#000000' # default assumption + stroke_width = '1.0' # default value + + try: + style = node.get('style') + if style != None: + declarations = style.split(';') + for i,declaration in enumerate(declarations): + parts = declaration.split(':', 2) + if len(parts) == 2: + (prop, val) = parts + prop = prop.strip().lower() + if prop == 'stroke-width': + stroke_width = val.strip() + elif prop == 'stroke': + val = val.strip() + stroke_color = val + # for i,declaration in enumerate(declarations): + # if style != 'none': + finally: + style = { 'stroke': '%s' % stroke_color, 'fill': 'none', 'stroke-width': '%s' % stroke_width } + line_attribs = { 'style':simplestyle.formatStyle( style ), 'd': path } + tran = node.get( 'transform' ) + if ( tran != None ) and ( tran != '' ): + line_attribs['transform'] = tran + inkex.etree.SubElement( g, inkex.addNS( 'path', 'svg' ), line_attribs ) + + def makeHatchGrid( self, angle, spacing, init=True ): # returns True if succeeds in making grid, else False + + ''' + Build a grid of hatch lines which encompasses the entire bounding + box of the graphical elements we are to hatch. + + 1. Figure out the bounding box for all of the graphical elements + 2. Pick a rectangle larger than that bounding box so that we can + later rotate the rectangle and still have it cover the bounding + box of the graphical elements. + 3. Center the rectangle of 2 on the origin (0, 0). + 4. Build the hatch line grid in this rectangle. + 5. Rotate the rectangle by the hatch angle. + 6. Translate the center of the rotated rectangle, (0, 0), to be + the center of the bounding box for the graphical elements. + 7. We now have a grid of hatch lines which overlay the graphical + elements and can now be intersected with those graphical elements. + ''' + + # If this is the first call, do some one time initializations + # When generating cross hatches, we may be called more than once + if init: + self.getBoundingBox() + self.grid = [] + + # Determine the width and height of the bounding box containing + # all the polygons to be hatched + w = self.xmax - self.xmin + h = self.ymax - self.ymin + + bBoundingBoxExists = ( ( w != ( EXTREME_NEGATIVE_NUMBER - EXTREME_POSITIVE_NUMBER ) ) and ( h != ( EXTREME_NEGATIVE_NUMBER - EXTREME_POSITIVE_NUMBER ) ) ) + retValue = bBoundingBoxExists + + if bBoundingBoxExists: + # Nice thing about rectangles is that the diameter of the circle + # encompassing them is the length the rectangle's diagonal... + r = math.sqrt ( w * w + h * h ) / 2.0 + + # Length of a hatch line will be 2r + # Now generate hatch lines within the square + # centered at (0, 0) and with side length at least d + + # While we could generate these lines running back and forth, + # that makes for weird behavior later when applying odd/even + # rules AND there are nested polygons. Instead, when we + # generate the SVG elements with the hatch line + # segments, we can do the back and forth weaving. + + # Rotation information + ca = math.cos( math.radians( 90 - angle ) ) + sa = math.sin( math.radians( 90 - angle ) ) + + # Translation information + cx = self.xmin + ( w / 2 ) + cy = self.ymin + ( h / 2 ) + + # Since the spacing may be fractional (e.g., 6.5), we + # don't try to use range() or other integer iterator + spacing = float( abs( spacing ) ) + i = -r + while i <= r: + # Line starts at (i, -r) and goes to (i, +r) + x1 = cx + ( i * ca ) + ( r * sa ) # i * ca - (-r) * sa + y1 = cy + ( i * sa ) - ( r * ca ) # i * sa + (-r) * ca + x2 = cx + ( i * ca ) - ( r * sa ) # i * ca - (+r) * sa + y2 = cy + ( i * sa ) + ( r * ca ) # i * sa + (+r) * ca + i += spacing + # Remove any potential hatch lines which are entirely + # outside of the bounding box + if (( x1 < self.xmin ) and ( x2 < self.xmin )) or \ + (( x1 > self.xmax ) and ( x2 > self.xmax )): + continue + if (( y1 < self.ymin ) and ( y2 < self.ymin )) or \ + (( y1 > self.ymax ) and ( y2 > self.ymax )): + continue + self.grid.append( ( x1, y1, x2, y2 ) ) + # while i <= r: + # if bBoundingBoxExists: + return retValue + # def makeHatchGrid( self, angle, spacing, init=True ): + + def effect( self ): + + global referenceCount + global ptLastPositionAbsolute + # Viewbox handling + self.handleViewBox() + + referenceCount = 0 + ptLastPositionAbsolute = [0,0] + + # Build a list of the vertices for the document's graphical elements + if self.options.ids: + # Traverse the selected objects + for id in self.options.ids: + self.recursivelyTraverseSvg( [self.selected[id]], self.docTransform ) + else: + # Traverse the entire document + self.recursivelyTraverseSvg( self.document.getroot(), self.docTransform ) + + # 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] + # 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 ): + 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, + relativePositionOfLastPlottedLineWasHeldInAbeyance ): + + global ptLastPositionAbsolute + fProposedNeighborhoodRadiusSquared = self.ProposeNeighborhoodRadiusSquared( transformedHatchSpacing ) + + # Look through all possibilities to choose the closest + bFoundSegmentToAdd = False # default assumption + nNewSegmentInitialEndIndexAtClosest = 0 + nOuterCountAtClosest = -1 + fClosestDistanceSquared = 123456789.0 # just a random large number + + ptReference = absoluteLineSegments[nReferenceSegmentCount][nReferenceEndIndex] + ptReferenceOtherEnd = absoluteLineSegments[nReferenceSegmentCount][not nReferenceEndIndex] + fReferenceDeltaX = ptReferenceOtherEnd[0] - ptReference[0] + fReferenceDeltaY = ptReferenceOtherEnd[1] - ptReference[1] + fReferenceDirectionRadians = math.atan2( fReferenceDeltaY, fReferenceDeltaX ) # from other end to this end + + for outerCount in range( nAbsoluteLineSegmentTotal ): # investigate all segments + if ( not absoluteLineSegments[outerCount][2] ): + # This segment currently undrawn, so it is a candidate for a path extension + + # Need to check both ends of each and every proposed segment until we find one in the neighborhood + # Defines pt2 in the reference as the end which we want to extend + + for nNewSegmentInitialEndIndex in range( 2 ): + # First try initial end of test segment (aka pt1) vs final end (aka pt2) of reference segment + if ( outerCount != nReferenceSegmentCount ): # don't investigate self ends + deltaX = absoluteLineSegments[outerCount][nNewSegmentInitialEndIndex][0] - ptReference[0] # proposed initial pt1 X minus existing final pt1 X + deltaY = absoluteLineSegments[outerCount][nNewSegmentInitialEndIndex][1] - ptReference[1] # proposed initial pt1 Y minus existing final pt1 Y + if ( ( deltaX * deltaX + deltaY * deltaY ) < fProposedNeighborhoodRadiusSquared ): + fThisDistanceSquared = deltaX * deltaX + deltaY * deltaY + ptNewSegmentThisEnd = absoluteLineSegments[outerCount][nNewSegmentInitialEndIndex] + ptNewSegmentOtherEnd = absoluteLineSegments[outerCount][not nNewSegmentInitialEndIndex] + fNewSegmentDeltaX = ptNewSegmentThisEnd[0] - ptNewSegmentOtherEnd[0] + fNewSegmentDeltaY = ptNewSegmentThisEnd[1] - ptNewSegmentOtherEnd[1] + fNewSegmentDirectionRadians = math.atan2( fNewSegmentDeltaY, fNewSegmentDeltaX ) # from other end to this end + if ( not self.WouldBeAnAlternatingDirection( fReferenceDirectionRadians, fNewSegmentDirectionRadians ) ): + # If this end would cause an alternating direction, + # then exclude it regardless of how close it is + pass + # if ( not self.WouldBeAnAlternatingDirection( fReferenceDirectionRadians, fNewSegmentDirectionRadians ) ): + + elif ( fThisDistanceSquared < fClosestDistanceSquared ): + # One other thing could rule out choosing this segment end: + # Want to screen and remove two segments that, while close enough, + # should be disqualified because they are colinear. The reason for this is that + # if they are colinear, they arose from the same global grid line, which means + # that the gap between them arises from intersections with the boundary. + # The idea here is that, all things being more-or-less equal, + # we would like to give preference to connecting to a segment + # which is the reverse of our current direction. This makes for better + # bezier curve join. + # The criterion for being colinear is that the reference segment angle is effectively + # the same as the line connecting the reference segment to the end of the new segment. + + fJoinerDirectionRadians = math.atan2( ptNewSegmentThisEnd[1] - ptReference[1], ptNewSegmentThisEnd[0] - ptReference[0] ) + if ( not self.AreCoLinear( fReferenceDirectionRadians, fJoinerDirectionRadians) ): + # not colinear + fClosestDistanceSquared = fThisDistanceSquared + bFoundSegmentToAdd = True + nNewSegmentInitialEndIndexAtClosest = nNewSegmentInitialEndIndex + nOuterCountAtClosest = outerCount + deltaXAtClosest = deltaX + deltaYAtClosest = deltaY + # if ( not self.AreCoLinear( fReferenceDirectionRadians, fJoinerDirectionRadians) ): + # if ( not self.WouldBeAnAlternatingDirection( fReferenceDirectionRadians, fNewSegmentDirectionRadians ) ): elif ( fThisDistanceSquared < fClosestDistanceSquared ): + # if ( ( deltaX * deltaX + deltaY * deltaY ) < fProposedNeighborhoodRadiusSquared ): + # if ( outerCount != nReferenceSegmentCount ): + # for nNewSegmentInitialEndIndex in range( 2 ): + # if ( not absoluteLineSegments[2] ): + # for outerCount in range( nAbsoluteLineSegmentTotal ): + + # At last we've looked at all the candidate segment ends + nRecursionCount += 1 + if ( ( not bFoundSegmentToAdd ) or ( ( nRecursionCount >= RECURSION_LIMIT ) ) ): + cumulativePath += '%f,%f ' % ( relativePositionOfLastPlottedLineWasHeldInAbeyance[0], relativePositionOfLastPlottedLineWasHeldInAbeyance[1] ) # close out this segment + ptLastPositionAbsolute[0] += relativePositionOfLastPlottedLineWasHeldInAbeyance[0] + ptLastPositionAbsolute[1] += relativePositionOfLastPlottedLineWasHeldInAbeyance[1] + return cumulativePath # No undrawn segments were suitable for appending, + # or there were so many that we worry about python recursion limit + else: # if ( not bFoundSegmentToAdd ): + nNewSegmentInitialEndIndex = nNewSegmentInitialEndIndexAtClosest + nNewSegmentFinalEndIndex = not nNewSegmentInitialEndIndex + # nNewSegmentInitialEndIndex is 0 for connecting to pt1, + # and is 1 for connecting to pt2 + count = nOuterCountAtClosest # count is the index of the segment to be appended. + deltaX = deltaXAtClosest # delta from final end of incoming segment to initial end of outgoing segment + deltaY = deltaYAtClosest + + # First, move pen to initial end (may be either its pt1 or its pt2) of new segment + + # Insert a bezier curve for this transition element + # To accomplish this, we need information on the incoming and outgoing segments. + # Specifically, we need to know the lengths and angles of the segments in + # order to decide on control points. + fIncomingDeltaX = absoluteLineSegments[nReferenceSegmentCount][nReferenceEndIndex][0] - absoluteLineSegments[nReferenceSegmentCount][not nReferenceEndIndex][0] + fIncomingDeltaY = absoluteLineSegments[nReferenceSegmentCount][nReferenceEndIndex][1] - absoluteLineSegments[nReferenceSegmentCount][not nReferenceEndIndex][1] + # The outgoing deltas are based on the reverse direction of the segment, i.e. the segment pointing back to the joiner bezier curve + fOutgoingDeltaX = absoluteLineSegments[count][nNewSegmentInitialEndIndex][0] - absoluteLineSegments[count][nNewSegmentFinalEndIndex][0] # index is [count][start point = 0, final point = 1][0=x, 1=y] + fOutgoingDeltaY = absoluteLineSegments[count][nNewSegmentInitialEndIndex][1] - absoluteLineSegments[count][nNewSegmentFinalEndIndex][1] + + lengthOfIncoming = math.hypot( fIncomingDeltaX, fIncomingDeltaY ) + lengthOfOutgoing = math.hypot( fOutgoingDeltaX, fOutgoingDeltaY ) + + # We are going to trim-up the ends of the incoming and outgoing segments, + # in order to get a curve which reliably does not extend beyond the boundary. + # Crude readings from inkscape on bezier curve overshoot, using control points extended hatch-spacing distance parallel to segment: + # when end points are in line, overshoot 12/16 in direction of segment + # when at 45 degrees, overshoot 12/16 in direction of segment + # when at 60 degrees, overshoot 12/16 in direction of segment + # Conclusion, at any angle, remove 0.75 * hatch spacing from the length of both lines, + # where 0.75 is, by no coincidence, BEZIER_OVERSHOOT_MULTIPLIER + + # If hatches are getting quite short, we can use a smaller Bezier loop at + # the end to squeeze into smaller spaces. We'll use a normal nice smooth + # curve for non-short hatches + fDesiredShortenForSmoothestJoin = transformedHatchSpacing * BEZIER_OVERSHOOT_MULTIPLIER # This is what we really want to use for smooth curves + # Separately check incoming vs outgoing lengths to see if bezier distances must be reduced, + # then choose greatest reduction to apply to both - lest we go off-course + # Finally, clip reduction to be no less than 1.0 + fControlPointDividerIncoming = 2.0 * fDesiredShortenForSmoothestJoin / lengthOfIncoming + fControlPointDividerOutgoing = 2.0 * fDesiredShortenForSmoothestJoin / lengthOfOutgoing + if ( fControlPointDividerIncoming > fControlPointDividerOutgoing ): + fLargestDesiredControlPointDivider = fControlPointDividerIncoming + else: # if ( fControlPointDividerIncoming > fControlPointDividerOutgoing ): + fLargestDesiredControlPointDivider = fControlPointDividerOutgoing + # if ( fControlPointDividerIncoming > fControlPointDividerOutgoing ): else: + if (fLargestDesiredControlPointDivider < 1.0): + fControlPointDivider = 1.0 + else: # if (fLargestDesiredControlPointDivider < 1.0): + fControlPointDivider = fLargestDesiredControlPointDivider + # if (fLargestDesiredControlPointDivider < 1.0): else: + fDesiredShorten = fDesiredShortenForSmoothestJoin / fControlPointDivider + + ptDeltaToSubtractFromIncomingEnd = self.RelativeControlPointPosition( fDesiredShorten, fIncomingDeltaX, fIncomingDeltaY, 0, 0 ) + # Note that this will be subtracted from the _point held in abeyance_. + relativePositionOfLastPlottedLineWasHeldInAbeyance[0] -= ptDeltaToSubtractFromIncomingEnd[0] + relativePositionOfLastPlottedLineWasHeldInAbeyance[1] -= ptDeltaToSubtractFromIncomingEnd[1] + + ptDeltaToAddToOutgoingStart = self.RelativeControlPointPosition( fDesiredShorten, fOutgoingDeltaX, fOutgoingDeltaY, 0, 0 ) + + # We know that when we tack on a curve, we must chop some off the end of the incoming segment, + # and also chop some off the start of the outgoing segment. + # Now, we know we want the control points to be on a projection of each segment, + # in order that there be no abrupt change of plotting angle. The question is, how + # far beyond the endpoint should we place the control point. + ptRelativeControlPointIncoming = self.RelativeControlPointPosition( + transformedHatchSpacing / fControlPointDivider, + fIncomingDeltaX, + fIncomingDeltaY, + 0, + 0 ) + ptRelativeControlPointOutgoing = self.RelativeControlPointPosition( + transformedHatchSpacing / fControlPointDivider, + fOutgoingDeltaX, + fOutgoingDeltaY, + deltaX, + deltaY) + + cumulativePath += '%f,%f ' % ( relativePositionOfLastPlottedLineWasHeldInAbeyance[0], relativePositionOfLastPlottedLineWasHeldInAbeyance[1] ) # close out this segment, which has been modified + ptLastPositionAbsolute[0] += relativePositionOfLastPlottedLineWasHeldInAbeyance[0] + ptLastPositionAbsolute[1] += relativePositionOfLastPlottedLineWasHeldInAbeyance[1] + # add bezier cubic curve + cumulativePath += ( 'c %f,%f %f,%f %f,%f l ' % + ( ptRelativeControlPointIncoming[0], + ptRelativeControlPointIncoming[1], + ptRelativeControlPointOutgoing[0], + ptRelativeControlPointOutgoing[1], + deltaX, + deltaY ) ) + ptLastPositionAbsolute[0] += deltaX + ptLastPositionAbsolute[1] += deltaY + # Next, move pen in appropriate direction to draw the new segment, given that + # we have just moved to the initial end of the new segment. + # This needs special treatment, as we just did some length changing. + deltaX = absoluteLineSegments[count][nNewSegmentFinalEndIndex][0] - absoluteLineSegments[count][nNewSegmentInitialEndIndex][0] + ptDeltaToAddToOutgoingStart[0] + deltaY = absoluteLineSegments[count][nNewSegmentFinalEndIndex][1] - absoluteLineSegments[count][nNewSegmentInitialEndIndex][1] + ptDeltaToAddToOutgoingStart[1] + relativePositionOfLastPlottedLineWasHeldInAbeyance[0] = deltaX # delta is from initial point + relativePositionOfLastPlottedLineWasHeldInAbeyance[1] = deltaY # Will be printed after we know if it must be modified + + # Mark this segment as drawn + absoluteLineSegments[count][2] = True + + cumulativePath = self.recursivelyAppendNearbySegmentIfAny( transformedHatchSpacing, nRecursionCount, count, nNewSegmentFinalEndIndex, nAbsoluteLineSegmentTotal, absoluteLineSegments, cumulativePath, relativePositionOfLastPlottedLineWasHeldInAbeyance ) + return cumulativePath + # if ( not bFoundSegmentToAdd ): else: + # def recursivelyAppendNearbySegmentIfAny( ... ): + + def ProposeNeighborhoodRadiusSquared( self, transformedHatchSpacing ): + return transformedHatchSpacing * transformedHatchSpacing * self.options.hatchScope * self.options.hatchScope + # The multiplier of x generates a radius of x^0.5 times the hatch spacing. + + def RelativeControlPointPosition( self, distance, fDeltaX, fDeltaY, deltaX, deltaY ): + + # returns the point, relative to 0, 0 offset by deltaX, deltaY, + # which extends a distance of "distance" at a slope defined by fDeltaX and fDeltaY + ptReturn = [0, 0] + + if ( fDeltaX == 0 ): + ptReturn[0] = deltaX + ptReturn[1] = math.copysign( distance, fDeltaY ) + deltaY + elif ( fDeltaY == 0 ): + ptReturn[0] = math.copysign( distance, fDeltaX ) + deltaX + ptReturn[1] = deltaY + else: + fSlope = math.atan2( fDeltaY, fDeltaX ) + ptReturn[0] = distance * math.cos( fSlope ) + deltaX + ptReturn[1] = distance * math.sin( fSlope ) + deltaY + + return ptReturn + + def WouldBeAnAlternatingDirection( self, fReferenceDirectionRadians, fNewSegmentDirectionRadians ): + # atan2 returns values in the range -pi to +pi, so we must evaluate difference values + # in the range of -2*pi to +2*pi + fDirectionDifferenceRadians = fReferenceDirectionRadians - fNewSegmentDirectionRadians + if ( fDirectionDifferenceRadians < 0 ): + fDirectionDifferenceRadians += 2 * math.pi + # Without having changed the vector direction of the difference, we have + # now reduced the range to 0 to 2*pi + fDirectionDifferenceRadians -= math.pi # flip opposite direction to coincide with same direction + # Of course they may not be _exactly_ pi different due to osmosis, so allow a tolerance + bRetVal = ( abs(fDirectionDifferenceRadians) < RADIAN_TOLERANCE_FOR_ALTERNATING_DIRECTION ) + + return bRetVal + + def AreCoLinear( self, fDirection1Radians, fDirection2Radians ): + # allow slight difference in angles, for floating-point indeterminacy + fAbsDeltaRadians = abs( fDirection1Radians - fDirection2Radians ) + if ( ( fAbsDeltaRadians < RADIAN_TOLERANCE_FOR_COLINEAR ) ): + return True + elif ( ( abs( fAbsDeltaRadians - math.pi ) < RADIAN_TOLERANCE_FOR_COLINEAR ) ): + return True + else: + return False + +if __name__ == '__main__': + + e = Eggbot_Hatch() + e.affect() diff --git a/plaster.inx b/plaster.inx new file mode 100644 index 0000000..6d1777d --- /dev/null +++ b/plaster.inx @@ -0,0 +1,143 @@ + + + <_name>The Plaster Tool - #YourMachine001 + plaster_YourMachine001 + plaster.py + inkex.py + + + + File export settings + ~/Desktop + output.gcode + false + false + + InkScape performance + false + (disable for slow computers or huge images; raises performance) + + + + 7000 + 2000 + 200 + 4 + 1 + 100,0 + + mm + in + + + + + Pen adjusting + 0 + 33,0 + 45,0 + + Laser adjusting + 0 + 0 + + + + 0 + G4 P1000; wait 1 second + 0.000 + 0.000 + 0,0 + + [Partial] Repeat geometry outline only + [Full] Repeat whole program + + 0,0 + 0,0 + + + + Random laser power + false + 0,0 + 0,0 + Random down position pen angle (pressure) + false + 0,0 + 0,0 + Random tooling speed + false + 0,0 + 0,0 + Note: minimum tooling speed is 1.0 + Random dwell time + false + 0,0 + 0,0 + + + + Machine type + + Laser + Plotter + + Controller firmware/wiring + + + + + + Other GCode modifications + ;plugin code by Mario (Stoutwind) + ;plugin code by Mario (Stoutwind) + Safety + true + true + + + + This dirty program creates GCode in x,y dimensions for cartesian CNC machines with tool type plotter/laser. It can handle a single servo motor for pen up and down movements. The z dimension is not implemented. Also multiple tools are not supported. Please define the hardware index of your single laser diode (tool index) and/or pen servo motor (servo index) to use this plugin. It's just intended to use with simple machine constructions. If you build a multi tool machine like a 3D printer with integrated/changeable sub tools for milling, 3D scanning, plotting or laser cutting, you just should pimp this plugin for your own needs. + + + 1) Header, Footer, Repeat commands: Separate each line with '\n' to put more commands on input line + 2) Usage of servo motor (common syntax): M340 P°TOOLINDEX° S°ANGLE° + 3) Export directory: Put in '~/Desktop' to quickly push the file to your Desktop + 4) Pen Up/Down Increment (+/-): You can use this to regulate the pressure or to grind deeper with your pen/cutter/... for each loop + 5) X/Y-Offset: Use this for quickly creating a pattern in X, Y or combined XY directions + 6) Tooling speed: Define the speed of your pen or laser diode in mm/min or in/min + 7) If your image does not update scale and returns wrong dimensions in GCode, please delete orientation points group 'gcodetools' and run this plugin again! + 8) Pen moves in wrong direction: Just swap Pos 1 and Pos 2! + 9) Use of this plugin with multiple machines: If you hate to reconfigure this plugin each time you swap the machine just make use of a dirty trick: Just copy and rename the plaster.inx file into °ROOT°/share/extensions. You'll have to change the value "_name" and "id" below. + + + + The Plaster (Plotter-Laser) Tool is based on ... + * Repetier Laser Tool (GNU GPL) from Hot-World GmbH & Co. KG (http://www.repetier.com) + * EggBot Hatch (GNU GPL v2) from Evil Mad Scientist (http://www.evilmadscientist.com) + * Inkscape Laser Tool Plug-in (GNU GPL) from JTECH Photonics (http://www.jtechphotonics.com) + * THLaser Laser Plug-in (GNU GPL) from think|haus (http://www.thinkhaus.org) + ------------------------------------------------------------------------ + (Re-)written by Mario Voigt from Stoutwind (https://stoutwind.de) + ------------------------------------------------------------------------ + Last update: 30.10.2016 + you found a bug or got some fresh code? Just report to info@stoutwind.de. Thanks! + + + + + + + + path + + + + + diff --git a/plaster.py b/plaster.py new file mode 100644 index 0000000..4fde207 --- /dev/null +++ b/plaster.py @@ -0,0 +1,3396 @@ +#!/usr/bin/env python +""" +Modified by Mario Voigt 2016, Stoutwind, stoutwind.de +Modified by Marcus Littwin 2015, Hot-World GmbH & Co. KG, repetier.com +Modified by Jay Johnson 2015, J Tech Photonics, Inc., jtechphotonics.com +modified by Adam Polak 2014, polakiumengineering.org + +based on Copyright (C) 2009 Nick Drobchenko, nick@cnc-club.ru +based on gcode.py (C) 2007 hugomatic... +based on addnodes.py (C) 2005,2007 Aaron Spike, aaron@ekips.org +based on dots.py (C) 2005 Aaron Spike, aaron@ekips.org +based on interp.py (C) 2005 Aaron Spike, aaron@ekips.org +based on bezmisc.py (C) 2005 Aaron Spike, aaron@ekips.org +based on cubicsuperpath.py (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +import inkex, simplestyle, simplepath +import cubicsuperpath, simpletransform, bezmisc + +import os +import math +import bezmisc +import re +import copy +import sys +import time +import cmath +import numpy +import codecs +import random +import gettext +_ = gettext.gettext + + +### Check if inkex has errormsg (0.46 version doesnot have one.) Could be removed later. +if "errormsg" not in dir(inkex): + inkex.errormsg = lambda msg: sys.stderr.write((unicode(msg) + "\n").encode("UTF-8")) + + +def bezierslopeatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t): + ax,ay,bx,by,cx,cy,x0,y0=bezmisc.bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + dx=3*ax*(t**2)+2*bx*t+cx + dy=3*ay*(t**2)+2*by*t+cy + if dx==dy==0 : + dx = 6*ax*t+2*bx + dy = 6*ay*t+2*by + if dx==dy==0 : + dx = 6*ax + dy = 6*ay + if dx==dy==0 : + print_("Slope error x = %s*t^3+%s*t^2+%s*t+%s, y = %s*t^3+%s*t^2+%s*t+%s, t = %s, dx==dy==0" % (ax,bx,cx,dx,ay,by,cy,dy,t)) + print_(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + dx, dy = 1, 1 + + return dx,dy +bezmisc.bezierslopeatt = bezierslopeatt + + +def ireplace(self,old,new,count=0): + pattern = re.compile(re.escape(old),re.I) + return re.sub(pattern,new,self,count) + +def get_delay(self): + delay = self.options.delay_time + if self.options.randomize_delay: + mindelay = self.options.delay_time - self.options.randomize_delay_lowerval + maxdelay = self.options.delay_time + self.options.randomize_delay_upperval + delay = round(random.uniform(mindelay, maxdelay),4) + if delay < 0: + delay = 0 + return delay + +################################################################################ +### +### Styles and additional parameters +### +################################################################################ + +math.pi2 = math.pi*2 +straight_tolerance = 0.0001 +straight_distance_tolerance = 0.0001 +options = {} + +intersection_recursion_depth = 10 +intersection_tolerance = 0.00001 + +styles = { + "loft_style" : { + 'main curve': simplestyle.formatStyle({ 'stroke': '#88f', 'fill': 'none', 'stroke-width':'1', 'marker-end':'url(#Arrow2Mend)' }), + }, + "biarc_style" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#88f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#8f8', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#f88', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#777', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + }, + "biarc_style_dark" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_dark_area" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_i" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#880', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#808', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#088', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#999', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_dark_i" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#dd5', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#d5d', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#5dd', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_lathe_feed" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#07f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#0f7', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'line': simplestyle.formatStyle({ 'stroke': '#f44', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_lathe_passing feed" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#07f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#0f7', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'line': simplestyle.formatStyle({ 'stroke': '#f44', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_lathe_fine feed" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#7f0', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#f70', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'line': simplestyle.formatStyle({ 'stroke': '#744', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "area artefact": simplestyle.formatStyle({ 'stroke': '#ff0000', 'fill': '#ffff00', 'stroke-width':'1' }), + "area artefact arrow": simplestyle.formatStyle({ 'stroke': '#ff0000', 'fill': '#ffff00', 'stroke-width':'1' }), + "dxf_points": simplestyle.formatStyle({ "stroke": "#ff0000", "fill": "#ff0000"}), + + } + + +################################################################################ +### Cubic Super Path additional functions +################################################################################ + +def csp_simple_bound(csp): + minx,miny,maxx,maxy = None,None,None,None + for subpath in csp: + for sp in subpath : + for p in sp: + minx = min(minx,p[0]) if minx!=None else p[0] + miny = min(miny,p[1]) if miny!=None else p[1] + maxx = max(maxx,p[0]) if maxx!=None else p[0] + maxy = max(maxy,p[1]) if maxy!=None else p[1] + return minx,miny,maxx,maxy + + +def csp_segment_to_bez(sp1,sp2) : + return sp1[1:]+sp2[:2] + + +def bound_to_bound_distance(sp1,sp2,sp3,sp4) : + min_dist = 1e100 + max_dist = 0 + points1 = csp_segment_to_bez(sp1,sp2) + points2 = csp_segment_to_bez(sp3,sp4) + for i in range(4) : + for j in range(4) : + min_, max_ = line_to_line_min_max_distance_2(points1[i-1], points1[i], points2[j-1], points2[j]) + min_dist = min(min_dist,min_) + max_dist = max(max_dist,max_) + print_("bound_to_bound", min_dist, max_dist) + return min_dist, max_dist + +def csp_to_point_distance(csp, p, dist_bounds = [0,1e100], tolerance=.01) : + min_dist = [1e100,0,0,0] + for j in range(len(csp)) : + for i in range(1,len(csp[j])) : + d = csp_seg_to_point_distance(csp[j][i-1],csp[j][i],p,sample_points = 5, tolerance = .01) + if d[0] < dist_bounds[0] : +# draw_pointer( list(csp_at_t(subpath[dist[2]-1],subpath[dist[2]],dist[3])) +# +list(csp_at_t(csp[dist[4]][dist[5]-1],csp[dist[4]][dist[5]],dist[6])),"red","line", comment = math.sqrt(dist[0])) + return [d[0],j,i,d[1]] + else : + if d[0] < min_dist[0] : min_dist = [d[0],j,i,d[1]] + return min_dist + +def csp_seg_to_point_distance(sp1,sp2,p,sample_points = 5, tolerance = .01) : + ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) + dx, dy = dx-p[0], dy-p[1] + if sample_points < 2 : sample_points = 2 + d = min( [(p[0]-sp1[1][0])**2 + (p[1]-sp1[1][1])**2,0.], [(p[0]-sp2[1][0])**2 + (p[1]-sp2[1][1])**2,1.] ) + for k in range(sample_points) : + t = float(k)/(sample_points-1) + i = 0 + while i==0 or abs(f)>0.000001 and i<20 : + t2,t3 = t**2,t**3 + f = (ax*t3+bx*t2+cx*t+dx)*(3*ax*t2+2*bx*t+cx) + (ay*t3+by*t2+cy*t+dy)*(3*ay*t2+2*by*t+cy) + df = (6*ax*t+2*bx)*(ax*t3+bx*t2+cx*t+dx) + (3*ax*t2+2*bx*t+cx)**2 + (6*ay*t+2*by)*(ay*t3+by*t2+cy*t+dy) + (3*ay*t2+2*by*t+cy)**2 + if df!=0 : + t = t - f/df + else : + break + i += 1 + if 0<=t<=1 : + p1 = csp_at_t(sp1,sp2,t) + d1 = (p1[0]-p[0])**2 + (p1[1]-p[1])**2 + if d1 < d[0] : + d = [d1,t] + return d + + +def csp_seg_to_csp_seg_distance(sp1,sp2,sp3,sp4, dist_bounds = [0,1e100], sample_points = 5, tolerance=.01) : + # check the ending points first + dist = csp_seg_to_point_distance(sp1,sp2,sp3[1],sample_points, tolerance) + dist += [0.] + if dist[0] <= dist_bounds[0] : return dist + d = csp_seg_to_point_distance(sp1,sp2,sp4[1],sample_points, tolerance) + if d[0]tolerance and i<30 : + #draw_pointer(csp_at_t(sp1,sp2,t1)) + f1x = 3*ax1*t12+2*bx1*t1+cx1 + f1y = 3*ay1*t12+2*by1*t1+cy1 + f2x = 3*ax2*t22+2*bx2*t2+cx2 + f2y = 3*ay2*t22+2*by2*t2+cy2 + F1[0] = 2*f1x*x + 2*f1y*y + F1[1] = -2*f2x*x - 2*f2y*y + F2[0][0] = 2*(6*ax1*t1+2*bx1)*x + 2*f1x*f1x + 2*(6*ay1*t1+2*by1)*y +2*f1y*f1y + F2[0][1] = -2*f1x*f2x - 2*f1y*f2y + F2[1][0] = -2*f2x*f1x - 2*f2y*f1y + F2[1][1] = -2*(6*ax2*t2+2*bx2)*x + 2*f2x*f2x - 2*(6*ay2*t2+2*by2)*y + 2*f2y*f2y + F2 = inv_2x2(F2) + if F2!=None : + t1 -= ( F2[0][0]*F1[0] + F2[0][1]*F1[1] ) + t2 -= ( F2[1][0]*F1[0] + F2[1][1]*F1[1] ) + t12, t13, t22, t23 = t1*t1, t1*t1*t1, t2*t2, t2*t2*t2 + x,y = ax1*t13+bx1*t12+cx1*t1+dx1 - (ax2*t23+bx2*t22+cx2*t2+dx2), ay1*t13+by1*t12+cy1*t1+dy1 - (ay2*t23+by2*t22+cy2*t2+dy2) + Flast = F + F = x*x+y*y + else : + break + i += 1 + if F < dist[0] and 0<=t1<=1 and 0<=t2<=1: + dist = [F,t1,t2] + if dist[0] <= dist_bounds[0] : + return dist + return dist + + +def csp_to_csp_distance(csp1,csp2, dist_bounds = [0,1e100], tolerance=.01) : + dist = [1e100,0,0,0,0,0,0] + for i1 in range(len(csp1)) : + for j1 in range(1,len(csp1[i1])) : + for i2 in range(len(csp2)) : + for j2 in range(1,len(csp2[i2])) : + d = csp_seg_bound_to_csp_seg_bound_max_min_distance(csp1[i1][j1-1],csp1[i1][j1],csp2[i2][j2-1],csp2[i2][j2]) + if d[0] >= dist_bounds[1] : continue + if d[1] < dist_bounds[0] : return [d[1],i1,j1,1,i2,j2,1] + d = csp_seg_to_csp_seg_distance(csp1[i1][j1-1],csp1[i1][j1],csp2[i2][j2-1],csp2[i2][j2], dist_bounds, tolerance=tolerance) + if d[0] < dist[0] : + dist = [d[0], i1,j1,d[1], i2,j2,d[2]] + if dist[0] <= dist_bounds[0] : + return dist + if dist[0] >= dist_bounds[1] : + return dist + return dist +# draw_pointer( list(csp_at_t(csp1[dist[1]][dist[2]-1],csp1[dist[1]][dist[2]],dist[3])) +# + list(csp_at_t(csp2[dist[4]][dist[5]-1],csp2[dist[4]][dist[5]],dist[6])), "#507","line") + + +def csp_split(sp1,sp2,t=.5) : + [x1,y1],[x2,y2],[x3,y3],[x4,y4] = sp1[1], sp1[2], sp2[0], sp2[1] + x12 = x1+(x2-x1)*t + y12 = y1+(y2-y1)*t + x23 = x2+(x3-x2)*t + y23 = y2+(y3-y2)*t + x34 = x3+(x4-x3)*t + y34 = y3+(y4-y3)*t + x1223 = x12+(x23-x12)*t + y1223 = y12+(y23-y12)*t + x2334 = x23+(x34-x23)*t + y2334 = y23+(y34-y23)*t + x = x1223+(x2334-x1223)*t + y = y1223+(y2334-y1223)*t + return [sp1[0],sp1[1],[x12,y12]], [[x1223,y1223],[x,y],[x2334,y2334]], [[x34,y34],sp2[1],sp2[2]] + +def csp_true_bounds(csp) : + # Finds minx,miny,maxx,maxy of the csp and return their (x,y,i,j,t) + minx = [float("inf"), 0, 0, 0] + maxx = [float("-inf"), 0, 0, 0] + miny = [float("inf"), 0, 0, 0] + maxy = [float("-inf"), 0, 0, 0] + for i in range(len(csp)): + for j in range(1,len(csp[i])): + ax,ay,bx,by,cx,cy,x0,y0 = bezmisc.bezierparameterize((csp[i][j-1][1],csp[i][j-1][2],csp[i][j][0],csp[i][j][1])) + roots = cubic_solver(0, 3*ax, 2*bx, cx) + [0,1] + for root in roots : + if type(root) is complex and abs(root.imag)<1e-10: + root = root.real + if type(root) is not complex and 0<=root<=1: + y = ay*(root**3)+by*(root**2)+cy*root+y0 + x = ax*(root**3)+bx*(root**2)+cx*root+x0 + maxx = max([x,y,i,j,root],maxx) + minx = min([x,y,i,j,root],minx) + + roots = cubic_solver(0, 3*ay, 2*by, cy) + [0,1] + for root in roots : + if type(root) is complex and root.imag==0: + root = root.real + if type(root) is not complex and 0<=root<=1: + y = ay*(root**3)+by*(root**2)+cy*root+y0 + x = ax*(root**3)+bx*(root**2)+cx*root+x0 + maxy = max([y,x,i,j,root],maxy) + miny = min([y,x,i,j,root],miny) + maxy[0],maxy[1] = maxy[1],maxy[0] + miny[0],miny[1] = miny[1],miny[0] + + return minx,miny,maxx,maxy + + +############################################################################ +### csp_segments_intersection(sp1,sp2,sp3,sp4) +### +### Returns array containig all intersections between two segmets of cubic +### super path. Results are [ta,tb], or [ta0, ta1, tb0, tb1, "Overlap"] +### where ta, tb are values of t for the intersection point. +############################################################################ +def csp_segments_intersection(sp1,sp2,sp3,sp4) : + a, b = csp_segment_to_bez(sp1,sp2), csp_segment_to_bez(sp3,sp4) + + def polish_intersection(a,b,ta,tb, tolerance = intersection_tolerance) : + ax,ay,bx,by,cx,cy,dx,dy = bezmisc.bezierparameterize(a) + ax1,ay1,bx1,by1,cx1,cy1,dx1,dy1 = bezmisc.bezierparameterize(b) + i = 0 + F, F1 = [.0,.0], [[.0,.0],[.0,.0]] + while i==0 or (abs(F[0])**2+abs(F[1])**2 > tolerance and i<10): + ta3, ta2, tb3, tb2 = ta**3, ta**2, tb**3, tb**2 + F[0] = ax*ta3+bx*ta2+cx*ta+dx-ax1*tb3-bx1*tb2-cx1*tb-dx1 + F[1] = ay*ta3+by*ta2+cy*ta+dy-ay1*tb3-by1*tb2-cy1*tb-dy1 + F1[0][0] = 3*ax *ta2 + 2*bx *ta + cx + F1[0][1] = -3*ax1*tb2 - 2*bx1*tb - cx1 + F1[1][0] = 3*ay *ta2 + 2*by *ta + cy + F1[1][1] = -3*ay1*tb2 - 2*by1*tb - cy1 + det = F1[0][0]*F1[1][1] - F1[0][1]*F1[1][0] + if det!=0 : + F1 = [ [ F1[1][1]/det, -F1[0][1]/det], [-F1[1][0]/det, F1[0][0]/det] ] + ta = ta - ( F1[0][0]*F[0] + F1[0][1]*F[1] ) + tb = tb - ( F1[1][0]*F[0] + F1[1][1]*F[1] ) + else: break + i += 1 + + return ta, tb + + + def recursion(a,b, ta0,ta1,tb0,tb1, depth_a,depth_b) : + global bezier_intersection_recursive_result + if a==b : + bezier_intersection_recursive_result += [[ta0,tb0,ta1,tb1,"Overlap"]] + return + tam, tbm = (ta0+ta1)/2, (tb0+tb1)/2 + if depth_a>0 and depth_b>0 : + a1,a2 = bez_split(a,0.5) + b1,b2 = bez_split(b,0.5) + if bez_bounds_intersect(a1,b1) : recursion(a1,b1, ta0,tam,tb0,tbm, depth_a-1,depth_b-1) + if bez_bounds_intersect(a2,b1) : recursion(a2,b1, tam,ta1,tb0,tbm, depth_a-1,depth_b-1) + if bez_bounds_intersect(a1,b2) : recursion(a1,b2, ta0,tam,tbm,tb1, depth_a-1,depth_b-1) + if bez_bounds_intersect(a2,b2) : recursion(a2,b2, tam,ta1,tbm,tb1, depth_a-1,depth_b-1) + elif depth_a>0 : + a1,a2 = bez_split(a,0.5) + if bez_bounds_intersect(a1,b) : recursion(a1,b, ta0,tam,tb0,tb1, depth_a-1,depth_b) + if bez_bounds_intersect(a2,b) : recursion(a2,b, tam,ta1,tb0,tb1, depth_a-1,depth_b) + elif depth_b>0 : + b1,b2 = bez_split(b,0.5) + if bez_bounds_intersect(a,b1) : recursion(a,b1, ta0,ta1,tb0,tbm, depth_a,depth_b-1) + if bez_bounds_intersect(a,b2) : recursion(a,b2, ta0,ta1,tbm,tb1, depth_a,depth_b-1) + else : # Both segments have been subdevided enougth. Let's get some intersections :). + intersection, t1, t2 = straight_segments_intersection([a[0]]+[a[3]],[b[0]]+[b[3]]) + if intersection : + if intersection == "Overlap" : + t1 = ( max(0,min(1,t1[0]))+max(0,min(1,t1[1])) )/2 + t2 = ( max(0,min(1,t2[0]))+max(0,min(1,t2[1])) )/2 + bezier_intersection_recursive_result += [[ta0+t1*(ta1-ta0),tb0+t2*(tb1-tb0)]] + + global bezier_intersection_recursive_result + bezier_intersection_recursive_result = [] + recursion(a,b,0.,1.,0.,1.,intersection_recursion_depth,intersection_recursion_depth) + intersections = bezier_intersection_recursive_result + for i in range(len(intersections)) : + if len(intersections[i])<5 or intersections[i][4] != "Overlap" : + intersections[i] = polish_intersection(a,b,intersections[i][0],intersections[i][1]) + return intersections + + +def csp_segments_true_intersection(sp1,sp2,sp3,sp4) : + intersections = csp_segments_intersection(sp1,sp2,sp3,sp4) + res = [] + for intersection in intersections : + if ( + (len(intersection)==5 and intersection[4] == "Overlap" and (0<=intersection[0]<=1 or 0<=intersection[1]<=1) and (0<=intersection[2]<=1 or 0<=intersection[3]<=1) ) + or ( 0<=intersection[0]<=1 and 0<=intersection[1]<=1 ) + ) : + res += [intersection] + return res + + +def csp_get_t_at_curvature(sp1,sp2,c, sample_points = 16): + # returns a list containning [t1,t2,t3,...,tn], 0<=ti<=1... + if sample_points < 2 : sample_points = 2 + tolerance = .0000000001 + res = [] + ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) + for k in range(sample_points) : + t = float(k)/(sample_points-1) + i, F = 0, 1e100 + while i<2 or abs(F)>tolerance and i<17 : + try : # some numerical calculation could exceed the limits + t2 = t*t + #slopes... + f1x = 3*ax*t2+2*bx*t+cx + f1y = 3*ay*t2+2*by*t+cy + f2x = 6*ax*t+2*bx + f2y = 6*ay*t+2*by + f3x = 6*ax + f3y = 6*ay + d = (f1x**2+f1y**2)**1.5 + F1 = ( + ( (f1x*f3y-f3x*f1y)*d - (f1x*f2y-f2x*f1y)*3.*(f2x*f1x+f2y*f1y)*((f1x**2+f1y**2)**.5) ) / + ((f1x**2+f1y**2)**3) + ) + F = (f1x*f2y-f1y*f2x)/d - c + t -= F/F1 + except: + break + i += 1 + if 0<=t<=1 and F<=tolerance: + if len(res) == 0 : + res.append(t) + for i in res : + if abs(t-i)<=0.001 : + break + if not abs(t-i)<=0.001 : + res.append(t) + return res + + +def csp_max_curvature(sp1,sp2): + ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) + tolerance = .0001 + F = 0. + i = 0 + while i<2 or F-Flast 0 : return 1e100 + if t1 < 0 : return -1e100 + # Use the Lapitals rule to solve 0/0 problem for 2 times... + t1 = 2*(bx*ay-ax*by)*t+(ay*cx-ax*cy) + if t1 > 0 : return 1e100 + if t1 < 0 : return -1e100 + t1 = bx*ay-ax*by + if t1 > 0 : return 1e100 + if t1 < 0 : return -1e100 + if depth>0 : + # little hack ;^) hope it wont influence anything... + return csp_curvature_at_t(sp1,sp2,t*1.004, depth-1) + return 1e100 + + +def csp_curvature_radius_at_t(sp1,sp2,t) : + c = csp_curvature_at_t(sp1,sp2,t) + if c == 0 : return 1e100 + else: return 1/c + + +def csp_special_points(sp1,sp2) : + # special points = curvature == 0 + ax,ay,bx,by,cx,cy,dx,dy = bezmisc.bezierparameterize((sp1[1],sp1[2],sp2[0],sp2[1])) + a = 3*ax*by-3*ay*bx + b = 3*ax*cy-3*cx*ay + c = bx*cy-cx*by + roots = cubic_solver(0, a, b, c) + res = [] + for i in roots : + if type(i) is complex and i.imag==0: + i = i.real + if type(i) is not complex and 0<=i<=1: + res.append(i) + return res + + +def csp_subpath_ccw(subpath): + # Remove all zerro length segments + s = 0 + #subpath = subpath[:] + if (P(subpath[-1][1])-P(subpath[0][1])).l2() > 1e-10 : + subpath[-1][2] = subpath[-1][1] + subpath[0][0] = subpath[0][1] + subpath += [ [subpath[0][1],subpath[0][1],subpath[0][1]] ] + pl = subpath[-1][2] + for sp1 in subpath: + for p in sp1 : + s += (p[0]-pl[0])*(p[1]+pl[1]) + pl = p + return s<0 + + +def csp_at_t(sp1,sp2,t): + ax,bx,cx,dx = sp1[1][0], sp1[2][0], sp2[0][0], sp2[1][0] + ay,by,cy,dy = sp1[1][1], sp1[2][1], sp2[0][1], sp2[1][1] + + x1, y1 = ax+(bx-ax)*t, ay+(by-ay)*t + x2, y2 = bx+(cx-bx)*t, by+(cy-by)*t + x3, y3 = cx+(dx-cx)*t, cy+(dy-cy)*t + + x4,y4 = x1+(x2-x1)*t, y1+(y2-y1)*t + x5,y5 = x2+(x3-x2)*t, y2+(y3-y2)*t + + x,y = x4+(x5-x4)*t, y4+(y5-y4)*t + return [x,y] + + +def csp_splitatlength(sp1, sp2, l = 0.5, tolerance = 0.01): + bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) + t = bezmisc.beziertatlength(bez, l, tolerance) + return csp_split(sp1, sp2, t) + + +def cspseglength(sp1,sp2, tolerance = 0.001): + bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) + return bezmisc.bezierlength(bez, tolerance) + + +def csplength(csp): + total = 0 + lengths = [] + for sp in csp: + for i in xrange(1,len(sp)): + l = cspseglength(sp[i-1],sp[i]) + lengths.append(l) + total += l + return lengths, total + + +def csp_segments(csp): + l, seg = 0, [0] + for sp in csp: + for i in xrange(1,len(sp)): + l += cspseglength(sp[i-1],sp[i]) + seg += [ l ] + + if l>0 : + seg = [seg[i]/l for i in xrange(len(seg))] + return seg,l + + +def rebuild_csp (csp, segs, s=None): + # rebuild_csp() adds to csp control points making it's segments looks like segs + if s==None : s, l = csp_segments(csp) + + if len(s)>len(segs) : return None + segs = segs[:] + segs.sort() + for i in xrange(len(s)): + d = None + for j in xrange(len(segs)): + d = min( [abs(s[i]-segs[j]),j], d) if d!=None else [abs(s[i]-segs[j]),j] + del segs[d[1]] + for i in xrange(len(segs)): + for j in xrange(0,len(s)): + if segs[i]t2 : t1, t2 = t2, t1 + if t1 == t2 : + sp1,sp2,sp3 = csp_split(sp1,sp2,t) + return [sp1,sp2,sp2,sp3] + elif t1 <= 1e-10 and t2 >= 1.-1e-10 : + return [sp1,sp1,sp2,sp2] + elif t1 <= 1e-10: + sp1,sp2,sp3 = csp_split(sp1,sp2,t2) + return [sp1,sp1,sp2,sp3] + elif t2 >= 1.-1e-10 : + sp1,sp2,sp3 = csp_split(sp1,sp2,t1) + return [sp1,sp2,sp3,sp3] + else: + sp1,sp2,sp3 = csp_split(sp1,sp2,t1) + sp2,sp3,sp4 = csp_split(sp2,sp3,(t2-t1)/(1-t1) ) + return [sp1,sp2,sp3,sp4] + + +def csp_subpath_split_by_points(subpath, points) : + # points are [[i,t]...] where i-segment's number + points.sort() + points = [[1,0.]] + points + [[len(subpath)-1,1.]] + parts = [] + for int1,int2 in zip(points,points[1:]) : + if int1==int2 : + continue + if int1[1] == 1. : + int1[0] += 1 + int1[1] = 0. + if int1==int2 : + continue + if int2[1] == 0. : + int2[0] -= 1 + int2[1] = 1. + if int1[0] == 0 and int2[0]==len(subpath)-1:# and small(int1[1]) and small(int2[1]-1) : + continue + if int1[0]==int2[0] : # same segment + sp = csp_split_by_two_points(subpath[int1[0]-1],subpath[int1[0]],int1[1], int2[1]) + if sp[1]!=sp[2] : + parts += [ [sp[1],sp[2]] ] + else : + sp5,sp1,sp2 = csp_split(subpath[int1[0]-1],subpath[int1[0]],int1[1]) + sp3,sp4,sp5 = csp_split(subpath[int2[0]-1],subpath[int2[0]],int2[1]) + if int1[0]==int2[0]-1 : + parts += [ [sp1, [sp2[0],sp2[1],sp3[2]], sp4] ] + else : + parts += [ [sp1,sp2]+subpath[int1[0]+1:int2[0]-1]+[sp3,sp4] ] + return parts + + +def csp_from_arc(start, end, center, r, slope_st) : + # Creates csp that approximise specified arc + r = abs(r) + alpha = (atan2(end[0]-center[0],end[1]-center[1]) - atan2(start[0]-center[0],start[1]-center[1])) % math.pi2 + + sectors = int(abs(alpha)*2/math.pi)+1 + alpha_start = atan2(start[0]-center[0],start[1]-center[1]) + cos_,sin_ = math.cos(alpha_start), math.sin(alpha_start) + k = (4.*math.tan(alpha/sectors/4.)/3.) + if dot(slope_st , [- sin_*k*r, cos_*k*r]) < 0 : + if alpha>0 : alpha -= math.pi2 + else: alpha += math.pi2 + if abs(alpha*r)<0.001 : + return [] + + sectors = int(abs(alpha)*2/math.pi)+1 + k = (4.*math.tan(alpha/sectors/4.)/3.) + result = [] + for i in range(sectors+1) : + cos_,sin_ = math.cos(alpha_start + alpha*i/sectors), math.sin(alpha_start + alpha*i/sectors) + sp = [ [], [center[0] + cos_*r, center[1] + sin_*r], [] ] + sp[0] = [sp[1][0] + sin_*k*r, sp[1][1] - cos_*k*r ] + sp[2] = [sp[1][0] - sin_*k*r, sp[1][1] + cos_*k*r ] + result += [sp] + result[0][0] = result[0][1][:] + result[-1][2] = result[-1][1] + + return result + + +def point_to_arc_distance(p, arc): + ### Distance calculattion from point to arc + P0,P2,c,a = arc + dist = None + p = P(p) + r = (P0-c).mag() + if r>0 : + i = c + (p-c).unit()*r + alpha = ((i-c).angle() - (P0-c).angle()) + if a*alpha<0: + if alpha>0: alpha = alpha-math.pi2 + else: alpha = math.pi2+alpha + if between(alpha,0,a) or min(abs(alpha),abs(alpha-a))tolerance and i<4): + i += 1 + dl = d1*1 + for j in range(n+1): + t = float(j)/n + p = csp_at_t(sp1,sp2,t) + d = min(point_to_arc_distance(p,arc1), point_to_arc_distance(p,arc2)) + d1 = max(d1,d) + n=n*2 + return d1[0] + + +def csp_simple_bound_to_point_distance(p, csp): + minx,miny,maxx,maxy = None,None,None,None + for subpath in csp: + for sp in subpath: + for p_ in sp: + minx = min(minx,p_[0]) if minx!=None else p_[0] + miny = min(miny,p_[1]) if miny!=None else p_[1] + maxx = max(maxx,p_[0]) if maxx!=None else p_[0] + maxy = max(maxy,p_[1]) if maxy!=None else p_[1] + return math.sqrt(max(minx-p[0],p[0]-maxx,0)**2+max(miny-p[1],p[1]-maxy,0)**2) + + +def csp_point_inside_bound(sp1, sp2, p): + bez = [sp1[1],sp1[2],sp2[0],sp2[1]] + x,y = p + c = 0 + for i in range(4): + [x0,y0], [x1,y1] = bez[i-1], bez[i] + if x0-x1!=0 and (y-y0)*(x1-x0)>=(x-x0)*(y1-y0) and x>min(x0,x1) and x<=max(x0,x1) : + c +=1 + return c%2==0 + + +def csp_bound_to_point_distance(sp1, sp2, p): + if csp_point_inside_bound(sp1, sp2, p) : + return 0. + bez = csp_segment_to_bez(sp1,sp2) + min_dist = 1e100 + for i in range(0,4): + d = point_to_line_segment_distance_2(p, bez[i-1],bez[i]) + if d <= min_dist : min_dist = d + return min_dist + + +def line_line_intersect(p1,p2,p3,p4) : # Return only true intersection. + if (p1[0]==p2[0] and p1[1]==p2[1]) or (p3[0]==p4[0] and p3[1]==p4[1]) : return False + x = (p2[0]-p1[0])*(p4[1]-p3[1]) - (p2[1]-p1[1])*(p4[0]-p3[0]) + if x==0 : # Lines are parallel + if (p3[0]-p1[0])*(p2[1]-p1[1]) == (p3[1]-p1[1])*(p2[0]-p1[0]) : + if p3[0]!=p4[0] : + t11 = (p1[0]-p3[0])/(p4[0]-p3[0]) + t12 = (p2[0]-p3[0])/(p4[0]-p3[0]) + t21 = (p3[0]-p1[0])/(p2[0]-p1[0]) + t22 = (p4[0]-p1[0])/(p2[0]-p1[0]) + else: + t11 = (p1[1]-p3[1])/(p4[1]-p3[1]) + t12 = (p2[1]-p3[1])/(p4[1]-p3[1]) + t21 = (p3[1]-p1[1])/(p2[1]-p1[1]) + t22 = (p4[1]-p1[1])/(p2[1]-p1[1]) + return ("Overlap" if (0<=t11<=1 or 0<=t12<=1) and (0<=t21<=1 or 0<=t22<=1) else False) + else: return False + else : + return ( + 0<=((p4[0]-p3[0])*(p1[1]-p3[1]) - (p4[1]-p3[1])*(p1[0]-p3[0]))/x<=1 and + 0<=((p2[0]-p1[0])*(p1[1]-p3[1]) - (p2[1]-p1[1])*(p1[0]-p3[0]))/x<=1 ) + + +def line_line_intersection_points(p1,p2,p3,p4) : # Return only points [ (x,y) ] + if (p1[0]==p2[0] and p1[1]==p2[1]) or (p3[0]==p4[0] and p3[1]==p4[1]) : return [] + x = (p2[0]-p1[0])*(p4[1]-p3[1]) - (p2[1]-p1[1])*(p4[0]-p3[0]) + if x==0 : # Lines are parallel + if (p3[0]-p1[0])*(p2[1]-p1[1]) == (p3[1]-p1[1])*(p2[0]-p1[0]) : + if p3[0]!=p4[0] : + t11 = (p1[0]-p3[0])/(p4[0]-p3[0]) + t12 = (p2[0]-p3[0])/(p4[0]-p3[0]) + t21 = (p3[0]-p1[0])/(p2[0]-p1[0]) + t22 = (p4[0]-p1[0])/(p2[0]-p1[0]) + else: + t11 = (p1[1]-p3[1])/(p4[1]-p3[1]) + t12 = (p2[1]-p3[1])/(p4[1]-p3[1]) + t21 = (p3[1]-p1[1])/(p2[1]-p1[1]) + t22 = (p4[1]-p1[1])/(p2[1]-p1[1]) + res = [] + if (0<=t11<=1 or 0<=t12<=1) and (0<=t21<=1 or 0<=t22<=1) : + if 0<=t11<=1 : res += [p1] + if 0<=t12<=1 : res += [p2] + if 0<=t21<=1 : res += [p3] + if 0<=t22<=1 : res += [p4] + return res + else: return [] + else : + t1 = ((p4[0]-p3[0])*(p1[1]-p3[1]) - (p4[1]-p3[1])*(p1[0]-p3[0]))/x + t2 = ((p2[0]-p1[0])*(p1[1]-p3[1]) - (p2[1]-p1[1])*(p1[0]-p3[0]))/x + if 0<=t1<=1 and 0<=t2<=1 : return [ [p1[0]*(1-t1)+p2[0]*t1, p1[1]*(1-t1)+p2[1]*t1] ] + else : return [] + + +def point_to_point_d2(a,b): + return (a[0]-b[0])**2 + (a[1]-b[1])**2 + + +def point_to_point_d(a,b): + return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2) + + +def point_to_line_segment_distance_2(p1, p2,p3) : + # p1 - point, p2,p3 - line segment + #draw_pointer(p1) + w0 = [p1[0]-p2[0], p1[1]-p2[1]] + v = [p3[0]-p2[0], p3[1]-p2[1]] + c1 = w0[0]*v[0] + w0[1]*v[1] + if c1 <= 0 : + return w0[0]*w0[0]+w0[1]*w0[1] + c2 = v[0]*v[0] + v[1]*v[1] + if c2 <= c1 : + return (p1[0]-p3[0])**2 + (p1[1]-p3[1])**2 + return (p1[0]- p2[0]-v[0]*c1/c2)**2 + (p1[1]- p2[1]-v[1]*c1/c2) + + +def line_to_line_distance_2(p1,p2,p3,p4): + if line_line_intersect(p1,p2,p3,p4) : return 0 + return min( + point_to_line_segment_distance_2(p1,p3,p4), + point_to_line_segment_distance_2(p2,p3,p4), + point_to_line_segment_distance_2(p3,p1,p2), + point_to_line_segment_distance_2(p4,p1,p2)) + + +def csp_seg_bound_to_csp_seg_bound_max_min_distance(sp1,sp2,sp3,sp4) : + bez1 = csp_segment_to_bez(sp1,sp2) + bez2 = csp_segment_to_bez(sp3,sp4) + min_dist = 1e100 + max_dist = 0. + for i in range(4) : + if csp_point_inside_bound(sp1, sp2, bez2[i]) or csp_point_inside_bound(sp3, sp4, bez1[i]) : + min_dist = 0. + break + for i in range(4) : + for j in range(4) : + d = line_to_line_distance_2(bez1[i-1],bez1[i],bez2[j-1],bez2[j]) + if d < min_dist : min_dist = d + d = (bez2[j][0]-bez1[i][0])**2 + (bez2[j][1]-bez1[i][1])**2 + if max_dist < d : max_dist = d + return min_dist, max_dist + + +def csp_reverse(csp) : + for i in range(len(csp)) : + n = [] + for j in csp[i] : + n = [ [j[2][:],j[1][:],j[0][:]] ] + n + csp[i] = n[:] + return csp + + +def csp_normalized_slope(sp1,sp2,t) : + ax,ay,bx,by,cx,cy,dx,dy=bezmisc.bezierparameterize((sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])) + if sp1[1]==sp2[1]==sp1[2]==sp2[0] : return [1.,0.] + f1x = 3*ax*t*t+2*bx*t+cx + f1y = 3*ay*t*t+2*by*t+cy + if abs(f1x*f1x+f1y*f1y) > 1e-20 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + + if t == 0 : + f1x = sp2[0][0]-sp1[1][0] + f1y = sp2[0][1]-sp1[1][1] + if abs(f1x*f1x+f1y*f1y) > 1e-20 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + else : + f1x = sp2[1][0]-sp1[1][0] + f1y = sp2[1][1]-sp1[1][1] + if f1x*f1x+f1y*f1y != 0 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + elif t == 1 : + f1x = sp2[1][0]-sp1[2][0] + f1y = sp2[1][1]-sp1[2][1] + if abs(f1x*f1x+f1y*f1y) > 1e-20 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + else : + f1x = sp2[1][0]-sp1[1][0] + f1y = sp2[1][1]-sp1[1][1] + if f1x*f1x+f1y*f1y != 0 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + else : + return [1.,0.] + + +def csp_normalized_normal(sp1,sp2,t) : + nx,ny = csp_normalized_slope(sp1,sp2,t) + return [-ny, nx] + + +def csp_parameterize(sp1,sp2): + return bezmisc.bezierparameterize(csp_segment_to_bez(sp1,sp2)) + + +def csp_concat_subpaths(*s): + + def concat(s1,s2) : + if s1 == [] : return s2 + if s2 == [] : return s1 + if (s1[-1][1][0]-s2[0][1][0])**2 + (s1[-1][1][1]-s2[0][1][1])**2 > 0.00001 : + return s1[:-1]+[ [s1[-1][0],s1[-1][1],s1[-1][1]], [s2[0][1],s2[0][1],s2[0][2]] ] + s2[1:] + else : + return s1[:-1]+[ [s1[-1][0],s2[0][1],s2[0][2]] ] + s2[1:] + + if len(s) == 0 : return [] + if len(s) ==1 : return s[0] + result = s[0] + for s1 in s[1:]: + result = concat(result,s1) + return result + + +def csp_draw(csp, color="#05f", group = None, style="fill:none;", width = .1, comment = "") : + if csp!=[] and csp!=[[]] : + if group == None : group = options.doc_root + style += "stroke:"+color+";"+ "stroke-width:%0.4fpx;"%width + args = {"d": cubicsuperpath.formatPath(csp), "style":style} + if comment!="" : args["comment"] = str(comment) + inkex.etree.SubElement( group, inkex.addNS('path','svg'), args ) + + +def csp_subpaths_end_to_start_distance2(s1,s2): + return (s1[-1][1][0]-s2[0][1][0])**2 + (s1[-1][1][1]-s2[0][1][1])**2 + + +def csp_clip_by_line(csp,l1,l2) : + result = [] + for i in range(len(csp)): + s = csp[i] + intersections = [] + for j in range(1,len(s)) : + intersections += [ [j,int_] for int_ in csp_line_intersection(l1,l2,s[j-1],s[j])] + splitted_s = csp_subpath_split_by_points(s, intersections) + for s in splitted_s[:] : + clip = False + for p in csp_true_bounds([s]) : + if (l1[1]-l2[1])*p[0] + (l2[0]-l1[0])*p[1] + (l1[0]*l2[1]-l2[0]*l1[1])<-0.01 : + clip = True + break + if clip : + splitted_s.remove(s) + result += splitted_s + return result + + +def csp_subpath_line_to(subpath, points) : + # Appends subpath with line or polyline. + if len(points)>0 : + if len(subpath)>0: + subpath[-1][2] = subpath[-1][1][:] + if type(points[0]) == type([1,1]) : + for p in points : + subpath += [ [p[:],p[:],p[:]] ] + else: + subpath += [ [points,points,points] ] + return subpath + + +def csp_join_subpaths(csp) : + result = csp[:] + done_smf = True + joined_result = [] + while done_smf : + done_smf = False + while len(result)>0: + s1 = result[-1][:] + del(result[-1]) + j = 0 + joined_smf = False + while j0, abc*bcd>0, abc*cad>0 + if m1 and m2 and m3 : return [a,b,c] + if m1 and m2 and not m3 : return [a,b,c,d] + if m1 and not m2 and m3 : return [a,b,d,c] + if not m1 and m2 and m3 : return [a,d,b,c] + if m1 and not (m2 and m3) : return [a,b,d] + if not (m1 and m2) and m3 : return [c,a,d] + if not (m1 and m3) and m2 : return [b,c,d] + + raise ValueError, "csp_segment_convex_hull happend something that shouldnot happen!" + + +################################################################################ +### Bezier additional functions +################################################################################ + +def bez_bounds_intersect(bez1, bez2) : + return bounds_intersect(bez_bound(bez2), bez_bound(bez1)) + + +def bez_bound(bez) : + return [ + min(bez[0][0], bez[1][0], bez[2][0], bez[3][0]), + min(bez[0][1], bez[1][1], bez[2][1], bez[3][1]), + max(bez[0][0], bez[1][0], bez[2][0], bez[3][0]), + max(bez[0][1], bez[1][1], bez[2][1], bez[3][1]), + ] + + +def bounds_intersect(a, b) : + return not ( (a[0]>b[2]) or (b[0]>a[2]) or (a[1]>b[3]) or (b[1]>a[3]) ) + + +def tpoint((x1,y1),(x2,y2),t): + return [x1+t*(x2-x1),y1+t*(y2-y1)] + + +def bez_to_csp_segment(bez) : + return [bez[0],bez[0],bez[1]], [bez[2],bez[3],bez[3]] + + +def bez_split(a,t=0.5) : + a1 = tpoint(a[0],a[1],t) + at = tpoint(a[1],a[2],t) + b2 = tpoint(a[2],a[3],t) + a2 = tpoint(a1,at,t) + b1 = tpoint(b2,at,t) + a3 = tpoint(a2,b1,t) + return [a[0],a1,a2,a3], [a3,b1,b2,a[3]] + + +def bez_at_t(bez,t) : + return csp_at_t([bez[0],bez[0],bez[1]],[bez[2],bez[3],bez[3]],t) + + +def bez_to_point_distance(bez,p,needed_dist=[0.,1e100]): + # returns [d^2,t] + return csp_seg_to_point_distance(bez_to_csp_segment(bez),p,needed_dist) + + +def bez_normalized_slope(bez,t): + return csp_normalized_slope([bez[0],bez[0],bez[1]], [bez[2],bez[3],bez[3]],t) + +################################################################################ +### Some vector functions +################################################################################ + +def normalize((x,y)) : + l = math.sqrt(x**2+y**2) + if l == 0 : return [0.,0.] + else : return [x/l, y/l] + + +def cross(a,b) : + return a[1] * b[0] - a[0] * b[1] + + +def dot(a,b) : + return a[0] * b[0] + a[1] * b[1] + + +def rotate_ccw(d) : + return [-d[1],d[0]] + + +def vectors_ccw(a,b): + return a[0]*b[1]-b[0]*a[1] < 0 + + +def vector_from_to_length(a,b): + return math.sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1])) + +################################################################################ +### Common functions +################################################################################ + +def matrix_mul(a,b) : + return [ [ sum([a[i][k]*b[k][j] for k in range(len(a[0])) ]) for j in range(len(b[0]))] for i in range(len(a))] + try : + return [ [ sum([a[i][k]*b[k][j] for k in range(len(a[0])) ]) for j in range(len(b[0]))] for i in range(len(a))] + except : + return None + + +def transpose(a) : + try : + return [ [ a[i][j] for i in range(len(a)) ] for j in range(len(a[0])) ] + except : + return None + + +def det_3x3(a): + return float( + a[0][0]*a[1][1]*a[2][2] + a[0][1]*a[1][2]*a[2][0] + a[1][0]*a[2][1]*a[0][2] + - a[0][2]*a[1][1]*a[2][0] - a[0][0]*a[2][1]*a[1][2] - a[0][1]*a[2][2]*a[1][0] + ) + + +def inv_3x3(a): # invert matrix 3x3 + det = det_3x3(a) + if det==0: return None + return [ + [ (a[1][1]*a[2][2] - a[2][1]*a[1][2])/det, -(a[0][1]*a[2][2] - a[2][1]*a[0][2])/det, (a[0][1]*a[1][2] - a[1][1]*a[0][2])/det ], + [ -(a[1][0]*a[2][2] - a[2][0]*a[1][2])/det, (a[0][0]*a[2][2] - a[2][0]*a[0][2])/det, -(a[0][0]*a[1][2] - a[1][0]*a[0][2])/det ], + [ (a[1][0]*a[2][1] - a[2][0]*a[1][1])/det, -(a[0][0]*a[2][1] - a[2][0]*a[0][1])/det, (a[0][0]*a[1][1] - a[1][0]*a[0][1])/det ] + ] + + +def inv_2x2(a): # invert matrix 2x2 + det = a[0][0]*a[1][1] - a[1][0]*a[0][1] + if det==0: return None + return [ + [a[1][1]/det, -a[0][1]/det], + [-a[1][0]/det, a[0][0]/det] + ] + + +def small(a) : + global small_tolerance + return abs(a)=0 : + t = m+math.sqrt(n) + m1 = pow(t/2,1./3) if t>=0 else -pow(-t/2,1./3) + t = m-math.sqrt(n) + n1 = pow(t/2,1./3) if t>=0 else -pow(-t/2,1./3) + else : + m1 = pow(complex((m+cmath.sqrt(n))/2),1./3) + n1 = pow(complex((m-cmath.sqrt(n))/2),1./3) + x1 = -1./3 * (a + m1 + n1) + x2 = -1./3 * (a + w1*m1 + w2*n1) + x3 = -1./3 * (a + w2*m1 + w1*n1) + return [x1,x2,x3] + elif b!=0: + det = c**2-4*b*d + if det>0 : + return [(-c+math.sqrt(det))/(2*b),(-c-math.sqrt(det))/(2*b)] + elif d == 0 : + return [-c/(b*b)] + else : + return [(-c+cmath.sqrt(det))/(2*b),(-c-cmath.sqrt(det))/(2*b)] + elif c!=0 : + return [-d/c] + else : return [] + + +################################################################################ +### print_ prints any arguments into specified log file +################################################################################ + +def print_(*arg): + f = open(options.log_filename,"a") + for s in arg : + s = str(unicode(s).encode('unicode_escape'))+" " + f.write( s ) + f.write("\n") + f.close() + + +################################################################################ +### Point (x,y) operations +################################################################################ +class P: + def __init__(self, x, y=None): + if not y==None: + self.x, self.y = float(x), float(y) + else: + self.x, self.y = float(x[0]), float(x[1]) + def __add__(self, other): return P(self.x + other.x, self.y + other.y) + def __sub__(self, other): return P(self.x - other.x, self.y - other.y) + def __neg__(self): return P(-self.x, -self.y) + def __mul__(self, other): + if isinstance(other, P): + return self.x * other.x + self.y * other.y + return P(self.x * other, self.y * other) + __rmul__ = __mul__ + def __div__(self, other): return P(self.x / other, self.y / other) + def mag(self): return math.hypot(self.x, self.y) + def unit(self): + h = self.mag() + if h: return self / h + else: return P(0,0) + def dot(self, other): return self.x * other.x + self.y * other.y + def rot(self, theta): + c = math.cos(theta) + s = math.sin(theta) + return P(self.x * c - self.y * s, self.x * s + self.y * c) + def angle(self): return math.atan2(self.y, self.x) + def __repr__(self): return '%f,%f' % (self.x, self.y) + def pr(self): return "%.2f,%.2f" % (self.x, self.y) + def to_list(self): return [self.x, self.y] + def ccw(self): return P(-self.y,self.x) + def l2(self): return self.x*self.x + self.y*self.y + +################################################################################ +### +### Offset function +### +### This function offsets given cubic super path. +### It's based on src/livarot/PathOutline.cpp from Inkscape's source code. +### +### +################################################################################ +def csp_offset(csp, r) : + offset_tolerance = 0.05 + offset_subdivision_depth = 10 + time_ = time.time() + time_start = time_ + print_("Offset start at %s"% time_) + print_("Offset radius %s"% r) + + + def csp_offset_segment(sp1,sp2,r) : + result = [] + t = csp_get_t_at_curvature(sp1,sp2,1/r) + if len(t) == 0 : t =[0.,1.] + t.sort() + if t[0]>.00000001 : t = [0.]+t + if t[-1]<.99999999 : t.append(1.) + for st,end in zip(t,t[1:]) : + c = csp_curvature_at_t(sp1,sp2,(st+end)/2) + sp = csp_split_by_two_points(sp1,sp2,st,end) + if sp[1]!=sp[2]: + if (c>1/r and r<0 or c<1/r and r>0) : + offset = offset_segment_recursion(sp[1],sp[2],r, offset_subdivision_depth, offset_tolerance) + else : # This part will be clipped for sure... TODO Optimize it... + offset = offset_segment_recursion(sp[1],sp[2],r, offset_subdivision_depth, offset_tolerance) + + if result==[] : + result = offset[:] + else: + if csp_subpaths_end_to_start_distance2(result,offset)<0.0001 : + result = csp_concat_subpaths(result,offset) + else: + + intersection = csp_get_subapths_last_first_intersection(result,offset) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(result[i-1],result[i],t1) + result = result[:i-1] + [ sp1_, sp2_ ] + sp1_,sp2_,sp3_ = csp_split(offset[j-1],offset[j],t2) + result = csp_concat_subpaths( result, [sp2_,sp3_] + offset[j+1:] ) + else : + pass # ??? + #raise ValueError, "Offset curvature clipping error" + #csp_draw([result]) + return result + + + def create_offset_segment(sp1,sp2,r) : + # See Gernot Hoffmann "Bezier Curves" p.34 -> 7.1 Bezier Offset Curves + p0,p1,p2,p3 = P(sp1[1]),P(sp1[2]),P(sp2[0]),P(sp2[1]) + s0,s1,s3 = p1-p0,p2-p1,p3-p2 + n0 = s0.ccw().unit() if s0.l2()!=0 else P(csp_normalized_normal(sp1,sp2,0)) + n3 = s3.ccw().unit() if s3.l2()!=0 else P(csp_normalized_normal(sp1,sp2,1)) + n1 = s1.ccw().unit() if s1.l2()!=0 else (n0.unit()+n3.unit()).unit() + + q0,q3 = p0+r*n0, p3+r*n3 + c = csp_curvature_at_t(sp1,sp2,0) + q1 = q0 + (p1-p0)*(1- (r*c if abs(c)<100 else 0) ) + c = csp_curvature_at_t(sp1,sp2,1) + q2 = q3 + (p2-p3)*(1- (r*c if abs(c)<100 else 0) ) + + + return [[q0.to_list(), q0.to_list(), q1.to_list()],[q2.to_list(), q3.to_list(), q3.to_list()]] + + + def csp_get_subapths_last_first_intersection(s1,s2): + _break = False + for i in range(1,len(s1)) : + sp11, sp12 = s1[-i-1], s1[-i] + for j in range(1,len(s2)) : + sp21,sp22 = s2[j-1], s2[j] + intersection = csp_segments_true_intersection(sp11,sp12,sp21,sp22) + if intersection != [] : + _break = True + break + if _break:break + if _break : + intersection = max(intersection) + return [len(s1)-i,intersection[0], j,intersection[1]] + else : + return [] + + + def csp_join_offsets(prev,next,sp1,sp2,sp1_l,sp2_l,r): + if len(next)>1 : + if (P(prev[-1][1])-P(next[0][1])).l2()<0.001 : + return prev,[],next + intersection = csp_get_subapths_last_first_intersection(prev,next) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(prev[i-1],prev[i],t1) + sp3_,sp4_,sp5_ = csp_split(next[j-1], next[j],t2) + return prev[:i-1] + [ sp1_, sp2_ ], [], [sp4_,sp5_] + next[j+1:] + + # Offsets do not intersect... will add an arc... + start = (P(csp_at_t(sp1_l,sp2_l,1.)) + r*P(csp_normalized_normal(sp1_l,sp2_l,1.))).to_list() + end = (P(csp_at_t(sp1,sp2,0.)) + r*P(csp_normalized_normal(sp1,sp2,0.))).to_list() + arc = csp_from_arc(start, end, sp1[1], r, csp_normalized_slope(sp1_l,sp2_l,1.) ) + if arc == [] : + return prev,[],next + else: + # Clip prev by arc + if csp_subpaths_end_to_start_distance2(prev,arc)>0.00001 : + intersection = csp_get_subapths_last_first_intersection(prev,arc) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(prev[i-1],prev[i],t1) + sp3_,sp4_,sp5_ = csp_split(arc[j-1],arc[j],t2) + prev = prev[:i-1] + [ sp1_, sp2_ ] + arc = [sp4_,sp5_] + arc[j+1:] + #else : raise ValueError, "Offset curvature clipping error" + # Clip next by arc + if next == [] : + return prev,[],arc + if csp_subpaths_end_to_start_distance2(arc,next)>0.00001 : + intersection = csp_get_subapths_last_first_intersection(arc,next) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(arc[i-1],arc[i],t1) + sp3_,sp4_,sp5_ = csp_split(next[j-1],next[j],t2) + arc = arc[:i-1] + [ sp1_, sp2_ ] + next = [sp4_,sp5_] + next[j+1:] + #else : raise ValueError, "Offset curvature clipping error" + + return prev,arc,next + + + def offset_segment_recursion(sp1,sp2,r, depth, tolerance) : + sp1_r,sp2_r = create_offset_segment(sp1,sp2,r) + err = max( + csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.25)) + P(csp_normalized_normal(sp1,sp2,.25))*r).to_list())[0], + csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.50)) + P(csp_normalized_normal(sp1,sp2,.50))*r).to_list())[0], + csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.75)) + P(csp_normalized_normal(sp1,sp2,.75))*r).to_list())[0], + ) + + if err>tolerance**2 and depth>0: + #print_(csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.25)) + P(csp_normalized_normal(sp1,sp2,.25))*r).to_list())[0], tolerance) + if depth > offset_subdivision_depth-2 : + t = csp_max_curvature(sp1,sp2) + t = max(.1,min(.9 ,t)) + else : + t = .5 + sp3,sp4,sp5 = csp_split(sp1,sp2,t) + r1 = offset_segment_recursion(sp3,sp4,r, depth-1, tolerance) + r2 = offset_segment_recursion(sp4,sp5,r, depth-1, tolerance) + return r1[:-1]+ [[r1[-1][0],r1[-1][1],r2[0][2]]] + r2[1:] + else : + #csp_draw([[sp1_r,sp2_r]]) + #draw_pointer(sp1[1]+sp1_r[1], "#057", "line") + #draw_pointer(sp2[1]+sp2_r[1], "#705", "line") + return [sp1_r,sp2_r] + + + ############################################################################ + # Some small definitions + ############################################################################ + csp_len = len(csp) + + ############################################################################ + # Prepare the path + ############################################################################ + # Remove all small segments (segment length < 0.001) + + for i in xrange(len(csp)) : + for j in xrange(len(csp[i])) : + sp = csp[i][j] + if (P(sp[1])-P(sp[0])).mag() < 0.001 : + csp[i][j][0] = sp[1] + if (P(sp[2])-P(sp[0])).mag() < 0.001 : + csp[i][j][2] = sp[1] + for i in xrange(len(csp)) : + for j in xrange(1,len(csp[i])) : + if cspseglength(csp[i][j-1], csp[i][j])<0.001 : + csp[i] = csp[i][:j] + csp[i][j+1:] + if cspseglength(csp[i][-1],csp[i][0])>0.001 : + csp[i][-1][2] = csp[i][-1][1] + csp[i]+= [ [csp[i][0][1],csp[i][0][1],csp[i][0][1]] ] + + # TODO Get rid of self intersections. + + original_csp = csp[:] + # Clip segments which has curvature>1/r. Because their offset will be selfintersecting and very nasty. + + print_("Offset prepared the path in %s"%(time.time()-time_)) + print_("Path length = %s"% sum([len(i)for i in csp] ) ) + time_ = time.time() + + ############################################################################ + # Offset + ############################################################################ + # Create offsets for all segments in the path. And join them together inside each subpath. + unclipped_offset = [[] for i in xrange(csp_len)] + offsets_original = [[] for i in xrange(csp_len)] + join_points = [[] for i in xrange(csp_len)] + intersection = [[] for i in xrange(csp_len)] + for i in xrange(csp_len) : + subpath = csp[i] + subpath_offset = [] + last_offset_len = 0 + for sp1,sp2 in zip(subpath, subpath[1:]) : + segment_offset = csp_offset_segment(sp1,sp2,r) + if subpath_offset == [] : + subpath_offset = segment_offset + + prev_l = len(subpath_offset) + else : + prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:],segment_offset,sp1,sp2,sp1_l,sp2_l,r) + #csp_draw([prev],"Blue") + #csp_draw([arc],"Magenta") + subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l+1],prev,arc,next) + prev_l = len(next) + sp1_l, sp2_l = sp1[:], sp2[:] + + # Join last and first offsets togother to close the curve + + prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], subpath_offset[:2], subpath[0], subpath[1], sp1_l,sp2_l, r) + subpath_offset[:2] = next[:] + subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l+1],prev,arc) + #csp_draw([prev],"Blue") + #csp_draw([arc],"Red") + #csp_draw([next],"Red") + + # Collect subpath's offset and save it to unclipped offset list. + unclipped_offset[i] = subpath_offset[:] + + #for k,t in intersection[i]: + # draw_pointer(csp_at_t(subpath_offset[k-1], subpath_offset[k], t)) + + #inkex.etree.SubElement( options.doc_root, inkex.addNS('path','svg'), {"d": cubicsuperpath.formatPath(unclipped_offset), "style":"fill:none;stroke:#0f0;"} ) + print_("Offsetted path in %s"%(time.time()-time_)) + time_ = time.time() + + #for i in range(len(unclipped_offset)): + # csp_draw([unclipped_offset[i]], color = ["Green","Red","Blue"][i%3], width = .1) + #return [] + ############################################################################ + # Now to the clipping. + ############################################################################ + # First of all find all intersection's between all segments of all offseted subpaths, including self intersections. + + #TODO define offset tolerance here + global small_tolerance + small_tolerance = 0.01 + summ = 0 + summ1 = 0 + for subpath_i in xrange(csp_len) : + for subpath_j in xrange(subpath_i,csp_len) : + subpath = unclipped_offset[subpath_i] + subpath1 = unclipped_offset[subpath_j] + for i in xrange(1,len(subpath)) : + # If subpath_i==subpath_j we are looking for self intersections, so + # we'll need search intersections only for xrange(i,len(subpath1)) + for j in ( xrange(i,len(subpath1)) if subpath_i==subpath_j else xrange(len(subpath1))) : + if subpath_i==subpath_j and j==i : + # Find self intersections of a segment + sp1,sp2,sp3 = csp_split(subpath[i-1],subpath[i],.5) + intersections = csp_segments_intersection(sp1,sp2,sp2,sp3) + summ +=1 + for t in intersections : + summ1 += 1 + if not ( small(t[0]-1) and small(t[1]) ) and 0<=t[0]<=1 and 0<=t[1]<=1 : + intersection[subpath_i] += [ [i,t[0]/2],[j,t[1]/2+.5] ] + else : + intersections = csp_segments_intersection(subpath[i-1],subpath[i],subpath1[j-1],subpath1[j]) + summ +=1 + for t in intersections : + summ1 += 1 + #TODO tolerance dependence to cpsp_length(t) + if len(t) == 2 and 0<=t[0]<=1 and 0<=t[1]<=1 and not ( + subpath_i==subpath_j and ( + (j-i-1) % (len(subpath)-1) == 0 and small(t[0]-1) and small(t[1]) or + (i-j-1) % (len(subpath)-1) == 0 and small(t[1]-1) and small(t[0]) ) ) : + intersection[subpath_i] += [ [i,t[0]] ] + intersection[subpath_j] += [ [j,t[1]] ] + #draw_pointer(csp_at_t(subpath[i-1],subpath[i],t[0]),"#f00") + #print_(t) + #print_(i,j) + elif len(t)==5 and t[4]=="Overlap": + intersection[subpath_i] += [ [i,t[0]], [i,t[1]] ] + intersection[subpath_j] += [ [j,t[1]], [j,t[3]] ] + + print_("Intersections found in %s"%(time.time()-time_)) + print_("Examined %s segments"%(summ)) + print_("found %s intersections"%(summ1)) + time_ = time.time() + + ######################################################################## + # Split unclipped offset by intersection points into splitted_offset + ######################################################################## + splitted_offset = [] + for i in xrange(csp_len) : + subpath = unclipped_offset[i] + if len(intersection[i]) > 0 : + parts = csp_subpath_split_by_points(subpath, intersection[i]) + # Close parts list to close path (The first and the last parts are joined together) + if [1,0.] not in intersection[i] : + parts[0][0][0] = parts[-1][-1][0] + parts[0] = csp_concat_subpaths(parts[-1], parts[0]) + splitted_offset += parts[:-1] + else: + splitted_offset += parts[:] + else : + splitted_offset += [subpath[:]] + + #for i in range(len(splitted_offset)): + # csp_draw([splitted_offset[i]], color = ["Green","Red","Blue"][i%3]) + print_("Splitted in %s"%(time.time()-time_)) + time_ = time.time() + + + ######################################################################## + # Clipping + ######################################################################## + result = [] + for subpath_i in range(len(splitted_offset)): + clip = False + s1 = splitted_offset[subpath_i] + for subpath_j in range(len(splitted_offset)): + s2 = splitted_offset[subpath_j] + if (P(s1[0][1])-P(s2[-1][1])).l2()<0.0001 and ( (subpath_i+1) % len(splitted_offset) != subpath_j ): + if dot(csp_normalized_normal(s2[-2],s2[-1],1.),csp_normalized_slope(s1[0],s1[1],0.))*r<-0.0001 : + clip = True + break + if (P(s2[0][1])-P(s1[-1][1])).l2()<0.0001 and ( (subpath_j+1) % len(splitted_offset) != subpath_i ): + if dot(csp_normalized_normal(s2[0],s2[1],0.),csp_normalized_slope(s1[-2],s1[-1],1.))*r>0.0001 : + clip = True + break + + if not clip : + result += [s1[:]] + elif options.offset_draw_clippend_path : + csp_draw([s1],color="Red",width=.1) + draw_pointer( csp_at_t(s2[-2],s2[-1],1.)+ + (P(csp_at_t(s2[-2],s2[-1],1.))+ P(csp_normalized_normal(s2[-2],s2[-1],1.))*10).to_list(),"Green", "line" ) + draw_pointer( csp_at_t(s1[0],s1[1],0.)+ + (P(csp_at_t(s1[0],s1[1],0.))+ P(csp_normalized_slope(s1[0],s1[1],0.))*10).to_list(),"Red", "line" ) + + # Now join all together and check closure and orientation of result + joined_result = csp_join_subpaths(result) + # Check if each subpath from joined_result is closed + #csp_draw(joined_result,color="Green",width=1) + + + for s in joined_result[:] : + if csp_subpaths_end_to_start_distance2(s,s) > 0.001 : + # Remove open parts + if options.offset_draw_clippend_path: + csp_draw([s],color="Orange",width=1) + draw_pointer(s[0][1], comment= csp_subpaths_end_to_start_distance2(s,s)) + draw_pointer(s[-1][1], comment = csp_subpaths_end_to_start_distance2(s,s)) + joined_result.remove(s) + else : + # Remove small parts + minx,miny,maxx,maxy = csp_true_bounds([s]) + if (minx[0]-maxx[0])**2 + (miny[1]-maxy[1])**2 < 0.1 : + joined_result.remove(s) + print_("Clipped and joined path in %s"%(time.time()-time_)) + time_ = time.time() + + ######################################################################## + # Now to the Dummy cliping: remove parts from splitted offset if their + # centers are closer to the original path than offset radius. + ######################################################################## + + r1,r2 = ( (0.99*r)**2, (1.01*r)**2 ) if abs(r*.01)<1 else ((abs(r)-1)**2, (abs(r)+1)**2) + for s in joined_result[:]: + dist = csp_to_point_distance(original_csp, s[int(len(s)/2)][1], dist_bounds = [r1,r2], tolerance = .000001) + if not r1 < dist[0] < r2 : + joined_result.remove(s) + if options.offset_draw_clippend_path: + csp_draw([s], comment = math.sqrt(dist[0])) + draw_pointer(csp_at_t(csp[dist[1]][dist[2]-1],csp[dist[1]][dist[2]],dist[3])+s[int(len(s)/2)][1],"blue", "line", comment = [math.sqrt(dist[0]),i,j,sp] ) + + print_("-----------------------------") + print_("Total offset time %s"%(time.time()-time_start)) + print_() + return joined_result + + + + + +################################################################################ +### +### Biarc function +### +### Calculates biarc approximation of cubic super path segment +### splits segment if needed or approximates it with straight line +### +################################################################################ +def biarc(sp1, sp2, z1, z2, depth=0): + def biarc_split(sp1,sp2, z1, z2, depth): + if depth 0 : raise ValueError, (a,b,c,disq,beta1,beta2) + beta = max(beta1, beta2) + elif asmall and bsmall: + return biarc_split(sp1, sp2, z1, z2, depth) + alpha = beta * r + ab = alpha + beta + P1 = P0 + alpha * TS + P3 = P4 - beta * TE + P2 = (beta / ab) * P1 + (alpha / ab) * P3 + + + def calculate_arc_params(P0,P1,P2): + D = (P0+P2)/2 + if (D-P1).mag()==0: return None, None + R = D - ( (D-P0).mag()**2/(D-P1).mag() )*(P1-D).unit() + p0a, p1a, p2a = (P0-R).angle()%(2*math.pi), (P1-R).angle()%(2*math.pi), (P2-R).angle()%(2*math.pi) + alpha = (p2a - p0a) % (2*math.pi) + if (p0a1000000 or abs(R.y)>1000000 or (R-P0).mag<.1 : + return None, None + else : + return R, alpha + R1,a1 = calculate_arc_params(P0,P1,P2) + R2,a2 = calculate_arc_params(P2,P3,P4) + if R1==None or R2==None or (R1-P0).mag() options.biarc_tolerance and depthls : + res += [seg] + else : + if seg[1] == "arc" : + r = math.sqrt((seg[0][0]-seg[2][0])**2+(seg[0][1]-seg[2][1])**2) + x,y = seg[0][0]-seg[2][0], seg[0][1]-seg[2][1] + a = seg[3]/ls*(l-lc) + x,y = x*math.cos(a) - y*math.sin(a), x*math.sin(a) + y*math.cos(a) + x,y = x+seg[2][0], y+seg[2][1] + res += [[ seg[0], "arc", seg[2], a, [x,y], [seg[5][0],seg[5][1]/ls*(l-lc)] ]] + if seg[1] == "line" : + res += [[ seg[0], "line", 0, 0, [(seg[4][0]-seg[0][0])/ls*(l-lc),(seg[4][1]-seg[0][1])/ls*(l-lc)], [seg[5][0],seg[5][1]/ls*(l-lc)] ]] + i += 1 + if i >= len(subcurve) and not subcurve_closed: + reverse = not reverse + i = i%len(subcurve) + return res + +################################################################################ +### Polygon class +################################################################################ +class Polygon: + def __init__(self, polygon=None): + self.polygon = [] if polygon==None else polygon[:] + + + def move(self, x, y) : + for i in range(len(self.polygon)) : + for j in range(len(self.polygon[i])) : + self.polygon[i][j][0] += x + self.polygon[i][j][1] += y + + + def bounds(self) : + minx,miny,maxx,maxy = 1e400, 1e400, -1e400, -1e400 + for poly in self.polygon : + for p in poly : + if minx > p[0] : minx = p[0] + if miny > p[1] : miny = p[1] + if maxx < p[0] : maxx = p[0] + if maxy < p[1] : maxy = p[1] + return minx*1,miny*1,maxx*1,maxy*1 + + + def width(self): + b = self.bounds() + return b[2]-b[0] + + + def rotate_(self,sin,cos) : + for i in range(len(self.polygon)) : + for j in range(len(self.polygon[i])) : + x,y = self.polygon[i][j][0], self.polygon[i][j][1] + self.polygon[i][j][0] = x*cos - y*sin + self.polygon[i][j][1] = x*sin + y*cos + + + def rotate(self, a): + cos, sin = math.cos(a), math.sin(a) + self.rotate_(sin,cos) + + + def drop_into_direction(self, direction, surface) : + # Polygon is a list of simple polygons + # Surface is a polygon + line y = 0 + # Direction is [dx,dy] + if len(self.polygon) == 0 or len(self.polygon[0])==0 : return + if direction[0]**2 + direction[1]**2 <1e-10 : return + direction = normalize(direction) + sin,cos = direction[0], -direction[1] + self.rotate_(-sin,cos) + surface.rotate_(-sin,cos) + self.drop_down(surface, zerro_plane = False) + self.rotate_(sin,cos) + surface.rotate_(sin,cos) + + + def centroid(self): + centroids = [] + sa = 0 + for poly in self.polygon: + cx,cy,a = 0,0,0 + for i in range(len(poly)): + [x1,y1],[x2,y2] = poly[i-1],poly[i] + cx += (x1+x2)*(x1*y2-x2*y1) + cy += (y1+y2)*(x1*y2-x2*y1) + a += (x1*y2-x2*y1) + a *= 3. + if abs(a)>0 : + cx /= a + cy /= a + sa += abs(a) + centroids += [ [cx,cy,a] ] + if sa == 0 : return [0.,0.] + cx,cy = 0.,0. + for c in centroids : + cx += c[0]*c[2] + cy += c[1]*c[2] + cx /= sa + cy /= sa + return [cx,cy] + + + def drop_down(self, surface, zerro_plane = True) : + # Polygon is a list of simple polygons + # Surface is a polygon + line y = 0 + # Down means min y (0,-1) + if len(self.polygon) == 0 or len(self.polygon[0])==0 : return + # Get surface top point + top = surface.bounds()[3] + if zerro_plane : top = max(0, top) + # Get polygon bottom point + bottom = self.bounds()[1] + self.move(0, top - bottom + 10) + # Now get shortest distance from surface to polygon in positive x=0 direction + # Such distance = min(distance(vertex, edge)...) where edge from surface and + # vertex from polygon and vice versa... + dist = 1e300 + for poly in surface.polygon : + for i in range(len(poly)) : + for poly1 in self.polygon : + for i1 in range(len(poly1)) : + st,end = poly[i-1], poly[i] + vertex = poly1[i1] + if st[0]<=vertex[0]<= end[0] or end[0]<=vertex[0]<=st[0] : + if st[0]==end[0] : d = min(vertex[1]-st[1],vertex[1]-end[1]) + else : d = vertex[1] - st[1] - (end[1]-st[1])*(vertex[0]-st[0])/(end[0]-st[0]) + if dist > d : dist = d + # and vice versa just change the sign because vertex now under the edge + st,end = poly1[i1-1], poly1[i1] + vertex = poly[i] + if st[0]<=vertex[0]<=end[0] or end[0]<=vertex[0]<=st[0] : + if st[0]==end[0] : d = min(- vertex[1]+st[1],-vertex[1]+end[1]) + else : d = - vertex[1] + st[1] + (end[1]-st[1])*(vertex[0]-st[0])/(end[0]-st[0]) + if dist > d : dist = d + + if zerro_plane and dist > 10 + top : dist = 10 + top + #print_(dist, top, bottom) + #self.draw() + self.move(0, -dist) + + + def draw(self,color="#075",width=.1) : + for poly in self.polygon : + csp_draw( [csp_subpath_line_to([],poly+[poly[0]])], color=color,width=width ) + + + def add(self, add) : + if type(add) == type([]) : + self.polygon += add[:] + else : + self.polygon += add.polygon[:] + + + def point_inside(self,p) : + inside = False + for poly in self.polygon : + for i in range(len(poly)): + st,end = poly[i-1], poly[i] + if p==st or p==end : return True # point is a vertex = point is on the edge + if st[0]>end[0] : st, end = end, st # This will be needed to check that edge if open only at rigth end + c = (p[1]-st[1])*(end[0]-st[0])-(end[1]-st[1])*(p[0]-st[0]) + #print_(c) + if st[0]<=p[0]0.000001 and point_to_point_d2(p,e)>0.000001 : + poly_ += [p] + # Check self intersections with other polys + for i2 in range(len(self.polygon)): + if i1==i2 : continue + poly2 = self.polygon[i2] + for j2 in range(len(poly2)): + s1, e1 = poly2[j2-1],poly2[j2] + int_ = line_line_intersection_points(s,e,s1,e1) + for p in int_ : + if point_to_point_d2(p,s)>0.000001 and point_to_point_d2(p,e)>0.000001 : + poly_ += [p] + hull += [poly_] + # Create the dictionary containing all edges in both directions + edges = {} + for poly in self.polygon : + for i in range(len(poly)): + s,e = tuple(poly[i-1]), tuple(poly[i]) + if (point_to_point_d2(e,s)<0.000001) : continue + break_s, break_e = False, False + for p in edges : + if point_to_point_d2(p,s)<0.000001 : + break_s = True + s = p + if point_to_point_d2(p,e)<0.000001 : + break_e = True + e = p + if break_s and break_e : break + l = point_to_point_d(s,e) + if not break_s and not break_e : + edges[s] = [ [s,e,l] ] + edges[e] = [ [e,s,l] ] + #draw_pointer(s+e,"red","line") + #draw_pointer(s+e,"red","line") + else : + if e in edges : + for edge in edges[e] : + if point_to_point_d2(edge[1],s)<0.000001 : + break + if point_to_point_d2(edge[1],s)>0.000001 : + edges[e] += [ [e,s,l] ] + #draw_pointer(s+e,"red","line") + + else : + edges[e] = [ [e,s,l] ] + #draw_pointer(s+e,"green","line") + if s in edges : + for edge in edges[s] : + if point_to_point_d2(edge[1],e)<0.000001 : + break + if point_to_point_d2(edge[1],e)>0.000001 : + edges[s] += [ [s,e, l] ] + #draw_pointer(s+e,"red","line") + else : + edges[s] = [ [s,e,l] ] + #draw_pointer(s+e,"green","line") + + + def angle_quadrant(sin,cos): + # quadrants are (0,pi/2], (pi/2,pi], (pi,3*pi/2], (3*pi/2, 2*pi], i.e. 0 is in the 4-th quadrant + if sin>0 and cos>=0 : return 1 + if sin>=0 and cos<0 : return 2 + if sin<0 and cos<=0 : return 3 + if sin<=0 and cos>0 : return 4 + + + def angle_is_less(sin,cos,sin1,cos1): + # 0 = 2*pi is the largest angle + if [sin1, cos1] == [0,1] : return True + if [sin, cos] == [0,1] : return False + if angle_quadrant(sin,cos)>angle_quadrant(sin1,cos1) : + return False + if angle_quadrant(sin,cos)=0 and cos>0 : return sin0 and cos<=0 : return sin>sin1 + if sin<=0 and cos<0 : return sin>sin1 + if sin<0 and cos>=0 : return sin len_edges : raise ValueError, "Hull error" + loops1 += 1 + next = get_closes_edge_by_angle(edges[last[1]],last) + #draw_pointer(next[0]+next[1],"Green","line", comment=i, width= 1) + #print_(next[0],"-",next[1]) + + last = next + poly += [ list(last[0]) ] + self.polygon += [ poly ] + # Remove all edges that are intersects new poly (any vertex inside new poly) + poly_ = Polygon([poly]) + for p in edges.keys()[:] : + if poly_.point_inside(list(p)) : del edges[p] + self.draw(color="Green", width=1) + + +class Arangement_Genetic: + # gene = [fittness, order, rotation, xposition] + # spieces = [gene]*shapes count + # population = [spieces] + def __init__(self, polygons, material_width): + self.population = [] + self.genes_count = len(polygons) + self.polygons = polygons + self.width = material_width + self.mutation_factor = 0.1 + self.order_mutate_factor = 1. + self.move_mutate_factor = 1. + + + def add_random_species(self,count): + for i in range(count): + specimen = [] + order = range(self.genes_count) + random.shuffle(order) + for j in order: + specimen += [ [j, random.random(), random.random()] ] + self.population += [ [None,specimen] ] + + + def species_distance2(self,sp1,sp2) : + # retun distance, each component is normalized + s = 0 + for j in range(self.genes_count) : + s += ((sp1[j][0]-sp2[j][0])/self.genes_count)**2 + (( sp1[j][1]-sp2[j][1]))**2 + ((sp1[j][2]-sp2[j][2]))**2 + return s + + + def similarity(self,sp1,top) : + # Define similarity as a simple distance between two points in len(gene)*len(spiece) -th dimentions + # for sp2 in top_spieces sum(|sp1-sp2|)/top_count + sim = 0 + for sp2 in top : + sim += math.sqrt(species_distance2(sp1,sp2[1])) + return sim/len(top) + + + def leave_top_species(self,count): + self.population.sort() + res = [ copy.deepcopy(self.population[0]) ] + del self.population[0] + for i in range(count-1) : + t = [] + for j in range(20) : + i1 = random.randint(0,len(self.population)-1) + t += [ [self.population[i1][0],i1] ] + t.sort() + res += [ copy.deepcopy(self.population[t[0][1]]) ] + del self.population[t[0][1]] + self.population = res + #del self.population[0] + #for c in range(count-1) : + # rank = [] + # for i in range(len(self.population)) : + # sim = self.similarity(self.population[i][1],res) + # rank += [ [self.population[i][0] / sim if sim>0 else 1e100,i] ] + # rank.sort() + # res += [ copy.deepcopy(self.population[rank[0][1]]) ] + # print_(rank[0],self.population[rank[0][1]][0]) + # print_(res[-1]) + # del self.population[rank[0][1]] + + self.population = res + + + def populate_species(self,count, parent_count): + self.population.sort() + self.inc = 0 + for c in range(count): + parent1 = random.randint(0,parent_count-1) + parent2 = random.randint(0,parent_count-1) + if parent1==parent2 : parent2 = (parent2+1) % parent_count + parent1, parent2 = self.population[parent1][1], self.population[parent2][1] + i1,i2 = 0, 0 + genes_order = [] + specimen = [ [0,0.,0.] for i in range(self.genes_count) ] + + self.incest_mutation_multiplyer = 1. + self.incest_mutation_count_multiplyer = 1. + + if self.species_distance2(parent1, parent2) <= .01/self.genes_count : + # OMG it's a incest :O!!! + # Damn you bastards! + self.inc +=1 + self.incest_mutation_multiplyer = 2. + self.incest_mutation_count_multiplyer = 2. + else : + if random.random()<.01 : print_(self.species_distance2(parent1, parent2)) + start_gene = random.randint(0,self.genes_count) + end_gene = (max(1,random.randint(0,self.genes_count),int(self.genes_count/4))+start_gene) % self.genes_count + if end_gene (int value; M106 S0 turns off diode) + # Repetier on FAN PIN: M106 S<1 .. 255> (int value; M106 S0 turns off diode) + # Repetier on TOOL PIN: M3 S<1 .. 255> (int value; M5 S0 turns off diode) you need to enable laser mode via M452 + # GRBL: M106 S<0 .. 12000> (int value; M107 turns off diode) + # + # notes to laser mode: + # laser diode should only be turned on when movement is done. Should be ensured in GCode to avoid burning material + # diode has to be turned off at travel moves + # in Repetier firmware this can be accomplished using code M452 to activate laser mode + # + # Pen Angle has to be converted from floating angle value to fitting integer values + # Marlin: M280 0 .. 180 (float value) + # Repetier: M340 500 .. 2500 (int value) + # Smoothie: M280 5 .. 10 (float value; 0 turns off the servo) + targetpower = str(round(self.laserpower_uneffected_converted,4)) + ";(target power: " + str(round(self.options.laserpower,4)) + " percent)\n" + if self.options.gcode_flavour_preset == "repetier_laser": + gcode_tool_header = "M452;enable laser mode\nM3 S" + targetpower + gcode_tool_footer = "M3 S0\n" + elif self.options.gcode_flavour_preset == "repetier_fan": + gcode_tool_header = "M106 S" + targetpower + gcode_tool_footer = "" + if self.options.machine_type == "plotter": + if self.options.gcode_flavour_preset == "repetier_laser" or self.options.gcode_flavour_preset == "repetier_fan": + gcode_tool_header = "M340 P" + str(self.options.pen_index) + " S" + str(round(self.pen_up_angle_uneffected_converted,4)) + ";(target: " + str(self.options.pen_up_angle) + " degrees) pen up\n" + gcode_tool_footer = "" + #inkex.errormsg("pen_down_angle_converted = " + str(self.pen_down_angle_converted) + \ + #"\npen_up_angle_converted = " + str(self.pen_up_angle_converted) + \ + #"\npen_down_angle_uneffected_converted = " + str(self.pen_down_angle_uneffected_converted) +\ + #"\npen_up_angle_uneffected_converted = " + str(self.pen_up_angle_uneffected_converted)) + + #Custom User Header + header_command_lines = self.options.header_command.split("\\n") + gcode_custom_header = "" + for header_command_line in header_command_lines: + gcode_custom_header += header_command_line + "\n" + + #Custom User Repeat command + repeatings_command_lines = self.options.repeatings_command.split("\\n") + gcode_custom_repeat = "\n;BEGIN OF CUSTOM REPEAT COMMAND\n" + for repeatings_command_line in repeatings_command_lines: + gcode_custom_repeat += repeatings_command_line + "\n" + gcode_custom_repeat += ";END OF CUSTOM REPEAT COMMAND\n" + + #Custom User Footer + footer_command_lines = self.options.footer_command.split("\\n") + gcode_custom_footer = "" + for footer_command_line in footer_command_lines: + gcode_custom_footer += footer_command_line + "\n" + + #Auto-Homing + option_autohoming = "" + if self.options.auto_homing: + option_autohoming = "G28 XY;homing\n" + + #Disable tool at the end + option_auto_disable_tool = "" + if self.options.auto_disable_tool: + if self.options.machine_type == "plotter": + if self.options.gcode_flavour_preset == "repetier_laser" or self.options.gcode_flavour_preset == "repetier_fan": + option_auto_disable_tool = "G4 P" + str(get_delay(self)) + ";dwell\n" +\ + "M340 P" + str(self.options.pen_index) + " S0; pen disable\n" +\ + "G4 P" + str(get_delay(self)) + ";dwell\n" + elif self.options.machine_type == "laser": + if self.options.gcode_flavour_preset == "repetier_fan": + option_auto_disable_tool = "G4 P" + str(get_delay(self)) + ";dwell\n" +\ + "M106 S0; laser disable\n" +\ + "G4 P" + str(get_delay(self)) + ";dwell\n" + #Create new file and write gcode into it + f = open(self.dirname+self.options.file, "w") + finalgcode = ";BEGIN OF GCODE" +\ + "\n;MACHINE TYPE: " +\ + self.options.machine_type +\ + "\n;USING GCODE FLAVOUR: " +\ + self.options.gcode_flavour_preset +\ + "\n\nG90;absolute coordinates\n" +\ + gcode_flavour_units +\ + ";units in mm or in\n" +\ + "T" + str(self.options.tool_index) + ";change to defined tool index\n" +\ + gcode_tool_header +\ + "\n;BEGIN OF CUSTOM HEADER\n" +\ + gcode_custom_header +\ + ";END OF CUSTOM HEADER\n\n" +\ + option_autohoming +\ + "\nG0 F" +\ + self.options.travel_speed +\ + ";init feedrate\n" +\ + gcode +\ + gcode_tool_footer +\ + "\n;BEGIN OF CUSTOM FOOTER\n" +\ + gcode_custom_footer +\ + ";END OF CUSTOM FOOTER\n\n" + \ + option_auto_disable_tool +\ + ";END OF GCODE\n" + gcode_pass = finalgcode + if self.options.repeatings_mode == "full" : + for y in range(1,self.options.repeatings + 1): + finalgcode += "\n;LOOP #" + str(y) + "\n" + gcode_custom_repeat + "\n" + gcode_pass + f.write(finalgcode) + f.close() + + def __init__(self): + self.dirname = '' + inkex.Effect.__init__(self) + self.OptionParser.add_option("", "--main_tabs", action="store", type="string", dest="main_tabs", default="", help="") + self.OptionParser.add_option("-d", "--directory", action="store", type="string", dest="directory", default="~/Desktop", help="Output directory") + self.OptionParser.add_option("", "--header-command", action="store", type="string", dest="header_command", default="", help="Header GCode") + self.OptionParser.add_option("", "--footer-command", action="store", type="string", dest="footer_command", default="", help="Footer GCode") + self.OptionParser.add_option("-f", "--filename", action="store", type="string", dest="file", default="output.gcode", help="File name") + self.OptionParser.add_option("", "--add-numeric-suffix-to-filename", action="store", type="inkbool", dest="add_numeric_suffix_to_filename", default=False, help="Add numeric suffix to file name") + self.OptionParser.add_option("", "--tooling-speed", action="store", type="int", dest="tooling_speed", default="2000", help="Plotter speed (mm/min)") + self.OptionParser.add_option("", "--travel-speed", action="store", type="string", dest="travel_speed", default="3000", help="Travel speed (mm/min)") + self.OptionParser.add_option("", "--pen-index", action="store", type="int", dest="pen_index", default="0", help="Servo Index") + self.OptionParser.add_option("", "--tool-index", action="store", type="int", dest="tool_index", default="0", help="Tool Index") + self.OptionParser.add_option("", "--pen-down-angle", action="store", type="float", dest="pen_down_angle", default="900", help="Pen Up Impulse (max. 2500)") + self.OptionParser.add_option("", "--pen-up-angle", action="store", type="float", dest="pen_up_angle", default="600", help="Pen Down Impulse (min. 500)") + self.OptionParser.add_option("", "--delay-time", action="store", type="int", dest="delay_time", default="500", help="Servo Speed (dwell time)") + self.OptionParser.add_option("", "--repeatings", action="store", type="int", dest="repeatings", default="0", help="Quantity of repeatings") + self.OptionParser.add_option("", "--repeatings-command", action="store", type="string", dest="repeatings_command", default="", help="Some special command before repeating") + self.OptionParser.add_option("", "--repeatings-offset-x", action="store", type="float", dest="repeatings_offset_x", default="0.000", help="") + self.OptionParser.add_option("", "--repeatings-offset-y", action="store", type="float", dest="repeatings_offset_y", default="0.000", help="") + self.OptionParser.add_option("", "--repeatings-mode", action="store", type="string", dest="repeatings_mode", default='partial', help="Defines the loop mode") + self.OptionParser.add_option("", "--repeatings-pen-increment", action="store", type="float", dest="repeatings_pen_increment", default='0', help="Defines the increment of pen movement") + self.OptionParser.add_option("", "--suppress-all-messages", action="store", type="inkbool", dest="suppress_all_messages", default=True, help="Hide messages during g-code generation") + self.OptionParser.add_option("", "--create-log", action="store", type="inkbool", dest="log_create_log", default=True, help="Create log files") + self.OptionParser.add_option("", "--log-filename", action="store", type="string", dest="log_filename", default='', help="Create log files") + self.OptionParser.add_option("", "--draw-calculation-paths", action="store", type="inkbool", dest="draw_calculation_paths", default=False, help="Draw additional graphics to debug engraving path") + self.OptionParser.add_option("", "--coordinates-unit", action="store", type="string", dest="coordinates_unit", default="MM", help="Units") + self.OptionParser.add_option("", "--biarc-max-split-depth", action="store", type="int", dest="biarc_max_split_depth", default="4", help="Defines maximum depth of splitting while approximating using biarcs.") + self.OptionParser.add_option("", "--biarc-tolerance", action="store", type="float", dest="biarc_tolerance", default="1", help="Tolerance used when calculating biarc interpolation") + self.OptionParser.add_option("", "--gcode-flavour-preset", action="store", type="string", dest="gcode_flavour_preset", default="repetier", help="Defines correct GCodes/MCodes") + self.OptionParser.add_option("", "--machine-type", action="store", type="string", dest="machine_type", default="plotter", help="Defines the machine type") + self.OptionParser.add_option("", "--show-output-path", action="store", type="inkbool", dest="show_output_path", default=True, help="Show popup with saved output") + self.OptionParser.add_option("", "--laserpower", action="store", type="float", dest="laserpower", default="10.0", help="Laser power in percentage") + self.OptionParser.add_option("", "--laserpower-increment", action="store", type="float", dest="laserpower_increment", default="0.0", help="Laser power increment/decrement") + self.OptionParser.add_option("", "--scale-uniform", action="store", type="float", dest="scale_uniform", default="100.0", help="Scale") + self.OptionParser.add_option("", "--scale-increment", action="store", type="float", dest="scale_increment", default="0.0", help="Scale increment") + self.OptionParser.add_option("", "--auto-homing", action="store", type="inkbool", dest="auto_homing", default=True, help="Auto homing XY") + self.OptionParser.add_option("", "--auto-disable-tool", action="store", type="inkbool", dest="auto_disable_tool", default=True, help="Auto disable servo motor") + self.OptionParser.add_option("", "--randomize-speed", action="store", type="inkbool", dest="randomize_speed", default=False, help="Randomize speed") + self.OptionParser.add_option("", "--randomize-speed-lowerval", action="store", type="float", dest="randomize_speed_lowerval", default="0.0", help="Randomize speed, lower value") + self.OptionParser.add_option("", "--randomize-speed-upperval", action="store", type="float", dest="randomize_speed_upperval", default="0.0", help="Randomize speed, upper value") + self.OptionParser.add_option("", "--randomize-penangle", action="store", type="inkbool", dest="randomize_penangle", default=False, help="Randomize angle") + self.OptionParser.add_option("", "--randomize-penangle-lowerval", action="store", type="float", dest="randomize_penangle_lowerval", default="0.0", help="Randomize angle, lower value") + self.OptionParser.add_option("", "--randomize-penangle-upperval", action="store", type="float", dest="randomize_penangle_upperval", default="0.0", help="Randomize angle, upper value") + self.OptionParser.add_option("", "--randomize-laserpower", action="store", type="inkbool", dest="randomize_laserpower", default=False, help="Randomize laser power") + self.OptionParser.add_option("", "--randomize-laserpower-lowerval", action="store", type="float", dest="randomize_laserpower_lowerval", default="0.0", help="Randomize laser power, lower value") + self.OptionParser.add_option("", "--randomize-laserpower-upperval", action="store", type="float", dest="randomize_laserpower_upperval", default="0.0", help="Randomize laser power, upper value") + self.OptionParser.add_option("", "--randomize-delay", action="store", type="inkbool", dest="randomize_delay", default=False, help="Randomize delay") + self.OptionParser.add_option("", "--randomize-delay-lowerval", action="store", type="float", dest="randomize_delay_lowerval", default="0.0", help="Randomize delay, lower value") + self.OptionParser.add_option("", "--randomize-delay-upperval", action="store", type="float", dest="randomize_delay_upperval", default="0.0", help="Randomize delay, upper value") + + #GLOBALS + self.pen_down_angle_uneffected_converted = 0 #converted + self.pen_up_angle_uneffected_converted = 0 #converted + self.repeatings_pen_increment_converted = 0 #converted + self.laserpower_uneffected_converted = 0 #converted + self.laserpower_increment_converted = 0 #converted + self.pen_down_angle_converted = 0 #converted + self.pen_up_angle_converted = 0 #converted + self.laserpower_converted = 0 #converted + self.offset_x = 0.0 + self.offset_y = 0.0 + self.pen_pos_min = 0 + self.pen_pos_max = 0 + self.laserpower_min = 0 + self.laserpower_max = 0 + + def parse_curve(self, p, layer, w = None, f = None): + c = [] + if len(p)==0 : + return [] + p = self.transform_csp(p, layer) + + + ### Sort to reduce Rapid distance + k = range(1,len(p)) + keys = [0] + while len(k)>0: + end = p[keys[-1]][-1][1] + dist = None + for i in range(len(k)): + start = p[k[i]][0][1] + dist = max( ( -( ( end[0]-start[0])**2+(end[1]-start[1])**2 ) ,i) , dist ) + keys += [k[dist[1]]] + del k[dist[1]] + for k in keys: + subpath = p[k] + c += [ [ [subpath[0][1][0],subpath[0][1][1]] , 'move', 0, 0] ] + for i in range(1,len(subpath)): + sp1 = [ [subpath[i-1][j][0], subpath[i-1][j][1]] for j in range(3)] + sp2 = [ [subpath[i ][j][0], subpath[i ][j][1]] for j in range(3)] + c += biarc(sp1,sp2,0,0) if w==None else biarc(sp1,sp2,-f(w[k][i-1]),-f(w[k][i])) +# l1 = biarc(sp1,sp2,0,0) if w==None else biarc(sp1,sp2,-f(w[k][i-1]),-f(w[k][i])) +# print_((-f(w[k][i-1]),-f(w[k][i]), [i1[5] for i1 in l1]) ) + c += [ [ [subpath[-1][1][0],subpath[-1][1][1]] ,'end',0,0] ] + print_("Curve: " + str(c)) + return c + + + def draw_curve(self, curve, layer, group=None, style=styles["biarc_style"]): + + self.get_defs() + # Add marker to defs if it does not exist + if "DrawCurveMarker" not in self.defs : + defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) + marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"DrawCurveMarker","orient":"auto","refX":"-8","refY":"-2.41063","style":"overflow:visible"}) + inkex.etree.SubElement( marker, inkex.addNS("path","svg"), + { "d":"m -6.55552,-2.41063 0,0 L -13.11104,0 c 1.0473,-1.42323 1.04126,-3.37047 0,-4.82126", + "style": "fill:#000044; fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;" } + ) + if "DrawCurveMarker_r" not in self.defs : + defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) + marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"DrawCurveMarker_r","orient":"auto","refX":"8","refY":"-2.41063","style":"overflow:visible"}) + inkex.etree.SubElement( marker, inkex.addNS("path","svg"), + { "d":"m 6.55552,-2.41063 0,0 L 13.11104,0 c -1.0473,-1.42323 -1.04126,-3.37047 0,-4.82126", + "style": "fill:#000044; fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;" } + ) + for i in [0,1]: + style['biarc%s_r'%i] = simplestyle.parseStyle(style['biarc%s'%i]) + style['biarc%s_r'%i]["marker-start"] = "url(#DrawCurveMarker_r)" + del(style['biarc%s_r'%i]["marker-end"]) + style['biarc%s_r'%i] = simplestyle.formatStyle(style['biarc%s_r'%i]) + + if group==None: + group = inkex.etree.SubElement( self.layers[min(1,len(self.layers)-1)], inkex.addNS('g','svg'), {"gcodetools": "Preview group"} ) + s, arcn = '', 0 + + + a,b,c = [0.,0.], [1.,0.], [0.,1.] + k = (b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]) + a,b,c = self.transform(a, layer, True), self.transform(b, layer, True), self.transform(c, layer, True) + if ((b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]))*k > 0 : reverse_angle = 1 + else : reverse_angle = -1 + for sk in curve: + si = sk[:] + si[0], si[2] = self.transform(si[0], layer, True), (self.transform(si[2], layer, True) if type(si[2])==type([]) and len(si[2])==2 else si[2]) + + if s!='': + if s[1] == 'line': + inkex.etree.SubElement( group, inkex.addNS('path','svg'), + { + 'style': style['line'], + 'd':'M %s,%s L %s,%s' % (s[0][0], s[0][1], si[0][0], si[0][1]), + "gcodetools": "Preview", + } + ) + elif s[1] == 'arc': + arcn += 1 + sp = s[0] + c = s[2] + s[3] = s[3]*reverse_angle + + a = ( (P(si[0])-P(c)).angle() - (P(s[0])-P(c)).angle() )%math.pi2 #s[3] + if s[3]*a<0: + if a>0: a = a-math.pi2 + else: a = math.pi2+a + r = math.sqrt( (sp[0]-c[0])**2 + (sp[1]-c[1])**2 ) + a_st = ( math.atan2(sp[0]-c[0],- (sp[1]-c[1])) - math.pi/2 ) % (math.pi*2) + st = style['biarc%s' % (arcn%2)][:] + if a>0: + a_end = a_st+a + st = style['biarc%s'%(arcn%2)] + else: + a_end = a_st*1 + a_st = a_st+a + st = style['biarc%s_r'%(arcn%2)] + inkex.etree.SubElement( group, inkex.addNS('path','svg'), + { + 'style': st, + inkex.addNS('cx','sodipodi'): str(c[0]), + inkex.addNS('cy','sodipodi'): str(c[1]), + inkex.addNS('rx','sodipodi'): str(r), + inkex.addNS('ry','sodipodi'): str(r), + inkex.addNS('start','sodipodi'): str(a_st), + inkex.addNS('end','sodipodi'): str(a_end), + inkex.addNS('open','sodipodi'): 'true', + inkex.addNS('type','sodipodi'): 'arc', + "gcodetools": "Preview", + }) + s = si + + def check_dir(self): + self.dirname = self.options.directory + if self.dirname == '' or self.dirname == None: + self.dirname = './' + + self.dirname = os.path.expanduser(self.dirname) + self.dirname = os.path.expandvars(self.dirname) + self.dirname = os.path.abspath(self.dirname) + if self.dirname[-1] != os.path.sep: + self.dirname += os.path.sep + if not os.path.isdir(self.dirname): + os.makedirs(self.dirname) + + if self.options.add_numeric_suffix_to_filename : + dir_list = os.listdir(self.dirname) + if "." in self.options.file : + r = re.match(r"^(.*)(\..*)$",self.options.file) + ext = r.group(2) + name = r.group(1) + else: + ext = "" + name = self.options.file + max_n = 0 + for s in dir_list : + r = re.match(r"^%s_0*(\d+)%s$"%(re.escape(name),re.escape(ext) ), s) + if r : + max_n = max(max_n,int(r.group(1))) + filename = name + "_" + ( "0"*(4-len(str(max_n+1))) + str(max_n+1) ) + ext + self.options.file = filename + + print_("Testing writing rights on '%s'"%(self.dirname+self.options.file)) + try: + f = open(self.dirname+self.options.file, "w") + f.close() + except: + self.error(_("Can not write to specified file!\n%s"%(self.dirname+self.options.file)),"error") + return False + return True + + + +################################################################################ +### +### Generate Gcode +### Generates Gcode on given curve. +### +### Curve definition [start point, type = {'arc','line','move','end'}, arc center, arc angle, end point, [zstart, zend]] +### +################################################################################ + def generate_gcode(self, curve, layer, depth): + + def get_cooordinate_line(index, c): + c = [c[i] if i 1: #blocks randomizing for the really first positioning line in gcode which means travelling to the start geometry with a pen in down position + #Randomize tooling speed + if self.options.randomize_speed: + minspeed = self.options.tooling_speed - self.options.randomize_speed_lowerval + maxspeed = self.options.tooling_speed + self.options.randomize_speed_upperval + if minspeed <= 0: + minspeed = 1.0 #disable feedrate of zero + coordinate_line += " F" + str(round(random.uniform(minspeed, maxspeed),4)) + + #Randomize pen angle + if self.options.machine_type == "plotter": + if self.options.randomize_penangle: + minangle = self.pen_down_angle_converted - math.ceil((self.pen_pos_max - self.pen_pos_min) / (180.0 - 0.0) * self.options.randomize_penangle_lowerval) + self.pen_pos_min + maxangle = self.pen_down_angle_converted + math.ceil((self.pen_pos_max - self.pen_pos_min) / (180.0 - 0.0) * self.options.randomize_penangle_upperval) + self.pen_pos_min + newangle = round(random.uniform(minangle, maxangle),4) + if newangle > self.pen_pos_max: + newangle = self.pen_pos_max + if newangle < self.pen_pos_min: + newangle = self.pen_pos_min + coordinate_line += "\nM340 P" + str(self.options.pen_index) + " S" + str(newangle) + + #Randomize laser power + elif self.options.machine_type == "laser": + if self.options.randomize_laserpower: + minpower = self.laserpower_converted - math.ceil((self.laserpower_max - self.laserpower_min) / (100.0 - 0.0) * self.options.randomize_laserpower_lowerval) + self.laserpower_min + maxpower = self.laserpower_converted + math.ceil((self.laserpower_max - self.laserpower_min) / (100.0 - 0.0) * self.options.randomize_laserpower_upperval) + self.laserpower_min + newpower = round(random.uniform(minpower, maxpower),4) + if newpower > self.laserpower_max: + newpower = self.laserpower_max + if newpower < self.laserpower_min: + newpower = self.laserpower_min + if self.options.gcode_flavour_preset == "repetier_fan": + coordinate_line += "\nM106 S" + str(newpower) + elif self.options.gcode_flavour_preset == "repetier_laser": + coordinate_line += "\nM3 S" + str(newpower) + return coordinate_line + + def calculate_angle(a, current_a): + return min( + [abs(a-current_a%math.pi2+math.pi2), a+current_a-current_a%math.pi2+math.pi2], + [abs(a-current_a%math.pi2-math.pi2), a+current_a-current_a%math.pi2-math.pi2], + [abs(a-current_a%math.pi2), a+current_a-current_a%math.pi2])[1] + if len(curve)==0 : return "" + + try : + self.last_used_tool == None + except : + self.last_used_tool = None + print_("working on curve") + print_("Curve: " + str(curve)) + g = "" + + lg, f = 'G00', "F" + str(self.options.tooling_speed) + ";feedrate" + current_a = 0 + if self.options.machine_type == "plotter": + gcode_after_path = \ + "G4 P" + str(get_delay(self)) + ";dwell\n" +\ + "M340 P" + str(self.options.pen_index) + " S" + str(round(self.pen_up_angle_converted,4)) + ";(target: " + str(self.options.pen_up_angle) + " degrees) pen up\n"+\ + "G0 F" + str(self.options.travel_speed) + ";feedrate\n"+\ + "G4 P" + str(get_delay(self)) + ";dwell\n" + elif self.options.machine_type == "laser": + gcode_after_path = \ + "G0 F" + str(self.options.travel_speed) + ";feedrate \n" + for index in range(1,len(curve)): + # Creating Gcode for curve between s=curve[index-1] and si=curve[index] start at s[0] end at s[4]=si[0] + s, si = curve[index-1], curve[index] + feed = f if lg not in ['G01','G02','G03'] else '' + if s[1] == 'move': + if self.options.machine_type == "plotter": + tempcmd = "G4 P" + str(get_delay(self)) + ";dwell\n" +\ + "M340 P" + str(self.options.pen_index) + " S" + str(round(self.pen_down_angle_converted,4)) + ";(target: " + str(self.options.pen_down_angle) + " degrees) pen down + new path begins\n" + elif self.options.machine_type == "laser": + tempcmd = "M3 S" + str(self.laserpower_converted) + ";(target power: " + str(round(self.options.laserpower,4)) + " percent)\n" + g += "G0" + get_cooordinate_line(index, si[0]) + "\n" +\ + tempcmd + lg = 'G00' + elif s[1] == 'end': + g += gcode_after_path + lg = 'G00' + elif s[1] == 'line': + if lg=="G00": g += "G0 " + feed + "\n" + g += "G1" + get_cooordinate_line(index, si[0]) + "\n" + lg = 'G01' + elif s[1] == 'arc': + r = [(s[2][0]-s[0][0]), (s[2][1]-s[0][1])] + if lg=="G00": g += "G0 " + feed + "\n" + if (r[0]**2 + r[1]**2)>.1: + r1, r2 = (P(s[0])-P(s[2])), (P(si[0])-P(s[2])) + if abs(r1.mag()-r2.mag()) < 0.001 : + g += ("G2" if s[3]<0 else "G3") + get_cooordinate_line(index, si[0]+[ None, (s[2][0]-s[0][0]),(s[2][1]-s[0][1]) ]) + "\n" + else: + r = (r1.mag()+r2.mag())/2 + g += ("G2" if s[3]<0 else "G3") + get_cooordinate_line(index, si[0]) + " R%f" % (r) + "\n" + lg = 'G02' + else: + g += "G1" +get_cooordinate_line(index, si[0]) + " " + feed + "\n" + lg = 'G01' + if si[1] == 'end': + g += gcode_after_path + return g + + + def get_transforms(self,g): + root = self.document.getroot() + trans = [] + while (g!=root): + if 'transform' in g.keys(): + t = g.get('transform') + t = simpletransform.parseTransform(t) + trans = simpletransform.composeTransform(t,trans) if trans != [] else t + print_(trans) + g=g.getparent() + return trans + + + def apply_transforms(self,g,csp): + trans = self.get_transforms(g) + if trans != []: + simpletransform.applyTransformToPath(trans, csp) + return csp + + + def transform(self, source_point, layer, reverse=False): + if layer == None : + layer = self.current_layer if self.current_layer is not None else self.document.getroot() + if layer not in self.transform_matrix: + for i in range(self.layers.index(layer),-1,-1): + if self.layers[i] in self.orientation_points : + break + + print_(str(self.layers)) + print_(str("I: " + str(i))) + print_("Transform: " + str(self.layers[i])) + if self.layers[i] not in self.orientation_points : + self.error(_("Orientation points for '%s' layer have not been found! Please add orientation points using Orientation tab!") % layer.get(inkex.addNS('label','inkscape')),"no_orientation_points") + elif self.layers[i] in self.transform_matrix : + self.transform_matrix[layer] = self.transform_matrix[self.layers[i]] + else : + orientation_layer = self.layers[i] + if len(self.orientation_points[orientation_layer])>1 : + self.error(_("There are more than one orientation point groups in '%s' layer") % orientation_layer.get(inkex.addNS('label','inkscape')),"more_than_one_orientation_point_groups") + points = self.orientation_points[orientation_layer][0] + if len(points)==2: + points += [ [ [(points[1][0][1]-points[0][0][1])+points[0][0][0], -(points[1][0][0]-points[0][0][0])+points[0][0][1]], [-(points[1][1][1]-points[0][1][1])+points[0][1][0], points[1][1][0]-points[0][1][0]+points[0][1][1]] ] ] + if len(points)==3: + print_("Layer '%s' Orientation points: " % orientation_layer.get(inkex.addNS('label','inkscape'))) + for point in points: + print_(point) + # Zcoordinates definition taken from Orientatnion point 1 and 2 + self.Zcoordinates[layer] = [max(points[0][1][2],points[1][1][2]), min(points[0][1][2],points[1][1][2])] + matrix = numpy.array([ + [points[0][0][0], points[0][0][1], 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, points[0][0][0], points[0][0][1], 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, points[0][0][0], points[0][0][1], 1], + [points[1][0][0], points[1][0][1], 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, points[1][0][0], points[1][0][1], 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, points[1][0][0], points[1][0][1], 1], + [points[2][0][0], points[2][0][1], 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, points[2][0][0], points[2][0][1], 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, points[2][0][0], points[2][0][1], 1] + ]) + + if numpy.linalg.det(matrix)!=0 : + m = numpy.linalg.solve(matrix, + numpy.array( + [[points[0][1][0]], [points[0][1][1]], [1], [points[1][1][0]], [points[1][1][1]], [1], [points[2][1][0]], [points[2][1][1]], [1]] + ) + ).tolist() + self.transform_matrix[layer] = [[m[j*3+i][0] for i in range(3)] for j in range(3)] + + else : + self.error(_("Orientation points are wrong! (if there are two orientation points they sould not be the same. If there are three orientation points they should not be in a straight line.)"),"wrong_orientation_points") + else : + self.error(_("Orientation points are wrong! (if there are two orientation points they sould not be the same. If there are three orientation points they should not be in a straight line.)"),"wrong_orientation_points") + + self.transform_matrix_reverse[layer] = numpy.linalg.inv(self.transform_matrix[layer]).tolist() + print_("\n Layer '%s' transformation matrixes:" % layer.get(inkex.addNS('label','inkscape')) ) + print_(self.transform_matrix) + print_(self.transform_matrix_reverse) + + ###self.Zauto_scale[layer] = math.sqrt( (self.transform_matrix[layer][0][0]**2 + self.transform_matrix[layer][1][1]**2)/2 ) + ### Zautoscale is absolete + self.Zauto_scale[layer] = 1 + print_("Z automatic scale = %s (computed according orientation points)" % self.Zauto_scale[layer]) + + x,y = source_point[0], source_point[1] + if not reverse : + t = self.transform_matrix[layer] + else : + t = self.transform_matrix_reverse[layer] + return [t[0][0]*x+t[0][1]*y+t[0][2], t[1][0]*x+t[1][1]*y+t[1][2]] + + + def transform_csp(self, csp_, layer, reverse = False): + csp = [ [ [csp_[i][j][0][:],csp_[i][j][1][:],csp_[i][j][2][:]] for j in range(len(csp_[i])) ] for i in range(len(csp_)) ] + for i in xrange(len(csp)): + for j in xrange(len(csp[i])): + for k in xrange(len(csp[i][j])): + csp[i][j][k] = self.transform(csp[i][j][k],layer, reverse) + return csp + + +################################################################################ +### Errors handling function, notes are just printed into Logfile, +### warnings are printed into log file and warning message is displayed but +### extension continues working, errors causes log and execution is halted +### Notes, warnings adn errors could be assigned to space or comma or dot +### sepparated strings (case is ignoreg). +################################################################################ + def error(self, s, type_= "Warning"): + notes = "Note " + warnings = """ + Warning tools_warning + bad_orientation_points_in_some_layers + more_than_one_orientation_point_groups + more_than_one_tool + orientation_have_not_been_defined + tool_have_not_been_defined + selection_does_not_contain_paths + selection_does_not_contain_paths_will_take_all + selection_is_empty_will_comupe_drawing + selection_contains_objects_that_are_not_paths + """ + errors = """ + Error + wrong_orientation_points + area_tools_diameter_error + no_tool_error + active_layer_already_has_tool + active_layer_already_has_orientation_points + """ + if type_.lower() in re.split("[\s\n,\.]+", errors.lower()) : + print_(s) + inkex.errormsg(s+"\n") + sys.exit() + elif type_.lower() in re.split("[\s\n,\.]+", warnings.lower()) : + print_(s) + if not self.options.suppress_all_messages : + inkex.errormsg(s+"\n") + elif type_.lower() in re.split("[\s\n,\.]+", notes.lower()) : + print_(s) + else : + print_(s) + inkex.errormsg(s) + sys.exit() + + +################################################################################ +### Get defs from svg +################################################################################ + def get_defs(self) : + self.defs = {} + def recursive(g) : + for i in g: + if i.tag == inkex.addNS("defs","svg") : + for j in i: + self.defs[j.get("id")] = i + if i.tag ==inkex.addNS("g",'svg') : + recursive(i) + recursive(self.document.getroot()) + + +################################################################################ +### +### Get Gcodetools info from the svg +### +################################################################################ + def get_info(self): + self.selected_paths = {} + self.paths = {} + self.orientation_points = {} + self.layers = [self.document.getroot()] + self.Zcoordinates = {} + self.transform_matrix = {} + self.transform_matrix_reverse = {} + self.Zauto_scale = {} + + def recursive_search(g, layer, selected=False): + items = g.getchildren() + items.reverse() + for i in items: + if selected: + self.selected[i.get("id")] = i + if i.tag == inkex.addNS("g",'svg') and i.get(inkex.addNS('groupmode','inkscape')) == 'layer': + self.layers += [i] + recursive_search(i,i) + elif i.get('gcodetools') == "Gcodetools orientation group" : + points = self.get_orientation_points(i) + if points != None : + self.orientation_points[layer] = self.orientation_points[layer]+[points[:]] if layer in self.orientation_points else [points[:]] + print_("Found orientation points in '%s' layer: %s" % (layer.get(inkex.addNS('label','inkscape')), points)) + else : + self.error(_("Warning! Found bad orientation points in '%s' layer. Resulting Gcode could be corrupt!") % layer.get(inkex.addNS('label','inkscape')), "bad_orientation_points_in_some_layers") + elif i.tag == inkex.addNS('path','svg'): + if "gcodetools" not in i.keys() : + self.paths[layer] = self.paths[layer] + [i] if layer in self.paths else [i] + if i.get("id") in self.selected : + self.selected_paths[layer] = self.selected_paths[layer] + [i] if layer in self.selected_paths else [i] + elif i.tag == inkex.addNS("g",'svg'): + recursive_search(i,layer, (i.get("id") in self.selected) ) + elif i.get("id") in self.selected : + self.error(_("This extension works with Paths and Dynamic Offsets and groups of them only! All other objects will be ignored!\nSolution 1: press Path->Object to path or Shift+Ctrl+C.\nSolution 2: Path->Dynamic offset or Ctrl+J.\nSolution 3: export all contours to PostScript level 2 (File->Save As->.ps) and File->Import this file."),"selection_contains_objects_that_are_not_paths") + + + recursive_search(self.document.getroot(),self.document.getroot()) + + + def get_orientation_points(self,g): + items = g.getchildren() + items.reverse() + p2, p3 = [], [] + p = None + for i in items: + if i.tag == inkex.addNS("g",'svg') and i.get("gcodetools") == "Gcodetools orientation point (2 points)": + p2 += [i] + if i.tag == inkex.addNS("g",'svg') and i.get("gcodetools") == "Gcodetools orientation point (3 points)": + p3 += [i] + if len(p2)==2 : p=p2 + elif len(p3)==3 : p=p3 + if p==None : return None + points = [] + for i in p : + point = [[],[]] + for node in i : + if node.get('gcodetools') == "Gcodetools orientation point arrow": + point[0] = self.apply_transforms(node,cubicsuperpath.parsePath(node.get("d")))[0][0][1] + if node.get('gcodetools') == "Gcodetools orientation point text": + r = re.match(r'(?i)\s*\(\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*\)\s*',node.text) + point[1] = [float(r.group(1)),float(r.group(2)),float(r.group(3))] + if point[0]!=[] and point[1]!=[]: points += [point] + if len(points)==len(p2)==2 or len(points)==len(p3)==3 : return points + else : return None + +################################################################################ +### +### dxfpoints +### +################################################################################ + def dxfpoints(self): + if self.selected_paths == {}: + self.error(_("Noting is selected. Please select something to convert to drill point (dxfpoint) or clear point sign."),"warning") + for layer in self.layers : + if layer in self.selected_paths : + for path in self.selected_paths[layer]: + if self.options.dxfpoints_action == 'replace': + path.set("dxfpoint","1") + r = re.match("^\s*.\s*(\S+)",path.get("d")) + if r!=None: + print_(("got path=",r.group(1))) + path.set("d","m %s 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z" % r.group(1)) + path.set("style",styles["dxf_points"]) + + if self.options.dxfpoints_action == 'save': + path.set("dxfpoint","1") + + if self.options.dxfpoints_action == 'clear' and path.get("dxfpoint") == "1": + path.set("dxfpoint","0") + +################################################################################ +### +### Machine +### +################################################################################ + def machine(self) : + # Define laser command and laser power. Power has to be converted from percentage to fitting integer values + # Marlin: M106 S<1 .. 255> (int value; M106 S0 turns off diode) + # Repetier on FAN PIN: M106 S<1 .. 255> (int value; M106 S0 turns off diode) + # Repetier on TOOL PIN: M3 S<1 .. 255> (int value; M5 S0 turns off diode) you need to enable laser mode via M452 + # GRBL: M106 S<0 .. 12000> (int value; M107 turns off diode) + # + # notes to laser mode: + # laser diode should only be turned on when movement is done. Should be ensured in GCode to avoid burning material + # diode has to be turned off at travel moves + # in Repetier firmware this can be accomplished using code M452 to activate laser mode + # Pen Angle has to be converted from floating angle value to fitting integer values + # Marlin: M280 0 .. 180 (float value) + # Repetier: M340 500 .. 2500 (int value) + # Smoothie: M280 5 .. 10 (float value; 0 turns off the servo) + if self.options.gcode_flavour_preset == "repetier_laser" or self.options.gcode_flavour_preset == "repetier_fan": + self.pen_pos_min = 500 + self.pen_pos_max = 2500 + self.laserpower_min = 0 + self.laserpower_max = 255 + + self.pen_down_angle_uneffected_converted = math.ceil((self.pen_pos_max - self.pen_pos_min) / (180.0 - 0.0) * self.options.pen_down_angle) + self.pen_pos_min + self.pen_down_angle_converted = self.pen_down_angle_uneffected_converted #this value gets modified by pen increment later + self.pen_up_angle_uneffected_converted = math.ceil((self.pen_pos_max - self.pen_pos_min) / (180.0 - 0.0) * self.options.pen_up_angle) + self.pen_pos_min + self.pen_up_angle_converted = self.pen_up_angle_uneffected_converted #this value gets modified by pen increment later + self.repeatings_pen_increment_converted = math.ceil((self.pen_pos_max - self.pen_pos_min) / (180.0 - 0.0) * self.options.repeatings_pen_increment) + self.laserpower_uneffected_converted = math.ceil((self.laserpower_max - self.laserpower_min) / (100.0 - 0.0) * self.options.laserpower) + self.laserpower_min + self.laserpower_converted = self.laserpower_uneffected_converted #this value gets modified by laser power increment later + self.laserpower_increment_converted = math.ceil((self.laserpower_max - self.laserpower_min) / (100.0 - 0.0) * self.options.laserpower_increment) + + def get_boundaries(points): + minx,miny,maxx,maxy=None,None,None,None + out=[[],[],[],[]] + for p in points: + if minx==p[0]: + out[0]+=[p] + if minx==None or p[0]maxx: + maxx=p[0] + out[2]=[p] + + if maxy==p[1]: + out[3]+=[p] + if maxy==None or p[1]>maxy: + maxy=p[1] + out[3]=[p] + return out + + + def remove_duplicates(points): + i=0 + out=[] + for p in points: + for j in xrange(i,len(points)): + if p==points[j]: points[j]=[None,None] + if p!=[None,None]: out+=[p] + i+=1 + return(out) + + + def get_way_len(points): + l=0 + for i in xrange(1,len(points)): + l+=math.sqrt((points[i][0]-points[i-1][0])**2 + (points[i][1]-points[i-1][1])**2) + return l + + + def sort_dxfpoints(points): + points=remove_duplicates(points) + + ways=[ + # l=0, d=1, r=2, u=3 + [3,0], # ul + [3,2], # ur + [1,0], # dl + [1,2], # dr + [0,3], # lu + [0,1], # ld + [2,3], # ru + [2,1], # rd + ] + + minimal_way=[] + minimal_len=None + minimal_way_type=None + for w in ways: + tpoints=points[:] + cw=[] + for j in xrange(0,len(points)): + p=get_boundaries(get_boundaries(tpoints)[w[0]])[w[1]] + tpoints.remove(p[0]) + cw+=p + curlen = get_way_len(cw) + if minimal_len==None or curlen < minimal_len: + minimal_len=curlen + minimal_way=cw + minimal_way_type=w + + return minimal_way + + if self.selected_paths == {} : + paths=self.paths + self.error(_("No paths are selected! Trying to work on all available paths."),"warning") + else : + paths = self.selected_paths + + self.check_dir() + gcode = "" + + biarc_group = inkex.etree.SubElement( self.selected_paths.keys()[0] if len(self.selected_paths.keys())>0 else self.layers[0], inkex.addNS('g','svg') ) + print_(("self.layers=",self.layers)) + print_(("paths=",paths)) + for layer in self.layers : + if layer in paths : + print_(("layer",layer)) + p = [] + dxfpoints = [] + for path in paths[layer] : + print_(str(layer)) + if "d" not in path.keys() : + self.error(_("Warning: One or more paths dont have 'd' parameter, try to Ungroup (Ctrl+Shift+G) and Object to Path (Ctrl+Shift+C)!"),"selection_contains_objects_that_are_not_paths") + continue + csp = cubicsuperpath.parsePath(path.get("d")) + csp = self.apply_transforms(path, csp) + if path.get("dxfpoint") == "1": + tmp_curve=self.transform_csp(csp, layer) + x=tmp_curve[0][0][0][0] + y=tmp_curve[0][0][0][1] + print_("got dxfpoint (scaled) at (%f,%f)" % (x,y)) + dxfpoints += [[x,y]] + else: + p += csp + dxfpoints=sort_dxfpoints(dxfpoints) + curve = self.parse_curve(p, layer) + if self.options.draw_calculation_paths : + self.draw_curve(curve, layer, biarc_group) + + #Generate Code (first) + gcode += self.generate_gcode(curve, layer, 0) + + #Generate more loop code and add it if users selected 'partial' + if self.options.repeatings_mode == "partial" : + for x in range(1,self.options.repeatings + 1): + #Pen Increment Modifications + self.pen_up_angle_converted += self.repeatings_pen_increment_converted + self.pen_down_angle_converted += self.repeatings_pen_increment_converted + self.options.pen_up_angle += self.options.repeatings_pen_increment + self.options.pen_down_angle += self.options.repeatings_pen_increment + if self.pen_up_angle_converted > self.pen_pos_max: + self.pen_up_angle_converted = self.pen_pos_max + if self.pen_up_angle_converted < self.pen_pos_min: + self.pen_up_angle_converted = self.pen_pos_min + if self.pen_down_angle_converted > self.pen_pos_max: + self.pen_down_angle_converted = self.pen_pos_max + if self.pen_down_angle_converted < self.pen_pos_min: + self.pen_down_angle_converted = self.pen_pos_min + #Laser Power Increment Modifications + self.laserpower_converted += self.laserpower_increment_converted + self.options.laserpower += self.options.laserpower_increment + if self.laserpower_converted > self.laserpower_max: + self.laserpower_converted = self.laserpower_max + if self.laserpower_converted < self.laserpower_min: + self.laserpower_converted = self.laserpower_min + #Offset Increment Modifications + self.offset_x += self.options.repeatings_offset_x + self.offset_y += self.options.repeatings_offset_y + #Scale Modifications + self.options.scale_uniform += self.options.scale_increment + + gcode += "\n;LOOP #" + str(x) + "\n" + self.generate_gcode(curve, layer, 0) + self.export_gcode(gcode) + if self.options.show_output_path: + inkex.errormsg(_("Saved at location:") + "\n" + self.dirname + self.options.file) + +################################################################################ +### +### Orientation +### +################################################################################ + def orientation(self, layer=None) : + print_("entering orientations") + if layer == None : + layer = self.current_layer if self.current_layer is not None else self.document.getroot() + if layer in self.orientation_points: + self.error(_("Active layer already has orientation points! Remove them or select another layer!"),"active_layer_already_has_orientation_points") + + orientation_group = inkex.etree.SubElement(layer, inkex.addNS('g','svg'), {"gcodetools":"Gcodetools orientation group"}) + + # translate == ['0', '-917.7043'] + if layer.get("transform") != None : + translate = layer.get("transform").replace("translate(", "").replace(")", "").split(",") + else : + translate = [0,0] + + # doc height in pixels (38 mm == 134.64566px) + doc_height = self.unittouu(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0]) + + if self.document.getroot().get('height') == "100%" : + doc_height = 1052.3622047 + print_("Overriding height from 100 percents to %s" % doc_height) + + print_("Document height: " + str(doc_height)); + + if self.options.coordinates_unit == "MM": + points = [[0.,0.,0.],[100.,0.,0.],[0.,100.,0.]] + orientation_scale = 3.5433070660 + print_("orientation_scale < 0 ===> switching to mm units=%0.10f"%orientation_scale ) + elif self.options.coordinates_unit == "IN": + points = [[0.,0.,0.],[5.,0.,0.],[0.,5.,0.]] + orientation_scale = 90 + print_("orientation_scale < 0 ===> switching to inches units=%0.10f"%orientation_scale ) + + points = points[:2] + + print_(("using orientation scale",orientation_scale,"i=",points)) + for i in points : + # X == Correct! + # si == x,y coordinate in px + # si have correct coordinates + # if layer have any tranform it will be in translate so lets add that + si = [i[0]*orientation_scale, (i[1]*orientation_scale)+float(translate[1])] + g = inkex.etree.SubElement(orientation_group, inkex.addNS('g','svg'), {'gcodetools': "Gcodetools orientation point (2 points)"}) + inkex.etree.SubElement( g, inkex.addNS('path','svg'), + { + 'style': "stroke:none;fill:#000000;", + 'd':'m %s,%s 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z z' % (si[0], -si[1]+doc_height), + 'gcodetools': "Gcodetools orientation point arrow" + }) + t = inkex.etree.SubElement( g, inkex.addNS('text','svg'), + { + 'style': "font-size:10px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;", + inkex.addNS("space","xml"):"preserve", + 'x': str(si[0]+10), + 'y': str(-si[1]-10+doc_height), + 'gcodetools': "Gcodetools orientation point text" + }) + t.text = "(%s; %s; %s)" % (i[0],i[1],i[2]) + + +################################################################################ +### +### Effect +### +### Main function of Gcodetools class +### +################################################################################ + def effect(self) : + global options + options = self.options + options.self = self + options.doc_root = self.document.getroot() + # define print_ function + global print_ + if self.options.log_create_log : + try : + if os.path.isfile(self.options.log_filename) : os.remove(self.options.log_filename) + f = open(self.options.log_filename,"a") + f.write("Gcodetools log file.\nStarted at %s.\n%s\n" % (time.strftime("%d.%m.%Y %H:%M:%S"),options.log_filename)) + f.close() + except : + print_ = lambda *x : None + else : print_ = lambda *x : None + self.get_info() + if self.orientation_points == {} : + self.error(_("Orientation points have not been defined! A default set of orientation points has been automatically added."),"warning") + self.orientation( self.layers[min(0,len(self.layers)-1)] ) + self.get_info() + + self.get_info() + self.machine() + +e = plotter_gcode() +e.affect() \ No newline at end of file diff --git a/plaster_hatch.inx b/plaster_hatch.inx new file mode 100644 index 0000000..7fd0d0f --- /dev/null +++ b/plaster_hatch.inx @@ -0,0 +1,77 @@ + + + <_name>Hatch fill + plaster_hatch + org.inkscape.output.svg.inkscape + eggbot_hatch.py + inkex.py + simplepath.py + simpletransform.py + simplestyle.py + cubicsuperpath.py + cspsubdiv.py + bezmisc.py + plot_utils.py + + + + <_param name="Header" type="description" xml:space="preserve"> +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. + +Hatched figures will be grouped with their fills. + + 5.0 + 45 + false + + true + 3.0 + true + 1.0 + 20.0 + + + + <_param name="aboutpage" type="description" xml:space="preserve"> +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. + +The Crosshatch option will apply a second set of +hatches, perpendicular to the first. + +The "Connect nearby ends" option will attempt to connect +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. +Large values may result in hatches where you don't want +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. +This can improve performance, as it allows you to more +reliably "color inside the lines" when using pens. + +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. + + + + + all + + + + + + diff --git a/plot_utils.py b/plot_utils.py new file mode 100644 index 0000000..9f167a6 --- /dev/null +++ b/plot_utils.py @@ -0,0 +1,226 @@ +# plot_utils.py +# Common geometric plotting utilities for EiBotBoard +# https://github.com/evil-mad/plotink +# +# Intended to provide some common interfaces that can be used by +# EggBot, WaterColorBot, AxiDraw, and similar machines. +# +# Version 0.4, Dated February 22, 2016. +# +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Evil Mad Scientist Laboratories +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from math import sqrt +import cspsubdiv +from bezmisc import * + +def version(): + return "0.3" # Version number for this document + +def distance( x, y ): + ''' + Pythagorean theorem! + ''' + return sqrt( x * x + y * y ) + +def parseLengthWithUnits( str ): + ''' + Parse an SVG value which may or may not have units attached + This version is greatly simplified in that it only allows: no units, + units of px, and units of %. Everything else, it returns None for. + There is a more general routine to consider in scour.py if more + generality is ever needed. + ''' + u = 'px' + s = str.strip() + if s[-2:] == 'px': + s = s[:-2] + elif s[-2:] == 'in': + s = s[:-2] + u = 'in' + elif s[-2:] == 'mm': + s = s[:-2] + u = 'mm' + elif s[-2:] == 'cm': + s = s[:-2] + u = 'cm' + elif s[-1:] == '%': + u = '%' + s = s[:-1] + + try: + v = float( s ) + except: + return None, None + + return v, u + + +def getLength( altself, name, default ): + ''' + Get the attribute with name "name" and default value "default" + Parse the attribute into a value and associated units. Then, accept + no units (''), units of pixels ('px'), and units of percentage ('%'). + ''' + str = altself.document.getroot().get( name ) + + if str: + v, u = parseLengthWithUnits( str ) + if not v: + # Couldn't parse the value + return None + elif ( u == '' ) or ( u == 'px' ): + return v + elif u == 'in' : + return (float( v ) * 90.0) #90 px per inch, as of Inkscape 0.91 + elif u == 'mm': + return (float( v ) * 90.0 / 25.4) + elif u == 'cm': + return (float( v ) * 90.0 / 2.54) + elif u == '%': + return float( default ) * v / 100.0 + else: + # Unsupported units + return None + else: + # No width specified; assume the default value + return float( default ) + +def getLengthInches( altself, name ): + ''' + Get the attribute with name "name" and default value "default" + Parse the attribute into a value and associated units. Then, accept + units of inches ('in'), millimeters ('mm'), or centimeters ('cm') + ''' + str = altself.document.getroot().get( name ) + if str: + v, u = parseLengthWithUnits( str ) + if not v: + # Couldn't parse the value + return None + elif u == 'in' : + return v + elif u == 'mm': + return (float( v ) / 25.4) + elif u == 'cm': + return (float( v ) / 2.54) + else: + # Unsupported units + return None + +def subdivideCubicPath( sp, flat, i=1 ): + """ + Break up a bezier curve into smaller curves, each of which + is approximately a straight line within a given tolerance + (the "smoothness" defined by [flat]). + + This is a modified version of cspsubdiv.cspsubdiv(). I rewrote the recursive + call because it caused recursion-depth errors on complicated line segments. + """ + + while True: + while True: + if i >= len( sp ): + return + + p0 = sp[i - 1][1] + p1 = sp[i - 1][2] + p2 = sp[i][0] + p3 = sp[i][1] + + b = ( p0, p1, p2, p3 ) + + if cspsubdiv.maxdist( b ) > flat: + break + i += 1 + + one, two = beziersplitatt( b, 0.5 ) + sp[i - 1][2] = one[1] + sp[i][0] = two[2] + p = [one[2], one[3], two[1]] + sp[i:1] = [p] + + +def checkLimits( value, lowerBound, upperBound ): + #Check machine size limit; truncate at edges + if (value > upperBound): + return upperBound, True + if (value < lowerBound): + return lowerBound, True + return value, False + + +def vFinal_Vi_A_Dx(Vinitial,Acceleration,DeltaX): + ''' + Kinematic calculation: Final velocity with constant linear acceleration. + + Calculate and return the (real) final velocity, given an initial velocity, + acceleration rate, and distance interval. + + Uses the kinematic equation Vf^2 = 2 a D_x + Vi^2, where + Vf is the final velocity, + a is the acceleration rate, + D_x (delta x) is the distance interval, and + Vi is the initial velocity. + + We are looking at the positive root only-- if the argument of the sqrt + is less than zero, return -1, to indicate a failure. + ''' + FinalVSquared = ( 2 * Acceleration * DeltaX ) + ( Vinitial * Vinitial ) + if (FinalVSquared > 0): + return sqrt(FinalVSquared) + else: + return -1 + +def vInitial_VF_A_Dx(VFinal,Acceleration,DeltaX): + ''' + Kinematic calculation: Maximum allowed initial velocity to arrive at distance X + with specified final velocity, and given maximum linear acceleration. + + Calculate and return the (real) initial velocity, given an final velocity, + acceleration rate, and distance interval. + + Uses the kinematic equation Vi^2 = Vf^2 - 2 a D_x , where + Vf is the final velocity, + a is the acceleration rate, + D_x (delta x) is the distance interval, and + Vi is the initial velocity. + + We are looking at the positive root only-- if the argument of the sqrt + is less than zero, return -1, to indicate a failure. + ''' + IntialVSquared = ( VFinal * VFinal ) - ( 2 * Acceleration * DeltaX ) + if (IntialVSquared > 0): + return sqrt(IntialVSquared) + else: + return -1 + + +def dotProductXY( inputVectorFirst, inputVectorSecond): + temp = inputVectorFirst[0] * inputVectorSecond[0] + inputVectorFirst[1] * inputVectorSecond[1] + if (temp > 1): + return 1 + elif (temp < -1): + return -1 + else: + return temp