Welcome to mirror list, hosted at ThFree Co, Russian Federation.

find_adjacent.py « bmesh « bl_operators « startup « scripts « release - git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 5e8b125e060fb2a9ae744a383b1a5775a14a49a3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# SPDX-License-Identifier: GPL-2.0-or-later

# <pep8 compliant>

# 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