def windingNumberOfPoint(self, pt): bounds = self.bounds() bounds.addMargin(10) ray1 = Line(Point(bounds.left, pt.y), pt) ray2 = Line(Point(bounds.right, pt.y), pt) leftIntersections = {} rightIntersections = {} leftWinding = 0 rightWinding = 0 for s in self.asSegments(): for i in s.intersections(ray1): # print("Found left intersection with %s: %s" % (ray1, i.point)) leftIntersections[i.point] = i for i in s.intersections(ray2): rightIntersections[i.point] = i for i in leftIntersections.values(): # XXX tangents here are all positive? Really? # print(i.seg1, i.t1, i.point) tangent = s.tangentAtTime(i.t1) # print("Tangent at left intersection %s is %f" % (i.point,tangent.y)) leftWinding += int(math.copysign(1, tangent.y)) for i in rightIntersections.values(): tangent = s.tangentAtTime(i.t1) # print("Tangent at right intersection %s is %f" % (i.point,tangent.y)) rightWinding += int(math.copysign(1, tangent.y)) # print("Left winding: %i right winding: %i " % (leftWinding,rightWinding)) return max(abs(leftWinding), abs(rightWinding))
def get_cached_glyph(self, name): if name in self.glyphcache: return self.glyphcache[name] paths = BezierPath.fromFonttoolsGlyph(self.font, name) pathbounds = [] paths = list(filter(lambda p: p.length > 0, paths)) for p in paths: p.hasAnchor = False p.glyphname = name if name in self.anchors: for a in self.anchors[name]: if p.pointIsInside(Point(*a)): p.hasAnchor = True bounds = p.bounds() pathbounds.append(bounds) glyphbounds = BoundingBox() if pathbounds: for p in pathbounds: glyphbounds.extend(p) else: glyphbounds.tr = Point(0, 0) glyphbounds.bl = Point(0, 0) self.glyphcache[name] = { "name": name, "paths": paths, "pathbounds": pathbounds, "glyphbounds": glyphbounds, "category": categorize_glyph(self.font, name)[0], "pathconvexhull": None # XXX } assert (len(self.glyphcache[name]["pathbounds"]) == len( self.glyphcache[name]["paths"])) return self.glyphcache[name]
def _line_line_intersections(self, other): a = self.start b = self.end c = other.start d = other.end if isclose(c.x, d.x) and isclose(a.x, b.x): return [] if isclose(c.y, d.y) and isclose(a.y, b.y): return [] if c == d or a == b: return [] if isclose(b.x,a.x): x = a.x slope34 = ( d.y - c.y) / ( d.x - c.x ) y = slope34 * ( x - c.x ) + c.y p = Point(x,y) i = Intersection(self,self.tOfPoint(p), other, other.tOfPoint(p)) return [ i ] if isclose(c.x,d.x): x = c.x slope12 = ( b.y - a.y) / ( b.x - a.x ) y = slope12 * ( x - a.x ) + a.y p = Point(x,y) i = Intersection(self,self.tOfPoint(p), other, other.tOfPoint(p)) return [ i ] slope12 = ( b.y - a.y) / ( b.x - a.x ) slope34 = ( d.y - c.y) / ( d.x - c.x ) if abs(slope12 - slope34) < my_epsilon: return [ ] x = ( slope12 * a.x - a.y - slope34 * c.x + c.y ) / ( slope12 - slope34 ) y = slope12 * ( x - a.x ) + a.y intersection = Point(x,y) if (self._bothPointsAreOnSameSideOfOrigin(intersection, b, a) and self._bothPointsAreOnSameSideOfOrigin(intersection, c, d)): return [ Intersection(self,self.tOfPoint(intersection), other, other.tOfPoint(intersection)) ] return []
def thicknessAtX(path, x): """Returns the thickness of the path at x-coordinate ``x``.""" bounds = path.bounds() bounds.addMargin(10) ray = Line(Point(x - 0.1, bounds.bottom), Point(x + 0.1, bounds.top)) intersections = [] for seg in path.asSegments(): intersections.extend(seg.intersections(ray)) if len(intersections) < 2: return None intersections = list(sorted(intersections, key=lambda i: i.point.y)) i1, i2 = intersections[0:2] inorm1 = i1.seg1.normalAtTime(i1.t1) ray1 = Line(i1.point + (inorm1 * 1000), i1.point + (inorm1 * -1000)) iii = i2.seg1.intersections(ray1) if iii: ll1 = i1.point.distanceFrom(iii[0].point) else: # Simple, vertical version return abs(i1.point.y - i2.point.y) inorm2 = i2.seg1.normalAtTime(i2.t1) ray2 = Line(i2.point + (inorm2 * 1000), i2.point + (inorm2 * -1000)) iii = i1.seg1.intersections(ray2) if iii: ll2 = i2.point.distanceFrom(iii[0].point) return (ll1 + ll2) * 0.5 else: return ll1
def test_extremes3(self): # console.log((new Bezier(127,242,27,5,210,60)).extrema()) q = QuadraticBezier(Point(127, 242), Point(27, 5), Point(210, 60)) r = q.findExtremes() self.assertEqual(len(r), 2) self.assertAlmostEqual(r[0], 0.35335689045936397) self.assertAlmostEqual(r[1], 0.8116438356164384)
def test_multiple_application(self): p = Point(10, 10) m = AffineTransformation() m.translate(Point(6, 5)) m.scale(1.5, 2) p.transform(m) self.assertEqual(p.x, 24) self.assertEqual(p.y, 30)
def test_quadratic_bounds(self): # console.log((new Bezier(150,40,80,30,105,150)).bbox()) q = QuadraticBezier(Point(150, 40), Point(80, 30), Point(105, 150)) b = q.bounds() self.assertAlmostEqual(b.bl.x, 98.42105263157895) self.assertAlmostEqual(b.tr.x, 150) self.assertAlmostEqual(b.bl.y, 39.23076923076923) self.assertAlmostEqual(b.tr.y, 150)
def test_overlap(self): b1 = BoundingBox() b2 = BoundingBox() b1.extend(Point(5, 5)) b1.extend(Point(10, 10)) b2.extend(Point(7, 7)) b2.extend(Point(14, 14)) print("%s v %s" % (b1, b2)) self.assertTrue(b1.overlaps(b2)) self.assertTrue(b2.overlaps(b1))
def drawWithBrush(self, other): """Assuming that `other` is a closed Bezier path representing a pen or brush of a certain shape and that `self` is an open path, this method traces the brush along the path, returning an array of Bezier paths. `other` may also be a function which, given a time `t` (0-1), returns a closed path representing the shape of the brush at the given time. This requires the `shapely` library to be installed. """ from shapely.geometry import Polygon from shapely.ops import unary_union polys = [] samples = self.sample(self.length / 2) def constantBrush(t): return other brush = other if not callable(brush): brush = constantBrush c = brush(0).centroid from itertools import tee def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = tee(iterable) next(b, None) return zip(a, b) t = 0 for n in samples: brushHere = brush(t).clone().flatten() brushHere.translate(n - brushHere.centroid) polys.append( Polygon([(x[0].x, x[0].y) for x in brushHere.asSegments()])) t = t + 1.0 / len(samples) concave_hull = unary_union(polys) ll = [] for x, y in pairwise(concave_hull.exterior.coords): l = Line(Point(x[0], x[1]), Point(y[0], y[1])) ll.append(l) paths = [BezierPath.fromSegments(ll)] for interior in concave_hull.interiors: ll = [] for x, y in pairwise(interior.coords): l = Line(Point(x[0], x[1]), Point(y[0], y[1])) ll.append(l) paths.append(BezierPath.fromSegments(ll)) return paths
def test_extremes(self): q = CubicBezier( Point(65,59), Point(194,90), Point(220,260), Point(70,261) ) # console.log(Bezier(65,59, 194,90, 220,260, 70,261).extrema()) r = q.findExtremes() self.assertEqual(len(r), 1) self.assertAlmostEqual(r[0], 0.5275787707261016) r = q.findExtremes(inflections = True) self.assertEqual(len(r), 2) self.assertAlmostEqual(r[0], 0.4512987012987013) self.assertAlmostEqual(r[1], 0.5275787707261016)
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 _fitCurve(self, points, tangent1, tangent2, error, cornerTolerance, maxSegments): if len(points) == 0: return if len(points) == 2: return [self.fitLine(points, tangent1, tangent2)] maxIterations = 3 isCorner = False u = self.chordLengthParameterize(points) if u[-1] == 0.0: return [] bez = self.generateBezier(points, u, tangent1, tangent2, error) self.reparameterize(bez, points, u) tolerance = math.sqrt(error + 1e-9) (maxErrorRatio, splitPoint) = self.computeMaxError(bez, points, u, tolerance, cornerTolerance) if abs(maxErrorRatio) <= 1.0: return [bez] if ( 0.0 <= maxErrorRatio and maxErrorRatio <= 3.0 ): for _ in range(0, maxIterations+1): bez = self.generateBezier(points, u, tangent1, tangent2, error) (maxErrorRatio, splitPoint) = self.computeMaxError( bez, points, u, tolerance, cornerTolerance) if abs(maxErrorRatio) <= 1.0: return [bez] isCorner = maxErrorRatio < 0 if isCorner: if splitPoint == 0: if tangent1 == None: splitPoint = splitPoint + 1 else: return self._fitCurve( points, Point(0.0,0.0), tangent2, error, cornerTolerance, maxSegments) elif splitPoint == len(points) - 1: if tangent2 == None: splitPoint = splitPoint - 1 else: return self._fitCurve(points, tangent1, Point(0.0,0.0), error, cornerTolerance, maxSegments) if 1 < maxSegments: segmentsRemaining = maxSegments - 1 if isCorner: if not (0 < splitPoint and splitPoint < len(points) - 1): return [] recTHat1 = Point(0.0,0.0) recTHat2 = Point(0.0,0.0) else: recTHat2 = self.centerTangent(points, splitPoint) recTHat1 = recTHat2 * -1 lPoints = points[:splitPoint+1] rPoints = points[splitPoint:] lbeziers = self._fitCurve(lPoints, tangent1, recTHat2, error, cornerTolerance, segmentsRemaining) if lbeziers: segmentsRemaining = segmentsRemaining - len(lbeziers) rbeziers = self._fitCurve(rPoints, recTHat1, tangent2, error, cornerTolerance, segmentsRemaining) return lbeziers + rbeziers else: return []
def test_align(self): q = CubicBezier( Point(120,160), Point(35,200), Point(220,260), Point(220,40) ) s = q.aligned() self.assertAlmostEqual(s[0].x,0.0) self.assertAlmostEqual(s[0].y,0.0) self.assertAlmostEqual(s[1].x,-85.14452515537582) self.assertAlmostEqual(s[1].y,-39.69143277919774) self.assertAlmostEqual(s[2].x,-12.803687993289572) self.assertAlmostEqual(s[2].y,140.84056792618557) self.assertAlmostEqual(s[3].x,156.2049935181331) self.assertAlmostEqual(s[3].y,0.0)
def test_cf1(self): nodes = [ Point(122, 102), Point(35, 200), Point(228, 145), Point(190, 46) ] t = CurveFit._leftTangent(nodes) self.assertAlmostEqual(t.x, -0.663890062102) self.assertAlmostEqual(t.y, 0.747830184896) t = CurveFit._rightTangent(nodes) self.assertAlmostEqual(t.x, -0.3583470773350791) self.assertAlmostEqual(t.y, -0.9335884383203376)
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 plot(self, ax, **kwargs): """Plot the path on a Matplot subplot which you supply :: import matplotlib.pyplot as plt fig, ax = plt.subplots() path.plot(ax) """ import matplotlib.pyplot as plt from matplotlib.lines import Line2D from matplotlib.path import Path import matplotlib.patches as patches path = self.asMatplot() if not "lw" in kwargs: kwargs["lw"] = 2 if not "fill" in kwargs: kwargs["fill"] = False drawNodes = (not ("drawNodes" in kwargs) or kwargs["drawNodes"] != False) if "drawNodes" in kwargs: kwargs.pop("drawNodes") patch = patches.PathPatch(path, **kwargs) ax.add_patch(patch) left, right = ax.get_xlim() top, bottom = ax.get_ylim() bounds = self.bounds() bounds.addMargin(5) if not (left == 0.0 and right == 1.0 and top == 0.0 and bottom == 1.0): bounds.extend(Point(left, top)) bounds.extend(Point(right, bottom)) ax.set_xlim(bounds.left, bounds.right) ax.set_ylim(bounds.bottom, bounds.top) if drawNodes: nl = self.asNodelist() for i in range(0, len(nl)): n = nl[i] if n.type == "offcurve": circle = plt.Circle((n.x, n.y), 1, fill=False) ax.add_artist(circle) if i + 1 < len(nl) and nl[i + 1].type != "offcurve": l = Line2D([n.x, nl[i + 1].x], [n.y, nl[i + 1].y]) ax.add_artist(l) if i - 0 >= 0 and nl[i - 1].type != "offcurve": l = Line2D([n.x, nl[i - 1].x], [n.y, nl[i - 1].y]) ax.add_artist(l) else: circle = plt.Circle((n.x, n.y), 1) ax.add_artist(circle)
def test_inside(self): p = BezierPath.fromNodelist([ Node(329,320,"line"), Node(564,190,"line"), Node(622,332,"offcurve"), Node(495,471,"offcurve"), Node(329,471,"curve"), Node(164,471,"offcurve"), Node(34,334,"offcurve"), Node(93,190,"curve") ]) self.assertTrue(p.pointIsInside(Point(326,423))) self.assertFalse(p.pointIsInside(Point(326,123))) self.assertFalse(p.pointIsInside(Point(326,251))) self.assertTrue(p.pointIsInside(Point(526,251))) self.assertTrue(p.pointIsInside(Point(126,251)))
def pointAtTime(self, t): """Returns the point at time t (0->1) along the curve.""" x = (1 - t) * (1 - t) * self[0].x + 2 * ( 1 - t) * t * self[1].x + t * t * self[2].x y = (1 - t) * (1 - t) * self[0].y + 2 * ( 1 - t) * t * self[1].y + t * t * self[2].y return Point(x, y)
def get_yb_clearance(self, parser, bariye): font = parser.font paths = get_bezier_paths(font, bariye) path = paths[0] bounds = path.bounds() x_of_tail = get_rise(font.font, bariye) ray = Line( Point(x_of_tail - 0.1, bounds.bottom - 5), Point(x_of_tail + 0.1, bounds.top + 5), ) intersections = [] for seg in path.asSegments(): intersections.extend(seg.intersections(ray)) intersections = list(sorted(intersections, key=lambda i: i.point.y)) i = intersections[-1] return i.point.y
def get_glyphs(self, text, buf=None): """Returns an list of dictionaries representing a shaped string. Args: text: text to check buf: (Optional) already shaped uharfbuzz buffer. This is the first step in collision detection; the dictionaries returned can be fed to ``draw_overlaps`` and ``has_collisions``.""" if not buf: buf = self.shape_a_text(text) glyf = self.font["glyf"] cursor = 0 glyphs = [] ix = 0 for info, pos in zip(buf.glyph_infos, buf.glyph_positions): position = Point(cursor + pos.position[0], pos.position[1]) name = glyf.getGlyphName(info.codepoint) g = self.get_positioned_glyph(name, position) g["advance"] = pos.position[2] for p in g["paths"]: p.origin = info.cluster p.glyphIndex = ix glyphs.append(g) ix = ix + 1 cursor = cursor + pos.position[2] return glyphs
def centerTangent(self, data, center): if data[center + 1] == data[center - 1]: ret = data[center] - data[center - 1] ret.rotate(Point(0, 0), math.pi / 2.0) return ret.toUnitVector() else: ret = data[center - 1] - data[center + 1] return ret.toUnitVector()
def factor(P, r_or_k): n = len(P) - 1 upsilon = min(r_or_k, n) theta = max(0, r_or_k - n) summand = Point(0, 0) for i in range(theta, upsilon + 1): summand += P[i] * C(i, n) * C(r_or_k - i, n) / C(r_or_k, 2 * n) return summand
def xheight_intersections(ttFont, glyph): glyphset = ttFont.getGlyphSet() if glyph not in glyphset: return [] paths = BezierPath.fromFonttoolsGlyph(ttFont, glyph) if len(paths) != 1: return [] path = paths[0] xheight = ttFont["OS/2"].sxHeight bounds = path.bounds() bounds.addMargin(10) ray = Line(Point(bounds.left, xheight), Point(bounds.right, xheight)) intersections = [] for seg in path.asSegments(): intersections.extend(seg.intersections(ray)) return sorted(intersections, key=lambda i: i.point.x)
def test_scale(self): p = Point(4, 5) m = AffineTransformation.scaling(2) p.transform(m) self.assertEqual(p.x, 8) self.assertEqual(p.y, 10) p = Point(4, 5) m = AffineTransformation.scaling(1.5, -2) p.transform(m) self.assertEqual(p.x, 6) self.assertEqual(p.y, -10)
def appendSegment(self, seg): seg = [Point(n[0], n[1]) for n in seg] if len(seg) == 2: self.segments.append(Line(*seg)) elif len(seg) == 3: self.segments.append(QuadraticBezier(*seg)) elif len(seg) == 4: self.segments.append(CubicBezier(*seg)) else: raise ValueError("Unknown segment type")
def bestcut(self, args=None): """ Find the best line that cuts this octabox and its segments so that the resulting two bounding octaboxes (of the two sets of segments) is minimised.""" currbest = OctaScore(self, None, self.area) for x, d in enumerate(((1, 0), (0, 1), (-1, 1), (1, 1))): splitline = Line(Point(d[0] * self.xi, d[1] * self.yi), Point(d[0] * self.xa, d[1] * self.ya)) for sl in findshifts(self.segs, splitline): r, l = splitWith(self.segs, sl) rightbox = Octabox(r) leftbox = Octabox(l) score = rightbox.area + leftbox.area if args is not None and args.detail & 8: print(" {}:L[{}, {}], R[{}, {}]".format( "xysd"[x], leftbox.area, sum(s.area for s in leftbox.segs), rightbox.area, sum(s.area for s in rightbox.segs))) if score < currbest.score: currbest = OctaScore(leftbox, rightbox, score) return currbest
def thickness_at_x(path, x): """Find the path thickness at a given X coordinate This measure the thickness of the lowest horizontal stem at the given coordinate. If there is no stem at this X coordinate, ``None`` is returned. Args: path: A ``beziers.path.BezierPath`` object x: X coordinate to search Returns: The thickness of the path at this point, in font units. """ bounds = path.bounds() bounds.addMargin(10) ray = Line(Point(x - 0.1, bounds.bottom), Point(x + 0.1, bounds.top)) intersections = [] for seg in path.asSegments(): intersections.extend(seg.intersections(ray)) if len(intersections) < 2: return None intersections = list(sorted(intersections, key=lambda i: i.point.y)) i1, i2 = intersections[0:2] inorm1 = i1.seg1.normalAtTime(i1.t1) ray1 = Line(i1.point + (inorm1 * 1000), i1.point + (inorm1 * -1000)) iii = i2.seg1.intersections(ray1) if iii: ll1 = i1.point.distanceFrom(iii[0].point) else: # Simple, vertical version return abs(i1.point.y - i2.point.y) inorm2 = i2.seg1.normalAtTime(i2.t1) ray2 = Line(i2.point + (inorm2 * 1000), i2.point + (inorm2 * -1000)) iii = i1.seg1.intersections(ray2) if iii: ll2 = i2.point.distanceFrom(iii[0].point) return (ll1 + ll2) * 0.5 else: return ll1
def test_overlap(self): nodes = [ Node(698.0, 413.0, "offcurve"), Node(401.0, 179.0, "offcurve"), Node(401.0, 274.0, "curve"), Node(401.0, 368.0, "offcurve"), Node(315.0, 445.0, "offcurve"), Node(210.0, 445.0, "curve"), Node(104.0, 445.0, "offcurve"), Node(18.0, 368.0, "offcurve"), Node(18.0, 274.0, "curve"), Node(18.0, 179.0, "offcurve"), Node(439.0, 400.0, "offcurve"), Node(533.0, 405.0, "curve") ] p = BezierPath.fromNodelist(nodes) p.closed = True i = p.getSelfIntersections() self.assertEqual(len(i), 1) self.assertEqual(i[0].point, Point(377.714262786, 355.53493137)) # import matplotlib.pyplot as plt # fig, ax = plt.subplots() # p.plot(ax) # for n in i: # circle = plt.Circle((n.point.x, n.point.y), 2, fill=True, color="red") # ax.add_artist(circle) # plt.show() p = BezierPath.fromNodelist([ Node(310.0, 389.0, "line"), Node(453.0, 222.0, "line"), Node(289.0, 251.0, "line"), Node(447.0, 367.0, "line"), Node(578.0, 222.0, "line"), Node(210.0, -8.0, "line"), ]) i = p.getSelfIntersections() self.assertEqual(len(i), 1) self.assertEqual(i[0].point, Point(374.448829525, 313.734583702))
def test_loop(self): q = CubicBezier( Point(171,272), Point(388,249), Point(167,444), Point(388,176) ) self.assertTrue(not q.hasLoop) q = CubicBezier( Point(171,272), Point(595,249), Point(167,444), Point(388,176) ) roots = q.hasLoop p1 = q.pointAtTime(roots[0]) p2 = q.pointAtTime(roots[1]) self.assertTrue(q.hasLoop) self.assertEqual(p1,p2)
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 fromRepr(klass,text): import re p = re.compile("^B<(.*?)-(.*?)-(.*?)-(.*?)>$") m = p.match(text) points = [ Point.fromRepr(m.group(t)) for t in range(1,5) ] return klass(*points)
def tangentAtTime(self,t): """Returns the tangent at time t (0->1) along the line.""" return Point.fromAngle(math.atan2(self.end.y-self.start.y,self.end.x-self.start.x))