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 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 flatten(self, degree=8): samples = self.regularSample(self.length/degree) ss = [] for i in range(1,len(samples)): l = Line(samples[i-1], samples[i]) l._orig = self ss.append(l) return ss
def flatten(self, degree=8): samples = self.sample(self.length / degree) ss = [] for i in range(1, len(samples)): l = Line(samples[i - 1], samples[i]) l._orig = self ss.append(l) return ss
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 tunniPoint(self): """Returns the Tunni point of this Bezier (the intersection of the handles).""" h1 = Line(self[0], self[1]) h2 = Line(self[2], self[3]) i = h1.intersections(h2, limited = False) if len(i)<1: return i = i[0].point if i.distanceFrom(self[0]) > 5 * self.length: return else: return i
def tunniPoint(self): """Returns the Tunni point of this Bezier (the intersection of the handles).""" h1 = Line(self[0], self[1]) h2 = Line(self[2], self[3]) i = h1.intersections(h2, limited=False) if len(i) < 1: return i = i[0].point if i.distanceFrom(self[0]) > 5 * self.length: return else: return i
def Rectangle(width, height, origin=None): """Returns a path representing an rectangle of given width and height. You can specify the `origin` as a Point.""" if not origin: origin = Point(0, 0) tl = origin + west * width / 2.0 + north * height / 2.0 tr = origin + east * width / 2.0 + north * height / 2.0 bl = origin + west * width / 2.0 + south * height / 2.0 br = origin + east * width / 2.0 + south * height / 2.0 return BezierPath.fromSegments( [Line(tl, tr), Line(tr, br), Line(br, bl), Line(bl, tl)])
def harmonize(self, seg1, seg2): if seg1.end.x != seg2.start.x or seg1.end.y != seg2.start.y: return a1, a2 = seg1[1], seg1[2] b1, b2 = seg2[1], seg2[2] intersections = Line(a1, a2).intersections(Line(b1, b2), limited=False) if not intersections[0]: return p0 = a1.distanceFrom(a2) / a2.distanceFrom(intersections[0].point) p1 = b1.distanceFrom(intersections[0].point) / b1.distanceFrom(b2) r = math.sqrt(p0 * p1) t = r / (r + 1) newA3 = a2.lerp(b1, t) fixup = seg2.start - newA3 seg1[2] += fixup seg2[1] += fixup
def test_cubic_line_2(self): s1 = CubicBezier.fromRepr( "B<<584.0,126.03783241124995>-<402.0,163.0378324112499>-<220.00000000000003,200.03783241124995>-<38.0,237.03783241124995>>" ), ray = Line.fromRepr( "L<<357.4,-9.99999999999999>--<357.6,250.2692949284206>>") assert (s1[0].intersections(ray))
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 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 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_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 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 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 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 test_intercept(self): l = Line(Point(0,10),Point(20,20.4)) self.assertAlmostEqual(l.intercept, 10)
def test_slope(self): l = Line(Point(0,10),Point(20,20.4)) self.assertAlmostEqual(l.slope, 0.52)
def clip(self, clip, cliptype, flat=False): splitlist1 = [] splitlist2 = [] intersections = {} cloned = self.clone() clip = clip.clone() # Split all segments at intersections for s1 in self.asSegments(): for s2 in clip.asSegments(): for i in s1.intersections(s2): if i.t1 > 1e-8 and i.t1 < 1 - 1e-8: if i.seg1 == s1: splitlist1.append((i.seg1, i.t1)) splitlist2.append((i.seg2, i.t2)) else: splitlist2.append((i.seg1, i.t1)) splitlist1.append((i.seg2, i.t2)) intersections[i.point] = i logging.debug("Split list: %s" % splitlist1) logging.debug("Split list 2: %s" % splitlist2) cloned.splitAtPoints(splitlist1) clip.splitAtPoints(splitlist2) logging.debug("Self:") logging.debug(cloned.asSegments()) logging.debug("Clip:") logging.debug(clip.asSegments()) segs1unflattened = cloned.asSegments() segs2unflattened = clip.asSegments() # Replace with flattened versions, building a dictionary of originals segs1 = [] reconstructionLUT = {} precision = 100. def fillLUT(flats): for line in flats: key = ((line.start * precision).rounded(), (line.end * precision).rounded()) reconstructionLUT[key] = (line._orig or line) key2 = ((line.end * precision).rounded(), (line.start * precision).rounded()) reconstructionLUT[key2] = (line._orig or line).reversed() for s in segs1unflattened: flats = s.flatten(2) fillLUT(flats) segs1.extend(flats) segs2 = [] for s in segs2unflattened: flats = s.flatten(2) fillLUT(flats) segs2.extend(flats) # Leave it to the professionals subj = [(s[0].x * precision, s[0].y * precision) for s in segs1] clip = [(s[0].x * precision, s[0].y * precision) for s in segs2] pc = pyclipper.Pyclipper() pc.AddPath(clip, pyclipper.PT_CLIP, True) pc.AddPath(subj, pyclipper.PT_SUBJECT, True) paths = pc.Execute(cliptype, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) outpaths = [] # Now reconstruct Bezier segments from flattened paths def pairwise(points): a = (p for p in points) b = (p for p in points) next(b) for curpoint, nextpoint in zip(a, b): yield curpoint, nextpoint newpaths = [] from beziers.path import BezierPath for p in paths: newpath = [] for scaledstart, scaledend in pairwise(p): key = (Point(*scaledstart), Point(*scaledend)) if key in reconstructionLUT and not flat: orig = reconstructionLUT[key] if len(newpath) == 0 or newpath[-1] != orig: newpath.append(orig) else: newpath.append(Line(key[0] / precision, key[1] / precision)) outpaths.append(BezierPath.fromSegments(newpath)) return outpaths
def test_cubic_line_3(self): seg = CubicBezier.fromRepr( "B<<320.0,454.0>-<277.0,454.0>-<230.0,439.0>-<189.0,417.0>>") ray = Line.fromRepr( "L<<254.5,221.5>--<254.5000000000001,887.6681469418963>>") assert seg.intersections(ray)
# Subdivision r1 = self.minDist(uinterval=(umin, newuMid), vinterval=(vmin, newvMid)) r2 = self.minDist(uinterval=(umin, newuMid), vinterval=(newvMid, vmax)) r3 = self.minDist(uinterval=(newuMid, umax), vinterval=(vmin, newvMid)) r4 = self.minDist(uinterval=(newuMid, umax), vinterval=(newvMid, vmax)) results = min([r1, r2, r3, r4], key=lambda x: x[0]) return results def curveDistance(bez1, bez2): """Find the distance between two curves.""" c = MinimumCurveDistanceFinder(bez1, bez2) dist, t1, t2 = c.minDist() return math.sqrt(dist), t1, t2 if __name__ == "__main__": bez1 = CubicBezier(Point(129, 139), Point(190, 139), Point(201, 364), Point(90, 364)) bez2 = CubicBezier(Point(309, 159), Point(178, 159), Point(215, 408), Point(309, 408)) bez3 = Line(Point(309, 159), Point(309, 408)) c = MinimumCurveDistanceFinder(bez1, bez3) dist, t1, t2 = c.minDist() print(bez1.pointAtTime(t1)) print(bez3.pointAtTime(t2)) print(math.sqrt(dist)) print(c.iterations)
def derivative(self): """Returns a `Line` representing the derivative of this curve.""" return Line((self[1] - self[0]) * 2, (self[2] - self[1]) * 2)
def splitWith(segs, splitline, args=None): """ Splits a list of segments given a straight splitting line. Returns 2 lists of segments such that the resulting segments are closed paths. """ trans = splitline.alignmentTransformation() lefts = [] rights = [] splits = [] for _s in segs: # transform so splitline is horizontal and is y=0 s = _s.transformed(trans) roots = s._findRoots("y") # does y=0 cut this segment? if len(roots) == 2: # chop a quadratic cuver and put parts in left and right collection l, r = s.splitAtTime(roots[0]) (rights if s.start.y < 0 else lefts).append(l) l, r = r.splitAtTime(roots[1]) (rights if s.end.y < 0 else lefts).append(r) # keep split points so we can add joinup lines later splits.append( (l.start.x, (l.start.x < s.start.x) ^ (s.start.y < 0))) splits.append((l.end.x, (l.end.x > s.end.x) ^ (s.start.y < 0))) elif len(roots) == 1: # chop a straight line l, r = s.splitAtTime(roots[0]) if s.start.y < 0: rights.append(l) lefts.append(r) splits.append((r.start.x, True)) else: lefts.append(l) rights.append(r) splits.append((r.start.x, False)) # otherwise no cut and simply allocate the segment elif s.start.y < 0 or s.end.y < 0: rights.append(s) else: lefts.append(s) # to convert from y=0 back to original co-ordinates backt = AffineTransformation() backt.apply_backwards(trans) backt.invert() if args is not None and args.detail & 8: print(" ", [(Point(s[0], 0).transformed(backt), s[1]) for s in sorted(splits)]) curr = False lastP = None # find adjacent pairs of bounding points on the splitline and join them up on either side # thus making closed paths, even if the segments aren't end to start all the way round. for x, d in sorted(splits): newP = Point(x, 0) if d != curr: curr = d if lastP is None: lastP = newP continue l = Line(lastP, newP) r = Line(newP, lastP) if not d: rights.append(l) lefts.append(r) lastP = newP rights = [s.transformed(backt) for s in rights] lefts = [s.transformed(backt) for s in lefts] return (rights, lefts)
def test_line_line(self): l1 = Line(Point(310, 389), Point(453, 222)) l2 = Line(Point(289, 251), Point(447, 367)) # Sanity check self.assertEqual(len(l1.intersections(l2)), 1)