Files
blender-portable-repo/extensions/user_default/retopoflow/addon_common/common/bezier.py
T
2026-03-17 14:58:51 -06:00

637 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'''
Copyright (C) 2023 CG Cookie
http://cgcookie.com
hello@cgcookie.com
Created by Jonathan Denning, Jonathan Williamson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
import math
from mathutils import Vector, Matrix
from .maths import Point, Vec
from .utils import iter_running_sum
def compute_quadratic_weights(t):
t0, t1 = t, (1-t)
return (t1**2, 2*t0*t1, t0**2)
def compute_cubic_weights(t):
t0, t1 = t, (1-t)
return (t1**3, 3*t0*t1**2, 3*t0**2*t1, t0**3)
def interpolate_cubic(v0, v1, v2, v3, t):
b0, b1, b2, b3 = compute_cubic_weights(t)
return v0*b0 + v1*b1 + v2*b2 + v3*b3
def compute_cubic_error(v0, v1, v2, v3, l_v, l_t):
return math.sqrt(sum(
(interpolate_cubic(v0, v1, v2, v3, t) - v)**2
for v, t in zip(l_v, l_t)
))
def fit_cubicbezier(l_v, l_t):
#########################################################
# http://nbviewer.ipython.org/gist/anonymous/5688579
# make the summation functions for A (16 of them)
A_fns = [
lambda l_t: sum([2*t**0*(t-1)**6 for t in l_t]),
lambda l_t: sum([-6*t**1*(t-1)**5 for t in l_t]),
lambda l_t: sum([6*t**2*(t-1)**4 for t in l_t]),
lambda l_t: sum([-2*t**3*(t-1)**3 for t in l_t]),
lambda l_t: sum([-6*t**1*(t-1)**5 for t in l_t]),
lambda l_t: sum([18*t**2*(t-1)**4 for t in l_t]),
lambda l_t: sum([-18*t**3*(t-1)**3 for t in l_t]),
lambda l_t: sum([6*t**4*(t-1)**2 for t in l_t]),
lambda l_t: sum([6*t**2*(t-1)**4 for t in l_t]),
lambda l_t: sum([-18*t**3*(t-1)**3 for t in l_t]),
lambda l_t: sum([18*t**4*(t-1)**2 for t in l_t]),
lambda l_t: sum([-6*t**5*(t-1)**1 for t in l_t]),
lambda l_t: sum([-2*t**3*(t-1)**3 for t in l_t]),
lambda l_t: sum([6*t**4*(t-1)**2 for t in l_t]),
lambda l_t: sum([-6*t**5*(t-1)**1 for t in l_t]),
lambda l_t: sum([2*t**6*(t-1)**0 for t in l_t])
]
# make the summation functions for b (4 of them)
b_fns = [
lambda l_t, l_v: sum(v * (-2 * (t**0) * ((t-1)**3))
for t, v in zip(l_t, l_v)),
lambda l_t, l_v: sum(v * (6 * (t**1) * ((t-1)**2))
for t, v in zip(l_t, l_v)),
lambda l_t, l_v: sum(v * (-6 * (t**2) * ((t-1)**1))
for t, v in zip(l_t, l_v)),
lambda l_t, l_v: sum(v * (2 * (t**3) * ((t-1)**0))
for t, v in zip(l_t, l_v)),
]
# compute the data we will put into matrix A
A_values = [fn(l_t) for fn in A_fns]
# fill the A matrix with data
A_matrix = Matrix(tuple(zip(*[iter(A_values)]*4)))
try:
A_inv = A_matrix.inverted()
except:
return (float('inf'), l_v[0], l_v[0], l_v[0], l_v[0])
# compute the data we will put into the b vector
b_values = [fn(l_t, l_v) for fn in b_fns]
# fill the b vector with data
b_vector = Vector(b_values)
# solve for the unknowns in vector x
v0, v1, v2, v3 = A_inv @ b_vector
err = compute_cubic_error(v0, v1, v2, v3, l_v, l_t) #/ len(l_v)
return (err, v0, v1, v2, v3)
def fit_cubicbezier_spline(
l_co, error_scale, depth=0,
t0=0, t3=-1, allow_split=True, force_split=False,
min_count_split=15, max_depth_split=4,
):
'''
fits cubic bezier to given points
returns list of tuples of (t0,t3,p0,p1,p2,p3)
that best fits the given points l_co
where t0 and t3 are the passed-in t0 and t3
and p0,p1,p2,p3 are the control points of bezier
'''
count = len(l_co)
if t3 == -1:
t3 = count-1
assert count > 2, "Need at least 2 points to fit cubic bezier"
if count == 2:
# special case: line
p0, p3 = l_co[0], l_co[-1]
diff = p3 - p0
return [(t0, t3, p0, p0+diff*0.33, p0+diff*0.66, p3)]
if count == 3:
new_co = [
l_co[0],
Point.average(l_co[:2]),
l_co[1],
Point.average(l_co[1:]),
l_co[2]
]
return fit_cubicbezier_spline(
new_co, error_scale,
depth=depth,
t0=t0, t3=t3,
allow_split=allow_split, force_split=force_split
)
l_d = [0] + [(v0-v1).length for v0, v1 in zip(l_co[:-1], l_co[1:])]
l_ad = [s for d, s in iter_running_sum(l_d)]
dist = sum(l_d)
if dist <= 0:
# print(spc + 'fit_cubicbezier_spline: returning []')
return [] # [(t0,t3,l_co[0],l_co[0],l_co[0],l_co[0])]
l_t = [ad/dist for ad in l_ad]
ex, x0, x1, x2, x3 = fit_cubicbezier([co[0] for co in l_co], l_t)
ey, y0, y1, y2, y3 = fit_cubicbezier([co[1] for co in l_co], l_t)
ez, z0, z1, z2, z3 = fit_cubicbezier([co[2] for co in l_co], l_t)
tot_error = ex+ey+ez
#print(f'error={tot_error} max={error_scale} force={force_split} allow={allow_split}') #, l=4)
if not force_split:
do_not_split = tot_error < error_scale
do_not_split |= depth == max_depth_split
do_not_split |= len(l_co) <= min_count_split
do_not_split |= not allow_split
if do_not_split:
p0, p1 = Point((x0, y0, z0)), Point((x1, y1, z1))
p2, p3 = Point((x2, y2, z2)), Point((x3, y3, z3))
return [(t0, t3, p0, p1, p2, p3)]
# too much error in fit. split sequence in two, and fit each sub-sequence
# find a good split point
ind_split = -1
mindot = 1.0
for ind in range(5, len(l_co)-5):
if l_t[ind] < 0.4:
continue
if l_t[ind] > 0.6:
break
# if l_ad[ind] < 0.1: continue
# if l_ad[ind] > dist-0.1: break
v0 = l_co[ind-4]
v1 = l_co[ind+0]
v2 = l_co[ind+4]
d0 = (v1-v0).normalized()
d1 = (v2-v1).normalized()
dot01 = d0.dot(d1)
if ind_split == -1 or dot01 < mindot:
ind_split = ind
mindot = dot01
if ind_split == -1:
# did not find a good splitting point!
p0, p1, p2, p3 = Point((x0, y0, z0)), Point(
(x1, y1, z1)), Point((x2, y2, z2)), Point((x3, y3, z3))
#p0,p3 = Point(l_co[0]),Point(l_co[-1])
return [(t0, t3, p0, p1, p2, p3)]
#print(spc + 'splitting at %d' % ind_split)
l_co0, l_co1 = l_co[:ind_split+1], l_co[ind_split:] # share split point
tsplit = ind_split # / (len(l_co)-1)
bezier0 = fit_cubicbezier_spline(
l_co0, error_scale, depth=depth+1, t0=t0, t3=tsplit)
bezier1 = fit_cubicbezier_spline(
l_co1, error_scale, depth=depth+1, t0=tsplit, t3=t3)
return bezier0 + bezier1
class CubicBezier:
split_default = 100
segments_default = 100
@staticmethod
def create_from_points(pts_list):
'''
Estimates best spline to fit given points
'''
count = len(pts_list)
if count == 0:
assert False
if count == 1:
assert False
if count == 2:
p0, p3 = pts_list
diff = p3-p0
p1, p2 = p0+diff*0.33, p0+diff*0.66
return CubicBezier(p0, p1, p2, p3)
if count == 3:
p0, p03, p3 = pts_list
d003, d303 = (p03-p0), (p03-p3)
p1, p2 = p0+d003*0.5, p3+d303*0.5
return CubicBezier(p0, p1, p2, p3)
l_d = [0] + [(p0-p1).length for p0,
p1 in zip(pts_list[:-1], pts_list[1:])]
l_ad = [s for d, s in iter_running_sum(l_d)]
dist = sum(l_d)
if dist <= 0:
p0 = pts_list[0]
return CubicBezier(p0, p0, p0, p0)
l_t = [ad/dist for ad in l_ad]
ex, x0, x1, x2, x3 = fit_cubicbezier([pt[0] for pt in pts_list], l_t)
ey, y0, y1, y2, y3 = fit_cubicbezier([pt[1] for pt in pts_list], l_t)
ez, z0, z1, z2, z3 = fit_cubicbezier([pt[2] for pt in pts_list], l_t)
p0 = Point((x0, y0, z0))
p1 = Point((x1, y1, z1))
p2 = Point((x2, y2, z2))
p3 = Point((x3, y3, z3))
return CubicBezier(p0, p1, p2, p3)
def __init__(self, p0, p1, p2, p3):
self.p0, self.p1, self.p2, self.p3 = p0, p1, p2, p3
self.tessellation = []
def __iter__(self): return iter([self.p0, self.p1, self.p2, self.p3])
def points(self): return (self.p0, self.p1, self.p2, self.p3)
def copy(self):
''' shallow copy '''
return CubicBezier(self.p0, self.p1, self.p2, self.p3)
def eval(self, t):
p0, p1, p2, p3 = self.p0, self.p1, self.p2, self.p3
b0, b1, b2, b3 = compute_cubic_weights(t)
return Point.weighted_average([
(b0, p0), (b1, p1), (b2, p2), (b3, p3)
])
def eval_derivative(self, t):
p0, p1, p2, p3 = self.p0, self.p1, self.p2, self.p3
q0, q1, q2 = 3*(p1-p0), 3*(p2-p1), 3*(p3-p2)
b0, b1, b2 = compute_quadratic_weights(t)
return q0*b0 + q1*b1 + q2*b2
def subdivide(self, iters=1):
if iters == 0:
return [self]
# de casteljau subdivide
p0, p1, p2, p3 = self.p0, self.p1, self.p2, self.p3
q0, q1, q2 = (p0+p1)/2, (p1+p2)/2, (p2+p3)/2
r0, r1 = (q0+q1)/2, (q1+q2)/2
s = (r0+r1)/2
cb0, cb1 = CubicBezier(p0, q0, r0, s), CubicBezier(s, r1, q2, p3)
if iters == 1:
return [cb0, cb1]
return cb0.subdivide(iters=iters-1) + cb1.subdivide(iters=iters-1)
def compute_linearity(self, fn_dist):
'''
Estimating measure of linearity as ratio of distances
of curve mid-point and mid-point of end control points
over half the distance between end control points
p1 _
/
|
p0 * * p3
_/
p2
'''
p0, p1, p2, p3 = Vector(self.p0), Vector(
self.p1), Vector(self.p2), Vector(self.p3)
q0, q1, q2 = (p0+p1)/2, (p1+p2)/2, (p2+p3)/2
r0, r1 = (q0+q1)/2, (q1+q2)/2
s = (r0+r1)/2
m = (p0+p3)/2
d03 = fn_dist(p0, p3)
dsm = fn_dist(s, m)
return 2 * dsm / d03
def subdivide_linesegments(self, fn_dist, max_linearity=None):
if self.compute_linearity(fn_dist) < (max_linearity or 0.1):
return [self]
# de casteljau subdivide:
p0, p1, p2, p3 = Vector(self.p0), Vector(
self.p1), Vector(self.p2), Vector(self.p3)
q0, q1, q2 = (p0+p1)/2, (p1+p2)/2, (p2+p3)/2
r0, r1 = (q0+q1)/2, (q1+q2)/2
s = (r0+r1)/2
cbs = CubicBezier(p0, q0, r0, s), CubicBezier(s, r1, q2, p3)
segs0, segs1 = [cb.subdivide_linesegments(
fn_dist, max_linearity=max_linearity) for cb in cbs]
return segs0 + segs1
def length(self, fn_dist, max_linearity=None):
l = self.subdivide_linesegments(fn_dist, max_linearity=max_linearity)
return sum(fn_dist(cb.p0, cb.p3) for cb in l)
def approximate_length_uniform(self, fn_dist, split=None):
split = split or self.split_default
p = self.p0
d = 0
for i in range(split):
q = self.eval((i+1) / split)
d += fn_dist(p, q)
p = q
return d
def approximate_t_at_interval_uniform(self, interval, fn_dist, split=None):
split = split or self.split_default
p = self.p0
d = 0
for i in range(split):
percent = (i+1) / split
q = self.eval(percent)
d += fn_dist(p, q)
if interval <= d:
return percent
p = q
return 1
def approximate_ts_at_intervals_uniform(
self, intervals, fn_dist, split=None
):
a = self.approximate_t_at_interval_uniform
def approx(i): return a(i, fn_dist, split=None)
return [approx(interval) for interval in intervals]
def get_tessellate_uniform(self, fn_dist, split=None):
split = split or self.split_default
ts = [i/(split-1) for i in range(split)]
ps = [self.eval(t) for t in ts]
ds = [0] + [fn_dist(p, q) for p, q in zip(ps[:-1], ps[1:])]
return [(t, p, d) for t, p, d in zip(ts, ps, ds)]
def tessellate_uniform_points(self, segments=None):
segments = segments or self.segments_default
ts = [i/(segments-1) for i in range(segments)]
ps = [self.eval(t) for t in ts]
return ps
#########################################
# #
# the following code **requires** that #
# self.tessellate_uniform() is called #
# beforehand! #
# #
#########################################
def tessellate_uniform(self, fn_dist, split=None):
self.tessellation = self.get_tessellate_uniform(fn_dist, split=split)
def approximate_t_at_point_tessellation(self, point, fn_dist):
bd, bt = None, None
for t, q, _ in self.tessellation:
d = fn_dist(point, q)
if bd is None or d < bd:
bd, bt = d, t
return bt
def approximate_totlength_tessellation(self):
return sum(self.approximate_lengths_tessellation())
def approximate_lengths_tessellation(self):
return [d for _, _, d in self.tessellation]
class CubicBezierSpline:
@staticmethod
def create_from_points(pts_list, max_error, **kwargs):
'''
Estimates best spline to fit given points
'''
cbs = []
inds = []
for pts in pts_list:
cbs_pts = fit_cubicbezier_spline(pts, max_error, **kwargs)
cbs += [CubicBezier(p0, p1, p2, p3) for _, _, p0, p1, p2, p3 in cbs_pts]
inds += [(ind0, ind1) for ind0, ind1, _, _, _, _ in cbs_pts]
return CubicBezierSpline(cbs=cbs, inds=inds)
def __init__(self, cbs=None, inds=None):
if cbs is None:
cbs = []
if inds is None:
inds = []
if type(cbs) is CubicBezierSpline:
cbs = [cb.copy() for cb in cbs.cbs]
assert type(cbs) is list, "expected list"
self.cbs = cbs
self.inds = inds
self.tessellation = []
def copy(self):
return CubicBezierSpline(
cbs=[cb.copy() for cb in self.cbs],
inds=list(self.inds)
)
def __add__(self, other):
t = type(other)
if t is CubicBezierSpline:
return CubicBezierSpline(
self.cbs + other.cbs,
self.inds + other.inds
)
if t is CubicBezier:
return CubicBezierSpline(self.cbs + [other])
if t is list:
return CubicBezierSpline(self.cbs + other)
assert False, "unhandled type: %s (%s)" % (str(other), str(t))
def __iadd__(self, other):
t = type(other)
if t is CubicBezierSpline:
self.cbs += other.cbs
self.inds += other.inds
elif t is CubicBezier:
self.cbs += [other]
self.inds = []
elif t is list:
self.cbs += other
self.inds = []
else:
assert False, "unhandled type: %s (%s)" % (str(other), str(t))
def __len__(self): return len(self.cbs)
def __iter__(self): return self.cbs.__iter__()
def __getitem__(self, idx): return self.cbs[idx]
def eval(self, t):
if t < 0.0:
t = 0
idx = 0
elif t >= len(self):
t = 1
idx = len(self)-1
else:
idx = int(t)
t = t - idx
return self.cbs[idx].eval(t)
def eval_derivative(self, t):
if t < 0.0:
t = 0
idx = 0
elif t >= len(self):
t = 1
idx = len(self)-1
else:
idx = int(t)
t = t - idx
return self.cbs[idx].eval_derivative(t)
def approximate_totlength_uniform(self, fn_dist, split=None):
return sum(self.approximate_lengths_uniform(fn_dist, split=split))
def approximate_lengths_uniform(self, fn_dist, split=None):
return [
cb.approximate_length_uniform(fn_dist, split=split)
for cb in self.cbs
]
def approximate_ts_at_intervals_uniform(
self, intervals, fn_dist, split=None
):
lengths = self.approximate_lengths_uniform(fn_dist, split=split)
totlength = sum(lengths)
ts = []
for interval in intervals:
if interval < 0:
ts.append(0)
continue
if interval >= totlength:
ts.append(len(self.cbs))
continue
for i, length in enumerate(lengths):
if interval <= length:
t = self.cbs[i].approximate_t_at_interval_uniform(
interval, fn_dist, split=split)
ts.append(i + t)
break
interval -= length
else:
assert False
return ts
def subdivide_linesegments(self, fn_dist, max_linearity=None):
return CubicBezierSpline(cbi
for cb in self.cbs
for cbi in cb.subdivide_linesegments(
fn_dist,
max_linearity=max_linearity
))
#########################################
# #
# the following code **requires** that #
# self.tessellate_uniform() is called #
# beforehand! #
# #
#########################################
def tessellate_uniform(self, fn_dist, split=None):
self.tessellation.clear()
for i, cb in enumerate(self.cbs):
cb_tess = cb.get_tessellate_uniform(fn_dist, split=split)
self.tessellation.append(cb_tess)
def approximate_totlength_tessellation(self):
return sum(self.approximate_lengths_tessellation())
def approximate_lengths_tessellation(self):
return [sum(d for _, _, d in cb_tess) for cb_tess in self.tessellation]
def approximate_ts_at_intervals_tessellation(self, intervals):
lengths = self.approximate_lengths_tessellation()
totlength = sum(lengths)
ts = []
for interval in intervals:
if interval < 0:
ts.append(0)
continue
if interval >= totlength:
ts.append(len(self.cbs))
continue
for i, length in enumerate(lengths):
if interval > length:
interval -= length
continue
cb_tess = self.tessellation[i]
for t, p, d in cb_tess:
if interval > d:
interval -= d
continue
ts.append(i+t)
break
else:
assert False
break
else:
assert False
return ts
def approximate_ts_at_points_tessellation(self, points, fn_dist):
ts = []
for p in points:
bd, bt = None, None
for i, cb_tess in enumerate(self.tessellation):
for t, q, _ in cb_tess:
d = fn_dist(p, q)
if bd is None or d < bd:
bd, bt = d, i+t
ts.append(bt)
return ts
def approximate_t_at_point_tessellation(self, point, fn_dist):
bd, bt = None, None
for i, cb_tess in enumerate(self.tessellation):
for t, q, _ in cb_tess:
d = fn_dist(point, q)
if bd is None or d < bd:
bd, bt = d, i+t
return bt
class GenVector(list):
'''
Generalized Vector, allows for some simple ordered items to be linearly combined
which is useful for interpolating arbitrary points of Bezier Spline.
'''
def __mul__(self, scalar: float): # ->GVector:
for idx in range(len(self)):
self[idx] *= scalar
return self
def __rmul__(self, scalar: float): # ->GVector:
return self.__mul__(scalar)
def __add__(self, other: list): # ->GVector:
for idx in range(len(self)):
self[idx] += other[idx]
return self
if __name__ == '__main__':
# run tests
print('-'*50)
l = GenVector([Vector((1, 2, 3)), 23])
print(l)
print(l * 2)
print(4 * l)
l2 = GenVector([Vector((0, 0, 1)), 10])
print(l + l2)
print(2 * l + l2 * 4)