previous commit for retopo converted the strokes into a curve first, better to use...
[blender.git] / release / scripts / modules / retopo.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 import bpy
22
23 EPS = 0.001
24 EPS_LINE_LINE = 0.02
25 EPS_COLLAPSE = 0.05
26 EPS_HUB = 0.05
27
28 def get_hub(co, _hubs):
29     
30     if 1:
31         for hub in _hubs.values():
32             if (hub.co - co).length < EPS_HUB:
33                 return hub
34         
35         key = co.toTuple(3)
36         hub = _hubs[key] = Hub(co, key, len(_hubs))
37         return hub
38     else:
39         pass
40         
41         '''
42         key = co.toTuple(3)
43         try:
44             return _hubs[key]
45         except:
46             hub = _hubs[key] = Hub(co, key, len(_hubs))
47             return hub
48         '''
49         
50
51 class Hub:
52     def __init__(self, co, key, index):
53         self.co = co.copy()
54         self.key = key
55         self.index = index
56         self.links = []
57
58     def get_weight(self):
59         f = 0.0
60         
61         for hub_other in self.links:
62             f += (self.co - hub_other.co).length
63     
64     def replace(self, other):
65         for hub in self.links:
66             try:
67                 hub.links.remove(self)
68             except:
69                 pass
70             if other not in hub.links:
71                 hub.links.append(other)
72             
73     
74     def dist(self, other):
75         return (self.co - other.co).length
76
77     def calc_faces(self, hub_ls):
78         faces = []
79         # first tris
80         for l_a in self.links:
81             for l_b in l_a.links:
82                 if l_b is not self and l_b in self.links:
83                     # will give duplicates
84                     faces.append((self.index, l_a.index, l_b.index))
85         
86         # now quads, check which links share 2 different verts
87         # directly
88         def validate_quad(face):
89             if len(set(face)) != len(face):
90                 return False
91             if hub_ls[face[0]] in hub_ls[face[2]].links:
92                 return False
93             if hub_ls[face[2]] in hub_ls[face[0]].links:
94                 return False
95                 
96             if hub_ls[face[1]] in hub_ls[face[3]].links:
97                 return False
98             if hub_ls[face[3]] in hub_ls[face[1]].links:
99                 return False
100             
101             return True
102
103         for i, l_a in enumerate(self.links):
104             links_a = set([l.index for l in l_a.links])
105             for j in range(i):
106                 l_b = self.links[j]
107                 
108                 links_b = set([l.index for l in l_b.links])
109                 
110                 isect = links_a.intersection(links_b)
111                 if len(isect) == 2:
112                     isect = list(isect)
113                     
114                     # check there are no diagonal lines
115                     face = (isect[0], l_a.index, isect[1], l_b.index)
116                     if validate_quad(face):
117                     
118                         faces.append(face)
119         
120         return faces
121     
122
123
124 class Spline:
125     def __init__(self, points):
126         self.points = points
127         self.hubs = []
128
129     def link(self):
130         if len(self.hubs) < 2:
131             return
132         
133         edges = list(set([i for i, hub in self.hubs]))
134         edges.sort()
135
136         edges_order = {}
137         for i in edges:
138             edges_order[i] = []
139         
140         
141         # self.hubs.sort()
142         for i, hub in self.hubs:
143             edges_order[i].append(hub)
144         
145         hubs_order = []
146         for i in edges:
147             ls = edges_order[i]
148             edge_start = self.points[i]
149             ls.sort(key=lambda hub: (hub.co - edge_start).length)
150             hubs_order.extend(ls)
151         
152         # Now we have the order, connect the hubs
153         hub_prev = hubs_order[0]
154         
155         for hub in hubs_order[1:]:
156             hub.links.append(hub_prev)
157             hub_prev.links.append(hub)
158             hub_prev = hub
159
160 def get_points(stroke):
161     from Mathutils import Vector
162     # TODO - why isnt point.co a Vector?
163     return [Vector(tuple(point.co)) for point in stroke.points]
164
165 def get_splines(gp):
166     for l in gp.layers:
167         if l.active: # XXX - should be layers.active
168             break
169     
170     frame = l.active_frame
171     
172     return [Spline(get_points(stroke)) for stroke in frame.strokes]
173
174 def xsect_spline(sp_a, sp_b, _hubs):
175     from Mathutils import LineIntersect
176     from Mathutils import MidpointVecs
177     from Geometry import ClosestPointOnLine
178     pt_a_prev = pt_b_prev = None
179     
180     pt_a_prev = sp_a.points[0]
181     for a, pt_a in enumerate(sp_a.points[1:]):
182         pt_b_prev = sp_b.points[0]
183         for b, pt_b in enumerate(sp_b.points[1:]):
184
185             # Now we have 2 edges
186             # print(pt_a, pt_a_prev, pt_b, pt_b_prev)
187             xsect = LineIntersect(pt_a, pt_a_prev, pt_b, pt_b_prev)
188             if xsect is not None:
189                 if (xsect[0]-xsect[1]).length <= EPS_LINE_LINE:
190                     f = ClosestPointOnLine(xsect[1], pt_a, pt_a_prev)[1]
191                     if f >= 0.0 and f <= 1.0:
192                         f = ClosestPointOnLine(xsect[0], pt_b, pt_b_prev)[1]
193                         if f >= 0.0 and f <= 1.0:
194                             # This wont happen often
195                             co = MidpointVecs(xsect[0], xsect[1])
196                             hub = get_hub(co, _hubs)
197
198                             sp_a.hubs.append((a, hub))
199                             sp_b.hubs.append((b, hub))
200
201             pt_b_prev = pt_b
202             
203         pt_a_prev = pt_a
204
205
206 def calculate(gp):
207     splines = get_splines(gp)
208     _hubs = {}
209     
210     for i, sp in enumerate(splines):
211         for j, sp_other in enumerate(splines):
212             if j<=i:
213                 continue
214
215             xsect_spline(sp, sp_other, _hubs)
216             
217     for sp in splines:
218         sp.link()
219     
220     # remove these
221     hubs_ls = [hub for hub in _hubs.values() if hub.index != -1]
222         
223     _hubs.clear()
224     _hubs = None
225     
226     for i, hub in enumerate(hubs_ls):
227         hub.index = i
228     
229     # Now we have connected hubs, write all edges!
230     def order(i1, i2):
231         if i1 > i2:
232             return i2, i1
233         return i1, i2
234     
235     edges = {}
236     
237     for hub in hubs_ls:
238         i1 = hub.index
239         for hub_other in hub.links: 
240             i2 = hub_other.index
241             edges[order(i1, i2)] = None
242     
243     verts = []
244     edges = edges.keys()
245     faces = []
246     
247     for hub in hubs_ls:
248         verts.append(hub.co)
249         faces.extend(hub.calc_faces(hubs_ls))
250     
251     # remove double faces
252     faces = dict([(tuple(sorted(f)), f) for f in faces]).values()
253         
254     mesh = bpy.data.add_mesh("Retopo")
255     mesh.from_pydata(verts, [], faces)
256     
257     scene = bpy.context.scene
258     mesh.update()
259     obj_new = bpy.data.add_object('MESH', "Torus")
260     obj_new.data = mesh
261     scene.objects.link(obj_new)
262     
263     return obj_new
264     
265     
266 def main():
267     scene = bpy.context.scene
268     obj = bpy.context.object
269     
270     gp = None
271     
272     if obj:
273         gp = obj.grease_pencil
274     
275     if not gp:
276         gp = scene.grease_pencil
277
278     if not gp:
279         raise Exception("no active grease pencil")
280     
281     obj_new = calculate(gp)
282     
283     scene.objects.active = obj_new
284     obj_new.selected = True
285     
286     # nasty, recalc normals
287     bpy.ops.object.mode_set(mode='EDIT', toggle=False)
288     bpy.ops.mesh.normals_make_consistent(inside=False)
289     bpy.ops.object.mode_set(mode='OBJECT', toggle=False)