License Headers: use SPDX-FileCopyrightText for mesh_tissue
[blender-addons.git] / mesh_tissue / polyhedra.py
blob15a08dd190b6abda41809388baf6c725def81c68
1 # SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
6 # ------------------------------- version 0.84 ------------------------------- #
7 # #
8 # Creates duplicates of selected mesh to active morphing the shape according #
9 # to target faces. #
10 # #
11 # (c) Alessandro Zomparelli #
12 # (2017) #
13 # #
14 # http://www.co-de-it.com/ #
15 # #
16 # ############################################################################ #
19 import bpy
20 from bpy.types import (
21 Operator,
22 Panel,
23 PropertyGroup,
25 from bpy.props import (
26 BoolProperty,
27 EnumProperty,
28 FloatProperty,
29 IntProperty,
30 StringProperty,
31 PointerProperty
33 from mathutils import Vector, Quaternion, Matrix
34 import numpy as np
35 from math import *
36 import random, time, copy
37 import bmesh
38 from .utils import *
40 def anim_polyhedra_active(self, context):
41 ob = context.object
42 props = ob.tissue_polyhedra
43 if ob.tissue.tissue_type=='POLYHEDRA' and not ob.tissue.bool_lock:
44 props.object.name
45 bpy.ops.object.tissue_update_polyhedra()
47 class tissue_polyhedra_prop(PropertyGroup):
48 object : PointerProperty(
49 type=bpy.types.Object,
50 name="Object",
51 description="Source object",
52 update = anim_polyhedra_active
55 mode : EnumProperty(
56 items=(
57 ('POLYHEDRA', "Polyhedra", "Polyhedral Complex Decomposition, the result are disconnected polyhedra geometries"),
58 ('WIREFRAME', "Wireframe", "Polyhedral Wireframe through edges tickening")
60 default='POLYHEDRA',
61 name="Polyhedra Mode",
62 update = anim_polyhedra_active
65 bool_modifiers : BoolProperty(
66 name="Use Modifiers",
67 description="",
68 default=True,
69 update = anim_polyhedra_active
72 dissolve : EnumProperty(
73 items=(
74 ('NONE', "None", "Keeps original topology"),
75 ('INNER', "Inner", "Dissolve inner loops"),
76 ('OUTER', "Outer", "Dissolve outer loops")
78 default='NONE',
79 name="Dissolve",
80 update = anim_polyhedra_active
83 thickness : FloatProperty(
84 name="Thickness", default=1, soft_min=0, soft_max=10,
85 description="Thickness along the edges",
86 update = anim_polyhedra_active
89 crease : FloatProperty(
90 name="Crease", default=0, min=0, max=1,
91 description="Crease Inner Loops",
92 update = anim_polyhedra_active
95 segments : IntProperty(
96 name="Segments",
97 default=0,
98 min=1,
99 soft_max=20,
100 description="Segments for every edge",
101 update = anim_polyhedra_active
104 proportional_segments : BoolProperty(
105 name="Proportional Segments", default=True,
106 description="The number of segments is proportional to the length of the edges",
107 update = anim_polyhedra_active
110 selective_wireframe : EnumProperty(
111 name="Selective",
112 items=(
113 ('NONE', "None", "Apply wireframe to every cell"),
114 ('THICKNESS', "Thickness", "Wireframe only on bigger cells compared to the thickness"),
115 ('AREA', "Area", "Wireframe based on cells dimensions"),
116 ('WEIGHT', "Weight", "Wireframe based on vertex groups")
118 default='NONE',
119 update = anim_polyhedra_active
122 thickness_threshold_correction : FloatProperty(
123 name="Correction", default=1, min=0, soft_max=2,
124 description="Adjust threshold based on thickness",
125 update = anim_polyhedra_active
128 area_threshold : FloatProperty(
129 name="Threshold", default=0, min=0, soft_max=10,
130 description="Use only faces with an area greater than the threshold",
131 update = anim_polyhedra_active
134 thicken_all : BoolProperty(
135 name="Thicken all",
136 description="Thicken original faces as well",
137 default=True,
138 update = anim_polyhedra_active
141 vertex_group_thickness : StringProperty(
142 name="Thickness weight", default='',
143 description="Vertex Group used for thickness",
144 update = anim_polyhedra_active
146 invert_vertex_group_thickness : BoolProperty(
147 name="Invert", default=False,
148 description="Invert the vertex group influence",
149 update = anim_polyhedra_active
151 vertex_group_thickness_factor : FloatProperty(
152 name="Factor",
153 default=0,
154 min=0,
155 max=1,
156 description="Thickness factor to use for zero vertex group influence",
157 update = anim_polyhedra_active
160 vertex_group_selective : StringProperty(
161 name="Thickness weight", default='',
162 description="Vertex Group used for selective wireframe",
163 update = anim_polyhedra_active
165 invert_vertex_group_selective : BoolProperty(
166 name="Invert", default=False,
167 description="Invert the vertex group influence",
168 update = anim_polyhedra_active
170 vertex_group_selective_threshold : FloatProperty(
171 name="Threshold",
172 default=0.5,
173 min=0,
174 max=1,
175 description="Selective wireframe threshold",
176 update = anim_polyhedra_active
178 bool_smooth : BoolProperty(
179 name="Smooth Shading",
180 default=False,
181 description="Output faces with smooth shading rather than flat shaded",
182 update = anim_polyhedra_active
185 error_message : StringProperty(
186 name="Error Message",
187 default=""
190 class polyhedral_wireframe(Operator):
191 bl_idname = "object.polyhedral_wireframe"
192 bl_label = "Tissue Polyhedral Wireframe"
193 bl_description = "Generate wireframes around the faces.\
194 \nDoesn't works with boundary edges.\
195 \n(Experimental)"
196 bl_options = {'REGISTER', 'UNDO'}
198 thickness : FloatProperty(
199 name="Thickness", default=0.1, min=0.001, soft_max=200,
200 description="Wireframe thickness"
203 crease : FloatProperty(
204 name="Crease", default=0, min=0, max=1,
205 description="Crease Inner Loops"
208 segments : IntProperty(
209 name="Segments", default=1, min=1, soft_max=10,
210 description="Segments for every edge"
213 proportional_segments : BoolProperty(
214 name="Proportional Segments", default=True,
215 description="The number of segments is proportional to the length of the edges"
218 mode : EnumProperty(
219 items=(
220 ('POLYHEDRA', "Polyhedra", "Polyhedral Complex Decomposition, the result are disconnected polyhedra geometries"),
221 ('WIREFRAME', "Wireframe", "Polyhedral Wireframe through edges tickening")
223 default='POLYHEDRA',
224 name="Polyhedra Mode"
227 dissolve : EnumProperty(
228 items=(
229 ('NONE', "None", "Keeps original topology"),
230 ('INNER', "Inner", "Dissolve inner loops"),
231 ('OUTER', "Outer", "Dissolve outer loops")
233 default='NONE',
234 name="Dissolve"
237 selective_wireframe : EnumProperty(
238 items=(
239 ('NONE', "None", "Apply wireframe to every cell"),
240 ('THICKNESS', "Thickness", "Wireframe only on bigger cells compared to the thickness"),
241 ('AREA', "Area", "Wireframe based on cells dimensions"),
242 ('WEIGHT', "Weight", "Wireframe based on vertex groups")
244 default='NONE',
245 name="Selective"
248 thickness_threshold_correction : FloatProperty(
249 name="Correction", default=1, min=0, soft_max=2,
250 description="Adjust threshold based on thickness"
253 area_threshold : FloatProperty(
254 name="Threshold", default=0, min=0, soft_max=10,
255 description="Use only faces with an area greater than the threshold"
258 thicken_all : BoolProperty(
259 name="Thicken all",
260 description="Thicken original faces as well",
261 default=True
264 vertex_group_thickness : StringProperty(
265 name="Thickness weight", default='',
266 description="Vertex Group used for thickness"
269 invert_vertex_group_thickness : BoolProperty(
270 name="Invert", default=False,
271 description="Invert the vertex group influence"
274 vertex_group_thickness_factor : FloatProperty(
275 name="Factor",
276 default=0,
277 min=0,
278 max=1,
279 description="Thickness factor to use for zero vertex group influence"
282 vertex_group_selective : StringProperty(
283 name="Thickness weight", default='',
284 description="Vertex Group used for thickness"
287 invert_vertex_group_selective : BoolProperty(
288 name="Invert", default=False,
289 description="Invert the vertex group influence"
292 vertex_group_selective_threshold : FloatProperty(
293 name="Threshold",
294 default=0.5,
295 min=0,
296 max=1,
297 description="Selective wireframe threshold"
300 bool_smooth : BoolProperty(
301 name="Smooth Shading",
302 default=False,
303 description="Output faces with smooth shading rather than flat shaded"
306 bool_hold : BoolProperty(
307 name="Hold",
308 description="Wait...",
309 default=False
312 def draw(self, context):
313 ob = context.object
314 layout = self.layout
315 col = layout.column(align=True)
316 self.bool_hold = True
317 if self.mode == 'WIREFRAME':
318 col.separator()
319 col.prop(self, "thickness")
320 col.separator()
321 col.prop(self, "segments")
322 return
324 def invoke(self, context, event):
325 return context.window_manager.invoke_props_dialog(self)
327 def execute(self, context):
328 ob0 = context.object
330 self.object_name = "Polyhedral Wireframe"
331 # Check if existing object with same name
332 names = [o.name for o in bpy.data.objects]
333 if self.object_name in names:
334 count_name = 1
335 while True:
336 test_name = self.object_name + '.{:03d}'.format(count_name)
337 if not (test_name in names):
338 self.object_name = test_name
339 break
340 count_name += 1
342 if ob0.type not in ('MESH'):
343 message = "Source object must be a Mesh!"
344 self.report({'ERROR'}, message)
346 if bpy.ops.object.select_all.poll():
347 bpy.ops.object.select_all(action='TOGGLE')
348 bpy.ops.object.mode_set(mode='OBJECT')
350 bool_update = False
351 auto_layer_collection()
352 new_ob = convert_object_to_mesh(ob0,False,False)
353 new_ob.data.name = self.object_name
354 new_ob.name = self.object_name
356 # Store parameters
357 props = new_ob.tissue_polyhedra
358 lock_status = new_ob.tissue.bool_lock
359 new_ob.tissue.bool_lock = True
360 props.mode = self.mode
361 props.thickness = self.thickness
362 props.segments = self.segments
363 props.dissolve = self.dissolve
364 props.proportional_segments = self.proportional_segments
365 props.crease = self.crease
366 props.object = ob0
368 new_ob.tissue.tissue_type = 'POLYHEDRA'
369 try: bpy.ops.object.tissue_update_polyhedra()
370 except RuntimeError as e:
371 bpy.data.objects.remove(new_ob)
372 remove_temp_objects()
373 self.report({'ERROR'}, str(e))
374 return {'CANCELLED'}
375 if not bool_update:
376 self.object_name = new_ob.name
377 new_ob.location = ob0.location
378 new_ob.matrix_world = ob0.matrix_world
380 # Assign collection of the base object
381 old_coll = new_ob.users_collection
382 if old_coll != ob0.users_collection:
383 for c in old_coll:
384 c.objects.unlink(new_ob)
385 for c in ob0.users_collection:
386 c.objects.link(new_ob)
387 context.view_layer.objects.active = new_ob
389 # unlock
390 new_ob.tissue.bool_lock = lock_status
392 return {'FINISHED'}
394 class tissue_update_polyhedra(Operator):
395 bl_idname = "object.tissue_update_polyhedra"
396 bl_label = "Tissue Update Polyhedral Wireframe"
397 bl_description = "Update a previously generated polyhedral object"
398 bl_options = {'REGISTER', 'UNDO'}
400 def execute(self, context):
401 ob = context.object
402 tissue_time(None,'Tissue: Polyhedral Wireframe of "{}"...'.format(ob.name), levels=0)
403 start_time = time.time()
404 begin_time = time.time()
405 props = ob.tissue_polyhedra
406 thickness = props.thickness
408 merge_dist = thickness*0.0001
410 subs = props.segments
411 if props.mode == 'POLYHEDRA': subs = 1
414 # Source mesh
415 ob0 = props.object
416 if props.bool_modifiers:
417 me = simple_to_mesh(ob0)
418 else:
419 me = ob0.data.copy()
421 bm = bmesh.new()
422 bm.from_mesh(me)
424 pre_processing(bm)
425 polyhedral_subdivide_edges(bm, subs, props.proportional_segments)
426 tissue_time(start_time,'Subdivide edges',levels=1)
427 start_time = time.time()
429 thickness = np.ones(len(bm.verts))*props.thickness
430 if(props.vertex_group_thickness in ob.vertex_groups.keys()):
431 dvert_lay = bm.verts.layers.deform.active
432 group_index_thickness = ob.vertex_groups[props.vertex_group_thickness].index
433 thickness_weight = bmesh_get_weight_numpy(group_index_thickness, dvert_lay, bm.verts)
434 if 'invert_vertex_group_thickness' in props.keys():
435 if props['invert_vertex_group_thickness']:
436 thickness_weight = 1-thickness_weight
437 fact = 0
438 if 'vertex_group_thickness_factor' in props.keys():
439 fact = props['vertex_group_thickness_factor']
440 if fact > 0:
441 thickness_weight = thickness_weight*(1-fact) + fact
442 thickness *= thickness_weight
443 thickness_dict = dict(zip([tuple(v.co) for v in bm.verts],thickness))
445 bm1 = get_double_faces_bmesh(bm)
446 polyhedra = get_decomposed_polyhedra(bm)
447 if(type(polyhedra) is str):
448 bm.free()
449 bm1.free()
450 self.report({'ERROR'}, polyhedra)
451 return {'CANCELLED'}
453 selective_dict = None
454 accurate = False
455 if props.selective_wireframe == 'THICKNESS':
456 filter_faces = True
457 accurate = True
458 area_threshold = (thickness*props.thickness_threshold_correction)**2
459 elif props.selective_wireframe == 'AREA':
460 filter_faces = True
461 area_threshold = props.area_threshold
462 elif props.selective_wireframe == 'WEIGHT':
463 filter_faces = True
464 if(props.vertex_group_selective in ob.vertex_groups.keys()):
465 dvert_lay = bm.verts.layers.deform.active
466 group_index_selective = ob.vertex_groups[props.vertex_group_selective].index
467 thresh = props.vertex_group_selective_threshold
468 selective_weight = bmesh_get_weight_numpy(group_index_selective, dvert_lay, bm.verts)
469 selective_weight = selective_weight >= thresh
470 invert = False
471 if 'invert_vertex_group_selective' in props.keys():
472 if props['invert_vertex_group_selective']:
473 invert = True
474 if invert:
475 selective_weight = selective_weight <= thresh
476 else:
477 selective_weight = selective_weight >= thresh
478 selective_dict = dict(zip([tuple(v.co) for v in bm.verts],selective_weight))
479 else:
480 filter_faces = False
481 else:
482 filter_faces = False
484 bm.free()
486 end_time = time.time()
487 tissue_time(start_time,'Found {} polyhedra'.format(len(polyhedra)),levels=1)
488 start_time = time.time()
490 bm1.faces.ensure_lookup_table()
491 bm1.faces.index_update()
493 #unique_verts_dict = dict(zip([tuple(v.co) for v in bm1.verts],bm1.verts))
494 bm1, all_faces_dict, polyhedra_faces_id, polyhedra_faces_id_neg = combine_polyhedra_faces(bm1, polyhedra)
496 if props.mode == 'POLYHEDRA':
497 poly_me = me.copy()
498 bm1.to_mesh(poly_me)
499 poly_me.update()
500 old_me = ob.data
501 ob.data = poly_me
502 mesh_name = old_me.name
503 bpy.data.meshes.remove(old_me)
504 bpy.data.meshes.remove(me)
505 ob.data.name = mesh_name
506 end_time = time.time()
507 print('Tissue: Polyhedral wireframe in {:.4f} sec'.format(end_time-start_time))
508 return {'FINISHED'}
510 delete_faces = set({})
511 wireframe_faces = []
512 not_wireframe_faces = []
513 #flat_faces = []
514 count = 0
515 outer_faces = get_outer_faces(bm1)
516 for faces_id in polyhedra_faces_id:
517 delete_faces_poly = []
518 wireframe_faces_poly = []
519 for id in faces_id:
520 if id in delete_faces: continue
521 delete = False
522 cen = None
523 f = None
524 if filter_faces:
525 f = all_faces_dict[id]
526 if selective_dict:
527 for v in f.verts:
528 if selective_dict[tuple(v.co)]:
529 delete = True
530 break
531 elif accurate:
532 cen = f.calc_center_median()
533 for e in f.edges:
534 v0 = e.verts[0]
535 v1 = e.verts[1]
536 mid = (v0.co + v1.co)/2
537 vec1 = v0.co - v1.co
538 vec2 = mid - cen
539 ang = Vector.angle(vec1,vec2)
540 length = vec2.length
541 length = sin(ang)*length
542 thick0 = thickness_dict[tuple(v0.co)]
543 thick1 = thickness_dict[tuple(v1.co)]
544 thick = (thick0 + thick1)/4
545 if length < thick*props.thickness_threshold_correction:
546 delete = True
547 break
548 else:
549 delete = f.calc_area() < area_threshold
550 if delete:
551 if props.thicken_all:
552 delete_faces_poly.append(id)
553 else:
554 wireframe_faces_poly.append(id)
555 if len(wireframe_faces_poly) <= 2:
556 delete_faces.update(set([id for id in faces_id]))
557 not_wireframe_faces += [polyhedra_faces_id_neg[id] for id in faces_id]
558 else:
559 wireframe_faces += wireframe_faces_poly
560 #flat_faces += delete_faces_poly
561 wireframe_faces_id = [i for i in wireframe_faces if i not in not_wireframe_faces]
562 wireframe_faces = [all_faces_dict[i] for i in wireframe_faces_id]
563 #flat_faces = [all_faces_dict[i] for i in flat_faces]
564 delete_faces = [all_faces_dict[i] for i in delete_faces if all_faces_dict[i] not in outer_faces]
566 tissue_time(start_time,'Merge and delete',levels=1)
567 start_time = time.time()
569 ############# FRAME #############
570 new_faces, outer_wireframe_faces = create_frame_faces(
571 bm1,
572 wireframe_faces,
573 wireframe_faces_id,
574 polyhedra_faces_id_neg,
575 thickness_dict,
576 outer_faces
578 faces_to_delete = wireframe_faces+delete_faces
579 outer_wireframe_faces += [f for f in outer_faces if not f in faces_to_delete]
580 bmesh.ops.delete(bm1, geom=faces_to_delete, context='FACES')
582 bm1.verts.ensure_lookup_table()
583 bm1.edges.ensure_lookup_table()
584 bm1.faces.ensure_lookup_table()
585 bm1.verts.index_update()
587 wireframe_indexes = [f.index for f in new_faces]
588 outer_indexes = [f.index for f in outer_wireframe_faces]
589 edges_to_crease = [f.edges[2].index for f in new_faces]
590 layer_is_wireframe = bm1.faces.layers.int.new('tissue_is_wireframe')
591 for id in wireframe_indexes:
592 bm1.faces[id][layer_is_wireframe] = 1
593 layer_is_outer = bm1.faces.layers.int.new('tissue_is_outer')
594 for id in outer_indexes:
595 bm1.faces[id][layer_is_outer] = 1
596 if props.crease > 0 and props.dissolve != 'INNER':
597 crease_layer = bm1.edges.layers.float.new('crease_edge')
598 bm1.edges.index_update()
599 crease_edges = []
600 for edge_index in edges_to_crease:
601 bm1.edges[edge_index][crease_layer] = props.crease
603 tissue_time(start_time,'Generate frames',levels=1)
604 start_time = time.time()
606 ### Displace vertices ###
607 corners = [[] for i in range(len(bm1.verts))]
608 normals = [0]*len(bm1.verts)
609 vertices = [0]*len(bm1.verts)
610 # Define vectors direction
611 for f in bm1.faces:
612 v0 = f.verts[0]
613 v1 = f.verts[1]
614 id = v0.index
615 corners[id].append((v1.co - v0.co).normalized())
616 v0.normal_update()
617 normals[id] = v0.normal.copy()
618 vertices[id] = v0
619 # Displace vertices
620 for i, vecs in enumerate(corners):
621 if len(vecs) > 0:
622 v = vertices[i]
623 nor = normals[i]
624 ang = 0
625 for vec in vecs:
626 if nor == Vector((0,0,0)): continue
627 ang += nor.angle(vec)
628 ang /= len(vecs)
629 div = sin(ang)
630 if div == 0: div = 1
631 v.co += nor*thickness_dict[tuple(v.co)]/div
633 tissue_time(start_time,'Corners displace',levels=1)
634 start_time = time.time()
636 if props.dissolve != 'NONE':
637 if props.dissolve == 'INNER': dissolve_id = 2
638 if props.dissolve == 'OUTER': dissolve_id = 0
639 bm1.edges.index_update()
640 dissolve_edges = []
641 for f in bm1.faces:
642 e = f.edges[dissolve_id]
643 if e not in dissolve_edges:
644 dissolve_edges.append(e)
645 bmesh.ops.dissolve_edges(bm1, edges=dissolve_edges, use_verts=True, use_face_split=False)
647 for v in bm1.verts: v.select_set(False)
648 for f in bm1.faces: f.select_set(False)
650 dissolve_verts = [v for v in bm1.verts if len(v.link_edges) < 3]
651 bmesh.ops.dissolve_verts(bm1, verts=dissolve_verts, use_face_split=False, use_boundary_tear=False)
653 # clean meshes
654 bm1.to_mesh(me)
655 if props.bool_smooth: me.shade_smooth()
656 me.update()
657 old_me = ob.data
658 ob.data = me
659 mesh_name = old_me.name
660 bpy.data.meshes.remove(old_me)
661 ob.data.name = mesh_name
662 bm1.free()
664 bpy.ops.object.mode_set(mode='EDIT')
665 bpy.ops.mesh.select_all(action='SELECT')
666 bpy.ops.uv.reset()
667 bpy.ops.object.mode_set(mode='OBJECT')
669 tissue_time(start_time,'Clean mesh',levels=1)
670 start_time = time.time()
672 tissue_time(begin_time,'Polyhedral Wireframe',levels=0)
673 return {'FINISHED'}
675 def pre_processing(bm):
676 delete = [e for e in bm.edges if len(e.link_faces) < 2]
677 while len(delete) > 0:
678 bmesh.ops.delete(bm, geom=delete, context='EDGES')
679 bm.faces.ensure_lookup_table()
680 bm.edges.ensure_lookup_table()
681 bm.verts.ensure_lookup_table()
682 delete = [e for e in bm.edges if len(e.link_faces) < 2]
683 return bm
685 def get_outer_faces(bm):
686 bm_copy = bm.copy()
687 bmesh.ops.recalc_face_normals(bm_copy, faces=bm_copy.faces)
688 outer = []
689 for f1, f2 in zip(bm.faces, bm_copy.faces):
690 f1.normal_update()
691 if f1.normal == f2.normal:
692 outer.append(f1)
693 return outer
695 def create_frame_faces(
697 wireframe_faces,
698 wireframe_faces_id,
699 polyhedra_faces_id_neg,
700 thickness_dict,
701 outer_faces
703 new_faces = []
704 for f in wireframe_faces:
705 f.normal_update()
706 all_loops = [[loop for loop in f.loops] for f in wireframe_faces]
707 is_outer = [f in outer_faces for f in wireframe_faces]
708 outer_wireframe_faces = []
709 frames_verts_dict = {}
710 for loops_index, loops in enumerate(all_loops):
711 n_loop = len(loops)
712 frame_id = wireframe_faces_id[loops_index]
713 single_face_id = min(frame_id,polyhedra_faces_id_neg[frame_id])
714 verts_inner = []
715 loops_keys = [tuple(loop.vert.co) + tuple((single_face_id,)) for loop in loops]
716 if loops_keys[0] in frames_verts_dict:
717 verts_inner = [frames_verts_dict[key] for key in loops_keys]
718 else:
719 tangents = []
720 nor = wireframe_faces[loops_index].normal
721 for loop in loops:
722 tan = loop.calc_tangent() #nor.cross(loop.calc_tangent().cross(nor)).normalized()
723 thickness = thickness_dict[tuple(loop.vert.co)]
724 tangents.append(tan/sin(loop.calc_angle()/2)*thickness)
725 for i in range(n_loop):
726 loop = loops[i]
727 new_co = loop.vert.co + tangents[i]
728 new_vert = bm.verts.new(new_co)
729 frames_verts_dict[loops_keys[i]] = new_vert
730 verts_inner.append(new_vert)
731 # add faces
732 loops += [loops[0]]
733 verts_inner += [verts_inner[0]]
734 for i in range(n_loop):
735 v0 = loops[i].vert
736 v1 = loops[i+1].vert
737 v2 = verts_inner[i+1]
738 v3 = verts_inner[i]
739 face_verts = [v0,v1,v2,v3]
740 new_face = bm.faces.new(face_verts)
741 new_face.select = True
742 new_faces.append(new_face)
743 if is_outer[loops_index]:
744 outer_wireframe_faces.append(new_face)
745 new_face.normal_update()
746 return new_faces, outer_wireframe_faces
748 def polyhedral_subdivide_edges(bm, subs, proportional_segments):
749 if subs > 1:
750 if proportional_segments:
751 wire_length = [e.calc_length() for e in bm.edges]
752 all_edges = list(bm.edges)
753 max_segment = max(wire_length)/subs+0.00001 # prevent out_of_bounds
754 split_edges = [[] for i in range(subs)]
755 for e, l in zip(all_edges, wire_length):
756 split_edges[int(l//max_segment)].append(e)
757 for i in range(1,subs):
758 bmesh.ops.bisect_edges(bm, edges=split_edges[i], cuts=i)
759 else:
760 bmesh.ops.bisect_edges(bm, edges=bm.edges, cuts=subs-1)
762 def get_double_faces_bmesh(bm):
763 double_faces = []
764 for f in bm.faces:
765 verts0 = [v.co for v in f.verts]
766 verts1 = verts0.copy()
767 verts1.reverse()
768 double_faces.append(verts0)
769 double_faces.append(verts1)
770 bm1 = bmesh.new()
771 for verts_co in double_faces:
772 bm1.faces.new([bm1.verts.new(v) for v in verts_co])
773 bm1.verts.ensure_lookup_table()
774 bm1.edges.ensure_lookup_table()
775 bm1.faces.ensure_lookup_table()
776 return bm1
778 def get_decomposed_polyhedra(bm):
779 polyhedra_from_facekey = {}
780 count = 0
781 to_merge = []
782 for e in bm.edges:
783 done = []
784 # ERROR: Naked edges
785 link_faces = e.link_faces
786 n_radial_faces = len(link_faces)
787 if n_radial_faces < 2:
788 return "Naked edges are not allowed"
789 vert0 = e.verts[0]
790 vert1 = e.verts[1]
791 edge_vec = vert1.co - vert0.co
793 for id1 in range(n_radial_faces-1):
794 f1 = link_faces[id1]
795 facekey1 = f1.index+1
796 verts1 = [v.index for v in f1.verts]
797 v0_index = verts1.index(vert0.index)
798 v1_index = verts1.index(vert1.index)
800 ref_loop_dir = v0_index == (v1_index+1)%len(verts1)
801 edge_vec1 = edge_vec if ref_loop_dir else -edge_vec
802 tan1 = f1.normal.cross(edge_vec1)
804 # faces to compare with
805 faceskeys2, normals2 = get_second_faces(
806 link_faces,
807 vert0.index,
808 vert1.index,
809 ref_loop_dir,
813 tangents2 = [nor.cross(-edge_vec1) for nor in normals2]
815 # positive side
816 facekey2_pos = get_closest_face(
817 faceskeys2,
818 tangents2,
819 tan1,
820 edge_vec1,
821 True
823 polyhedra_from_facekey, count, to_merge = store_neighbor_faces(
824 facekey1,
825 facekey2_pos,
826 polyhedra_from_facekey,
827 count,
828 to_merge
830 # negative side
831 facekey2_neg = get_closest_face(
832 faceskeys2,
833 tangents2,
834 tan1,
835 edge_vec1,
836 False
838 polyhedra_from_facekey, count, to_merge = store_neighbor_faces(
839 -facekey1,
840 facekey2_neg,
841 polyhedra_from_facekey,
842 count,
843 to_merge
846 polyhedra = [ [] for i in range(count)]
847 unique_index = get_unique_polyhedra_index(count, to_merge)
848 for key, val in polyhedra_from_facekey.items():
849 polyhedra[unique_index[val]].append(key)
850 polyhedra = list(set(tuple(i) for i in polyhedra if i))
851 polyhedra = remove_double_faces_from_polyhedra(polyhedra)
852 return polyhedra
854 def remove_double_faces_from_polyhedra(polyhedra):
855 new_polyhedra = []
856 for polyhedron in polyhedra:
857 new_polyhedron = [key for key in polyhedron if not -key in polyhedron]
858 new_polyhedra.append(new_polyhedron)
859 return new_polyhedra
861 def get_unique_polyhedra_index(count, to_merge):
862 out = list(range(count))
863 keep_going = True
864 while keep_going:
865 keep_going = False
866 for pair in to_merge:
867 if out[pair[1]] != out[pair[0]]:
868 out[pair[0]] = out[pair[1]] = min(out[pair[0]], out[pair[1]])
869 keep_going = True
870 return out
872 def get_closest_face(faces, tangents, ref_vector, axis, is_positive):
873 facekey = None
874 min_angle = 1000000
875 for fk, tangent in zip(faces, tangents):
876 rot_axis = -axis if is_positive else axis
877 angle = round_angle_with_axis(ref_vector, tangent, rot_axis)
878 if angle < min_angle:
879 facekey = fk
880 min_angle = angle
881 return facekey if is_positive else -facekey
883 def get_second_faces(face_list, edge_v0, edge_v1, reference_loop_dir, self):
884 nFaces = len(face_list)-1
885 facekeys = [None]*nFaces
886 normals = [None]*nFaces
887 count = 0
888 for face in face_list:
889 if(face == self): continue
890 verts = [v.index for v in face.verts]
891 v0_index = verts.index(edge_v0)
892 v1_index = verts.index(edge_v1)
893 loop_dir = v0_index == (v1_index+1)%len(verts)
894 if reference_loop_dir != loop_dir:
895 facekeys[count] = face.index+1
896 normals[count] = face.normal
897 else:
898 facekeys[count] = -(face.index+1)
899 normals[count] = -face.normal
900 count+=1
901 return facekeys, normals
903 def store_neighbor_faces(
904 key1,
905 key2,
906 polyhedra,
907 polyhedra_count,
908 to_merge
910 poly1 = polyhedra.get(key1)
911 poly2 = polyhedra.get(key2)
912 if poly1 and poly2:
913 if poly1 != poly2:
914 to_merge.append((poly1, poly2))
915 elif poly1:
916 polyhedra[key2] = poly1
917 elif poly2:
918 polyhedra[key1] = poly2
919 else:
920 polyhedra[key1] = polyhedra[key2] = polyhedra_count
921 polyhedra_count += 1
922 return polyhedra, polyhedra_count, to_merge
924 def add_polyhedron(bm,source_faces):
925 faces_verts_key = [[tuple(v.co) for v in f.verts] for f in source_faces]
926 polyhedron_verts_key = [key for face_key in faces_verts_key for key in face_key]
927 polyhedron_verts = [bm.verts.new(co) for co in polyhedron_verts_key]
928 polyhedron_verts_dict = dict(zip(polyhedron_verts_key, polyhedron_verts))
929 new_faces = [None]*len(faces_verts_key)
930 count = 0
931 for verts_keys in faces_verts_key:
932 new_faces[count] = bm.faces.new([polyhedron_verts_dict.get(key) for key in verts_keys])
933 count+=1
935 bm.faces.ensure_lookup_table()
936 bm.faces.index_update()
937 return new_faces
939 def combine_polyhedra_faces(bm,polyhedra):
940 new_bm = bmesh.new()
941 polyhedra_faces_id = [None]*len(polyhedra)
942 all_faces_dict = {}
943 #polyhedra_faces_pos = {}
944 polyhedra_faces_id_neg = {}
945 vertices_key = [tuple(v.co) for v in bm.verts]
946 count = 0
947 for p in polyhedra:
948 faces_id = [(f-1)*2 if f > 0 else (-f-1)*2+1 for f in p]
949 faces_id_neg = [(-f-1)*2 if f < 0 else (f-1)*2+1 for f in p]
950 new_faces = add_polyhedron(new_bm,[bm.faces[f_id] for f_id in faces_id])
951 faces_dict = {}
952 for i in range(len(new_faces)):
953 face = new_faces[i]
954 id = faces_id[i]
955 id_neg = faces_id_neg[i]
956 polyhedra_faces_id_neg[id] = id_neg
957 all_faces_dict[id] = face
958 polyhedra_faces_id[count] = faces_id
959 count+=1
960 return new_bm, all_faces_dict, polyhedra_faces_id, polyhedra_faces_id_neg
962 class TISSUE_PT_polyhedra_object(Panel):
963 bl_space_type = 'PROPERTIES'
964 bl_region_type = 'WINDOW'
965 bl_context = "data"
966 bl_label = "Tissue Polyhedra"
967 bl_options = {'DEFAULT_CLOSED'}
969 @classmethod
970 def poll(cls, context):
971 try:
972 ob = context.object
973 return ob.type == 'MESH' and ob.tissue.tissue_type == 'POLYHEDRA'
974 except: return False
976 def draw(self, context):
977 ob = context.object
978 props = ob.tissue_polyhedra
979 tissue_props = ob.tissue
981 bool_polyhedra = tissue_props.tissue_type == 'POLYHEDRA'
982 layout = self.layout
983 if not bool_polyhedra:
984 layout.label(text="The selected object is not a Polyhedral object",
985 icon='INFO')
986 else:
987 if props.error_message != "":
988 layout.label(text=props.error_message,
989 icon='ERROR')
990 col = layout.column(align=True)
991 row = col.row(align=True)
993 #set_tessellate_handler(self,context)
994 row.operator("object.tissue_update_tessellate_deps", icon='FILE_REFRESH', text='Refresh') ####
995 lock_icon = 'LOCKED' if tissue_props.bool_lock else 'UNLOCKED'
996 #lock_icon = 'PINNED' if props.bool_lock else 'UNPINNED'
997 deps_icon = 'LINKED' if tissue_props.bool_dependencies else 'UNLINKED'
998 row.prop(tissue_props, "bool_dependencies", text="", icon=deps_icon)
999 row.prop(tissue_props, "bool_lock", text="", icon=lock_icon)
1000 col2 = row.column(align=True)
1001 col2.prop(tissue_props, "bool_run", text="",icon='TIME')
1002 col2.enabled = not tissue_props.bool_lock
1003 col2 = row.column(align=True)
1004 col2.operator("mesh.tissue_remove", text="", icon='X')
1005 #layout.use_property_split = True
1006 #layout.use_property_decorate = False # No animation.
1007 col = layout.column(align=True)
1008 col.label(text='Polyhedral Mode:')
1009 col.prop(props, 'mode', text='')
1010 col.separator()
1011 col.label(text='Source object:')
1012 row = col.row(align=True)
1013 row.prop_search(props, "object", context.scene, "objects", text='')
1014 col2 = row.column(align=True)
1015 col2.prop(props, "bool_modifiers", text='Use Modifiers',icon='MODIFIER')
1016 if props.mode == 'WIREFRAME':
1017 col.separator()
1018 col.prop(props, 'thickness')
1019 row = col.row(align=True)
1020 ob0 = props.object
1021 row.prop_search(props, 'vertex_group_thickness',
1022 ob0, "vertex_groups", text='')
1023 col2 = row.column(align=True)
1024 row2 = col2.row(align=True)
1025 row2.prop(props, "invert_vertex_group_thickness", text="",
1026 toggle=True, icon='ARROW_LEFTRIGHT')
1027 row2.prop(props, "vertex_group_thickness_factor")
1028 row2.enabled = props.vertex_group_thickness in ob0.vertex_groups.keys()
1029 col.prop(props, 'bool_smooth')
1030 col.separator()
1031 col.label(text='Selective Wireframe:')
1032 col.prop(props, 'selective_wireframe', text='Mode')
1033 col.separator()
1034 if props.selective_wireframe == 'THICKNESS':
1035 col.prop(props, 'thickness_threshold_correction')
1036 elif props.selective_wireframe == 'AREA':
1037 col.prop(props, 'area_threshold')
1038 elif props.selective_wireframe == 'WEIGHT':
1039 row = col.row(align=True)
1040 row.prop_search(props, 'vertex_group_selective',
1041 ob0, "vertex_groups", text='')
1042 col2 = row.column(align=True)
1043 row2 = col2.row(align=True)
1044 row2.prop(props, "invert_vertex_group_selective", text="",
1045 toggle=True, icon='ARROW_LEFTRIGHT')
1046 row2.prop(props, "vertex_group_selective_threshold")
1047 row2.enabled = props.vertex_group_selective in ob0.vertex_groups.keys()
1048 #if props.selective_wireframe != 'NONE':
1049 # col.prop(props, 'thicken_all')
1050 col.separator()
1051 col.label(text='Subdivide edges:')
1052 row = col.row()
1053 row.prop(props, 'segments')
1054 row.prop(props, 'proportional_segments', text='Proportional')
1055 col.separator()
1056 col.label(text='Loops:')
1057 col.prop(props, 'dissolve')
1058 col.separator()
1059 col.prop(props, 'crease')