5e6461befe79c8529fad0398cc2340988953f477
[blender-addons-contrib.git] / mesh_edge_intersection_tools.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 bl_info = {
22     "name": "Edge tools : tinyCAD VTX",
23     "author": "zeffii",
24     "version": (0, 5, 1),
25     "blender": (2, 5, 6),
26     "api": 34840,
27     "category": "Mesh",
28     "location": "View3D > EditMode > (w) Specials",
29     "warning": "Still under development, bug reports appreciated",
30     "wiki_url": "http://wiki.blender.org/index.php/"\
31         "Extensions:2.5/Py/Scripts/Modeling/Edge_Slice",
32     "tracker_url": "http://projects.blender.org/tracker/"\
33         "?func=detail&aid=25227"
34    }
35
36 '''
37 parts based on Keith (Wahooney) Boshoff, cursor to intersection script and
38 Paul Bourke's Shortest Line Between 2 lines, and thanks to PKHG from BA.org
39 for attempting to explain things to me that i'm not familiar with.
40 TODO: [ ] allow multi selection ( > 2 ) for Slice/Weld intersection mode
41 TODO: [ ] streamline this code !
42
43 1) Edge Extend To Edge ( T )
44 2) Edge Slice Intersecting ( X )
45 3) Edge Project Converging  ( V )
46
47 '''
48
49 import bpy
50 import sys
51 from mathutils import Vector, geometry
52 from mathutils.geometry import intersect_line_line as LineIntersect
53
54 VTX_PRECISION = 1.0e-5 # or 1.0e-6 ..if you need
55
56 #   returns distance between two given points
57 def mDist(A, B): return (A-B).length
58
59
60 #   returns True / False if a point happens to lie on an edge
61 def isPointOnEdge(point, A, B):
62     eps = ((mDist(A, B) - mDist(point,B)) - mDist(A,point))
63     if abs(eps) < VTX_PRECISION: return True
64     else:
65         print('distance is ' + str(eps))
66         return False
67
68
69 #   returns the number of edges that a point lies on.
70 def CountPointOnEdges(point, outer_points):
71     count = 0
72     if(isPointOnEdge(point, outer_points[0][0], outer_points[0][1])): count+=1
73     if(isPointOnEdge(point, outer_points[1][0], outer_points[1][1])): count+=1
74     return count
75
76
77 #   takes Vector List and returns tuple of points in expected order. 
78 def edges_to_points(edges):
79     (vp1, vp2) = (Vector((edges[0][0])), Vector((edges[0][1])))
80     (vp3, vp4) = (Vector((edges[1][0])), Vector((edges[1][1])))
81     return (vp1,vp2,vp3,vp4)
82
83
84 #   takes a list of 4 vectors and returns True or False depending on checks
85 def checkIsMatrixCoplanar(verti):
86     (v1, v2, v3, v4) = edges_to_points(verti)   #unpack
87     shortest_line = LineIntersect(v1, v2, v3, v4)
88     if mDist(shortest_line[1], shortest_line[0]) > VTX_PRECISION: return False
89     else: return True
90
91
92 #   point = the halfway mark on the shortlest line between two lines
93 def checkEdges(Edge, obj):
94     (p1, p2, p3, p4) = edges_to_points(Edge)
95     line = LineIntersect(p1, p2, p3, p4)
96     point = ((line[0] + line[1]) / 2) # or point = line[0]
97     return point
98
99 #   returns (object, number of verts, number of edges) && object mode == True
100 def GetActiveObject():
101     bpy.ops.object.mode_set(mode='EDIT')
102     bpy.ops.mesh.delete(type='EDGE') # removes edges + verts
103     (vert_count, edge_count) = getVertEdgeCount()
104     (vert_num, edge_num) = (len(vert_count),len(edge_count))
105
106     bpy.ops.object.mode_set(mode='OBJECT') # to be sure.
107     o = bpy.context.active_object
108     return (o, vert_num, edge_num)
109
110
111 def AddVertsToObject(vert_count, o, mvX, mvA, mvB, mvC, mvD):
112     o.data.vertices.add(5)
113     pointlist = [mvX, mvA, mvB, mvC, mvD]
114     for vpoint in range(len(pointlist)):
115         o.data.vertices[vert_count+vpoint].co = pointlist[vpoint]
116
117
118 #   Used when the user chooses to slice/Weld, vX is intersection point
119 def makeGeometryWeld(vX,outer_points):
120     (o, vert_count, edge_count) =  GetActiveObject()
121     (vA, vB, vC, vD) =  edges_to_points(outer_points)
122     AddVertsToObject(vert_count, o, vA, vX, vB, vC, vD) # o is the object
123
124     oe = o.data.edges
125     oe.add(4)
126     oe[edge_count].vertices = [vert_count,vert_count+1]
127     oe[edge_count+1].vertices = [vert_count+2,vert_count+1]
128     oe[edge_count+2].vertices = [vert_count+3,vert_count+1]
129     oe[edge_count+3].vertices = [vert_count+4,vert_count+1]
130
131
132 #   Used for extending an edge to a point on another edge.
133 def ExtendEdge(vX, outer_points, count):
134     (o, vert_count, edge_count) =  GetActiveObject()
135     (vA, vB, vC, vD) =  edges_to_points(outer_points)
136     AddVertsToObject(vert_count, o, vX, vA, vB, vC, vD)
137
138     oe = o.data.edges
139     oe.add(4)
140     # Candidate for serious optimization.
141     if isPointOnEdge(vX, vA, vB):
142         oe[edge_count].vertices = [vert_count, vert_count+1]
143         oe[edge_count+1].vertices = [vert_count, vert_count+2]
144         # find which of C and D is farthest away from X
145         if mDist(vD, vX) > mDist(vC, vX):
146             oe[edge_count+2].vertices = [vert_count, vert_count+3]
147             oe[edge_count+3].vertices = [vert_count+3, vert_count+4]
148         if mDist(vC, vX) > mDist(vD, vX):
149             oe[edge_count+2].vertices = [vert_count, vert_count+4]
150             oe[edge_count+3].vertices = [vert_count+3, vert_count+4]
151
152     if isPointOnEdge(vX, vC, vD):
153         oe[edge_count].vertices = [vert_count, vert_count+3]
154         oe[edge_count+1].vertices = [vert_count, vert_count+4]
155         # find which of A and B is farthest away from X 
156         if mDist(vB, vX) > mDist(vA, vX):
157             oe[edge_count+2].vertices = [vert_count, vert_count+1]
158             oe[edge_count+3].vertices = [vert_count+1, vert_count+2]
159         if mDist(vA, vX) > mDist(vB, vX):
160             oe[edge_count+2].vertices = [vert_count, vert_count+2]
161             oe[edge_count+3].vertices = [vert_count+1, vert_count+2]
162
163
164 #   ProjectGeometry is used to extend two edges to their intersection point.
165 def ProjectGeometry(vX, opoint):
166
167     def return_distance_checked(X, A, B):
168         dist1 = mDist(X, A)
169         dist2 = mDist(X, B)
170         point_choice = min(dist1, dist2)
171         if point_choice == dist1: return A, B
172         else: return B, A
173
174     (o, vert_count, edge_count) =  GetActiveObject()
175     vA, vB = return_distance_checked(vX, Vector((opoint[0][0])), Vector((opoint[0][1])))
176     vC, vD = return_distance_checked(vX, Vector((opoint[1][0])), Vector((opoint[1][1])))
177     AddVertsToObject(vert_count, o, vX, vA, vB, vC, vD)
178
179     oe = o.data.edges
180     oe.add(4)
181     oe[edge_count].vertices = [vert_count, vert_count+1]
182     oe[edge_count+1].vertices = [vert_count, vert_count+3]
183     oe[edge_count+2].vertices = [vert_count+1, vert_count+2]
184     oe[edge_count+3].vertices = [vert_count+3, vert_count+4]
185
186
187 def getMeshMatrix(obj):
188     is_editmode = (obj.mode == 'EDIT')
189     if is_editmode:
190        bpy.ops.object.mode_set(mode='OBJECT')
191
192     (edges, meshMatrix) = ([],[])
193     mesh = obj.data
194     verts = mesh.vertices
195     for e in mesh.edges:
196         if e.select:
197             edges.append(e)
198
199     edgenum = 0
200     for edge_to_test in edges:
201         p1 = verts[edge_to_test.vertices[0]].co
202         p2 = verts[edge_to_test.vertices[1]].co
203         meshMatrix.append([Vector(p1),Vector(p2)])
204         edgenum += 1
205
206     return meshMatrix
207
208
209 def getVertEdgeCount():
210     bpy.ops.object.mode_set(mode='OBJECT')
211     vert_count = bpy.context.active_object.data.vertices
212     edge_count = bpy.context.active_object.data.edges
213     return (vert_count, edge_count)
214
215
216 def runCleanUp():
217     bpy.ops.object.mode_set(mode='EDIT')
218     bpy.ops.mesh.select_all(action='TOGGLE')
219     bpy.ops.mesh.select_all(action='TOGGLE')
220     bpy.ops.mesh.remove_doubles(limit=VTX_PRECISION)
221     bpy.ops.mesh.select_all(action='TOGGLE') #unselect all
222
223
224 def initScriptV(context, self):
225     obj = bpy.context.active_object
226     meshMatrix = getMeshMatrix(obj)
227     (vert_count, edge_count) = getVertEdgeCount()
228
229     #need 2 edges to be of any use.
230     if len(meshMatrix) < 2: 
231         print(str(len(meshMatrix)) +" select, make sure (only) 2 are selected")
232         return
233
234     #dont go any further if the verts are not coplanar
235     if checkIsMatrixCoplanar(meshMatrix): print("seems within tolerance, proceed")
236     else: 
237         print("check your geometry, or decrease tolerance value")
238         return
239
240     # if we reach this point, the edges are coplanar
241     # force edit mode
242     bpy.ops.object.mode_set(mode='EDIT')
243     vSel = bpy.context.active_object.data.total_vert_sel
244
245     if checkEdges(meshMatrix, obj) == None: print("lines dont intersect")
246     else:
247         count = CountPointOnEdges(checkEdges(meshMatrix, obj), meshMatrix)
248         if count == 0:
249             ProjectGeometry(checkEdges(meshMatrix, obj), meshMatrix)
250             runCleanUp()
251         else:
252             print("The intersection seems to lie on 1 or 2 edges already")
253
254
255 def initScriptT(context, self):
256     obj = bpy.context.active_object
257     meshMatrix = getMeshMatrix(obj)
258     ## force edit mode
259     bpy.ops.object.mode_set(mode='EDIT')
260     vSel = bpy.context.active_object.data.total_vert_sel
261
262     if len(meshMatrix) != 2:
263         print(str(len(meshMatrix)) +" select 2 edges")
264     else:
265         count = CountPointOnEdges(checkEdges(meshMatrix, obj), meshMatrix)
266         if count == 1:
267             print("Good, Intersection point lies on one of the two edges!")
268             ExtendEdge(checkEdges(meshMatrix, obj), meshMatrix, count)
269             runCleanUp()    #neutral function, for removing potential doubles
270         else:
271             print("Intersection point not on chosen edges")
272
273
274 def initScriptX(context, self):
275     obj = bpy.context.active_object
276     meshMatrix = getMeshMatrix(obj)
277     ## force edit mode
278     bpy.ops.object.mode_set(mode='EDIT')
279
280     if len(meshMatrix) != 2:
281         print(str(len(meshMatrix)) +" select, make sure (only) 2 are selected")
282     else:
283         if checkEdges(meshMatrix, obj) == None:
284             print("lines dont intersect")
285         else: 
286             count = CountPointOnEdges(checkEdges(meshMatrix, obj), meshMatrix)
287             if count == 2:
288                 makeGeometryWeld(checkEdges(meshMatrix, obj), meshMatrix)
289                 runCleanUp()
290
291
292 class EdgeIntersections(bpy.types.Operator):
293     '''Makes a weld/slice/extend to intersecting edges/lines'''
294     bl_idname = 'mesh.intersections'
295     bl_label = 'Edge tools : tinyCAD VTX'
296     # bl_options = {'REGISTER', 'UNDO'}
297
298     mode = bpy.props.IntProperty(name = "Mode",
299                     description = "switch between intersection modes",
300                     default = 2)
301
302     @classmethod
303     def poll(self, context):
304         obj = context.active_object
305         return obj != None and obj.type == 'MESH'
306
307     def execute(self, context):
308         if self.mode == -1:
309             initScriptV(context, self)
310         if self.mode == 0:
311             initScriptT(context, self)
312         if self.mode == 1:
313             initScriptX(context, self)
314         if self.mode == 2:
315             print("something undefined happened, send me a test case!")
316         return {'FINISHED'}
317
318
319 def menu_func(self, context):
320     self.layout.operator(EdgeIntersections.bl_idname, text="Edges V Intersection").mode = -1
321     self.layout.operator(EdgeIntersections.bl_idname, text="Edges T Intersection").mode = 0
322     self.layout.operator(EdgeIntersections.bl_idname, text="Edges X Intersection").mode = 1
323
324 def register():
325     bpy.utils.register_class(EdgeIntersections)
326     bpy.types.VIEW3D_MT_edit_mesh_specials.append(menu_func)
327
328 def unregister():
329     bpy.utils.unregister_class(EdgeIntersections)
330     bpy.types.VIEW3D_MT_edit_mesh_specials.remove(menu_func)
331
332 if __name__ == "__main__":
333     register()