前回も理想のドローソフトについての記事でしたが、今だその熱を持っていて、今回はパスとかハッチングとかやってみました。
EMFやSVGなどに出力することを考えると、ライブラリが持つ機能に依存するよりは、なるだけ自前で実装したほうが良いはず。どちらのファイル形式もベジェ曲線はサポートしているので、3次ベジェ曲線によるパスのプログラミングを試みました。
ドローソフトと言えばこれだよね。
[ソース1](ソースは最後にあります)
機能として必須なのが、パターンハッチング。
ベジェ曲線と直線の交点の計算なんてどうやるんだーっと思ったけど、情報を載せてくれる人はいるもんですなぁ。ありがたい。
5(その2):直線と曲線の交点(解の公式)
これで直線ならどんな模様でもできる。いずれ楕円も加えたい。
虚数とか出てきてなかなか大変でしたが、出張により久々に一人の夜を過ごし、たっぷり作業できたので、なんとか形にはなりました(後々不具合が出てくるのは必至だと思いますが)。久しぶりにがっつりプログラミング、楽しい。
次にしたいと思っているのが、ブーリアン演算による差分とか合成とか。果たして僕の能力が及ぶのか。。。
[ソース1:ベジェ曲線パス]
# -*- 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): self.p = p self.p0 = p0 self.p1 = p1 class Path: nodes = [] closed = False steps = 20 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 = self.bezier_curve_points(n0.p,n0.p1,n1.p0,n1.p) points += pts[1:] return points def bezier_curve_points(self,p0,p1,p2,p3): points = [] for i in range(self.steps+1): t = float(i)/self.steps 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] points.append((x,y)) return points def main(): window = Tkinter.Tk() canvas = Tkinter.Canvas(window,width=300,height=200) canvas.pack() n0 = Node([50,50],[50,10],[80,50]) n1 = Node([100,150],[100,110],[100,190]) n2 = Node([250,100],[250,150],[180,30]) path = Path([n0,n1,n2]) path.closed = True #n1.p1_avoid = True #n2.p0_avoid = True if path.closed: canvas.create_polygon(path.points(),fill='gray',outline='red') else: canvas.create_line(path.points(),fill='red') r = 3 for n in path.nodes: canvas.create_oval(n.p[0]-r,n.p[1]-r,n.p[0]+r,n.p[1]+r) canvas.create_oval(n.p0[0]-r,n.p0[1]-r,n.p0[0]+r,n.p0[1]+r) canvas.create_oval(n.p1[0]-r,n.p1[1]-r,n.p1[0]+r,n.p1[1]+r) canvas.create_line(n.p[0],n.p[1],n.p0[0],n.p0[1]) canvas.create_line(n.p[0],n.p[1],n.p1[0],n.p1[1]) window.mainloop() if __name__ == '__main__': main()
[ソース2:斜線ハッチング]
# -*- coding: utf-8 -*- import math 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): self.p = p self.p0 = p0 self.p1 = p1 class Path: nodes = [] closed = False steps = 20 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 = self.bezier_curve_points(n0.p,n0.p1,n1.p0,n1.p) points += pts[1:] return points def bezier_curve_points(self,p0,p1,p2,p3): points = [] for i in range(self.steps+1): t = float(i)/self.steps 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] points.append((x,y)) return points class PattanStripe: degree = -55 # -90 <= degree <= 90 lines = [] space = 10 def __init__(self,path): self.path = path self.update() def update(self): #range area = [float('inf'),float('inf'),0.0,0.0] #xmin,ymin,xmax,ymax for p in self.path.points(): if area[0] > p[0]: area[0] = p[0] elif area[2] < p[0]: area[2] = p[0] if area[1] > p[1]: area[1] = p[1] elif area[3] < p[1]: area[3] = p[1] #lines lines = [] sp = self.space if self.degree == 90 or self.degree == -90: a = 1.0 b = 0.0 count = int((area[2]-area[0])/sp+1) for n in range(count): x = area[0] +sp*n -sp/2 c = -x lines.append((a,b,c)) else: a = math.tan(math.radians(self.degree)) b = -1.0 sp = abs(sp/math.cos(math.radians(self.degree))) dy = abs(a*(area[2]-area[0])) count = int(abs(area[3]-area[1]+dy)/sp+1) if a < 0: start_y = area[1]-sp/2 else: start_y = area[1]-dy-sp/2 x = area[0] for n in range(count): y = start_y +sp*n c = -a*x -b*y lines.append((a,b,c)) #cut lines self.lines = [] self.cross_points = [] nodes = self.path.nodes + [self.path.nodes[0]] for line in lines: cross = [] for i in range(len(self.path.nodes)): p0 = nodes[i].p p1 = nodes[i].p1 p2 = nodes[i+1].p0 p3 = nodes[i+1].p if (nodes[i].p1_avoid == True and nodes[i+1].p0_avoid == True): pts = intersection_of_line_segment(p0,p3,line) else: pts = intersection_of_bezier_and_line((p0,p1,p2,p3),line) cross += pts self.cross_points += cross #for debug if len(cross) % 2 != 0: print('error Pattan update()') continue cross.sort() for j in range(0,len(cross),2): new_line = cross[j] + cross[j+1] self.lines.append(new_line) def intersection_of_line_segment((x0,y0),(x1,y1),(a,b,c)): line = line_generalization((x0,y0),(x1,y1)) points = intersection_2line(line,(a,b,c)) m = 0.000001 xs = [x0,x1] ys = [y0,y1] new_points = [] for p in points: if (min(xs)-m <= p[0] and p[0] <= max(xs)+m and min(ys)-m <= p[1] and p[1] <= max(ys)+m): new_points.append(p) return new_points def intersection_2line((a0,b0,c0),(a1,b1,c1)): t = float(a0*b1-a1*b0) if t == 0: return [] x = (b0*c1-b1*c0)/t y = (a1*c0-a0*c1)/t return [(x,y)] def intersection_of_bezier_and_line((p0,p1,p2,p3),(a,b,c)): 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) points = [] for t in tlist: if t < 0 or 1 < t: continue 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] points.append((x,y)) return points 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 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 main(): window = Tkinter.Tk() canvas = Tkinter.Canvas(window,width=300,height=200) canvas.pack() n0 = Node([20,50],[30,10],[10,100]) n1 = Node([50,180],[20,170],[80,190]) n2 = Node([140,100],[50,90],[120,190]) n3 = Node([280,70],[250,150],[220,10]) path = Path([n0,n1,n2,n3]) path.closed = True #n1.p1_avoid = True #n2.p0_avoid = True path.pattan = PattanStripe(path) if path.closed: canvas.create_polygon(path.points(),fill='gray',outline='red') else: canvas.create_line(path.points(),fill='red') r = 3 for n in path.nodes: canvas.create_oval(n.p[0]-r,n.p[1]-r,n.p[0]+r,n.p[1]+r) canvas.create_oval(n.p0[0]-r,n.p0[1]-r,n.p0[0]+r,n.p0[1]+r) canvas.create_oval(n.p1[0]-r,n.p1[1]-r,n.p1[0]+r,n.p1[1]+r) canvas.create_line(n.p[0],n.p[1],n.p0[0],n.p0[1]) canvas.create_line(n.p[0],n.p[1],n.p1[0],n.p1[1]) for xy in path.pattan.lines: canvas.create_line(xy) r = 2 for p in path.pattan.cross_points: #canvas.create_oval(p[0]-r,p[1]-r,p[0]+r,p[1]+r) pass window.mainloop() if __name__ == '__main__': main()
[ソース3:パターンハッチング]
# -*- coding: utf-8 -*- #import math 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): self.p = p self.p0 = p0 self.p1 = p1 class Path: nodes = [] closed = False steps = 50 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 and n1.p0_avoid: points.append(n1.p) else: pts = self.bezier_curve_points(n0.p,n0.p1,n1.p0,n1.p) points += pts[1:] return points def bezier_curve_points(self,p0,p1,p2,p3): points = [] for i in range(self.steps+1): t = float(i)/self.steps 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] points.append((x,y)) return points class PattanTile: width = 20 height = 20 polylines = [] lines = [] cross_points = [] xn = 0 yn = 0 def __init__(self,path): self.path = path self.polylines = [[0,0,0,20,20,20,20,0,0,0],[5,5,5,15,15,15]] self.update() def setPattan(self,width,height,polylines): self.width = width self.height = height self.polylines = polylines def update(self): area = self._area( self.path.points()) self.xn = int((area[2]-area[0])/self.width)+1 self.yn = int((area[3]-area[1])/self.height)+2 tiles = [[[0,0,0,0] for j in range(self.yn)] for i in range(self.xn)] tiles = self._tile_cross_points(tiles,area) tiles = self._tile_states(tiles) self.lines = self._lines(tiles) def _area(self,points): area = [float('inf'),float('inf'),0.0,0.0] #xmin,ymin,xmax,ymax for p in points: if area[0] > p[0]: area[0] = p[0] elif area[2] < p[0]: area[2] = p[0] if area[1] > p[1]: area[1] = p[1] elif area[3] < p[1]: area[3] = p[1] return area def _tile_cross_points(self,tiles,area): nodes = self.path.nodes + [self.path.nodes[0]] sx = area[0]-self.width/2 sy = area[1]-self.height/2 for i in range(self.xn): for j in range(self.yn): x0 = sx + self.width*i y0 = sy + self.height*j x1 = x0 + self.width y1 = y0 + self.height blines = [(x0,y0,x0,y1),(x0,y0,x1,y0), (x1,y0,x1,y1),(x0,y1,x1,y1)] cross_points = [] for border_line in blines: cross_points += self.crossPointsOfPath(border_line) tile = [x0,y0,0,len(cross_points)] tiles[i][j] = tile self.cross_points += cross_points return tiles def _tile_states(self,tiles): tile_states = [[0 for j in range(self.yn)] for i in range(self.xn)] for i in range(self.xn): for j in range(self.yn): cross_point_n = tiles[i][j][3] if cross_point_n > 1: tile_states[i][j] = 4 #on border for i in range(self.xn): for j in range(self.yn): state = tile_states[i][j] if state > 0: continue self._bflag = False tile_states = self._tile_fill(i,j,tile_states) if self._bflag: state = 2 #outside path else: state = 3 #inside path for k in range(self.xn): for l in range(self.yn): if tile_states[k][l] == 1: tile_states[k][l] = state for i in range(self.xn): for j in range(self.yn): tiles[i][j][2] = tile_states[i][j] return tiles def _lines(self,tiles): lines = [] for i in range(self.xn): for j in range(self.yn): state = tiles[i][j][2] if state == 3: #inside path lines += self._pattan_lines(tiles[i][j]) elif state == 4: #on border _lines = self._pattan_lines(tiles[i][j]) lines += self._trim_lines(_lines) return lines def _pattan_lines(self,tile): x,y,s,cpts = tile pattan_lines = [] for polyline in self.polylines: for k in range(0,len(polyline)-2,2): x0 = x + polyline[k] y0 = y + polyline[k+1] x1 = x + polyline[k+2] y1 = y + polyline[k+3] line = [x0,y0,x1,y1] pattan_lines.append(line) return pattan_lines def _trim_lines(self,pattan_lines): trim_lines = [] nodes = self.path.nodes + [self.path.nodes[0]] for line in pattan_lines: x0,y0,x1,y1 = line cross_points = self.crossPointsOfPath(line) p0_in_path = self.isPointInPath(x0,y0) p1_in_path = self.isPointInPath(x1,y1) cnt = len(cross_points) if cnt == 0: if p0_in_path and p1_in_path: trim_lines.append(line) pass elif cnt == 1: p = cross_points[0] if p0_in_path: line = (x0,y0,p[0],p[1]) else: line = (p[0],p[1],x1,y1) pass trim_lines.append(line) else: p = [(x0,y0)]+cross_points+[(x1,y1)] p.sort() if p0_in_path: in_path = True else: in_path = False for i in range(len(cross_points)+1): if in_path: line = (p[i][0],p[i][1],p[i+1][0],p[i+1][1]) trim_lines.append(line) if in_path: in_path = False else: in_path = True self.cross_points += cross_points return trim_lines def _tile_fill(self,x,y,array): array[x][y] = 1 if y-1 > 0: if array[x][y-1]==0: array = self._tile_fill(x,y-1,array) if x+1 < self.xn: if array[x+1][y]==0: array = self._tile_fill(x+1,y,array) if y+1 < self.yn: if array[x][y+1]==0: array = self._tile_fill(x,y+1,array) if x-1 > 0: if array[x-1][y]==0: array = self._tile_fill(x-1,y,array) if x==0 or y==0 or x==self.xn-1 or y==self.yn-1: self._bflag = True return array def isPointInPath(self,x,y): nodes = self.path.nodes + [self.path.nodes[0]] cross_points = [] line = (0,1,-y) pts = self.crossPointsOfPath(line) for p in pts: if x < p[0]: cross_points.append(p) if len(cross_points) % 2 == 1: return True else: return False def crossPointsOfPath(self,line): # line = [x0,y0,x1,y1] or [p0,p1] or [a,b,c] cnt = len(line) if cnt == 2: x0,y0 = line[0] x1,y1 = line[1] line_eq = line_generalization(line[0],line[1]) elif cnt == 3: line_eq = line elif cnt == 4: x0,y0,x1,y1 = line line_eq = line_generalization((x0,y0),(x1,y1)) cross_points = [] nodes = self.path.nodes + [self.path.nodes[0]] m = 0.000001 for k in range(len(self.path.nodes)): p0 = nodes[k].p p1 = nodes[k].p1 p2 = nodes[k+1].p0 p3 = nodes[k+1].p if nodes[k].p1_avoid and nodes[k+1].p0_avoid: ps = intersection_of_line_segment(p0,p3,line_eq) else: ps = intersection_of_bezier_and_line((p0,p1,p2,p3),line_eq) if cnt == 3: #no limit line lengh cross_points += ps else: for p in ps: xmax,xmin = max((x0,x1)),min(x0,x1) ymax,ymin = max((y0,y1)),min(y0,y1) if (xmin-m <= p[0] and p[0] <= xmax+m and ymin-m <= p[1] and p[1] <= ymax+m): cross_points.append(p) return cross_points def intersection_of_line_segment((x0,y0),(x1,y1),(a,b,c)): line = line_generalization((x0,y0),(x1,y1)) points = intersection_2line(line,(a,b,c)) m = 0.000001 xs = [x0,x1] ys = [y0,y1] new_points = [] for p in points: if (min(xs)-m <= p[0] and p[0] <= max(xs)+m and min(ys)-m <= p[1] and p[1] <= max(ys)+m): new_points.append(p) return new_points def intersection_2line((a0,b0,c0),(a1,b1,c1)): t = float(a0*b1-a1*b0) if t == 0: return [] x = (b0*c1-b1*c0)/t y = (a1*c0-a0*c1)/t return [(x,y)] def intersection_of_bezier_and_line((p0,p1,p2,p3),(a,b,c)): 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) points = [] for t in tlist: if t < 0 or 1 < t: continue 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] points.append((x,y)) return points 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 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 main(): window = Tkinter.Tk() canvas = Tkinter.Canvas(window,width=300,height=200) canvas.pack() n0 = Node([20,50],[30,10],[10,100]) n1 = Node([50,180],[20,170],[80,190]) n2 = Node([140,100],[50,90],[120,190]) n3 = Node([280,70],[250,150],[220,10]) path = Path([n0,n1,n2,n3]) path.closed = True n1.p1_avoid = True n2.p0_avoid = True path.pattan = PattanTile(path) if path.closed: canvas.create_polygon(path.points(),fill='gray',outline='red') else: canvas.create_line(path.points(),fill='red') r = 3 for n in path.nodes: canvas.create_oval(n.p[0]-r,n.p[1]-r,n.p[0]+r,n.p[1]+r) canvas.create_oval(n.p0[0]-r,n.p0[1]-r,n.p0[0]+r,n.p0[1]+r) canvas.create_oval(n.p1[0]-r,n.p1[1]-r,n.p1[0]+r,n.p1[1]+r) canvas.create_line(n.p[0],n.p[1],n.p0[0],n.p0[1]) canvas.create_line(n.p[0],n.p[1],n.p1[0],n.p1[1]) for xy in path.pattan.lines: canvas.create_line(xy,fill='blue') r = 2 for p in path.pattan.cross_points: #canvas.create_oval(p[0]-r,p[1]-r,p[0]+r,p[1]+r) pass window.mainloop() if __name__ == '__main__': main()