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