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# <pep8 compliant>
20#
21# ----------------------------------------------------------
22# Author: Zeffii
23# Modified by: Alan Odom (Clockmender) & Rune Morling (ermo)
24# ----------------------------------------------------------
25#
26import bpy
27import bmesh
28from mathutils.geometry import intersect_line_line as LineIntersect
29import itertools
30from collections import defaultdict
31from . import pdt_cad_module as cm
32from .pdt_functions import oops
33from .pdt_msg_strings import (
34    PDT_ERR_EDOB_MODE
35)
36
37
38def order_points(edge, point_list):
39    """Order these edges from distance to v1, then sandwich the sorted list with v1, v2."""
40    v1, v2 = edge
41
42    def dist(coord):
43        """Measure distance between two coordinates."""
44
45        return (v1 - coord).length
46
47    point_list = sorted(point_list, key=dist)
48    return [v1] + point_list + [v2]
49
50
51def remove_permutations_that_share_a_vertex(bm, permutations):
52    """Get useful Permutations.
53
54    Args:
55        bm: Object's Bmesh
56        permutations: Possible Intersection Edges as a list
57
58    Returns:
59        List of Edges.
60    """
61
62    final_permutations = []
63    for edges in permutations:
64        raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
65        if len(set(raw_vert_indices)) < 4:
66            continue
67
68        # reaches this point if they do not share.
69        final_permutations.append(edges)
70
71    return final_permutations
72
73
74def get_valid_permutations(bm, edge_indices):
75    """Get useful Permutations.
76
77    Args:
78        bm: Object's Bmesh
79        edge_indices: List of indices of Edges to consider
80
81    Returns:
82        List of suitable Edges.
83    """
84
85    raw_permutations = itertools.permutations(edge_indices, 2)
86    permutations = [r for r in raw_permutations if r[0] < r[1]]
87    return remove_permutations_that_share_a_vertex(bm, permutations)
88
89
90def can_skip(closest_points, vert_vectors):
91    """Check if the intersection lies on both edges and return True
92    when criteria are not met, and thus this point can be skipped.
93
94    Args:
95        closest_points: List of Coordinates of points to consider
96        vert_vectors: List of Coordinates of vertices to consider
97
98    Returns:
99        Boolean.
100    """
101
102    if not closest_points:
103        return True
104    if not isinstance(closest_points[0].x, float):
105        return True
106    if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2:
107        return True
108
109    # if this distance is larger than than 1.0e-5, we can skip it.
110    cpa, cpb = closest_points
111    return (cpa - cpb).length > 1.0e-5
112
113
114def get_intersection_dictionary(bm, edge_indices):
115    """Return a dictionary of edge indices and points found on those edges.
116
117    Args:
118        bm, Object's Bmesh
119        edge_indices: List of Edge Indices
120
121    Returns:
122        Dictionary of Vectors.
123    """
124
125    bm.verts.ensure_lookup_table()
126    bm.edges.ensure_lookup_table()
127
128    permutations = get_valid_permutations(bm, edge_indices)
129
130    list_k = defaultdict(list)
131    list_d = defaultdict(list)
132
133    for edges in permutations:
134        raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
135        vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices)
136
137        points = LineIntersect(*vert_vectors)
138
139        # some can be skipped.    (NaN, None, not on both edges)
140        if can_skip(points, vert_vectors):
141            continue
142
143        # reaches this point only when an intersection happens on both edges.
144        [list_k[edge].append(points[0]) for edge in edges]
145
146    # list_k will contain a dict of edge indices and points found on those edges.
147    for edge_idx, unordered_points in list_k.items():
148        tv1, tv2 = bm.edges[edge_idx].verts
149        v1 = bm.verts[tv1.index].co
150        v2 = bm.verts[tv2.index].co
151        ordered_points = order_points((v1, v2), unordered_points)
152        list_d[edge_idx].extend(ordered_points)
153
154    return list_d
155
156
157def update_mesh(bm, int_dict):
158    """Make new geometry (delete old first).
159
160    Args:
161        bm, Object's Bmesh
162        int_dict: Dictionary of Indices of Vertices
163
164    Returns:
165        Nothing.
166    """
167
168    orig_e = bm.edges
169    orig_v = bm.verts
170
171    new_verts = []
172    collect = new_verts.extend
173    for _, point_list in int_dict.items():
174        num_edges_to_add = len(point_list) - 1
175        for i in range(num_edges_to_add):
176            coord_a = orig_v.new(point_list[i])
177            coord_b = orig_v.new(point_list[i + 1])
178            orig_e.new((coord_a, coord_b))
179            bm.normal_update()
180            collect([coord_a, coord_b])
181
182    bmesh.ops.delete(bm, geom=[edge for edge in bm.edges if edge.select], context="EDGES")
183    bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
184
185
186def unselect_nonintersecting(bm, d_edges, edge_indices):
187    """Deselects Non-Intersection Edges.
188
189    Args:
190        bm, Object's Bmesh
191        d_edges: List of Intersecting Edges
192        edge_indices: List of Edge Indices to consider
193
194    Returns:
195        Nothing.
196    """
197
198    if len(edge_indices) > len(d_edges):
199        reserved_edges = set(edge_indices) - set(d_edges)
200        for edge in reserved_edges:
201            bm.edges[edge].select = False
202
203
204def intersect_all(context):
205    """Computes All intersections with Crossing Geometry.
206
207    Note:
208        Deletes original edges and replaces with new intersected edges
209
210    Args:
211        context: Blender bpy.context instance.
212
213    Returns:
214        Status Set.
215    """
216
217    pg = context.scene.pdt_pg
218    obj = context.active_object
219    if all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"]):
220        # must force edge selection mode here
221        bpy.context.tool_settings.mesh_select_mode = (False, True, False)
222
223
224        if obj.mode == "EDIT":
225            bm = bmesh.from_edit_mesh(obj.data)
226
227            selected_edges = [edge for edge in bm.edges if edge.select]
228            edge_indices = [i.index for i in selected_edges]
229
230            int_dict = get_intersection_dictionary(bm, edge_indices)
231
232            unselect_nonintersecting(bm, int_dict.keys(), edge_indices)
233            update_mesh(bm, int_dict)
234
235            bmesh.update_edit_mesh(obj.data)
236        else:
237            pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
238            context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
239            return
240
241        return
242    else:
243        pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
244        context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
245        return
246
247class PDT_OT_IntersectAllEdges(bpy.types.Operator):
248    """Cut Selected Edges at All Intersections."""
249
250    bl_idname = "pdt.intersectall"
251    bl_label = "Intersect All Edges"
252    bl_options = {"REGISTER", "UNDO"}
253
254    @classmethod
255    def poll(cls, context):
256        """Check to see object is in correct condition.
257
258        Args:
259            context: Blender bpy.context instance.
260
261        Returns:
262            Boolean
263        """
264        obj = context.active_object
265        if obj is None:
266            return False
267        return obj is not None and obj.type == "MESH" and obj.mode == "EDIT"
268
269    def execute(self, context):
270        """Computes All intersections with Crossing Geometry.
271
272        Note:
273            Deletes original edges and replaces with new intersected edges
274
275        Args:
276            context: Blender bpy.context instance.
277
278        Returns:
279            Status Set.
280        """
281
282        pg = context.scene.pdt_pg
283        pg.command = f"intall"
284        return {"FINISHED"}
285