adds lookup tests to support older versions of blender too
[blender-addons-contrib.git] / mesh_tinyCAD / XALL.py
1 '''
2 BEGIN GPL LICENSE BLOCK
3
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License
6 as published by the Free Software Foundation; either version 2
7 of the License, or (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software Foundation,
16 Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17
18 END GPL LICENCE BLOCK
19 '''
20
21 import bpy
22 import bmesh
23 from mathutils import Vector
24 from mathutils.geometry import intersect_line_line as LineIntersect
25
26 import itertools
27 from collections import defaultdict
28 from mesh_tinyCAD import cad_module as cm
29
30
31 ''' helpers '''
32
33
34 def order_points(edge, point_list):
35     ''' order these edges from distance to v1, then
36     sandwich the sorted list with v1, v2 '''
37     v1, v2 = edge
38     dist = lambda co: (v1-co).length
39     point_list = sorted(point_list, key=dist)
40     return [v1] + point_list + [v2]
41
42
43 def remove_permutations_that_share_a_vertex(bm, permutations):
44     ''' Get useful Permutations '''
45     final_permutations = []
46     for edges in permutations:
47         raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
48         if cm.duplicates(raw_vert_indices):
49             continue
50
51         # reaches this point if they do not share.
52         final_permutations.append(edges)
53
54     return final_permutations
55
56
57 def get_valid_permutations(bm, edge_indices):
58     raw_permutations = itertools.permutations(edge_indices, 2)
59     permutations = [r for r in raw_permutations if r[0] < r[1]]
60     return remove_permutations_that_share_a_vertex(bm, permutations)
61
62
63 def can_skip(closest_points, vert_vectors):
64     '''this checks if the intersection lies on both edges, returns True
65     when criteria are not met, and thus this point can be skipped'''
66     if not closest_points:
67         return True
68     if not isinstance(closest_points[0].x, float):
69         return True
70     if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2:
71         return True
72
73     # if this distance is larger than than VTX_PRECISION, we can skip it.
74     cpa, cpb = closest_points
75     return (cpa-cpb).length > cm.CAD_prefs.VTX_PRECISION
76
77
78 def get_intersection_dictionary(bm, edge_indices):
79
80     if hasattr(bm.verts, "ensure_lookup_table"):
81         bm.verts.ensure_lookup_table()
82         bm.edges.ensure_lookup_table()
83
84     permutations = get_valid_permutations(bm, edge_indices)
85
86     k = defaultdict(list)
87     d = defaultdict(list)
88
89     for edges in permutations:
90         raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
91         vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices)
92
93         points = LineIntersect(*vert_vectors)
94
95         # some can be skipped.    (NaN, None, not on both edges)
96         if can_skip(points, vert_vectors):
97             continue
98
99         # reaches this point only when an intersection happens on both edges.
100         [k[edge].append(points[0]) for edge in edges]
101
102     # k will contain a dict of edge indices and points found on those edges.
103     for edge_idx, unordered_points in k.items():
104         tv1, tv2 = bm.edges[edge_idx].verts
105         v1 = bm.verts[tv1.index].co
106         v2 = bm.verts[tv2.index].co
107         ordered_points = order_points((v1, v2), unordered_points)
108         d[edge_idx].extend(ordered_points)
109
110     return d
111
112
113 def update_mesh(obj, d):
114     ''' Make new geometry (delete old first) '''
115
116     bpy.ops.mesh.delete(type='EDGE')
117     bpy.ops.object.editmode_toggle()
118
119     oe = obj.data.edges
120     ov = obj.data.vertices
121     vert_count = len(ov)
122     edge_count = len(oe)
123
124     for old_edge, point_list in d.items():
125         num_points = len(point_list)
126         num_edges_to_add = num_points-1
127
128         for i in range(num_edges_to_add):
129             oe.add(1)
130             ov.add(2)
131
132             # if hasattr(bm.verts, "ensure_lookup_table"):
133             #     bm.verts.ensure_lookup_table()
134             #     bm.edges.ensure_lookup_table()
135
136             ov[vert_count].co = point_list[i]
137             ov[vert_count+1].co = point_list[i+1]
138
139             oe[edge_count].vertices = [vert_count, vert_count+1]
140             vert_count = len(ov)
141             edge_count = len(oe)
142
143     # set edit mode
144     bpy.ops.object.editmode_toggle()
145     bpy.ops.mesh.remove_doubles(
146         threshold=cm.CAD_prefs.VTX_DOUBLES_THRSHLD,
147         use_unselected=False)
148
149
150 def unselect_nonintersecting(bm, d_edges, edge_indices):
151     if len(edge_indices) > len(d_edges):
152         reserved_edges = set(edge_indices) - set(d_edges)
153         for edge in reserved_edges:
154             bm.edges[edge].select = False
155         print("unselected {}, non intersecting edges".format(reserved_edges))
156
157
158 class IntersectAllEdges(bpy.types.Operator):
159
160     bl_idname = 'mesh.intersectall'
161     bl_label = 'XALL intersect all edges'
162     # bl_options = {'REGISTER', 'UNDO'}
163
164     @classmethod
165     def poll(self, context):
166         obj = context.active_object
167         return obj is not None and obj.type == 'MESH' and obj.mode == 'EDIT'
168
169     def execute(self, context):
170         # must force edge selection mode here
171         bpy.context.tool_settings.mesh_select_mode = (False, True, False)
172
173         obj = context.active_object
174         if obj.mode == "EDIT":
175             bm = bmesh.from_edit_mesh(obj.data)
176
177             selected_edges = [edge for edge in bm.edges if edge.select]
178             edge_indices = [i.index for i in selected_edges]
179
180             d = get_intersection_dictionary(bm, edge_indices)
181
182             unselect_nonintersecting(bm, d.keys(), edge_indices)
183             update_mesh(obj, d)
184         else:
185             print('must be in edit mode')
186
187         return {'FINISHED'}