import svgpathtools from PES_Emb_mathutils import * from PES_render_utils import * from PES import * import re import numpy class StitchLevel: def __init__(self, lines, infLine): self.lines = lines self.infLine = infLine #self.barriers = [] # Make barriers out of the spaces between the lines # numBarriers = len(lines) - 1 # for x in range(0, numBarriers): # p1 = self.lines[x].end # p2 = self.lines[x+1].start # b = Line(start=p1, end=p2) # #GenericRenderer.globalRenderer.addLine(b, 55, 55, 55) # # Rotate line around center # invertedB = invertLine(b) # self.barriers.append(invertedB) #GenericRenderer.globalRenderer.addLine(invertLine(b, scale = 1), 55, 0, 255) def lineFallsInValidProjectionOfUngroupedLine(self, line, ungroupedLine): # Project the line onto the intersection line pLine = projectLineOntoInfLine(line, self.infLine) #GenericRenderer.globalRenderer.addLine(pLine, 255, 0, 255) ipLine = invertLine(pLine) # # Avoid rounding errors by doing this with two # # perpendicular lines at the start and end points. # l1 = InfLine(1, (0+0j)) # l1.matchLine(ipLine) # l1.moveToIncludePoint(pLine.start) # l1 = l1.to_svg_Line(pLine.start, 1) # # l2 = InfLine(1, (0+0j)) # l2.matchLine(ipLine) # l2.moveToIncludePoint(pLine.end) # l2 = l2.to_svg_Line(pLine.end, 1) l = ungroupedLine lLength = l.length() pLength = pLine.length() maxDist = max(lLength, pLength) # For a line to be valid with another line, # the two points of one line can't be farther # than maxDist more than once. dists = [Line(l.start, pLine.start).length(), Line(l.start, pLine.end).length(), Line(l.end, pLine.start).length(), Line(l.end, pLine.end).length()] totalTooFar = 0 for d in dists: if d > maxDist: totalTooFar += 1 if totalTooFar <= 1: return True return False def loadVectorGraphic(filename): svg = None attributes = None try: svg, attributes = svgpathtools.svg2paths(filename) except Exception as e: print("Couldn't load SVG file. Perhaps it doesn't exist?") print(e.message) return svg, attributes def getColorOfPathAtIndex(attributes, index): color = (0, 0, 0) if attributes is None: return color try: m = re.search("fill:#(\S{6})", attributes[index]["style"]) last = m.group(1) r = int(last[0:2], 16) g = int(last[2:4], 16) b = int(last[4:6], 16) color = (r, g, b) except: None return color def makeStitchLevels(shape, fillColor=(0,0,0), threadWidth=2, slope=1, debug=False): """ :type shape: svgpathtools.CubicBezier """ stitchLevels = [] # Draw the shape GenericRenderer.globalRenderer.addPath(shape, 50, 120, 255) # Get the bounds of the shape left, right, bottom, top = shape.bbox() width = right - left height = top - bottom center = complex( (left + right) / 2, (top + bottom) / 2) # Draw the bounding box GenericRenderer.globalRenderer.addLine(Line( start=complex(left,top) ,end=complex(right,top) ), 0, 255, 255) GenericRenderer.globalRenderer.addLine(Line(start=complex(right, top), end=complex(right, bottom)), 0, 255, 255) GenericRenderer.globalRenderer.addLine(Line(start=complex(right, bottom), end=complex(left, bottom)), 0, 255, 255) GenericRenderer.globalRenderer.addLine(Line(start=complex(left, bottom), end=complex(left, top)), 0, 255, 255) GenericRenderer.globalRenderer.addPoint(center, 0, 255, 255) # Intersect a line with the given angle over and over # to find where stitches should start/end. # If the shape isn't convex, the line might intersect # more than once but should always intersect an even # number of times (as long as the shape is closed.) # Place the intersection line at various points along # a straight path that has the opposite slope. # The path should cross through the center of the bounding- # box square. intersectionPath = InfLine(m=slope, center=center) GenericRenderer.globalRenderer.addLine( Line(start=complex(1000, intersectionPath.y_for_x(1000)), end=complex(-1000, intersectionPath.y_for_x(-1000)) ), 0, 255, 0) intersectionPath.invertSlope() GenericRenderer.globalRenderer.addLine( Line(start=complex(1000, intersectionPath.y_for_x(1000)), end=complex(-1000, intersectionPath.y_for_x(-1000))), 0, 255, 0) intersectionPath = getIntersectionPathFromBox(intersectionPath, left, right, top, bottom) GenericRenderer.globalRenderer.addLine(intersectionPath, 255, 255, 0) #print("Start and end points: {}, {}".format(p1,p2)) #intersectionPath = intersectionPath.to_svg_Line(center=center, length=intersectionLineLength) # Make sure we cover everything based on the thread width. # Line objects can be enumerated from 0 < t < 1 # Depending on the Line's length, we need to increment t # by a value such that we move threadWidth each time. totalIntersections = int((intersectionPath.length() / threadWidth)) + 1 pathLength = intersectionPath.length() tIncrementAmount = pathLength / float(totalIntersections) # Get the max length of the intersection lines we'll need intersectionLineLength = getBoxDiagonalLength(left, right, top, bottom) print("Performing up to {} intersections along path: {}".format(totalIntersections, intersectionPath)) print("(From {} to {})".format(intersectionPath.point(0), intersectionPath.point(1.0))) for x in range(0, totalIntersections): # Get a new (infinite) line using the point at the current t value # as the center center = intersectionPath.point((tIncrementAmount * x) / pathLength) #GenericRenderer.globalRenderer.addPoint(center, 255, 255, 0) infLine = InfLine(m=slope, center=center) # Convert it to a bezier line l = infLine.to_svg_Line(center=center, length=intersectionLineLength) #GenericRenderer.globalRenderer.addLine(l, 255, 255, 255) # Intersect with the shape intersections = shape.intersect(l) if len(intersections) % 2 is not 0: s = "Number of intersections should always be even (its {} ). Make sure all shapes are closed shapes.".format(len(intersections)) if debug: print(s) else: raise Exception(s) intersectionPoints = [] # Get the intersection points and order them by location # on the intersection line. for i in range(0, len( intersections )): p1 = l.point(intersections[i][1][0]) intersectionPoints.append(p1) def dist1(p): return getDistanceBetweenPoints(p, l.start) intersectionPoints.sort(key=dist1) if debug: #print("{} intersection points for this iteration.".format(len(intersectionPoints))) for i in intersectionPoints: None#GenericRenderer.globalRenderer.addPoint(i, 255, 0, 255) genericColors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (0, 255, 255), (255, 255, 0), (255, 0, 255)] currentLines = [] # Iterate through the intersections to find where to put stitches for i in range(0, len( intersectionPoints )/2): start = intersectionPoints[i*2] end = intersectionPoints[(i*2)+1] # Create a stitch for the given start and end points stitchLine = Line(start=start, end=end) currentLines.append(stitchLine) # Draw debug lines if debug: GenericRenderer.globalRenderer.addLine(stitchLine, genericColors[i%6][0], genericColors[i%6][1], genericColors[i%6][2]) else: GenericRenderer.globalRenderer.addLine(stitchLine, fillColor[0], fillColor[1], fillColor[2]) stitchLevel = StitchLevel(currentLines, infLine) stitchLevels.append(stitchLevel) return stitchLevels def prependShapeTraces(shapePaths, subshapeLineGroups, maxStitchDistance=10.0): for i, shape in enumerate(shapePaths): lineGroups = subshapeLineGroups[i] print("Shape length: {}".format(shape.length())) increment = int( round(shape.length() / maxStitchDistance) ) traceLines = [] for x in range(0, increment): p1 = shape.point(float(x) / increment) p2 = shape.point(min(float(x + 1) / increment, 1.0)) traceLines.append(Line(start=p1, end=p2)) lineGroups.insert(0, traceLines) return subshapeLineGroups def switchPointsInLine(line): return Line(start=line.end, end=line.start) def pointWithinPoint(p1, p2, dist): return math.sqrt( math.pow(p1.real - p2.real, 2) + math.pow(p1.imag - p2.imag, 2) ) <= dist # Take all the stitches we created and actually make # a continuous set of stitches for the machine to follow # on a per sub-shape basis. # This should return a list of sublists, with each sublist # containing an array of continuous stitch lines. def createSubshapeLineGroups(subshapeLevelGroups, mode, fillColors, threadWidth=2, maxStitchDistance=10.0): subshapeLineGroups = [] lastUsedGroups = [] # For each set of lines corresponding to each SVG shape... for subshapeLevels in subshapeLevelGroups: # Find an order of lines that works with (relatively) minimum jumping. # This requires us to group lines by continuity. # Each group is stitchable with no jumps. # Jumps will occur between these groups. # This holds lists of lines that can be stitched continuously with no jumping. lineGroups = [] # SVG files can have multiple shapes # Hold all the line groups in "shapeLineGroups" # This contains a list of lineGroups, one for each subshape. subshapeLineGroups.append(lineGroups) # Each level can contain multiple lines for level in subshapeLevels: newUsedGroups = [] # Check each ungrouped line in the level level.lines.reverse() for ungroupedLine in level.lines: # Remove lines that are super short # TODO: Find a good value for this. #if ungroupedLine.length() < ???: # continue # Does the line connect to any of the current groups? foundGroup = None for lineGroup in lastUsedGroups: lastLine = lineGroup[-1] # Does it cross a barrier? if level.lineFallsInValidProjectionOfUngroupedLine( lastLine, ungroupedLine ): foundGroup = lineGroup break # Did we find a group that works? if foundGroup is None: # No current group works for this line. # We probably started intersecting a new part of the shape # Start a new group foundGroup = [] lineGroups.append(foundGroup) foundGroup.append(ungroupedLine) newUsedGroups.append(foundGroup) # This group found a member. It's off the table # for the rest of this iteration. if foundGroup in lastUsedGroups: lastUsedGroups.remove(foundGroup) lastUsedGroups = newUsedGroups print("Made {} groups for a shape.".format(len(lineGroups))) # Break up the lines into intermediate stitches. # We don't want giant stitches traversing the whole object. # We want little stitches along the way. def breakUpBigStitchLine(l): # Not less than one segment segments = max(1, int( round(l.length() / maxStitchDistance) ) ) increment = 1.0 / float(segments) lines = [] for x in range(0, segments): p1 = l.point( x*increment ) p2 = l.point( min((x+1)*increment, 1.0) ) lines.append(Line(start=p1,end=p2)) return lines # Holds groups for every subshape shortenedSubshapeLineGroups = [] if mode == "zigzag": for lineGroups in subshapeLineGroups: # Holds the groups for a single subshhape shortenedLineGroups = [] shortenedSubshapeLineGroups.append(shortenedLineGroups) for lineGroup in lineGroups: # Holds shortened lines for a single continuous region shortenedLines = [] shortenedLineGroups.append(shortenedLines) lastLine = None for line in lineGroup: if lastLine is not None: # Make a line connecting this line and the previous one. l = Line(lastLine.end, line.start) shortenedLines.extend( breakUpBigStitchLine(l) ) shortenedLines.extend(breakUpBigStitchLine(line)) lastLine = line elif mode == "closest": for lineGroups in subshapeLineGroups: # Holds the groups for a single subshhape shortenedLineGroups = [] shortenedSubshapeLineGroups.append(shortenedLineGroups) for lineGroup in lineGroups: # Holds shortened lines for a single continuous region shortenedLines = [] shortenedLineGroups.append(shortenedLines) lastLine = None for line in lineGroup: if lastLine is not None: # Which point on the next line is closest? d1 = Line(lastLine.end, line.start).length() d2 = Line(lastLine.end, line.end).length() # Make a line connecting this line and the previous one. if d2 < d1: # Invert the line line = Line(line.end, line.start) l = Line(lastLine.end, line.start) shortenedLines.extend(breakUpBigStitchLine(l)) else: l = Line(lastLine.end, line.start) shortenedLines.extend( breakUpBigStitchLine(l) ) else: # The first line. shortenedLines.extend(breakUpBigStitchLine(line)) shortenedLines.extend(breakUpBigStitchLine(line)) lastLine = line # Render the lines GenericRenderer.globalRenderer.clearAll() curColor = 0 for lineGroups in shortenedSubshapeLineGroups: for lineGroup in lineGroups: curColor += 1 currentColor = PES.colors[curColor % len(PES.colors)] for line in lineGroup: # Add slight color variation to be able to see individual stitches. variation = 50 c1 = min(255, max(0, currentColor[1] + int(round(random.uniform(-variation, variation))) )) c2 = min(255, max(0, currentColor[2] + int(round(random.uniform(-variation, variation))))) c3 = min(255, max(0, currentColor[3] + int(round(random.uniform(-variation, variation))))) GenericRenderer.globalRenderer.addLine(line, c1, c2, c3) return shortenedSubshapeLineGroups def createPECStitchRoutines(subshapeLineGroups, fillColors, threadWidth, maxStitchDistance): # Add color change, convert lines to stitches and add jump commands PECCommands = [] maxDist = math.sqrt(2 * math.pow(threadWidth, 2)) for i, subshapeLineGroup in enumerate(subshapeLineGroups): # Create the color change command. fillColor = fillColors[i] colorData = PES.getClosestColor(fillColor) # First color is automatically set. if i != 0: colorChange = ColorChange(colorIndex=colorData[0], indexInColorList=i ) PECCommands.append(colorChange) for singleLineGroup in subshapeLineGroup: # Was the last command a stitch? if len(PECCommands) > 0 and isinstance(PECCommands[-1], Stitch): lastStitch = PECCommands[-1] # Is the distance greater than the minimum? if pointWithinPoint(lastStitch.point, singleLineGroup[0].start, maxDist) is not True: # Jump to the location of this shape. jump = Stitch( singleLineGroup[0].start ) jump.type = Stitch.TYPE_JUMP PECCommands.append(jump) if len(singleLineGroup) > 0: PECCommands.append(Stitch(singleLineGroup[0].start)) for singleLine in singleLineGroup: # s = Stitch(singleLine.start) # PECCommands.append(s) s = Stitch(singleLine.end) PECCommands.append(s) print("Created {} PEC commands (most are stitches.)".format(len(PECCommands))) return PECCommands def renderPEC(pec): PECCommands = pec.commands colors = pec.colors GenericRenderer.globalRenderer.clearAll() lastPoint = (0+0j) currentColor = colors[0][1:] jumps = [] for command in PECCommands: if isinstance(command, Stitch): if command.type is Stitch.TYPE_JUMP: jumps.append(Line(lastPoint, command.point)) else: # Regular stitch GenericRenderer.globalRenderer.addLine(Line(lastPoint, command.point), currentColor[1], currentColor[2], currentColor[3]) lastPoint = command.point if isinstance(command, ColorChange): currentColor = colors[command.indexInColorList][1:] # Render all the jumps on top for jump in jumps: GenericRenderer.globalRenderer.addLine(jump, 255, 255, 255)