Move arc length calculations from pens.perimeterPen to misc.bezierTools

This commit is contained in:
Jens Kutilek 2017-11-14 13:03:58 +01:00
parent 62df8ba108
commit dd558f5df8
2 changed files with 145 additions and 62 deletions

View File

@ -1,10 +1,20 @@
# -*- coding: utf-8 -*-
"""fontTools.misc.bezierTools.py -- tools for working with bezier path segments. """fontTools.misc.bezierTools.py -- tools for working with bezier path segments.
""" """
from __future__ import print_function, division, absolute_import from __future__ import print_function, division, absolute_import
from fontTools.misc.arrayTools import calcBounds
from fontTools.misc.py23 import * from fontTools.misc.py23 import *
import math
__all__ = [ __all__ = [
"approximateCubicArcLength",
"approximateCubicArcLengthC",
"approximateQuadraticArcLength",
"approximateQuadraticArcLengthC",
"calcQuadraticArcLength",
"calcQuadraticArcLengthC",
"calcQuadraticBounds", "calcQuadraticBounds",
"calcCubicBounds", "calcCubicBounds",
"splitLine", "splitLine",
@ -16,12 +26,98 @@ __all__ = [
"solveCubic", "solveCubic",
] ]
from fontTools.misc.arrayTools import calcBounds
epsilonDigits = 6 epsilonDigits = 6
epsilon = 1e-10 epsilon = 1e-10
def _dot(v1, v2):
return (v1 * v2.conjugate()).real
def _intSecAtan(x):
# In : sympy.integrate(sp.sec(sp.atan(x)))
# Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2
return x * math.sqrt(x**2 + 1)/2 + math.asinh(x)/2
def calcQuadraticArcLength(pt1, pt2, pt3, approximate_fallback=False):
"""Return the arc length for a qudratic bezier segment.
pt1 and pt3 are the "anchor" points, pt2 is the "handle".
>>> calcQuadraticArcLength((0, 0), (0, 0), (0, 0)) # empty segment
0.0
>>> calcQuadraticArcLength((0, 0), (50, 0), (80, 0)) # collinear points
80.0
>>> calcQuadraticArcLength((0, 0), (0, 50), (0, 80)) # collinear points vertical
80.0
>>> calcQuadraticArcLength((0, 0), (50, 20), (100, 40)) # collinear points
107.70329614269008
>>> calcQuadraticArcLength((0, 0), (0, 100), (100, 0))
154.02976155645263
>>> calcQuadraticArcLength((0, 0), (0, 50), (100, 0))
120.21581243984076
>>> calcQuadraticArcLength((0, 0), (50, -10), (80, 50))
102.53273816445825
>>> calcQuadraticArcLength((0, 0), (40, 0), (-40, 0), True) # collinear points, control point outside, exact result should be 66.6666666666667
69.41755572720999
>>> calcQuadraticArcLength((0, 0), (40, 0), (0, 0), True) # collinear points, looping back, exact result should be 40
34.4265186329548
"""
return calcQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), approximate_fallback)
def calcQuadraticArcLengthC(pt1, pt2, pt3, approximate_fallback=False):
"""Return the arc length for a qudratic bezier segment using complex points.
pt1 and pt3 are the "anchor" points, pt2 is the "handle"."""
# Analytical solution to the length of a quadratic bezier.
# I'll explain how I arrived at this later.
d0 = pt2 - pt1
d1 = pt3 - pt2
d = d1 - d0
n = d * 1j
scale = abs(n)
if scale == 0.:
return abs(pt3-pt1)
origDist = _dot(n,d0)
if origDist == 0.:
if _dot(d0,d1) >= 0:
return abs(pt3-pt1)
if approximate_fallback:
return approximateQuadraticArcLengthC(pt1, pt2, pt3)
assert 0 # TODO handle cusps
x0 = _dot(d,d0) / origDist
x1 = _dot(d,d1) / origDist
Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0)))
return Len
def approximateQuadraticArcLength(pt1, pt2, pt3):
# Approximate length of quadratic Bezier curve using Gauss-Legendre quadrature
# with n=3 points.
return approximateQuadraticArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3))
def approximateQuadraticArcLengthC(pt1, pt2, pt3):
# Approximate length of quadratic Bezier curve using Gauss-Legendre quadrature
# with n=3 points for complex points.
#
# This, essentially, approximates the length-of-derivative function
# to be integrated with the best-matching fifth-degree polynomial
# approximation of it.
#
#https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature
# abs(BezierCurveC[2].diff(t).subs({t:T})) for T in sorted(.5, .5±sqrt(3/5)/2),
# weighted 5/18, 8/18, 5/18 respectively.
v0 = abs(-0.492943519233745*pt1 + 0.430331482911935*pt2 + 0.0626120363218102*pt3)
v1 = abs(pt3-pt1)*0.4444444444444444
v2 = abs(-0.0626120363218102*pt1 - 0.430331482911935*pt2 + 0.492943519233745*pt3)
return v0 + v1 + v2
def calcQuadraticBounds(pt1, pt2, pt3): def calcQuadraticBounds(pt1, pt2, pt3):
"""Return the bounding rectangle for a qudratic bezier segment. """Return the bounding rectangle for a qudratic bezier segment.
pt1 and pt3 are the "anchor" points, pt2 is the "handle". pt1 and pt3 are the "anchor" points, pt2 is the "handle".
@ -43,6 +139,50 @@ def calcQuadraticBounds(pt1, pt2, pt3):
return calcBounds(points) return calcBounds(points)
def approximateCubicArcLength(pt1, pt2, pt3, pt4):
"""Return the approximate arc length for a cubic bezier segment.
pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".
>>> approximateCubicArcLength((0, 0), (25, 100), (75, 100), (100, 0))
190.04332968932817
>>> approximateCubicArcLength((0, 0), (50, 0), (100, 50), (100, 100))
154.8852074945903
>>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (150, 0)) # line; exact result should be 150.
149.99999999999991
>>> approximateCubicArcLength((0, 0), (50, 0), (100, 0), (-50, 0)) # cusp; exact result should be 150.
136.9267662156362
>>> approximateCubicArcLength((0, 0), (50, 0), (100, -50), (-50, 0)) # cusp
154.80848416537057
"""
# Approximate length of cubic Bezier curve using Gauss-Lobatto quadrature
# with n=5 points.
return approximateCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4))
def approximateCubicArcLengthC(pt1, pt2, pt3, pt4):
"""Return the approximate arc length for a cubic bezier segment of complex points.
pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles"."""
# Approximate length of cubic Bezier curve using Gauss-Lobatto quadrature
# with n=5 points for complex points.
#
# This, essentially, approximates the length-of-derivative function
# to be integrated with the best-matching seventh-degree polynomial
# approximation of it.
#
# https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Lobatto_rules
# abs(BezierCurveC[3].diff(t).subs({t:T})) for T in sorted(0, .5±(3/7)**.5/2, .5, 1),
# weighted 1/20, 49/180, 32/90, 49/180, 1/20 respectively.
v0 = abs(pt2-pt1)*.15
v1 = abs(-0.558983582205757*pt1 + 0.325650248872424*pt2 + 0.208983582205757*pt3 + 0.024349751127576*pt4)
v2 = abs(pt4-pt1+pt3-pt2)*0.26666666666666666
v3 = abs(-0.024349751127576*pt1 - 0.208983582205757*pt2 - 0.325650248872424*pt3 + 0.558983582205757*pt4)
v4 = abs(pt4-pt3)*.15
return v0 + v1 + v2 + v3 + v4
def calcCubicBounds(pt1, pt2, pt3, pt4): def calcCubicBounds(pt1, pt2, pt3, pt4):
"""Return the bounding rectangle for a cubic bezier segment. """Return the bounding rectangle for a cubic bezier segment.
pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles". pt1 and pt4 are the "anchor" points, pt2 and pt3 are the "handles".

View File

@ -4,7 +4,7 @@
from __future__ import print_function, division, absolute_import from __future__ import print_function, division, absolute_import
from fontTools.misc.py23 import * from fontTools.misc.py23 import *
from fontTools.pens.basePen import BasePen from fontTools.pens.basePen import BasePen
from fontTools.misc.bezierTools import splitQuadraticAtT, splitCubicAtT from fontTools.misc.bezierTools import splitQuadraticAtT, splitCubicAtT, approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC
import math import math
@ -13,12 +13,6 @@ __all__ = ["PerimeterPen"]
def _distance(p0, p1): def _distance(p0, p1):
return math.hypot(p0[0] - p1[0], p0[1] - p1[1]) return math.hypot(p0[0] - p1[0], p0[1] - p1[1])
def _dot(v1, v2):
return (v1 * v2.conjugate()).real
def _intSecAtan(x):
# In : sympy.integrate(sp.sec(sp.atan(x)))
# Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2
return x * math.sqrt(x**2 + 1)/2 + math.asinh(x)/2
def _split_cubic_into_two(p0, p1, p2, p3): def _split_cubic_into_two(p0, p1, p2, p3):
mid = (p0 + 3 * (p1 + p2) + p3) * .125 mid = (p0 + 3 * (p1 + p2) + p3) * .125
@ -52,44 +46,10 @@ class PerimeterPen(BasePen):
self.value += _distance(p0, p1) self.value += _distance(p0, p1)
def _addQuadraticExact(self, c0, c1, c2): def _addQuadraticExact(self, c0, c1, c2):
# Analytical solution to the length of a quadratic bezier. self.value += calcQuadraticArcLengthC(c0, c1, c2)
# I'll explain how I arrived at this later.
d0 = c1 - c0
d1 = c2 - c1
d = d1 - d0
n = d * 1j
scale = abs(n)
if scale == 0.:
self.value += abs(c2-c0)
return
origDist = _dot(n,d0)
if origDist == 0.:
if _dot(d0,d1) >= 0:
self.value += abs(c2-c0)
return
assert 0 # TODO handle cusps
x0 = _dot(d,d0) / origDist
x1 = _dot(d,d1) / origDist
Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0)))
self.value += Len
def _addQuadraticQuadrature(self, c0, c1, c2): def _addQuadraticQuadrature(self, c0, c1, c2):
# Approximate length of quadratic Bezier curve using Gauss-Legendre quadrature self.value += approximateQuadraticArcLengthC(c0, c1, c2)
# with n=3 points.
#
# This, essentially, approximates the length-of-derivative function
# to be integrated with the best-matching fifth-degree polynomial
# approximation of it.
#
#https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature
# abs(BezierCurveC[2].diff(t).subs({t:T})) for T in sorted(.5, .5±sqrt(3/5)/2),
# weighted 5/18, 8/18, 5/18 respectively.
v0 = abs(-0.492943519233745*c0 + 0.430331482911935*c1 + 0.0626120363218102*c2)
v1 = abs(c2-c0)*0.4444444444444444
v2 = abs(-0.0626120363218102*c0 - 0.430331482911935*c1 + 0.492943519233745*c2)
self.value += v0 + v1 + v2
def _qCurveToOne(self, p1, p2): def _qCurveToOne(self, p1, p2):
p0 = self._getCurrentPoint() p0 = self._getCurrentPoint()
@ -106,24 +66,7 @@ class PerimeterPen(BasePen):
self._addCubicRecursive(*two) self._addCubicRecursive(*two)
def _addCubicQuadrature(self, c0, c1, c2, c3): def _addCubicQuadrature(self, c0, c1, c2, c3):
# Approximate length of cubic Bezier curve using Gauss-Lobatto quadrature self.value += approximateCubicArcLengthC(c0, c1, c2, c3)
# with n=5 points.
#
# This, essentially, approximates the length-of-derivative function
# to be integrated with the best-matching seventh-degree polynomial
# approximation of it.
#
# https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Lobatto_rules
# abs(BezierCurveC[3].diff(t).subs({t:T})) for T in sorted(0, .5±(3/7)**.5/2, .5, 1),
# weighted 1/20, 49/180, 32/90, 49/180, 1/20 respectively.
v0 = abs(c1-c0)*.15
v1 = abs(-0.558983582205757*c0 + 0.325650248872424*c1 + 0.208983582205757*c2 + 0.024349751127576*c3)
v2 = abs(c3-c0+c2-c1)*0.26666666666666666
v3 = abs(-0.024349751127576*c0 - 0.208983582205757*c1 - 0.325650248872424*c2 + 0.558983582205757*c3)
v4 = abs(c3-c2)*.15
self.value += v0 + v1 + v2 + v3 + v4
def _curveToOne(self, p1, p2, p3): def _curveToOne(self, p1, p2, p3):
p0 = self._getCurrentPoint() p0 = self._getCurrentPoint()