Fix T78854: Cell fracture fails in background mode
[blender-addons.git] / precision_drawing_tools / pdt_xall.py
blob9fe405d602bdbbccabb20a2d3684c885b9651890
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 # ----------------------------------------------------------
6 # Author: Zeffii
7 # Modified by: Alan Odom (Clockmender) & Rune Morling (ermo)
8 # ----------------------------------------------------------
10 import bpy
11 import bmesh
12 from mathutils.geometry import intersect_line_line as LineIntersect
13 import itertools
14 from collections import defaultdict
15 from . import pdt_cad_module as cm
16 from .pdt_functions import oops
17 from .pdt_msg_strings import (
18 PDT_ERR_EDOB_MODE
22 def order_points(edge, point_list):
23 """Order these edges from distance to v1, then sandwich the sorted list with v1, v2."""
24 v1, v2 = edge
26 def dist(coord):
27 """Measure distance between two coordinates."""
29 return (v1 - coord).length
31 point_list = sorted(point_list, key=dist)
32 return [v1] + point_list + [v2]
35 def remove_permutations_that_share_a_vertex(bm, permutations):
36 """Get useful Permutations.
38 Args:
39 bm: Object's Bmesh
40 permutations: Possible Intersection Edges as a list
42 Returns:
43 List of Edges.
44 """
46 final_permutations = []
47 for edges in permutations:
48 raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
49 if len(set(raw_vert_indices)) < 4:
50 continue
52 # reaches this point if they do not share.
53 final_permutations.append(edges)
55 return final_permutations
58 def get_valid_permutations(bm, edge_indices):
59 """Get useful Permutations.
61 Args:
62 bm: Object's Bmesh
63 edge_indices: List of indices of Edges to consider
65 Returns:
66 List of suitable Edges.
67 """
69 raw_permutations = itertools.permutations(edge_indices, 2)
70 permutations = [r for r in raw_permutations if r[0] < r[1]]
71 return remove_permutations_that_share_a_vertex(bm, permutations)
74 def can_skip(closest_points, vert_vectors):
75 """Check if the intersection lies on both edges and return True
76 when criteria are not met, and thus this point can be skipped.
78 Args:
79 closest_points: List of Coordinates of points to consider
80 vert_vectors: List of Coordinates of vertices to consider
82 Returns:
83 Boolean.
84 """
86 if not closest_points:
87 return True
88 if not isinstance(closest_points[0].x, float):
89 return True
90 if cm.num_edges_point_lies_on(closest_points[0], vert_vectors) < 2:
91 return True
93 # if this distance is larger than than 1.0e-5, we can skip it.
94 cpa, cpb = closest_points
95 return (cpa - cpb).length > 1.0e-5
98 def get_intersection_dictionary(bm, edge_indices):
99 """Return a dictionary of edge indices and points found on those edges.
101 Args:
102 bm, Object's Bmesh
103 edge_indices: List of Edge Indices
105 Returns:
106 Dictionary of Vectors.
109 bm.verts.ensure_lookup_table()
110 bm.edges.ensure_lookup_table()
112 permutations = get_valid_permutations(bm, edge_indices)
114 list_k = defaultdict(list)
115 list_d = defaultdict(list)
117 for edges in permutations:
118 raw_vert_indices = cm.vertex_indices_from_edges_tuple(bm, edges)
119 vert_vectors = cm.vectors_from_indices(bm, raw_vert_indices)
121 points = LineIntersect(*vert_vectors)
123 # some can be skipped. (NaN, None, not on both edges)
124 if can_skip(points, vert_vectors):
125 continue
127 # reaches this point only when an intersection happens on both edges.
128 [list_k[edge].append(points[0]) for edge in edges]
130 # list_k will contain a dict of edge indices and points found on those edges.
131 for edge_idx, unordered_points in list_k.items():
132 tv1, tv2 = bm.edges[edge_idx].verts
133 v1 = bm.verts[tv1.index].co
134 v2 = bm.verts[tv2.index].co
135 ordered_points = order_points((v1, v2), unordered_points)
136 list_d[edge_idx].extend(ordered_points)
138 return list_d
141 def update_mesh(bm, int_dict):
142 """Make new geometry (delete old first).
144 Args:
145 bm, Object's Bmesh
146 int_dict: Dictionary of Indices of Vertices
148 Returns:
149 Nothing.
152 orig_e = bm.edges
153 orig_v = bm.verts
155 new_verts = []
156 collect = new_verts.extend
157 for _, point_list in int_dict.items():
158 num_edges_to_add = len(point_list) - 1
159 for i in range(num_edges_to_add):
160 coord_a = orig_v.new(point_list[i])
161 coord_b = orig_v.new(point_list[i + 1])
162 orig_e.new((coord_a, coord_b))
163 bm.normal_update()
164 collect([coord_a, coord_b])
166 bmesh.ops.delete(bm, geom=[edge for edge in bm.edges if edge.select], context="EDGES")
167 bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
170 def unselect_nonintersecting(bm, d_edges, edge_indices):
171 """Deselects Non-Intersection Edges.
173 Args:
174 bm, Object's Bmesh
175 d_edges: List of Intersecting Edges
176 edge_indices: List of Edge Indices to consider
178 Returns:
179 Nothing.
182 if len(edge_indices) > len(d_edges):
183 reserved_edges = set(edge_indices) - set(d_edges)
184 for edge in reserved_edges:
185 bm.edges[edge].select = False
188 def intersect_all(context):
189 """Computes All intersections with Crossing Geometry.
191 Note:
192 Deletes original edges and replaces with new intersected edges
194 Args:
195 context: Blender bpy.context instance.
197 Returns:
198 Status Set.
201 pg = context.scene.pdt_pg
202 obj = context.active_object
203 if all([bool(obj), obj.type == "MESH", obj.mode == "EDIT"]):
204 # must force edge selection mode here
205 bpy.context.tool_settings.mesh_select_mode = (False, True, False)
208 if obj.mode == "EDIT":
209 bm = bmesh.from_edit_mesh(obj.data)
211 selected_edges = [edge for edge in bm.edges if edge.select]
212 edge_indices = [i.index for i in selected_edges]
214 int_dict = get_intersection_dictionary(bm, edge_indices)
216 unselect_nonintersecting(bm, int_dict.keys(), edge_indices)
217 update_mesh(bm, int_dict)
219 bmesh.update_edit_mesh(obj.data)
220 else:
221 pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
222 context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
223 return
225 return
226 else:
227 pg.error = f"{PDT_ERR_EDOB_MODE},{obj.mode})"
228 context.window_manager.popup_menu(oops, title="Error", icon="ERROR")
229 return
231 class PDT_OT_IntersectAllEdges(bpy.types.Operator):
232 """Cut Selected Edges at All Intersections"""
234 bl_idname = "pdt.intersectall"
235 bl_label = "Intersect All Edges"
236 bl_options = {"REGISTER", "UNDO"}
238 @classmethod
239 def poll(cls, context):
240 """Check to see object is in correct condition.
242 Args:
243 context: Blender bpy.context instance.
245 Returns:
246 Boolean
248 obj = context.active_object
249 if obj is None:
250 return False
251 return obj is not None and obj.type == "MESH" and obj.mode == "EDIT"
253 def execute(self, context):
254 """Computes All intersections with Crossing Geometry.
256 Note:
257 Deletes original edges and replaces with new intersected edges
259 Args:
260 context: Blender bpy.context instance.
262 Returns:
263 Status Set.
266 pg = context.scene.pdt_pg
267 pg.command = f"intall"
268 return {"FINISHED"}