find_adjacent.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  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. # <pep8 compliant>
  19. # Utilities to detect the next matching element (vert/edge/face)
  20. # based on an existing pair of elements.
  21. import bmesh
  22. __all__ = (
  23. "select_prev",
  24. "select_next",
  25. )
  26. def other_edges_over_face(e):
  27. # Can yield same edge multiple times, its fine.
  28. for l in e.link_loops:
  29. yield l.link_loop_next.edge
  30. yield l.link_loop_prev.edge
  31. def other_edges_over_edge(e):
  32. # Can yield same edge multiple times, its fine.
  33. for v in e.verts:
  34. for e_other in v.link_edges:
  35. if e_other is not e:
  36. if not e.is_wire:
  37. yield e_other
  38. def verts_from_elem(ele):
  39. ele_type = type(ele)
  40. if ele_type is bmesh.types.BMFace:
  41. return [l.vert for l in ele.loops]
  42. elif ele_type is bmesh.types.BMEdge:
  43. return [v for v in ele.verts]
  44. elif ele_type is bmesh.types.BMVert:
  45. return [ele]
  46. else:
  47. raise TypeError("wrong type")
  48. def edges_from_elem(ele):
  49. ele_type = type(ele)
  50. if ele_type is bmesh.types.BMFace:
  51. return [l.edge for l in ele.loops]
  52. elif ele_type is bmesh.types.BMEdge:
  53. return [ele]
  54. elif ele_type is bmesh.types.BMVert:
  55. return [e for e in ele.link_edges]
  56. else:
  57. raise TypeError("wrong type")
  58. def elems_depth_search(ele_init, depths, other_edges_over_cb, results_init=None):
  59. """
  60. List of depths -> List of elems that match those depths.
  61. """
  62. depth_max = max(depths)
  63. depth_min = min(depths)
  64. depths_sorted = tuple(sorted(depths))
  65. stack_old = edges_from_elem(ele_init)
  66. stack_new = []
  67. stack_visit = set(stack_old)
  68. vert_depths = {}
  69. vert_depths_setdefault = vert_depths.setdefault
  70. depth = 0
  71. while stack_old and depth <= depth_max:
  72. for ele in stack_old:
  73. for v in verts_from_elem(ele):
  74. vert_depths_setdefault(v, depth)
  75. for ele_other in other_edges_over_cb(ele):
  76. stack_visit_len = len(stack_visit)
  77. stack_visit.add(ele_other)
  78. if stack_visit_len != len(stack_visit):
  79. stack_new.append(ele_other)
  80. stack_new, stack_old = stack_old, stack_new
  81. stack_new[:] = []
  82. depth += 1
  83. # now we have many verts in vert_depths which are attached to elements
  84. # which are candidates for matching with depths
  85. if type(ele_init) is bmesh.types.BMFace:
  86. test_ele = {
  87. l.face for v, depth in vert_depths.items()
  88. if depth >= depth_min for l in v.link_loops}
  89. elif type(ele_init) is bmesh.types.BMEdge:
  90. test_ele = {
  91. e for v, depth in vert_depths.items()
  92. if depth >= depth_min for e in v.link_edges if not e.is_wire}
  93. else:
  94. test_ele = {
  95. v for v, depth in vert_depths.items()
  96. if depth >= depth_min}
  97. result_ele = set()
  98. vert_depths_get = vert_depths.get
  99. # re-used each time, will always be the same length
  100. depths_test = [None] * len(depths)
  101. for ele in test_ele:
  102. verts_test = verts_from_elem(ele)
  103. if len(verts_test) != len(depths):
  104. continue
  105. if results_init is not None and ele not in results_init:
  106. continue
  107. if ele in result_ele:
  108. continue
  109. ok = True
  110. for i, v in enumerate(verts_test):
  111. depth = vert_depths_get(v)
  112. if depth is not None:
  113. depths_test[i] = depth
  114. else:
  115. ok = False
  116. break
  117. if ok:
  118. if depths_sorted == tuple(sorted(depths_test)):
  119. # Note, its possible the order of sorted items moves the values out-of-order.
  120. # for this we could do a circular list comparison,
  121. # however - this is such a rare case that we're ignoring it.
  122. result_ele.add(ele)
  123. return result_ele
  124. def elems_depth_measure(ele_dst, ele_src, other_edges_over_cb):
  125. """
  126. Returns·ele_dst vert depths from ele_src, aligned with ele_dst verts.
  127. """
  128. stack_old = edges_from_elem(ele_src)
  129. stack_new = []
  130. stack_visit = set(stack_old)
  131. # continue until we've reached all verts in the destination
  132. ele_dst_verts = verts_from_elem(ele_dst)
  133. all_dst = set(ele_dst_verts)
  134. all_dst_discard = all_dst.discard
  135. vert_depths = {}
  136. depth = 0
  137. while stack_old and all_dst:
  138. for ele in stack_old:
  139. for v in verts_from_elem(ele):
  140. len_prev = len(all_dst)
  141. all_dst_discard(v)
  142. if len_prev != len(all_dst):
  143. vert_depths[v] = depth
  144. for ele_other in other_edges_over_cb(ele):
  145. stack_visit_len = len(stack_visit)
  146. stack_visit.add(ele_other)
  147. if stack_visit_len != len(stack_visit):
  148. stack_new.append(ele_other)
  149. stack_new, stack_old = stack_old, stack_new
  150. stack_new[:] = []
  151. depth += 1
  152. if not all_dst:
  153. return [vert_depths[v] for v in ele_dst_verts]
  154. else:
  155. return None
  156. def find_next(ele_dst, ele_src):
  157. depth_src_a = elems_depth_measure(ele_dst, ele_src, other_edges_over_edge)
  158. depth_src_b = elems_depth_measure(ele_dst, ele_src, other_edges_over_face)
  159. # path not found
  160. if depth_src_a is None or depth_src_b is None:
  161. return []
  162. depth_src = tuple(zip(depth_src_a, depth_src_b))
  163. candidates = elems_depth_search(ele_dst, depth_src_a, other_edges_over_edge)
  164. candidates = elems_depth_search(ele_dst, depth_src_b, other_edges_over_face, candidates)
  165. candidates.discard(ele_src)
  166. candidates.discard(ele_dst)
  167. if not candidates:
  168. return []
  169. # Now we have to pick which is the best next-element,
  170. # do this by calculating the element with the largest
  171. # variation in depth from the relationship to the source.
  172. # ... So we have the highest chance of stepping onto the opposite element.
  173. diff_best = 0
  174. ele_best = None
  175. ele_best_ls = []
  176. for ele_test in candidates:
  177. depth_test_a = elems_depth_measure(ele_dst, ele_test, other_edges_over_edge)
  178. depth_test_b = elems_depth_measure(ele_dst, ele_test, other_edges_over_face)
  179. if depth_test_a is None or depth_test_b is None:
  180. continue
  181. depth_test = tuple(zip(depth_test_a, depth_test_b))
  182. # square so a few high values win over many small ones
  183. diff_test = sum((abs(a[0] - b[0]) ** 2) +
  184. (abs(a[1] - b[1]) ** 2) for a, b in zip(depth_src, depth_test))
  185. if diff_test > diff_best:
  186. diff_best = diff_test
  187. ele_best = ele_test
  188. ele_best_ls[:] = [ele_best]
  189. elif diff_test == diff_best:
  190. if ele_best is None:
  191. ele_best = ele_test
  192. ele_best_ls.append(ele_test)
  193. if len(ele_best_ls) > 1:
  194. ele_best_ls_init = ele_best_ls
  195. ele_best_ls = []
  196. depth_accum_max = -1
  197. for ele_test in ele_best_ls_init:
  198. depth_test_a = elems_depth_measure(ele_src, ele_test, other_edges_over_edge)
  199. depth_test_b = elems_depth_measure(ele_src, ele_test, other_edges_over_face)
  200. if depth_test_a is None or depth_test_b is None:
  201. continue
  202. depth_accum_test = (
  203. sum(depth_test_a) + sum(depth_test_b))
  204. if depth_accum_test > depth_accum_max:
  205. depth_accum_max = depth_accum_test
  206. ele_best = ele_test
  207. ele_best_ls[:] = [ele_best]
  208. elif depth_accum_test == depth_accum_max:
  209. # we have multiple bests, don't return any
  210. ele_best_ls.append(ele_test)
  211. return ele_best_ls
  212. # expose for operators
  213. def select_next(bm, report):
  214. import bmesh
  215. ele_pair = [None, None]
  216. for i, ele in enumerate(reversed(bm.select_history)):
  217. ele_pair[i] = ele
  218. if i == 1:
  219. break
  220. if ele_pair[-1] is None:
  221. report({'INFO'}, "Selection pair not found")
  222. return False
  223. ele_pair_next = find_next(*ele_pair)
  224. if len(ele_pair_next) > 1:
  225. # We have multiple options,
  226. # check topology around the element and find the closest match
  227. # (allow for sloppy comparison if exact checks fail).
  228. def ele_uuid(ele):
  229. ele_type = type(ele)
  230. if ele_type is bmesh.types.BMFace:
  231. ret = [len(f.verts) for l in ele.loops for f in l.edge.link_faces if f is not ele]
  232. elif ele_type is bmesh.types.BMEdge:
  233. ret = [len(l.face.verts) for l in ele.link_loops]
  234. elif ele_type is bmesh.types.BMVert:
  235. ret = [len(l.face.verts) for l in ele.link_loops]
  236. else:
  237. raise TypeError("wrong type")
  238. return tuple(sorted(ret))
  239. def ele_uuid_filter():
  240. def pass_fn(seq):
  241. return seq
  242. def sum_set(seq):
  243. return sum(set(seq))
  244. uuid_cmp = ele_uuid(ele_pair[0])
  245. ele_pair_next_uuid = [(ele, ele_uuid(ele)) for ele in ele_pair_next]
  246. # Attempt to find the closest match,
  247. # start specific, use increasingly more approximate comparisons.
  248. for fn in (pass_fn, set, sum_set, len):
  249. uuid_cmp_test = fn(uuid_cmp)
  250. ele_pair_next_uuid_test = [
  251. (ele, uuid) for (ele, uuid) in ele_pair_next_uuid
  252. if uuid_cmp_test == fn(uuid)
  253. ]
  254. if len(ele_pair_next_uuid_test) > 1:
  255. ele_pair_next_uuid = ele_pair_next_uuid_test
  256. elif len(ele_pair_next_uuid_test) == 1:
  257. return [ele for (ele, uuid) in ele_pair_next_uuid_test]
  258. return []
  259. ele_pair_next[:] = ele_uuid_filter()
  260. del ele_uuid, ele_uuid_filter
  261. if len(ele_pair_next) != 1:
  262. report({'INFO'}, "No single next item found")
  263. return False
  264. ele = ele_pair_next[0]
  265. if ele.hide:
  266. report({'INFO'}, "Next element is hidden")
  267. return False
  268. ele.select_set(False)
  269. ele.select_set(True)
  270. bm.select_history.discard(ele)
  271. bm.select_history.add(ele)
  272. if type(ele) is bmesh.types.BMFace:
  273. bm.faces.active = ele
  274. return True
  275. def select_prev(bm, report):
  276. import bmesh
  277. for ele in reversed(bm.select_history):
  278. break
  279. else:
  280. report({'INFO'}, "Last selected not found")
  281. return False
  282. ele.select_set(False)
  283. for i, ele in enumerate(reversed(bm.select_history)):
  284. if i == 1:
  285. if type(ele) is bmesh.types.BMFace:
  286. bm.faces.active = ele
  287. break
  288. return True