def test_cubic_cubic(self): # q1 = Bezier(10,100, 90,30, 40,140, 220,220) # q2 = Bezier(5,150, 180,20, 80,250, 210,190) # console.log(q1.intersects(q2)) q1 = CubicBezier(Point(10, 100), Point(90, 30), Point(40, 140), Point(220, 220)) q2 = CubicBezier(Point(5, 150), Point(180, 20), Point(80, 250), Point(210, 190)) i = q1.intersections(q2) # self.assertEqual(len(i),3) # self.assertAlmostEqual(i[0].point.x,81.7904225873) # self.assertAlmostEqual(i[0].point.y,109.899396337) # self.assertAlmostEqual(i[1].point.x,133.186831292) # self.assertAlmostEqual(i[1].point.y,167.148173322) # self.assertAlmostEqual(i[2].point.x,179.869157678) # self.assertAlmostEqual(i[2].point.y,199.661989162) import matplotlib.pyplot as plt fig, ax = plt.subplots() path = BezierPath() path.closed = False path.activeRepresentation = SegmentRepresentation(path, [q1]) path.plot(ax) path.activeRepresentation = SegmentRepresentation(path, [q2]) path.plot(ax) for n in i: circle = plt.Circle((n.point.x, n.point.y), 2, fill=True, color="red") ax.add_artist(circle)
def append(self, other, joinType="line"): """Append another path to this one. If the end point of the first path is not the same as the start point of the other path, a line will be drawn between them.""" segs1 = self.asSegments() segs2 = other.asSegments() if len(segs1) < 1: self.activeRepresentation = SegmentRepresentation(self, segs2) return if len(segs2) < 1: self.activeRepresentation = SegmentRepresentation(self, segs1) return # Which way around should they go? dist1 = segs1[-1].end.distanceFrom(segs2[0].start) dist2 = segs1[-1].end.distanceFrom(segs2[-1].end) if dist2 > 2 * dist1: segs2 = list(reversed([x.reversed() for x in segs2])) # Add a line between if they don't match up if segs1[-1].end != segs2[0].start: segs1.append(Line(segs1[-1].end, segs2[0].start)) # XXX Check for discontinuities and harmonize if needed segs1.extend(segs2) self.activeRepresentation = SegmentRepresentation(self, segs1) return self
def removeOverlap(self): """Resolves a path's self-intersections by 'walking around the outside'.""" if not self.closed: raise "Can only remove overlap on closed paths" splitlist = [] splitpoints = {} def roundoff(point): return (int(point.x * 1), int(point.y * 1)) for i in self.getSelfIntersections(): splitlist.append((i.seg1, i.t1)) splitlist.append((i.seg2, i.t2)) splitpoints[roundoff(i.point)] = {"in": [], "out": []} self.splitAtPoints(splitlist) # Trace path segs = self.asSegments() for i in range(0, len(segs)): seg = segs[i] if i < len(segs) - 1: seg.next = segs[i + 1] else: seg.next = segs[0] seg.visited = False segWinding = self.windingNumberOfPoint(seg.pointAtTime(0.5)) seg.windingNumber = segWinding if roundoff(seg.end) in splitpoints: splitpoints[roundoff(seg.end)]["in"].append(seg) if roundoff(seg.start) in splitpoints: splitpoints[roundoff(seg.start)]["out"].append(seg) newsegs = [] copying = True logging.debug("Split points:", splitpoints) seg = segs[0] while not seg.visited: logging.debug("Starting at %s, visiting %s" % (seg.start, seg)) newsegs.append(seg) seg.visited = True if roundoff(seg.end) in splitpoints and len(splitpoints[roundoff( seg.end)]["out"]) > 0: logging.debug("\nI am at %s and have a decision: " % seg.end) inAngle = seg.tangentAtTime(1).angle logging.debug("My angle is %s" % inAngle) # logging.debug("Options are: ") # for s in splitpoints[roundoff(seg.end)]["out"]: # logging.debug(s.end, s.tangentAtTime(0).angle, self.windingNumberOfPoint(s.pointAtTime(0.5))) # Filter out the inside points splitpoints[roundoff(seg.end)]["out"] = [ o for o in splitpoints[roundoff(seg.end)]["out"] if o.windingNumber < 2 ] splitpoints[roundoff(seg.end)]["out"].sort( key=lambda x: x.tangentAtTime(0).angle - inAngle) seg = splitpoints[roundoff(seg.end)]["out"].pop(-1) # seg = seg.next # logging.debug("I chose %s\n" % seg) else: seg = seg.next self.activeRepresentation = SegmentRepresentation(self, newsegs)
def removeIrrelevantSegments(self, relLength=1 / 50000, absLength=0): """Removes small and collinear line segments. Collinear line segments are adjacent line segments which are heading in the same direction, and hence can be collapsed into a single segment. Small segments (those less than ``absLength`` units, or less than ``relLength`` as a fraction of the path's total length) are removed entirely.""" segs = self.asSegments() newsegs = [segs[0]] smallLength = self.length * relLength for i in range(1, len(segs)): prev = newsegs[-1] this = segs[i] if this.length < smallLength or this.length < absLength: this[0] = prev[0] newsegs[-1] = this continue if len(prev) == 2 and len(this) == 2: if math.isclose( prev.tangentAtTime(0).angle, this.tangentAtTime(0).angle): this[0] = prev[0] newsegs[-1] = this continue newsegs.append(this) self.activeRepresentation = SegmentRepresentation(self, newsegs) return self
def balance(self): """Performs Tunni balancing on the path.""" segs = self.asSegments() for x in segs: if isinstance(x, CubicBezier): x.balance() self.activeRepresentation = SegmentRepresentation(self, segs) return self
def fromSegments(klass, array): """Construct a path from an array of Segment objects.""" self = klass() for a in array: assert (isinstance(a, Segment)) self.activeRepresentation = SegmentRepresentation(self, array) return self
def splitAtPoints(self, splitlist): def mapx(v, ds): return (v - ds) / (1 - ds) segs = self.asSegments() newsegs = [] # Cluster splitlist by seg newsplitlist = {} for (seg, t) in splitlist: if not seg in newsplitlist: newsplitlist[seg] = [] newsplitlist[seg].append(t) for k in newsplitlist: newsplitlist[k] = sorted(newsplitlist[k]) # Now walk the path for seg in segs: if seg in newsplitlist: tList = newsplitlist[seg] while len(tList) > 0: t = tList.pop(0) if t < 1e-8: continue seg1, seg2 = seg.splitAtTime(t) newsegs.append(seg1) seg = seg2 for i in range(0, len(tList)): tList[i] = mapx(tList[i], t) newsegs.append(seg) self.activeRepresentation = SegmentRepresentation(self, newsegs)
def fromPoints(self, points, error=50.0, cornerTolerance=20.0, maxSegments=20): """Fit a poly-bezier curve to the points given. This operation should be familiar from 'pencil' tools in a vector drawing application: the application samples points where your mouse pointer has been dragged, and then turns the sketch into a Bezier path. The goodness of fit can be controlled by tuning the `error` parameter. Corner detection can be controlled with `cornerTolerance`. Here are some points fit with `error=100.0`: .. figure:: curvefit1.png :scale: 75 % :alt: curvefit1 And with `error=10.0`: .. figure:: curvefit2.png :scale: 75 % :alt: curvefit1 """ from beziers.utils.curvefitter import CurveFit segs = CurveFit.fitCurve(points, error, cornerTolerance, maxSegments) path = BezierPath() path.closed = False path.activeRepresentation = SegmentRepresentation(path, segs) return path
def test_addextremes(self): q = CubicBezier(Point(42, 135), Point(129, 242), Point(167, 77), Point(65, 59)) ex = q.findExtremes() self.assertEqual(len(ex), 2) path = BezierPath() path.closed = False path.activeRepresentation = SegmentRepresentation(path, [q]) path.addExtremes() path.balance() segs = path.asSegments() self.assertEqual(len(segs), 3)
def test_cubic_line(self): q = CubicBezier(Point(100, 240), Point(30, 60), Point(210, 230), Point(160, 30)) l = Line(Point(25, 260), Point(230, 20)) path = BezierPath() path.closed = False path.activeRepresentation = SegmentRepresentation(path, [q]) i = q.intersections(l) self.assertEqual(len(i), 3) self.assertEqual(i[0].point, q.pointAtTime(0.117517031451)) self.assertEqual(i[1].point, q.pointAtTime(0.518591792307)) self.assertEqual(i[2].point, q.pointAtTime(0.867886610031))
def offset(self, vector, rotateVector=True): """Returns a new BezierPath which approximates offsetting the current Bezier path by the given vector. Note that the vector will be rotated around the normal of the curve so that the offsetting always happens on the same 'side' of the curve: .. figure:: offset1.png :scale: 75 % :alt: offset1 If you don't want that and you want 'straight' offsetting instead (which may intersect with the original curve), pass `rotateVector=False`: .. figure:: offset2.png :scale: 75 % :alt: offset1 """ # Method 1 - curve fit newsegs = [] points = [] def finishPoints(newsegs, points): if len(points) > 0: bp = BezierPath.fromPoints(points, error=0.1, cornerTolerance=1) newsegs.extend(bp.asSegments()) while len(points) > 0: points.pop() for seg in self.asSegments(): if isinstance(seg, Line): finishPoints(newsegs, points) newsegs.append(seg.translated(vector)) else: t = 0.0 while t < 1.0: if rotateVector: points.append( seg.pointAtTime(t) + vector.rotated(Point(0, 0), seg.normalAtTime(t).angle)) else: points.append(seg.pointAtTime(t) + vector) step = max(abs(seg.curvatureAtTime(t)), 0.1) t = t + min(seg.length / step, 0.1) finishPoints(newsegs, points) newpath = BezierPath() newpath.activeRepresentation = SegmentRepresentation(newpath, newsegs) return newpath
def smooth(self, maxCollectionSize=30, lengthLimit=20, cornerTolerance=10): """Smooths a curve, by collating lists of small (at most ``lengthLimit`` units long) segments at most ``maxCollectionSize`` segments at a time, and running them through a curve fitting algorithm. The list collation also stops when one segment turns more than ``cornerTolerance`` degrees away from the previous one, so that corners are not smoothed.""" smallLineLength = lengthLimit segs = self.asSegments() i = 0 collection = [] while i < len(segs): s = segs[i] if s.length < smallLineLength and len( collection) <= maxCollectionSize: collection.append(s) else: corner = False if len(collection) > 1: last = collection[-1] if abs( last.tangentAtTime(1).angle - s.tangentAtTime(0).angle) > math.radians( cornerTolerance): corner = True if len(collection ) > maxCollectionSize or corner or i == len(segs) - 2: points = [x.start for x in collection] bp = BezierPath.fromPoints(points) if len(bp.asSegments()) > 0: segs[i - len(collection):i] = bp.asSegments() i -= len(collection) collection = [] i += 1 if len(collection) > 0: points = [x.start for x in collection] bp = BezierPath.fromPoints(points) if len(bp.asSegments()) > 0: segs[i - (1 + len(collection)):i - 1] = bp.asSegments() self.activeRepresentation = SegmentRepresentation(self, segs) return self
def scale(self, by): """Scales the path by a given magnitude.""" seg2 = [x.scaled(by) for x in self.asSegments()] self.activeRepresentation = SegmentRepresentation(self, seg2) return self
def rotate(self, about, angle): """Rotate the path by a given vector.""" seg2 = [x.rotated(about, angle) for x in self.asSegments()] self.activeRepresentation = SegmentRepresentation(self, seg2) return self
def translate(self, vector): """Translates the path by a given vector.""" seg2 = [x.translated(vector) for x in self.asSegments()] self.activeRepresentation = SegmentRepresentation(self, seg2) return self
def reverse(self): """Reverse this path (mutates path).""" seg2 = [x.reversed() for x in self.asSegments()] self.activeRepresentation = SegmentRepresentation( self, list(reversed(seg2))) return self
def round(self): """Rounds the points of this path to integer coordinates.""" segs = self.asSegments() for s in segs: s.round() self.activeRepresentation = SegmentRepresentation(self, segs)