Use URL icon, add Tip: prefix, increase lower margin
[blender-addons-contrib.git] / wetted_mesh.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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 bl_info = {
20     "name": "Add Wetted Mesh",
21     "author": "freejack",
22     "version": (0, 2, 1),
23     "blender": (2, 58, 0),
24     "location": "View3D > Tool Shelf > Wetted Mesh Panel",
25     "description": "Adds separated fluid, dry and wetted mesh for selected pair.",
26     "warning": "",
27     "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28         "Scripts/Mesh/Wetted_Mesh",
29     "tracker_url": "https://developer.blender.org/T27156",
30     "category": "Mesh"}
31
32
33 import bpy
34 import collections
35 import math
36
37 ### Tool Panel ###
38 class VIEW3D_PT_tools_WettedMesh(bpy.types.Panel):
39     """Wetted Mesh Tool Panel"""
40     bl_space_type = 'VIEW_3D'
41     bl_region_type = 'TOOLS'
42     bl_label = 'Wetted Mesh'
43     bl_context = 'objectmode'
44     bl_category = 'Addons'
45     bl_options = {'DEFAULT_CLOSED'}
46
47     def draw(self, context):
48         layout = self.layout
49         col = layout.column(align=True)
50         slcnt = len(context.selected_objects)
51
52         if slcnt != 2:
53             col.label(text = 'Select two mesh objects')
54             col.label(text = 'to generate separated')
55             col.label(text = 'fluid, dry and wetted')
56             col.label(text = 'meshes.')
57         else:
58             (solid, fluid) = getSelectedPair(context)
59             col.label(text = 'solid = '+solid.name)
60             col.label(text = 'fluid = '+fluid.name)
61             col.operator('mesh.primitive_wetted_mesh_add', text='Generate Meshes')
62
63 ### Operator ###
64 class AddWettedMesh(bpy.types.Operator):
65     """Add wetted mesh for selected mesh pair"""
66     bl_idname = "mesh.primitive_wetted_mesh_add"
67     bl_label = "Add Wetted Mesh"
68     bl_options = {'REGISTER', 'UNDO'}
69     statusMessage = ''
70
71     def draw(self, context):
72         layout = self.layout
73         col = layout.column(align=True)
74         col.label(text = self.statusMessage)
75
76     def execute(self, context):
77         # make sure a pair of objects is selected
78         if len(context.selected_objects) != 2:
79             # should not happen if called from tool panel
80             self.report({'WARNING'}, "no mesh pair selected, operation cancelled")
81             return {'CANCELLED'}
82
83         print("add_wetted_mesh begin")
84
85         # super-selected object is solid, other object is fluid
86         (solid, fluid) = getSelectedPair(context)
87         print("   solid = "+solid.name)
88         print("   fluid = "+fluid.name)
89
90         # make a copy of fluid object, convert to mesh if required
91         print("   copy fluid")
92         bpy.ops.object.select_all(action='DESELECT')
93         fluid.select = True
94         context.scene.objects.active = fluid
95         bpy.ops.object.duplicate()
96         bpy.ops.object.convert(target='MESH', keep_original=False)
97         bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
98         fluidCopy = context.object
99
100         # substract solid from fluidCopy
101         print("   bool: fluidCopy DIFFERENCE solid")
102         bpy.ops.object.modifier_add(type='BOOLEAN')
103         bop = fluidCopy.modifiers.items()[0]
104         bop[1].operation = 'DIFFERENCE'
105         bop[1].object = solid
106         bpy.ops.object.modifier_apply(apply_as='DATA', modifier=bop[0])
107         fluidMinusSolid = fluidCopy
108         fluidMinusSolid.name = "fluidMinusSolid"
109
110         # make a second copy of fluid object
111         print("   copy fluid")
112         bpy.ops.object.select_all(action='DESELECT')
113         fluid.select = True
114         context.scene.objects.active = fluid
115         bpy.ops.object.duplicate()
116         bpy.ops.object.convert(target='MESH', keep_original=False)
117         bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
118         fluidCopy = context.object
119
120         # make union from fluidCopy and solid
121         print("   bool: fluidCopy UNION solid")
122         bpy.ops.object.modifier_add(type='BOOLEAN')
123         bop = fluidCopy.modifiers.items()[0]
124         bop[1].operation = 'UNION'
125         bop[1].object = solid
126         bpy.ops.object.modifier_apply(apply_as='DATA', modifier=bop[0])
127         fluidUnionSolid = fluidCopy
128         fluidUnionSolid.name = "fluidUnionSolid"
129
130         # index meshes
131         print("   KDTree index fluidMinusSolid")
132         fluidMinusSolidKDT = KDTree(3, fluidMinusSolid.data.vertices)
133         print("   KDTree index fluidUnionSolid")
134         fluidUnionSolidKDT = KDTree(3, fluidUnionSolid.data.vertices)
135         kdtrees = (fluidMinusSolidKDT, fluidUnionSolidKDT)
136
137         # build mesh face sets
138         faceDict = { }
139         vertDict = { }
140
141         print("   processing fluidMinusSolid faces")
142         cacheDict = { }
143         setFMSfaces = set()
144         numFaces = len(fluidUnionSolid.data.faces)
145         i = 0
146         for f in fluidMinusSolid.data.faces:
147             if i % 500 == 0:
148                 print("      ", i, " / ", numFaces)
149             i += 1
150             fuid = unifiedFaceId(kdtrees, f, fluidMinusSolid.data.vertices, \
151                                  faceDict, vertDict, cacheDict)
152             setFMSfaces.add(fuid)
153
154         print("   processing fluidUnionSolid faces")
155         cacheDict = { }
156         setFUSfaces = set()
157         numFaces = len(fluidUnionSolid.data.faces)
158         i = 0
159         for f in fluidUnionSolid.data.faces:
160             if i % 500 == 0:
161                 print("      ", i, " / ", numFaces)
162             i += 1
163             fuid = unifiedFaceId(kdtrees, f, fluidUnionSolid.data.vertices, \
164                                  faceDict, vertDict, cacheDict)
165             setFUSfaces.add(fuid)
166
167         # remove boolean helpers
168         print("   delete helper objects")
169         bpy.ops.object.select_all(action='DESELECT')
170         fluidUnionSolid.select = True
171         fluidMinusSolid.select = True
172         bpy.ops.object.delete()
173
174         # wetted = FMS - FUS
175         print("   set operation FMS diff FUS")
176         setWetFaces = setFMSfaces.difference(setFUSfaces)
177         print("   build wetted mesh")
178         verts, faces = buildMesh(setWetFaces, faceDict, vertDict)
179         print("   create wetted mesh")
180         wetted = createMesh("Wetted", verts, faces)
181
182         # fluid = FMS x FUS
183         print("   set operation FMS intersect FUS")
184         setFluidFaces = setFMSfaces.intersection(setFUSfaces)
185         print("   build fluid mesh")
186         verts, faces = buildMesh(setFluidFaces, faceDict, vertDict)
187         print("   create fluid mesh")
188         fluid = createMesh("Fluid", verts, faces)
189
190         # solid = FUS - FMS
191         print("   set operation FUS diff FMS")
192         setSolidFaces = setFUSfaces.difference(setFMSfaces)
193         print("   build solid mesh")
194         verts, faces = buildMesh(setSolidFaces, faceDict, vertDict)
195         print("   create solid mesh")
196         solid = createMesh("Solid", verts, faces)
197
198         # parent wetted mesh
199         print("   parent mesh")
200         bpy.ops.object.add(type='EMPTY')
201         wettedMesh = context.object
202         solid.select = True
203         fluid.select = True
204         wetted.select = True
205         wettedMesh.select = True
206         bpy.ops.object.parent_set(type='OBJECT')
207         wettedMesh.name = 'WettedMesh'
208
209         print("add_wetted_mesh done")
210         self.statusMessage = 'created '+wettedMesh.name
211
212         return {'FINISHED'}
213
214
215 ### Registration ###
216 def register():
217     bpy.utils.register_class(VIEW3D_PT_tools_WettedMesh)
218     bpy.utils.register_class(AddWettedMesh)
219
220
221 def unregister():
222     bpy.utils.unregister_class(VIEW3D_PT_tools_WettedMesh)
223     bpy.utils.unregister_class(AddWettedMesh)
224
225 if __name__ == "__main__":
226     register()
227
228
229 #
230 # KD tree (used to create a geometric index of mesh vertices)
231 #
232
233 def distance(a, b):
234     return (a-b).length
235
236 Node = collections.namedtuple("Node", 'point axis label left right')
237
238 class KDTree(object):
239     """A tree for nearest neighbor search in a k-dimensional space.
240
241     For information about the implementation, see
242     http://en.wikipedia.org/wiki/Kd-tree
243
244     Usage:
245     objects is an iterable of (co, index) tuples (so MeshVertex is useable)
246     k is the number of dimensions (=3)
247
248     t = KDTree(k, objects)
249     point, label, distance = t.nearest_neighbor(destination)
250     """
251
252     def __init__(self, k, objects=[]):
253
254         def build_tree(objects, axis=0):
255
256             if not objects:
257                 return None
258
259             objects.sort(key=lambda o: o.co[axis])
260             median_idx = len(objects) // 2
261             median_point = objects[median_idx].co
262             median_label = objects[median_idx].index
263
264             next_axis = (axis + 1) % k
265             return Node(median_point, axis, median_label,
266                         build_tree(objects[:median_idx], next_axis),
267                         build_tree(objects[median_idx + 1:], next_axis))
268
269         self.root = build_tree(list(objects))
270         self.size = len(objects)
271
272
273     def nearest_neighbor(self, destination):
274
275         best = [None, None, float('inf')]
276         # state of search: best point found, its label,
277         # lowest distance
278
279         def recursive_search(here):
280
281             if here is None:
282                 return
283             point, axis, label, left, right = here
284
285             here_sd = distance(point, destination)
286             if here_sd < best[2]:
287                 best[:] = point, label, here_sd
288
289             diff = destination[axis] - point[axis]
290             close, away = (left, right) if diff <= 0 else (right, left)
291
292             recursive_search(close)
293             if math.fabs(diff) < best[2]:
294                 recursive_search(away)
295
296         recursive_search(self.root)
297         return best[0], best[1], best[2]
298
299
300 #
301 # helper functions
302 #
303
304 # get super-selected object and other object from selected pair
305 def getSelectedPair(context):
306     objA = context.object
307     objB = context.selected_objects[0]
308     if objA == objB:
309         objB = context.selected_objects[1]
310     return (objA, objB)
311
312 # get a unified vertex id for given coordinates
313 def unifiedVertexId(kdtrees, location, vertDict):
314     eps = 0.0001
315     offset = 0
316     for t in kdtrees:
317         co, index, d = t.nearest_neighbor(location)
318         if d < eps:
319             uvid = offset + index
320             if uvid not in vertDict:
321                 vertDict[uvid] = co
322             return uvid
323         offset += t.size
324     return -1
325
326 # get a unified face id tuple
327 #    Stores the ordered face id tuple in faceDict
328 #    and the used coordinates for vertex id in vertDict.
329 #    cacheDict caches the unified vertex id (lookup in kdtree is expensive).
330 #    For each mesh (where the face belongs to) a separate cacheDict is expected.
331 def unifiedFaceId(kdtrees, face, vertices, faceDict, vertDict, cacheDict):
332     fids = [ ]
333     for v in face.vertices:
334         uvid = cacheDict.get(v)
335         if uvid == None:
336             uvid = unifiedVertexId(kdtrees, vertices[v].co, vertDict)
337             cacheDict[v] = uvid
338         fids.append(uvid)
339     ofids = tuple(fids)
340     fids.sort()
341     fuid = tuple(fids)
342     if fuid not in faceDict:
343         faceDict[fuid] = ofids
344     return fuid
345
346 # build vertex and face array from unified face sets
347 def buildMesh(unifiedFaceSet, faceDict, vertDict):
348     verts = [ ]
349     nextV = 0
350     myV = { }
351     faces = [ ]
352     for uf in unifiedFaceSet:
353         of = faceDict[uf]
354         myf = [ ]
355         for uV in of:
356             v = myV.get(uV)
357             if v == None:
358                 v = nextV
359                 myV[uV] = nextV
360                 verts.append(vertDict[uV])
361                 nextV += 1
362             myf.append(v)
363         faces.append(myf)
364     return verts, faces
365
366 # create mesh object and link to scene
367 def createMesh(name, verts, faces):
368     me = bpy.data.meshes.new(name+"Mesh")
369     ob = bpy.data.objects.new(name, me)
370     ob.show_name = True
371     bpy.context.scene.objects.link(ob)
372     me.from_pydata(verts, [], faces)
373     me.update(calc_edges=True)
374     return ob