さて、前回こしらえた点と点をベジェ曲線で結ぶ自前のパスをブーリアン演算で差分を取りたいと言っていたのはどうなったのか。できました、一応。
ソースは一番最後
まぁ、最後の合成は余分なパスを生成してるけど、ご愛嬌ってことで。
以下、よもやまプログラミング話。
点と点をベジェ曲線で結んでいるから交差判定は違うけど、きっと、ポリゴン(多角形)のブーリアン演算は共有するところが多いはずだよね。ってことで、まずは既存のアルゴリズムを探し漁りました。Wikiによると、代表的なアルゴリズムは以下の3つ。
・Vattiクリッピング法
・Sutherland–Hodgman法
・Weiler–Athertonクリッピング法
下2つは特殊ケースのアルゴリズムと書かれているので、おそらくベーシックなのは、一番上のVattiクリッピング法という方法なのでしょう。だがしかし、日本語の情報が見つからない。
Vatti Polygon Clipping
オープンソースのライブラリ
http://www.angusj.com/delphi/clipper.php
Vattiと違うのかもわかりませんが、他の日本語の解説してくれてるページを発見。んー、なんとなく曲線パスへの転用が難しそう。。。
平面図形の論理演算(ブーリアン)処理
http://www7b.biglobe.ne.jp/~garaku/Boolean.html
第9回 凸多角形の交差計算(前編)
http://gihyo.jp/dev/serial/01/geometry/0009
んー、自分で考えたものでも、案外上手くいくんじゃね?っといういつもの取り敢えずやってみよう精神で考えてみました。
パスとパスをつなぐセグメントに対する交点を順番に探していって、相手の内側にあるならそのセグメントをリストに追加っといった処理を繰り返して、閉領域を探し出します。
パス内にいる時リストに加えるのか、パス外にいる時に加えるのかで、合成するのか差分するのかを選択できます。
ただし、問題があって、困った事に閉じたパスを前提で考えしまったので、開いたパスは対応していません。(内側にいるとき、交点を通れば必ず外側になるっということを前提としているので)
その他、最後パスAとパスBそれぞれの処理の中で、全く同じ交点探索を繰り返してたり、明らかに無駄な部分もあったり、自己交差は考えてなかったり、色々と問題があるのですが、意欲も失せてにきたのでこの辺で・・・。
この中で、ベジェ曲線同士の交点とか、ベジェ曲線の分割とか要所のコードは備忘録サイトの方にまとめています。
http://code.tiblab.net/python/calculate/
ほぼ先人のコピペです。数学がてんで駄目の僕でも、人の知識を借りて、こうした処理ができるのだから、ウェブ社会素晴らしい。
そして、本当に素晴らしいのは、こうやって自前でコードを書かなくても、matplotlibやshapelyと言ったライブラリでできちゃうってことさ!
そう、もはや頑張ってアルゴリズムを考えて、実装する必要なんてなかった。後で知ったー。。。orz
# -*- coding: utf-8 -*-
import Tkinter
class Node:
p = [0.0,0.0] #self point
p0 = [0.0,0.0] #control point
p1 = [0.0,0.0] #control point
p0_avoid = False
p1_avoid = False
def __init__(self,p,p0,p1,p0_avoid=False,p1_avoid=False):
self.p = p
self.p0 = p0
self.p1 = p1
self.p0_avoid = p0_avoid
self.p1_avoid = p1_avoid
class Path:
nodes = []
closed = False
steps = 20
fill_color = ''
line_color = 'black'
node_color = 'black'
point_color = 'gray'
def __init__(self,nodes = None):
if nodes == None:
return
self.nodes = nodes
def points(self):
if len(self.nodes) == 0:
return []
points = []
points.append(self.nodes[0].p)
for i in range(1,len(self.nodes)):
points += self.segment_points(i-1,i)
if self.closed:
points += self.segment_points(i,0)
points = points[:-1]
return points
def segment_points(self,i,j):
points = []
n0 = self.nodes[i]
n1 = self.nodes[j]
if n0.p1_avoid == True and n1.p0_avoid == True:
points.append(n1.p)
else:
pts = bezier_points(n0.p,n0.p1,n1.p0,n1.p,self.steps)
points += pts[1:]
return points
def segment(self,i):
j = i + 1
if len(self.nodes) <= j:
j = j - len(self.nodes)
p0 = self.nodes[i].p
p1 = self.nodes[i].p1
p2 = self.nodes[j].p0
p3 = self.nodes[j].p
if self.nodes[i].p1_avoid and self.nodes[j].p0_avoid:
segment = [p0,p3]
else:
segment = [p0,p1,p2,p3]
return segment
def boundary(self):
cnt = len(self.nodes)
if not self.closed:
cnt -= 1
f = float('inf')
bound = [f,f,-f,-f]
for i in range(cnt):
segment = self.segment(i)
if len(segment) == 2:
p0,p1 = segment
xmin = min(p0[0],p1[0])
xmax = max(p0[0],p1[0])
ymin = min(p0[1],p1[1])
ymax = max(p0[1],p1[1])
else:
p0,p1,p2,p3 = segment
xmin,ymin,xmax,ymax = bezeir_boundary(p0,p1,p2,p3)
if xmin < bound[0]:
bound[0] = xmin
if ymin < bound[1]:
bound[1] = ymin
if bound[2] < xmax:
bound[2] = xmax
if bound[3] < ymax:
bound[3] = ymax
return bound #xmin,ymin,xmax,ymax
def is_clockwise(self):
total = 0.0
for i in range(len(self.nodes)-1):
p0 = self.nodes[i].p
p1 = self.nodes[i+1].p
total += p0[0] * p1[1] - p1[0] * p0[1]
if total > 0:
return True
else:
return False
def reverse(self):
new_nodes = []
for i in range(len(self.nodes)-1,-1,-1):
node = self.nodes[i]
p = node.p
p0 = node.p1
p1 = node.p0
n = Node(p,p0,p1)
new_nodes.append(n)
self.nodes = new_nodes
return
def is_inside(self,x,y):
if not self.closed:
return False
nodes = self.nodes + [self.nodes[0]]
box = self.boundary()
p0 = (x,y)
p1 = (box[2]+10,y)
line = (p0,p1)
cross_points = self.cross_points(line)
if len(cross_points) % 2 == 1:
return True
else:
return False
def cross_points(self,line):
# line = [p0,p1] or [a,b,c]
cnt = len(self.nodes)
cross_points = []
if not self.closed:
cnt -= 1
for i in range(cnt):
seg = self.segment(i)
ps = intersection_of_segments(seg,line,True)
cross_points += ps
return cross_points
debug_segments = []
debug_points = []
def boolean_paths(path0,path1,disc0,disc1):
global debug_segments,debug_points
path = [path0,path1]
for i in [0,1]:
if path[i].is_clockwise():
path[i].reverse()
group0 = boolean_segments(path0,path1,disc0)
group1 = boolean_segments(path1,path0,disc1)
group1.reverse()
if len(group0) != len(group1):
print('Error path_boolean() 1')
#return []
#group1 reverse
if disc0 != disc1:
for segments in group1:
for segment in segments:
segment.reverse()
segments.reverse()
#debug
for group in [group0,group1]:
for segments in group:
for segment in segments:
debug_segments.append(segment)
#group1 sort
find = False
p0 = group0[0][-1][-1]
for i in range(len(group1)):
p1 = group1[i][0][0]
if is_same_point(p0,p1):
find = True
break
if find:
group1 = group1[i:]+group1[:i]
else:
print('Error path_boolean() 2')
return []
#join group0 and group1
group = []
segments = []
start_point = None
for i in range(len(group0)):
segments0 = group0[i]
segments1 = group1[i]
segments += segments0 + segments1
if not start_point:
start_point = segments[0][0]
last_point = segments[-1][-1]
if is_same_point(start_point,last_point):
group.append(segments)
segments = []
if len(segments) > 0:
group.append(segments)
#make path
paths = []
for i in range(len(group)):
is_added = True
nodes = []
for j in range(len(group[i])):
segments = group[i]
s0 = segments[j-1]
s1 = segments[j]
if not is_same_point(s0[-1],s1[0]):
print('Error path_boolean() 3')
return []
p = s1[0]
for k in [0,1]:
if not path[k].closed:
if(is_same_point(p,path[k].nodes[0].p) or
is_same_point(p,path[k].nodes[-1].p)):
is_added = False
break
if len(s0) == 2:
_p0,_p1 = s0
x = _p0[0] + 0.75*(_p1[0] - _p0[0])
y = _p0[1] + 0.75*(_p1[1] - _p0[1])
p0 = (x,y)
p0_avoid = True
else:
p0 = s0[2]
p0_avoid = False
if len(s1) == 2:
_p0,_p1 = s1
x = _p0[0] + 0.25*(_p1[0] - _p0[0])
y = _p0[1] + 0.25*(_p1[1] - _p0[1])
p1 = (x,y)
p1_avoid = True
else:
p1 = s1[1]
p1_avoid = False
nodes.append(Node(p,p0,p1,p0_avoid,p1_avoid))
if is_added:
new_path = Path(nodes)
new_path.closed = True
paths.append(new_path)
return paths
def boolean_segments(path0,path1,disc):
groups = []
segments = []
cnt0 = len(path0.nodes)
cnt1 = len(path1.nodes)
## if not path0.closed:
## cnt0 -= 1
## if not path1.closed:
## cnt1 -= 1
is_inside = path1.is_inside(*path0.nodes[0].p)
for i in range(cnt0):
seg0 = path0.segment(i)
tlist = []
for j in range(cnt1):
seg1 = path1.segment(j)
ts = intersection_of_segments(seg0,seg1,False)
for t in ts:
tlist.append(t[0])
if len(tlist) > 0:
tlist.sort()
pre_t = 0.0
for t in tlist:
_t = (t-pre_t)/(1.0-pre_t)
s0,s1 = segment_split(seg0,_t)
if is_inside == disc:
segments.append(s0)
groups.append(segments)
segments = []
if is_inside:
is_inside = False
else:
is_inside = True
pre_t = t
seg0 = s1
if is_inside == disc:
segments.append(s1)
else: #if not crossed
if is_inside == disc:
segments.append(seg0)
if len(segments)>0:
groups[0] = segments + groups[0]
return groups
def is_same_point(p0,p1):
x0,y0 = p0
x1,y1 = p1
s = abs(x1-x0)+abs(y1-y0)
if s < 0.00001:
return True
else:
return False
def segment_split(segment,t):
#segment = (p0,p1)[line] or (p0,p1,p2,p3)[bezier]
n = len(segment)
if n == 2: #if line
return line_split(segment,t)
elif n == 4:
return bezier_split(segment,t)
def point_on_line((p0,p1),t):
x = p0[0] + t*(p1[0] - p0[0])
y = p0[1] + t*(p1[1] - p0[1])
return (x,y)
def line_generalization(p0,p1):
if p0[0] == p1[0]:
a = 1.0
b = 0.0
c = float(-p0[0])
elif p0[1] == p1[1]:
a = 0.0
b = 1.0
c = float(-p0[1])
else:
slope = float(p1[1]-p0[1])/(p1[0]-p0[0])
intercept = p0[1] - slope*p0[0]
a = slope
b = -1.0
c = intercept
return a,b,c
def line_split((p0,p1),t):
p = point_on_line((p0,p1),t)
return [[p0,p],[p,p1]]
def bezier_point((p0,p1,p2,p3),t):
x = (1-t)**3*p0[0]+3*(1-t)**2*t*p1[0]+3*(1-t)*t**2*p2[0]+t**3*p3[0]
y = (1-t)**3*p0[1]+3*(1-t)**2*t*p1[1]+3*(1-t)*t**2*p2[1]+t**3*p3[1]
return (x,y)
def bezier_points(p0,p1,p2,p3,steps=20):
points = []
for i in range(steps+1):
t = i/float(steps)
p = bezier_point((p0,p1,p2,p3),t)
points.append(p)
return points
def bezeir_boundary(p0,p1,p2,p3):
bounds = [[],[]]
bounds[0] += [p0[0],p3[0]]
bounds[1] += [p0[1],p3[1]]
for i in [0,1]:
a = float(-3*p0[i] + 9*p1[i] - 9*p2[i] + 3*p3[i])
b = float( 6*p0[i] - 12*p1[i] + 6*p2[i])
c = float( 3*p1[i] - 3*p0[i])
if a == 0:
if b == 0: continue
tlist = [-c / b]
else:
tlist = quadratic_equation(a,b,c)
for t in tlist:
if 0 < t < 1:
v = (1-t)**3*p0[i] + 3*(1-t)**2*t*p1[i] + 3*(1-t)*t**2*p2[i] + t**3*p3[i]
bounds[i].append(v)
return [min(bounds[0]),min(bounds[1]),
max(bounds[0]),max(bounds[1])]
def bezier_split(points,t):
if not 0 < t < 1:
print('Error bezier_split(): t must be 0 xmax1 or xmax0 < xmin1 or
ymin0 > ymax1 or ymax0 < ymin1): #if overlap
b0,b1 = bezier_split(bezier0,0.5)
b2,b3 = bezier_split(bezier1,0.5)
c0 = [b0,t0,(t1+t0)/2]
c1 = [b1,(t1+t0)/2,t1]
c2 = [b2,t2,(t3+t2)/2]
c3 = [b3,(t3+t2)/2,t3]
buff += [[c0,c2],[c0,c3],[c1,c2],[c1,c3]]
if len(buff) == 0:
break
if n == 9999:
print('Error intersection_of_bezier_curves(): cannot convergence.')
return []
#marge
groups = []
for t in result:
is_included = False
for group in groups:
d0 = abs(group[0][0]-t[0])
d1 = abs(group[0][1]-t[1])
if d0 < 0.00001 and d1 < 0.00001:
is_included = True
continue
if is_included:
group.append(t)
else:
groups.append([t])
result = []
for group in groups:
t = [0,0]
for _t in group:
for i in range(2):
t[i] += _t[i]
for i in range(2):
t[i] = t[i]/len(group)
result.append(t)
#print(str(len(position))+' points found.')
return result
def intersection_of_bezier_and_line((p0,p1,p2,p3),line,return_coord=True):
#line = (a,b,c) or (p0,p1)
if len(line) == 2:
a,b,c = line_generalization(*line)
else:
a,b,c = line
f0 = a * p0[0] + b * p0[1] + c
f1 = 3 * (a * p1[0] + b * p1[1] + c)
f2 = 3 * (a * p2[0] + b * p2[1] + c)
f3 = a * p3[0] + b * p3[1] + c
_a = -f0 + f1 - f2 + f3
_b = 3 * f0 - 2 * f1 + f2
_c = -3 * f0 + f1
_d = f0
if _a == 0:
if _b == 0:
if _c == 0: tlist = []
else: tlist = [-_d/_c]
else:
tlist = quadratic_equation(_b,_c,_d)
else:
tlist = cubic_equation(_a,_b,_c,_d)
result = []
for t in tlist:
if t < 0 or 1 < t:
continue
x,y = bezier_point((p0,p1,p2,p3),t)
if len(line) == 2:
p0,p1 = line
xmin = min(line[0][0],line[1][0])
xmax = max(line[0][0],line[1][0])
if x < xmin or xmax < x:
continue
if return_coord:
result.append([x,y])
else:
if len(line) == 2:
t1 = (x-p0[0])/(p1[0]-p0[0])
else:
t1 = 1.0
result.append([t,t1])
return result
def intersection_of_lines(line0,line1,return_coord=True):
#line = (p0,p1) or (a,b,c)
lines = [line0,line1]
line_n = [len(line0),len(line1)]
for i in range(2):
if line_n[i] == 3:
a,b,c = lines[i]
if a == 0:
x0 = x1 = 0
y0 = y1 = -c/b
elif b == 0:
x0 = x1 = -c/a
y0 = y1 = 0
else:
x0 = 0.0
x1 = 1.0
y0 = -c/b
y1 = (-a-c)/b
lines[i] = [(x0,y0),(x1,y1)]
p0,p1 = lines[0]
p2,p3 = lines[1]
d = float((p1[0]-p0[0])*(p3[1]-p2[1])-(p1[1]-p0[1])*(p3[0]-p2[0]))
if d == 0:
return []
ac = (p2[0]-p0[0],p2[1]-p0[1])
t0 = ((p3[1]-p2[1])*ac[0] - (p3[0]-p2[0])*ac[1]) / d
t1 = ((p1[1]-p0[1])*ac[0] - (p1[0]-p0[0])*ac[1]) / d
if line_n[0] == 2:
if t0 < 0 or 1 < t0:
return []
if line_n[1] == 2:
if t1 < 0 or 1 < t1:
return []
x = p0[0] + t0*(p1[0] - p0[0])
y = p0[1] + t0*(p1[1] - p0[1])
if return_coord:
return [[x,y]]
else:
return [[t0,t1]]
def cubic_equation(a,b,c,d):
p = -b**2/(9.0*a**2) + c/(3.0*a)
q = b**3/(27.0*a**3) - b*c/(6.0*a**2) + d/(2.0*a)
t = complex(q**2+p**3)
w =(-1.0 +1j*3.0**0.5)/2.0
u = [0,0,0]
u[0] = (-q +t**0.5)**(1.0/3.0)
u[1] = u[0] * w
u[2] = u[0] * w**2
v = [0,0,0]
v[0] = (-q -t**0.5)**(1.0/3.0)
v[1] = v[0] * w
v[2] = v[0] * w**2
x_list = []
for i in range(3):
for j in range(3):
if abs(u[i]*v[j] + p) < 0.0001:
x = u[i] + v[j]
if abs(x.imag) < 0.0000001:
x = x.real - b/(3.0*a)
x_list.append(x)
return x_list
def quadratic_equation(a,b,c):
d = b*b-4*a*c
if d < 0:
return []
if d == 0:
x = -b/(2.0*a)
return [x]
else:
x0 = (-b+d**0.5)/(2.0*a)
x1 = (-b-d**0.5)/(2.0*a)
return [x0,x1]
def main():
window = Tkinter.Tk()
canvas = Tkinter.Canvas(window,width=300,height=200,bg='white')
canvas.pack()
n0 = Node([10,30],[90,10],[20,50])
n1 = Node([10,170],[20,100],[90,190])
n2 = Node([180,130],[180,160],[180,90])
n7 = Node([90,120],[90,140],[90,100])
n8 = Node([160,70],[160,80],[160,60])
n3 = Node([150,20],[250,10],[120,30])
n4 = Node([120,80],[100,20],[150,80])
n5 = Node([140,170],[180,150],[180,190])
n6 = Node([270,170],[220,190],[280,30])
path0 = Path([n0,n1,n2,n7,n8])
path0.closed = True
n1.p1_avoid = True
n2.p0_avoid = True
path1 = Path([n3,n4,n5,n6])
path1.closed = True
#n3.p1_avoid = True
#n4.p0_avoid = True
bpaths = boolean_paths(path0,path1,True,True)
for path in bpaths:
path.fill_color = 'yellow'
path.point_color = ''
path.node_color = 'red'
path.line_color = 'red'
paths = [path0,path1]+bpaths
r = 3
for path in paths:
if path.closed:
canvas.create_polygon(path.points(),fill=path.fill_color,outline=path.line_color)
for n in path.nodes:
canvas.create_oval(n.p[0]-r,n.p[1]-r,n.p[0]+r,n.p[1]+r,outline=path.node_color)
canvas.create_oval(n.p0[0]-r,n.p0[1]-r,n.p0[0]+r,n.p0[1]+r,outline=path.point_color)
canvas.create_oval(n.p1[0]-r,n.p1[1]-r,n.p1[0]+r,n.p1[1]+r,outline=path.point_color)
canvas.create_line(n.p[0],n.p[1],n.p0[0],n.p0[1],fill=path.point_color)
canvas.create_line(n.p[0],n.p[1],n.p1[0],n.p1[1],fill=path.point_color)
canvas.create_line(path.points(),fill=path.line_color)
for p in debug_points:
#canvas.create_oval(p[0]-r,p[1]-r,p[0]+r,p[1]+r,outline='red')
pass
for seg in debug_segments:
if len(seg) == 4:
ps = bezier_points(*seg)
else:
ps = seg
#canvas.create_line(ps,fill='red',arrow=Tkinter.LAST)
window.mainloop()
if __name__ == '__main__':
main()