This commit implements described in the #104573. The goal is to fix the confusion of the submodule hashes change, which are not ideal for any of the supported git-module configuration (they are either always visible causing confusion, or silently staged and committed, also causing confusion). This commit replaces submodules with a checkout of addons and addons_contrib, covered by the .gitignore, and locale and developer tools are moved to the main repository. This also changes the paths: - /release/scripts are moved to the /scripts - /source/tools are moved to the /tools - /release/datafiles/locale is moved to /locale This is done to avoid conflicts when using bisect, and also allow buildbot to automatically "recover" wgen building older or newer branches/patches. Running `make update` will initialize the local checkout to the changed repository configuration. Another aspect of the change is that the make update will support Github style of remote organization (origin remote pointing to thy fork, upstream remote pointing to the upstream blender/blender.git). Pull Request #104755
336 lines
10 KiB
Python
336 lines
10 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
# Utilities to detect the next matching element (vert/edge/face)
|
|
# based on an existing pair of elements.
|
|
|
|
import bmesh
|
|
|
|
__all__ = (
|
|
"select_prev",
|
|
"select_next",
|
|
)
|
|
|
|
|
|
def other_edges_over_face(e):
|
|
# Can yield same edge multiple times, its fine.
|
|
for l in e.link_loops:
|
|
yield l.link_loop_next.edge
|
|
yield l.link_loop_prev.edge
|
|
|
|
|
|
def other_edges_over_edge(e):
|
|
# Can yield same edge multiple times, its fine.
|
|
for v in e.verts:
|
|
for e_other in v.link_edges:
|
|
if e_other is not e:
|
|
if not e.is_wire:
|
|
yield e_other
|
|
|
|
|
|
def verts_from_elem(ele):
|
|
ele_type = type(ele)
|
|
if ele_type is bmesh.types.BMFace:
|
|
return [l.vert for l in ele.loops]
|
|
elif ele_type is bmesh.types.BMEdge:
|
|
return [v for v in ele.verts]
|
|
elif ele_type is bmesh.types.BMVert:
|
|
return [ele]
|
|
else:
|
|
raise TypeError("wrong type")
|
|
|
|
|
|
def edges_from_elem(ele):
|
|
ele_type = type(ele)
|
|
if ele_type is bmesh.types.BMFace:
|
|
return [l.edge for l in ele.loops]
|
|
elif ele_type is bmesh.types.BMEdge:
|
|
return [ele]
|
|
elif ele_type is bmesh.types.BMVert:
|
|
return [e for e in ele.link_edges]
|
|
else:
|
|
raise TypeError("wrong type")
|
|
|
|
|
|
def elems_depth_search(ele_init, depths, other_edges_over_cb, results_init=None):
|
|
"""
|
|
List of depths -> List of elems that match those depths.
|
|
"""
|
|
|
|
depth_max = max(depths)
|
|
depth_min = min(depths)
|
|
depths_sorted = tuple(sorted(depths))
|
|
|
|
stack_old = edges_from_elem(ele_init)
|
|
stack_new = []
|
|
|
|
stack_visit = set(stack_old)
|
|
|
|
vert_depths = {}
|
|
vert_depths_setdefault = vert_depths.setdefault
|
|
|
|
depth = 0
|
|
while stack_old and depth <= depth_max:
|
|
for ele in stack_old:
|
|
for v in verts_from_elem(ele):
|
|
vert_depths_setdefault(v, depth)
|
|
for ele_other in other_edges_over_cb(ele):
|
|
stack_visit_len = len(stack_visit)
|
|
stack_visit.add(ele_other)
|
|
if stack_visit_len != len(stack_visit):
|
|
stack_new.append(ele_other)
|
|
stack_new, stack_old = stack_old, stack_new
|
|
stack_new[:] = []
|
|
depth += 1
|
|
|
|
# now we have many verts in vert_depths which are attached to elements
|
|
# which are candidates for matching with depths
|
|
if type(ele_init) is bmesh.types.BMFace:
|
|
test_ele = {
|
|
l.face for v, depth in vert_depths.items()
|
|
if depth >= depth_min for l in v.link_loops}
|
|
elif type(ele_init) is bmesh.types.BMEdge:
|
|
test_ele = {
|
|
e for v, depth in vert_depths.items()
|
|
if depth >= depth_min for e in v.link_edges if not e.is_wire}
|
|
else:
|
|
test_ele = {
|
|
v for v, depth in vert_depths.items()
|
|
if depth >= depth_min}
|
|
|
|
result_ele = set()
|
|
|
|
vert_depths_get = vert_depths.get
|
|
# re-used each time, will always be the same length
|
|
depths_test = [None] * len(depths)
|
|
|
|
for ele in test_ele:
|
|
verts_test = verts_from_elem(ele)
|
|
if len(verts_test) != len(depths):
|
|
continue
|
|
if results_init is not None and ele not in results_init:
|
|
continue
|
|
if ele in result_ele:
|
|
continue
|
|
|
|
ok = True
|
|
for i, v in enumerate(verts_test):
|
|
depth = vert_depths_get(v)
|
|
if depth is not None:
|
|
depths_test[i] = depth
|
|
else:
|
|
ok = False
|
|
break
|
|
|
|
if ok:
|
|
if depths_sorted == tuple(sorted(depths_test)):
|
|
# Note, its possible the order of sorted items moves the values out-of-order.
|
|
# for this we could do a circular list comparison,
|
|
# however - this is such a rare case that we're ignoring it.
|
|
result_ele.add(ele)
|
|
|
|
return result_ele
|
|
|
|
|
|
def elems_depth_measure(ele_dst, ele_src, other_edges_over_cb):
|
|
"""
|
|
Returns·ele_dst vert depths from ele_src, aligned with ele_dst verts.
|
|
"""
|
|
|
|
stack_old = edges_from_elem(ele_src)
|
|
stack_new = []
|
|
|
|
stack_visit = set(stack_old)
|
|
|
|
# continue until we've reached all verts in the destination
|
|
ele_dst_verts = verts_from_elem(ele_dst)
|
|
all_dst = set(ele_dst_verts)
|
|
all_dst_discard = all_dst.discard
|
|
|
|
vert_depths = {}
|
|
|
|
depth = 0
|
|
while stack_old and all_dst:
|
|
for ele in stack_old:
|
|
for v in verts_from_elem(ele):
|
|
len_prev = len(all_dst)
|
|
all_dst_discard(v)
|
|
if len_prev != len(all_dst):
|
|
vert_depths[v] = depth
|
|
|
|
for ele_other in other_edges_over_cb(ele):
|
|
stack_visit_len = len(stack_visit)
|
|
stack_visit.add(ele_other)
|
|
if stack_visit_len != len(stack_visit):
|
|
stack_new.append(ele_other)
|
|
stack_new, stack_old = stack_old, stack_new
|
|
stack_new[:] = []
|
|
depth += 1
|
|
|
|
if not all_dst:
|
|
return [vert_depths[v] for v in ele_dst_verts]
|
|
else:
|
|
return None
|
|
|
|
|
|
def find_next(ele_dst, ele_src):
|
|
depth_src_a = elems_depth_measure(ele_dst, ele_src, other_edges_over_edge)
|
|
depth_src_b = elems_depth_measure(ele_dst, ele_src, other_edges_over_face)
|
|
|
|
# path not found
|
|
if depth_src_a is None or depth_src_b is None:
|
|
return []
|
|
|
|
depth_src = tuple(zip(depth_src_a, depth_src_b))
|
|
|
|
candidates = elems_depth_search(ele_dst, depth_src_a, other_edges_over_edge)
|
|
candidates = elems_depth_search(ele_dst, depth_src_b, other_edges_over_face, candidates)
|
|
candidates.discard(ele_src)
|
|
candidates.discard(ele_dst)
|
|
if not candidates:
|
|
return []
|
|
|
|
# Now we have to pick which is the best next-element,
|
|
# do this by calculating the element with the largest
|
|
# variation in depth from the relationship to the source.
|
|
# ... So we have the highest chance of stepping onto the opposite element.
|
|
diff_best = 0
|
|
ele_best = None
|
|
ele_best_ls = []
|
|
for ele_test in candidates:
|
|
depth_test_a = elems_depth_measure(ele_dst, ele_test, other_edges_over_edge)
|
|
depth_test_b = elems_depth_measure(ele_dst, ele_test, other_edges_over_face)
|
|
if depth_test_a is None or depth_test_b is None:
|
|
continue
|
|
depth_test = tuple(zip(depth_test_a, depth_test_b))
|
|
# square so a few high values win over many small ones
|
|
diff_test = sum((abs(a[0] - b[0]) ** 2) +
|
|
(abs(a[1] - b[1]) ** 2) for a, b in zip(depth_src, depth_test))
|
|
if diff_test > diff_best:
|
|
diff_best = diff_test
|
|
ele_best = ele_test
|
|
ele_best_ls[:] = [ele_best]
|
|
elif diff_test == diff_best:
|
|
if ele_best is None:
|
|
ele_best = ele_test
|
|
ele_best_ls.append(ele_test)
|
|
|
|
if len(ele_best_ls) > 1:
|
|
ele_best_ls_init = ele_best_ls
|
|
ele_best_ls = []
|
|
depth_accum_max = -1
|
|
for ele_test in ele_best_ls_init:
|
|
depth_test_a = elems_depth_measure(ele_src, ele_test, other_edges_over_edge)
|
|
depth_test_b = elems_depth_measure(ele_src, ele_test, other_edges_over_face)
|
|
if depth_test_a is None or depth_test_b is None:
|
|
continue
|
|
depth_accum_test = (
|
|
sum(depth_test_a) + sum(depth_test_b))
|
|
|
|
if depth_accum_test > depth_accum_max:
|
|
depth_accum_max = depth_accum_test
|
|
ele_best = ele_test
|
|
ele_best_ls[:] = [ele_best]
|
|
elif depth_accum_test == depth_accum_max:
|
|
# we have multiple bests, don't return any
|
|
ele_best_ls.append(ele_test)
|
|
|
|
return ele_best_ls
|
|
|
|
|
|
# expose for operators
|
|
def select_next(bm, report):
|
|
import bmesh
|
|
ele_pair = [None, None]
|
|
for i, ele in enumerate(reversed(bm.select_history)):
|
|
ele_pair[i] = ele
|
|
if i == 1:
|
|
break
|
|
|
|
if ele_pair[-1] is None:
|
|
report({'INFO'}, "Selection pair not found")
|
|
return False
|
|
|
|
ele_pair_next = find_next(*ele_pair)
|
|
|
|
if len(ele_pair_next) > 1:
|
|
# We have multiple options,
|
|
# check topology around the element and find the closest match
|
|
# (allow for sloppy comparison if exact checks fail).
|
|
|
|
def ele_uuid(ele):
|
|
ele_type = type(ele)
|
|
if ele_type is bmesh.types.BMFace:
|
|
ret = [len(f.verts) for l in ele.loops for f in l.edge.link_faces if f is not ele]
|
|
elif ele_type is bmesh.types.BMEdge:
|
|
ret = [len(l.face.verts) for l in ele.link_loops]
|
|
elif ele_type is bmesh.types.BMVert:
|
|
ret = [len(l.face.verts) for l in ele.link_loops]
|
|
else:
|
|
raise TypeError("wrong type")
|
|
return tuple(sorted(ret))
|
|
|
|
def ele_uuid_filter():
|
|
|
|
def pass_fn(seq):
|
|
return seq
|
|
|
|
def sum_set(seq):
|
|
return sum(set(seq))
|
|
|
|
uuid_cmp = ele_uuid(ele_pair[0])
|
|
ele_pair_next_uuid = [(ele, ele_uuid(ele)) for ele in ele_pair_next]
|
|
|
|
# Attempt to find the closest match,
|
|
# start specific, use increasingly more approximate comparisons.
|
|
for fn in (pass_fn, set, sum_set, len):
|
|
uuid_cmp_test = fn(uuid_cmp)
|
|
ele_pair_next_uuid_test = [
|
|
(ele, uuid) for (ele, uuid) in ele_pair_next_uuid
|
|
if uuid_cmp_test == fn(uuid)
|
|
]
|
|
if len(ele_pair_next_uuid_test) > 1:
|
|
ele_pair_next_uuid = ele_pair_next_uuid_test
|
|
elif len(ele_pair_next_uuid_test) == 1:
|
|
return [ele for (ele, uuid) in ele_pair_next_uuid_test]
|
|
return []
|
|
|
|
ele_pair_next[:] = ele_uuid_filter()
|
|
|
|
del ele_uuid, ele_uuid_filter
|
|
|
|
if len(ele_pair_next) != 1:
|
|
report({'INFO'}, "No single next item found")
|
|
return False
|
|
|
|
ele = ele_pair_next[0]
|
|
if ele.hide:
|
|
report({'INFO'}, "Next element is hidden")
|
|
return False
|
|
|
|
ele.select_set(False)
|
|
ele.select_set(True)
|
|
bm.select_history.discard(ele)
|
|
bm.select_history.add(ele)
|
|
if type(ele) is bmesh.types.BMFace:
|
|
bm.faces.active = ele
|
|
return True
|
|
|
|
|
|
def select_prev(bm, report):
|
|
import bmesh
|
|
for ele in reversed(bm.select_history):
|
|
break
|
|
else:
|
|
report({'INFO'}, "Last selected not found")
|
|
return False
|
|
|
|
ele.select_set(False)
|
|
|
|
for i, ele in enumerate(reversed(bm.select_history)):
|
|
if i == 1:
|
|
if type(ele) is bmesh.types.BMFace:
|
|
bm.faces.active = ele
|
|
break
|
|
|
|
return True
|