Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/rfmesh/rfmesh.py
T
2026-03-17 14:30:01 -06:00

1984 lines
82 KiB
Python

'''
Copyright (C) 2023 CG Cookie
http://cgcookie.com
hello@cgcookie.com
Created by Jonathan Denning, Jonathan Williamson
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
import math
import copy
import heapq
import numpy as np
import random
from dataclasses import dataclass, field
from itertools import takewhile, filterfalse
import bpy
import bmesh
from bmesh.types import BMVert, BMEdge, BMFace
from bmesh.ops import (
bisect_plane, holes_fill,
dissolve_verts, dissolve_edges, dissolve_faces,
remove_doubles, mirror, recalc_face_normals,
pointmerge,
)
from mathutils import Vector, Matrix
from mathutils.bvhtree import BVHTree
from mathutils.kdtree import KDTree
from mathutils.geometry import normal as compute_normal, intersect_point_tri, intersect_point_tri_2d
from ...addon_common.common.blender import ModifierWrapper_Mirror
from ...addon_common.common.maths import Point, Normal, Direction
from ...addon_common.common.maths import Point2D
from ...addon_common.common.maths import Ray, XForm, BBox, Plane
from ...addon_common.common.hasher import hash_object, Hasher
from ...addon_common.common.utils import min_index, UniqueCounter, iter_pairs, accumulate_last, deduplicate_list, has_duplicates
from ...addon_common.common.decorators import stats_wrapper, blender_version_wrapper
from ...addon_common.common.debug import dprint
from ...addon_common.common.profiler import profiler, time_it
from ...addon_common.terminal import term_printer
from ...config.options import options
from .rfmesh_wrapper import (
BMElemWrapper, RFVert, RFEdge, RFFace, RFEdgeSequence
)
class RFMesh():
'''
RFMesh wraps a mesh object, providing extra machinery such as
- computing hashes on the object (know when object has been modified)
- maintaining a corresponding bmesh and bvhtree of the object
- handling snapping and raycasting
- translates to/from local space (transformations)
'''
create_count = 0
delete_count = 0
def __init__(self):
assert False, 'Do not create new RFMesh directly! Use RFSource.new() or RFTarget.new()'
def __deepcopy__(self, memo):
assert False, 'Do not copy me'
@staticmethod
# @profiler.function
def get_bmesh_from_object(obj, deform=False):
bme = bmesh.new()
if deform:
depsgraph = bpy.context.evaluated_depsgraph_get()
bme.from_object(obj, depsgraph)
else:
bme.from_mesh(obj.data)
return bme
@stats_wrapper
# @profiler.function
def __setup__(
self, obj,
deform=False, bme=None, triangulate=False,
selection=True, keepeme=False
):
# checking for NaNs
# print('RFMesh.__setup__: checking for NaNs')
hasnan = any(
math.isnan(v)
for emv in obj.data.vertices
for v in emv.co
)
if hasnan:
# print('RFMesh.__setup__: Mesh data contains NaN in vertex coordinate! Cleaning and validating mesh...')
obj.data.validate(verbose=True, clean_customdata=False)
else:
# cleaning mesh quietly
# print('skipping mesh validation')
# print('RFMesh.__setup__: validating')
obj.data.validate(verbose=False, clean_customdata=False)
# setup init
self.obj = obj
self.xform = XForm(self.obj.matrix_world)
self.hash = hash_object(self.obj)
self._version = None
self._version_selection = None
if bme is not None:
self.bme = bme
else:
# print('RFMesh.__setup__: creating bmesh from object')
self.bme = self.get_bmesh_from_object(self.obj, deform=deform)
if selection:
# print('RFMesh.__setup__: copying selection')
if True: # with profiler.code('copying selection'):
self.bme.select_mode = {'FACE', 'EDGE', 'VERT'}
# copy selection from editmesh
for bmf, emf in zip(self.bme.faces, self.obj.data.polygons):
bmf.select = emf.select
for bme, eme in zip(self.bme.edges, self.obj.data.edges):
bme.select = eme.select
for bmv, emv in zip(self.bme.verts, self.obj.data.vertices):
bmv.select = emv.select
else:
self.deselect_all()
if triangulate:
# print('RFMesh.__setup__: triangulating')
self.triangulate()
for bmv in self.bme.verts:
if not bmv.is_wire:
bmv.normal_update()
# setup finishing
self.selection_center = Point((0, 0, 0))
self.store_state()
self.dirty()
if False:
term_printer.boxed(
f'{obj.name}',
f'Options: {deform=} {triangulate=} {selection=} {keepeme=}',
f'Counts: v={len(self.bme.verts)} e={len(self.bme.edges)} f={len(self.bme.faces)}',
title=f'RFMesh.setup',
)
def __del__(self):
RFMesh.delete_count += 1
# print('RFMesh.__del__', self, RFMesh.create_count, RFMesh.delete_count)
self.bme.free()
##########################################################
def get_frame(self): return self.xform.to_frame()
def w2l_point(self, p): return self.xform.w2l_point(p)
def w2l_normal(self, n): return self.xform.w2l_normal(n)
def w2l_vec(self, v): return self.xform.w2l_vector(v)
def w2l_direction(self, d): return self.xform.w2l_direction(d)
def l2w_point(self, p): return self.xform.l2w_point(p)
def l2w_normal(self, n): return self.xform.l2w_normal(n)
def l2w_vec(self, v): return self.xform.l2w_vector(v)
def l2w_direction(self, d): return self.xform.l2w_direction(d)
##########################################################
def dirty(self, selectionOnly=False):
if not selectionOnly:
if hasattr(self, 'bvh'): del self.bvh
self._version = UniqueCounter.next()
self._version_selection = UniqueCounter.next()
def clean(self):
pass
def get_version(self, selection=True):
return Hasher(self._version, (self._version_selection if selection else 0))
# @profiler.function
def get_bvh(self):
ver = self.get_version(selection=False)
if not hasattr(self, 'bvh') or self.bvh_version != ver:
self.bvh = BVHTree.FromBMesh(self.bme)
self.bvh_version = ver
return self.bvh
# @profiler.function
def get_bbox(self):
ver = self.get_version(selection=False)
if not hasattr(self, 'bbox') or self.bbox_version != ver:
self.bbox = BBox(from_object=self.obj, xform_point=self.l2w_point)
self.bbox_version = ver
return self.bbox
# @profiler.function
def get_local_bbox(self, w2l_point):
ver = self.get_version(selection=False)
if not hasattr(self, 'local_bbox') or self.local_bbox_version != ver or self.local_w2l_point != w2l_point:
fn = lambda p: w2l_point(self.l2w_point(p))
# self.local_bbox = BBox(from_bmverts=self.bme.verts, xform_point=fn)
self.local_bbox = BBox(from_object=self.obj, xform_point=fn)
self.local_bbox_version = ver
self.local_w2l_point = w2l_point
return self.local_bbox
# @profiler.function
def get_kdtree(self):
ver = self.get_version(selection=False)
if not hasattr(self, 'kdt') or self.kdt_version != ver:
self.kdt = KDTree(len(self.bme.verts))
for i, bmv in enumerate(self.bme.verts):
self.kdt.insert(bmv.co, i)
self.kdt.balance()
self.kdt_version = ver
return self.kdt
def get_geometry_counts(self):
ver = self.get_version(selection=False)
if not hasattr(self, 'geocounts') or self.geocounts_version != ver:
nv = len(self.bme.verts)
ne = len(self.bme.edges)
nf = len(self.bme.faces)
self.geocounts = (nv,ne,nf)
self.geocounts_version = ver
return self.geocounts
##########################################################
def store_state(self):
attributes = ['viewport_hide', 'render_hide'] # list of attributes to remember
self.prev_state = { attr: self.obj_attr_get(attr) for attr in attributes }
def restore_state(self):
for attr, val in self.prev_state.items():
self.obj_attr_set(attr, val)
def get_obj_name(self):
return self.obj.name
def obj_viewport_hide_get(self): return self.obj.hide_viewport
def obj_viewport_hide_set(self, v): self.obj.hide_viewport = v
def obj_select_get(self): return self.obj.select_get()
def obj_select_set(self, v): self.obj.select_set(v)
def obj_render_hide_get(self): return self.obj.hide_render
def obj_render_hide_set(self, v): self.obj.hide_render = v
def obj_viewport_hide(self): self.obj_viewport_hide_set(True)
def obj_viewport_unhide(self): self.obj_viewport_hide_set(False)
def obj_render_hide(self): self.obj_render_hide_set(True)
def obj_render_unhide(self): self.obj_render_hide_set(False)
def obj_select(self): self.obj_select_set(True)
def obj_unselect(self): self.obj_select_set(False)
def obj_attr_get(self, attr): return getattr(self, 'obj_%s_get'%attr)()
def obj_attr_set(self, attr, v): getattr(self, 'obj_%s_set'%attr)(v)
##########################################################
def ensure_lookup_tables(self):
self.bme.verts.ensure_lookup_table()
self.bme.edges.ensure_lookup_table()
self.bme.faces.ensure_lookup_table()
@property
def tag(self):
return self.obj.data.tag
@tag.setter
def tag(self, v):
self.obj.data.tag = v
##########################################################
# @profiler.function
def triangulate(self):
# faces = [face for face in self.bme.faces if len(face.verts) != 3]
# print('RFMesh.triangulate: found %d non-triangles' % len(faces))
# bmesh.ops.triangulate(self.bme, faces=faces)
bmesh.ops.triangulate(self.bme, faces=self.bme.faces)
# @profiler.function
def plane_split(self, plane: Plane):
plane_local = self.xform.w2l_plane(plane)
dist = 0.00000001
geom = (
list(self.bme.verts) +
list(self.bme.edges) +
list(self.bme.faces)
)
bisect_plane(
self.bme,
geom=geom, dist=dist,
plane_co=plane_local.o, plane_no=plane_local.n,
use_snap_center=True,
clear_outer=False, clear_inner=False
)
# @profiler.function
def plane_intersection(self, plane: Plane):
# TODO: do not duplicate vertices!
l2w_point = self.xform.l2w_point
plane_local = self.xform.w2l_plane(plane)
side = plane_local.side
triangle_intersection = plane_local.triangle_intersection
# vert sides
vert_side = {
bmv: side(bmv.co)
for bmv in self.bme.verts
}
# split edges
edges = {
bme
for bme in self.bme.edges
if vert_side[bme.verts[0]] != vert_side[bme.verts[1]]
}
# split faces
faces = {
bmf
for bme in edges
for bmf in bme.link_faces
}
# intersections
yield from (
(l2w_point(p0), l2w_point(p1))
for bmf in faces
for (p0, p1) in triangle_intersection([
bmv.co for bmv in bmf.verts
])
)
def get_xy_plane(self):
o = self.xform.l2w_point(Point((0, 0, 0)))
n = self.xform.l2w_normal(Normal((0, 0, 1)))
return Plane(o, n)
def get_xz_plane(self):
o = self.xform.l2w_point(Point((0, 0, 0)))
n = self.xform.l2w_normal(Normal((0, 1, 0)))
return Plane(o, n)
def get_yz_plane(self):
o = self.xform.l2w_point(Point((0, 0, 0)))
n = self.xform.l2w_normal(Normal((1, 0, 0)))
return Plane(o, n)
# @profiler.function
def _crawl(self, bmf_start, plane):
'''
crawl about RFMesh along plane starting with bmf
returns list of tuples (face0, edge between face0 and face1, face1, intersection of edge and plane)
'''
def intersect_edge(bme):
nonlocal plane
bmv0, bmv1 = bme.verts
return plane.edge_intersection(bmv0.co, bmv1.co)
def intersect_face(bmf):
crosses = [(bme, intersect_edge(bme)) for bme in bmf.edges]
return [(bme, cross) for (bme, cross) in crosses if cross]
def intersected_face(bmf):
nonlocal plane
sides = [plane.side(bmv.co, threshold=0) for bmv in bmf.verts]
if any(s == 0 for s in sides): return True
return any(s0 != s1 for (s0, s1) in iter_pairs(sides, True))
def adjacent_faces(bmf):
return {bmf_adj for bmv in bmf.verts for bmf_adj in bmv.link_faces}
def next_face(bmf, bme):
return next((bmf_other for bmf_other in bme.link_faces if bmf_other != bmf), None)
###########################################################
# find all bmfaces that are connected to bmf_start and the plane intersects
bmfs_intersect = set()
bmfs_touched = set()
bmfs_working = { bmf_start }
while bmfs_working:
bmf = bmfs_working.pop()
if bmf in bmfs_touched: continue
bmfs_touched.add(bmf)
if not intersected_face(bmf): continue
bmfs_intersect.add(bmf)
bmfs_working.update(adjacent_faces(bmf))
# find all bmverts and bmedges that intersect plane and compute the corresponding intersection point
points = {}
bmvs_touched = set()
bmes_touched = set()
for bmf in bmfs_intersect:
points[bmf] = []
for bmv in bmf.verts:
if bmv in points:
pt,l = points[bmv]
points[bmf].append((pt, bmv))
l.append(bmf)
continue
if bmv in bmvs_touched: continue
bmvs_touched.add(bmv)
if plane.side(bmv.co, threshold=0) != 0: continue
pt = bmv.co
points[bmf].append((pt, bmv))
points[bmv] = (pt, [bmf])
for bme in bmf.edges:
if bme in points:
pt,l = points[bme]
points[bmf].append((pt, bme))
l.append(bmf)
if bme in bmes_touched: continue
bmes_touched.add(bme)
v0, v1 = bme.verts
if v0 in points or v1 in points: continue
pt = plane.edge_intersection(v0.co, v1.co, threshold=0)
if not pt: continue
points[bmf].append((pt, bme))
points[bme] = (pt, [bmf])
bmfs_intersect = {bmf for bmf in bmfs_intersect if len(points[bmf]) == 2}
for bmv in bmvs_touched:
if bmv not in points: continue
pt, l = points[bmv]
l = [bmf for bmf in l if bmf in bmfs_intersect]
if len(l) in {1,2}: points[bmv] = (pt, l)
else: del points[bmv]
for bme in bmes_touched:
if bme not in points: continue
pt, l = points[bme]
l = [bmf for bmf in l if bmf in bmfs_intersect]
if len(l) in {1,2}: points[bme] = (pt, l)
else: del points[bme]
if not bmfs_intersect: return [] # something bad happened
if bmf_start not in bmfs_intersect:
# bmf_start must have had only one intersection point, so pick any other to be new bmf_start
bmf_start = next(iter(bmfs_intersect))
# create adjacency graph that we'll use to crawl over
graph = {}
# graph.update({ bmf:adjacent_faces(bmf) for bmf in bmfs_intersect })
graph.update({ bmv:[bmf for bmf in bmv.link_faces if bmf in bmfs_intersect] for bmv in bmvs_touched })
graph.update({ bme:[bmf for bmf in bme.link_faces if bmf in bmfs_intersect] for bme in bmes_touched })
graph = { k:v for (k,v) in graph.items() if v }
ret = []
def crawl(i_current):
nonlocal ret, bmf_start, points, graph
bmf_current = bmf_start
while True:
pt_current, bmelem_current = i_current
bmfs_adj = points[bmelem_current][1] if bmelem_current in points else []
bmf_next = next((bmf_adj for bmf_adj in bmfs_adj if bmf_adj != bmf_current), None)
ret.append((bmf_current, pt_current, bmf_next))
if bmf_next is None: return False
if bmf_next == bmf_start: return True
i0, i1 = points[bmf_next]
i_current = i0 if i_current == i1 else i1
bmf_current = bmf_next
wrapped = crawl(points[bmf_start][0])
if not wrapped:
# did not wrap, so switch directions
ret = [(f1,c,f0) for (f0,c,f1) in reversed(ret)]
crawl(points[bmf_start][1])
return ret
# crosses = intersect_face(bmf_start) # assuming all faces are triangles!
# if len(crosses) != 2: return [] # face does not cross plane
# bme_start0, cross0 = crosses[0]
# bme_start1, cross1 = crosses[1]
# ret = []
# bme_next = bme_start0
# cross = cross0
# def crawl(bme_next, cross_next):
# nonlocal ret, bmf_start
# bmf_current = bmf_start
# while True:
# bmf_next = next_face(bmf_current, bme_next)
# if bmf_next:
# crosses = intersect_face(bmf_next)
# if len(crosses) != 2: bmf_next = None # bmvert of bmf_next lies on plane
# ret += [(bmf_current, bme_next, bmf_next, cross_next)]
# if not bmf_next: return False # cannot continue this direction
# if bmf_next == bmf_start: return True # wrapped around!
# bmf_current = bmf_next
# bme_next, cross_next = next(((e,c) for (e,c) in crosses if e != bme_next), (None, None))
# if not bme_next: return False # something bad happened!
# return False
# wrapped = crawl(bme_start0, cross0) # crawl one direction
# if not wrapped:
# # did not wrap around, so should continue crawling the other way
# ret = [(f1, e, f0, c) for (f0, e, f1, c) in reversed(ret)] # reverse results
# crawl(bme_start1, cross1) # crawl other direction
# return ret
# @profiler.function
def plane_intersection_crawl(self, ray:Ray, plane:Plane, walk_to_plane:bool=False):
'''
intersect object with ray, (possibly) walk to plane, then crawl about
'''
# intersect self with ray
ray,plane = self.xform.w2l_ray(ray),self.xform.w2l_plane(plane)
_,_,i,_ = self.get_bvh().ray_cast(ray.o, ray.d, ray.max)
bmf = self.bme.faces[i]
if walk_to_plane:
# follow link_faces of verts that walk us toward the plane until we find a bmface that crosses/touches
# we have two different greedy implementations. one follows bmfaces and uses a heap; the other greedily
# follows bmedges one at a time.
# https://docs.python.org/3.8/library/heapq.html#priority-queue-implementation-notes
@dataclass(order=True)
class PrioritizedBMV:
dot: float
bmv: BMVert=field(compare=False)
def __init__(self, bmv, dot):
self.dot = dot
self.bmv = bmv
def walk_to_plane_heap(bmf, ignore_touching=False):
'''
this implementation uses a heap (priority queue) to greedily follow link_faces of bmverts.
NOTE: the route taken is not necessarily the shortest in terms of
edge lengths or distance to from initial to final bmf!
if ignore_touching: do not consider bmverts that lie exactly on plane. this is useful because _crawl (above)
assumes that there will be exactly two bmedges of the bmface that cross the plane
'''
bmvs = [bmv for bmv in bmf.verts]
bmvs_dot = [plane.signed_distance_to(bmv.co) for bmv in bmvs] # which side of plane are bmverts?
if max(bmvs_dot) >= 0 and min(bmvs_dot) <= 0: return bmf # bmf crosses/touches plane already!
sign = -1 if bmvs_dot[0] < 0 else 1 # indicates direction that we need to walk
bmv_heap = []
touched = { bmf }
for bmv,bmv_dot in zip(bmvs, bmvs_dot):
heapq.heappush(bmv_heap, PrioritizedBMV(bmv, abs(bmv_dot)))
touched.add(bmv)
while True:
if not bmv_heap: return None
data = heapq.heappop(bmv_heap) # get next bmvert to process
bmv,bmv_dot = data.bmv, data.dot
if bmv_dot <= 0: break # found a vert at or across the plane!
for bmf in bmv.link_faces:
if bmf in touched: continue
touched.add(bmf)
for bmv in bmf.verts:
if bmv in touched: continue
touched.add(bmv)
bmv_dot = plane.signed_distance_to(bmv.co)
bmv_dot = abs(bmv_dot) if ignore_touching else bmv_dot*sign
heapq.heappush(bmv_heap, PrioritizedBMV(bmv, bmv_dot))
# find a bmface adjacent to bmv that crosses the plane
for bmf in bmv.link_faces:
bmvs = [bmv for bmv in bmf.verts]
bmvs_dot = [plane.signed_distance_to(bmv.co) for bmv in bmvs] # which side of plane are bmverts?
if max(bmvs_dot) >= 0 and min(bmvs_dot) <= 0: return bmf # bmf crosses/touches plane!
assert False
def walk_to_plane_single(bmf):
'''
this implementation uses a greedy algorithm to follow link_edges of bmverts.
NOTE: the route taken is not necessarily the shortest in terms of
edge lengths or distance to from initial to final bmf!
'''
bmvs = [bmv for bmv in bmf.verts]
bmvs_dot = [plane.signed_distance_to(bmv.co) for bmv in bmvs]
if max(bmvs_dot) >= 0 and min(bmvs_dot) <= 0:
# bmf crosses plane already
return bmf
idx = min_index(bmvs_dot)
bmv,bmv_dot,sign = bmvs[idx],abs(bmvs_dot[idx]),(-1 if bmvs_dot[idx] < 0 else 1)
touched = set()
while True:
# search all verts that are connected to bmv (via an edge) and find
# the other vert that gets us closer to the plane. if the other vert
# allows us to cross the plane, we're done!
touched.add(bmv)
obmvs = [bme.other_vert(bmv) for bme in bmv.link_edges]
obmvs = [obmv for obmv in obmvs if obmv not in touched]
if not obmvs: return None
obmvs_dot = [plane.signed_distance_to(obmv.co)*sign for obmv in obmvs]
idx = min_index(obmvs_dot)
obmv,obmv_dot = obmvs[idx],obmvs_dot[idx]
if obmv_dot <= 0:
# found plane!
return next(iter(set(bmv.link_faces) & set(obmv.link_faces)))
if obmv_dot > bmv_dot: return None
bmv = obmv
bmv_dot = obmv_dot
bmf = walk_to_plane_heap(bmf) # walk_to_plane_single(bmf)
if not bmf: return None
# crawl about self along plane
ret = self._crawl(bmf, plane)
w,l2w_point = self._wrap,self.xform.l2w_point
ret = [(w(f0),l2w_point(c),w(f1)) for (f0,c,f1) in ret]
return ret
# @profiler.function
def plane_intersections_crawl(self, plane:Plane):
plane = self.xform.w2l_plane(plane)
w,l2w_point = self._wrap,self.xform.l2w_point
# find all faces that cross the plane
# finding all edges crossing plane
dot = plane.n.dot
o = dot(plane.o)
edges = [bme for bme in self.bme.edges if (dot(bme.verts[0].co)-o) * (dot(bme.verts[1].co)-o) <= 0]
# finding faces crossing plane
faces = set(bmf for bme in edges for bmf in bme.link_faces)
# crawling faces along plane
rets = []
touched = set()
for bmf in faces:
if bmf in touched: continue
ret = self._crawl(bmf, plane)
touched |= set(f0 for f0,_,_,_ in ret if f0)
touched |= set(f1 for _,_,f1,_ in ret if f1)
ret = [(w(f0),w(e),w(f1),l2w_point(c)) for f0,e,f1,c in ret]
rets.append(ret)
return rets
##########################################################
@staticmethod
def _wrap(bmelem):
match bmelem:
case None: return None
case BMVert(): return RFVert(bmelem)
case BMEdge(): return RFEdge(bmelem)
case BMFace(): return RFFace(bmelem)
case _: assert False
@staticmethod
def _wrap_bmvert(bmv): return RFVert(bmv)
@staticmethod
def _wrap_bmedge(bme): return RFEdge(bme)
@staticmethod
def _wrap_bmface(bmf): return RFFace(bmf)
@staticmethod
def _unwrap(elem): return elem if not hasattr(elem, 'bmelem') else elem.bmelem
##########################################################
def raycast(self, ray:Ray, *, ignore_backface=False, backface_push=0.00001, max_backface_pushes=20):
ray_local = self.xform.w2l_ray(ray)
for _ in range(max_backface_pushes):
p,n,i,d = self.get_bvh().ray_cast(ray_local.o, ray_local.d, ray_local.max)
if not p: return (None, None, None, None)
if not (ignore_backface and n.dot(ray_local.d) > 0): break
ray_local.max -= (p - ray_local.o).length
ray_local.o = p + ray_local.d * backface_push
else:
return (None, None, None, None)
p_w,n_w = self.xform.l2w_point(p), self.xform.l2w_normal(n)
d_w = (ray.o - p_w).length
if math.isinf(d_w) or math.isnan(d_w): return (None, None, None, None)
return (p_w,n_w,i,d_w)
def raycast_all(self, ray:Ray):
l2w_point,l2w_normal = self.xform.l2w_point,self.xform.l2w_normal
ray_local = self.xform.w2l_ray(ray)
hits = []
origin,direction,maxdist = ray_local.o,ray_local.d,ray_local.max
dist = 0
while True:
p,n,i,d = self.get_bvh().ray_cast(origin, direction, maxdist)
if not p: break
p,n = l2w_point(p),l2w_normal(n)
d = (origin - p).length
dist += d
hits.append((p, n, i, dist))
origin += direction * (d + 0.00001)
maxdist -= d
return hits
# @profiler.function
def raycast_hit(self, ray:Ray, *, ignore_backface=False, backface_push=0.00001, max_backface_pushes=20):
ray_local = self.xform.w2l_ray(ray)
for _ in range(max_backface_pushes):
p,n,i,d = self.get_bvh().ray_cast(ray_local.o, ray_local.d, ray_local.max)
if not p: return False
if not (ignore_backface and n.dot(ray_local.d) > 0): break
ray_local.max -= (p - ray_local.o).length
ray_local.o = p + ray_local.d * backface_push
else:
return False
return True
def nearest(self, point:Point, max_dist=float('inf')): #sys.float_info.max):
point_local = self.xform.w2l_point(point)
p,n,i,_ = self.get_bvh().find_nearest(point_local, max_dist)
if p is None: return (None,None,None,None)
wp,wn = self.xform.l2w_point(p), self.xform.l2w_normal(n)
d = (point - wp).length
return (wp,wn,i,d)
def nearest_bmvert_Point(self, point:Point, verts=None):
if verts is None:
verts = [bmv for bmv in self.bme.verts if bmv.is_valid and not bmv.hide]
else:
verts = [self._unwrap(bmv) for bmv in verts if bmv.is_valid and not bmv.hide]
point_local = self.xform.w2l_point(point)
bv,bd = None,None
for bmv in verts:
d3d = (bmv.co - point_local).length
if bv is None or d3d < bd: bv,bd = bmv,d3d
bmv_world = self.xform.l2w_point(bv.co)
return (self._wrap_bmvert(bv),(point-bmv_world).length)
def nearest_bmverts_Point(self, point:Point, dist3d:float, bmverts=None):
nearest = []
unwrap = bmverts is not None
for bmv in (bmverts or self.bme.verts):
if bmv.hide: continue
if not bmv.is_valid: continue
if unwrap: bmv = self._unwrap(bmv)
bmv_world = self.xform.l2w_point(bmv.co)
d3d = (bmv_world - point).length
if d3d > dist3d: continue
nearest.append((self._wrap_bmvert(bmv), d3d))
return nearest
def nearest_bmedge_Point(self, point:Point, edges=None):
if edges is None:
edges = [bme for bme in self.bme.edges if bme.is_valid and not bme.hide]
else:
edges = [self._unwrap(bme) for bme in edges if bme.is_valid and not bme.hide]
l2w_point = self.xform.l2w_point
be,bd,bpp = None,None,None
for bme in self.bme.edges:
bmv0,bmv1 = l2w_point(bme.verts[0].co), l2w_point(bme.verts[1].co)
diff = bmv1 - bmv0
l = diff.length
d = diff / l
pp = bmv0 + d * max(0, min(l, (point - bmv0).dot(d)))
dist = (point - pp).length
if be is None or dist < bd: be,bd,bpp = bme,dist,pp
if be is None: return (None,None)
return (self._wrap_bmedge(be), (point-self.xform.l2w_point(bpp)).length)
def nearest_bmedges_Point(self, point:Point, dist3d:float):
l2w_point = self.xform.l2w_point
nearest = []
for bme in self.bme.edges:
if not bme.is_valid: continue
if bme.hide: continue
bmv0,bmv1 = l2w_point(bme.verts[0].co), l2w_point(bme.verts[1].co)
diff = bmv1 - bmv0
l = diff.length
d = diff / l
pp = bmv0 + d * max(0, min(l, (point - bmv0).dot(d)))
dist = (point - pp).length
if dist > dist3d: continue
nearest.append((self._wrap_bmedge(bme), dist))
return nearest
def nearest2D_bmverts_Point2D(self, xy:Point2D, dist2D:float, Point_to_Point2Ds, *, verts=None, fwd=None):
# TODO: compute distance from camera to point
# TODO: sort points based on 3d distance
if verts is None:
verts = [bmv for bmv in self.bme.verts if bmv.is_valid and not bmv.hide]
else:
verts = [self._unwrap(bmv) for bmv in verts if bmv.is_valid and not bmv.hide]
l2w_point, l2w_normal = self.xform.l2w_point, self.xform.l2w_normal
nearest = []
for bmv in verts:
co, no = l2w_point(bmv.co), l2w_normal(bmv.normal)
for p2d in Point_to_Point2Ds(co, no, fwd=fwd):
if p2d is None: continue
if (p2d - xy).length > dist2D: continue
d3d = 0
nearest.append((self._wrap_bmvert(bmv), d3d))
return nearest
def nearest2D_bmvert_Point2D(self, xy:Point2D, Point_to_Point2Ds, *, verts=None, max_dist=None, fwd=None):
if not max_dist or max_dist < 0: max_dist = float('inf')
# TODO: compute distance from camera to point
# TODO: sort points based on 3d distance
if verts is None:
verts = [bmv for bmv in self.bme.verts if bmv.is_valid and not bmv.hide]
else:
verts = [self._unwrap(bmv) for bmv in verts if bmv.is_valid and not bmv.hide]
l2w_point, l2w_normal = self.xform.l2w_point, self.xform.l2w_normal
bv,bd = None,None
for bmv in verts:
co, no = l2w_point(bmv.co), l2w_normal(bmv.normal)
for p2d in Point_to_Point2Ds(co, no, fwd=fwd):
if p2d is None: continue
d2d = (xy - p2d).length
if d2d > max_dist: continue
if bv is None or d2d < bd: bv,bd = bmv,d2d
if bv is None: return (None,None)
return (self._wrap_bmvert(bv),bd)
def nearest2D_bmedges_Point2D(self, xy:Point2D, dist2D:float, Point_to_Point2Ds, *, edges=None, shorten=0.01, fwd=None):
# TODO: compute distance from camera to point
# TODO: sort points based on 3d distance
if edges is None:
edges = [bme for bme in self.bme.edges if bme.is_valid and not bme.hide]
else:
edges = [self._unwrap(bme) for bme in edges if bme.is_valid and not bme.hide]
l2w_point, l2w_normal = self.xform.l2w_point, self.xform.l2w_normal
nearest = []
dist2D2 = dist2D**2
s0,s1 = shorten/2,1-shorten/2
for bme in edges:
bmv0, bmv1 = bme.verts
co0, no0 = l2w_point(bmv0.co), l2w_normal(bmv0.normal)
co1, no1 = l2w_point(bmv1.co), l2w_normal(bmv1.normal)
for v0, v1 in zip(Point_to_Point2Ds(co0, no0, fwd=fwd), Point_to_Point2Ds(co1, no1, fwd=fwd)):
if not v0 or not v1: continue
l = v0.distance_to(v1)
if l == 0:
pp = v0
else:
d = (v1 - v0) / l
pp = v0 + d * max(l*s0, min(l*s1, d.dot(xy-v0)))
dist2 = pp.distance_squared_to(xy)
if dist2 > dist2D2: continue
nearest.append((self._wrap_bmedge(bme), math.sqrt(dist2)))
return nearest
def nearest2D_bmedge_Point2D(self, xy:Point2D, Point_to_Point2Ds, *, edges=None, shorten=0.01, max_dist=None, fwd=None):
if not max_dist or max_dist < 0: max_dist = float('inf')
if edges is None:
edges = [bme for bme in self.bme.edges if bme.is_valid and not bme.hide]
else:
edges = [self._unwrap(bme) for bme in edges if bme.is_valid and not bme.hide]
l2w_point, l2w_normal = self.xform.l2w_point, self.xform.l2w_normal
be,bd,bpp = None,None,None
for bme in edges:
bmv0, bmv1 = bme.verts
co0, no0 = l2w_point(bmv0.co), l2w_normal(bmv0.normal)
co1, no1 = l2w_point(bmv1.co), l2w_normal(bmv1.normal)
for v0, v1 in zip(Point_to_Point2Ds(co0, no0, fwd=fwd), Point_to_Point2Ds(co1, no1, fwd=fwd)):
if v0 is None or v1 is None: continue
diff = v1 - v0
l = diff.length
if l == 0:
dist = (xy - v0).length
pp = v0
else:
d = diff / l
margin = l * shorten / 2
pp = v0 + d * max(margin, min(l-margin, (xy - v0).dot(d)))
dist = (xy - pp).length
if dist > max_dist: continue
if be is None or dist < bd: be,bd,bpp = bme,dist,pp
if be is None: return (None,None)
return (self._wrap_bmedge(be), (xy-bpp).length)
def nearest2D_bmfaces_Point2D(self, xy:Point2D, Point_to_Point2Ds, *, faces=None, fwd=None):
# TODO: compute distance from camera to point
# TODO: sort points based on 3d distance
if faces is None:
faces = [bmf for bmf in self.bme.faces if bmf.is_valid and not bmf.hide]
else:
faces = [self._unwrap(bmf) for bmf in faces if bmf.is_valid and not bmf.hide]
l2w_point, l2w_normal = self.xform.l2w_point, self.xform.l2w_normal
nearest = []
for bmf in faces:
ptsets = [Point_to_Point2Ds(l2w_point(bmv.co), l2w_normal(bmv.normal), fwd=fwd) for bmv in bmf.verts]
ptsets = list(zip(*ptsets))
for pts in ptsets:
pts = [pt for pt in pts if pt]
if len(pts) < 3: continue
pt0 = pts[0]
# TODO: Get dist?
for pt1,pt2 in zip(pts[1:-1],pts[2:]):
if intersect_point_tri_2d(xy, pt0, pt1, pt2):
nearest.append((self._wrap_bmface(bmf), dist))
return nearest
def nearest2D_bmface_Point2D(self, forward:Direction, xy:Point2D, Point_to_Point2Ds, *, faces=None, fwd=None):
# TODO: compute distance from camera to point
# TODO: sort points based on 3d distance
if faces is None:
faces = [bmf for bmf in self.bme.faces if bmf.is_valid and not bmf.hide]
else:
faces = [self._unwrap(bmf) for bmf in faces if bmf.is_valid and not bmf.hide]
l2w_point, l2w_normal = self.xform.l2w_point, self.xform.l2w_normal
bv,bd = None,None
best_d = float('inf')
best_f = None
for bmf in faces:
ptsets = [Point_to_Point2Ds(l2w_point(bmv.co), l2w_normal(bmv.normal), fwd=fwd) for bmv in bmf.verts]
ptsets = list(zip(*ptsets))
for pts in ptsets:
pts = [pt for pt in pts if pt]
if len(pts) < 3: continue
pt0 = pts[0]
for pt1,pt2 in zip(pts[1:-1],pts[2:]):
if intersect_point_tri_2d(xy, pt0, pt1, pt2):
f = self._wrap_bmface(bmf)
d = forward.dot(f.center())
if d < best_d: best_d, best_f = d, f
if not best_f: return (None, None)
return (best_f, 0)
##########################################################
fn_is_valid = lambda bmelem: bmelem.is_valid
fn_is_hidden = lambda bmelem: bmelem.is_valid and bmelem.hide
fn_is_revealed = lambda bmelem: not bmelem.hide
fn_is_valid_revealed = lambda bmelem: bmelem.is_valid and not bmelem.hide
fn_is_selected = lambda bmelem: bmelem.is_valid and bmelem.select
fn_is_unselected = lambda bmelem: bmelem.is_valid and not bmelem.select
fn_is_selected_revealed = lambda bmelem: bmelem.is_valid and bmelem.select and not bmelem.hide
fn_is_unselected_revealed = lambda bmelem: bmelem.is_valid and not bmelem.select and not bmelem.hide
def _iter_visible_verts(self, is_vis, bmvs=None):
if bmvs is None: bmvs = self.bme.verts
return filter(is_vis, filter(RFMesh.fn_is_revealed, bmvs))
def _iter_visible_edges(self, is_vis, bmvs=None, bmes=None):
if bmvs is None: bmvs = set(self._iter_visible_verts(is_vis))
if bmes is None: bmes = self.bme.edges
has_vis_verts = lambda bme: all(bmv in bmvs for bmv in bme.verts)
return filter(has_vis_verts, filter(RFMesh.fn_is_revealed, bmes))
def _iter_visible_faces(self, is_vis, bmvs=None):
if bmvs is None: bmvs = set(self._iter_visible_verts(is_vis))
has_vis_verts = lambda bmf: all(bmv in bmvs for bmv in bmf.verts)
return filter(has_vis_verts, filter(RFMesh.fn_is_revealed, self.bme.faces))
def _gen_is_vis(self, is_visible):
l2w_point, l2w_normal = self.xform.l2w_point, self.xform.l2w_normal
m = 0.002 * options['normal offset multiplier']
is_valid_revealed = RFMesh.fn_is_valid_revealed
def is_vis(bmv):
if not is_valid_revealed(bmv): return False
p, n = l2w_point(bmv.co), l2w_normal(bmv.normal)
return is_visible(p, n) or is_visible(p + m * n, n)
return is_vis
def visible_verts(self, is_visible, verts=None):
is_vis = self._gen_is_vis(is_visible)
verts = self.bme.verts if verts is None else map(self._unwrap, verts)
return { self._wrap_bmvert(bmv) for bmv in filter(is_vis, verts) }
def visible_edges(self, is_visible, verts=None, edges=None):
is_valid = RFMesh.fn_is_valid
is_vert_vis = self._gen_is_vis(is_visible)
is_edge_vis = lambda bme: is_valid(bme) and all(bmv in verts for bmv in bme.verts)
verts = set(filter(is_vert_vis, self.bme.verts) if verts is None else filter(is_valid, map(self._unwrap, verts)))
edges = self.bme.edges if edges is None else map(self._unwrap, edges)
return { self._wrap_bmedge(bme) for bme in filter(is_edge_vis, edges) }
def visible_faces(self, is_visible, verts=None, faces=None):
is_valid = RFMesh.fn_is_valid
is_vert_vis = self._gen_is_vis(is_visible)
is_face_vis = lambda bme: is_valid(bme) and all(bmv in verts for bmv in bme.verts)
verts = set(filter(is_vert_vis, self.bme.verts) if verts is None else filter(is_valid, map(self._unwrap, verts)))
faces = self.bme.faces if faces is None else map(self._unwrap, faces)
return { self._wrap_bmface(bme) for bme in filter(is_face_vis, faces) }
##########################################################
def iter_wrap(self, bmelems, *, wrap_fn=None):
if wrap_fn is None: wrap_fn = self._wrap
yield from map(wrap_fn, bmelems)
def set_wrap(self, bmelems, *, wrap_fn=None):
if wrap_fn is None: wrap_fn = self._wrap
return { wrap_fn(bmelem) for bmelem in bmelems }
def list_wrap(self, bmelems, *, wrap_fn=None):
if wrap_fn is None: wrap_fn = self._wrap
return [ wrap_fn(bmelem) for bmelem in bmelems ]
def iter_verts(self): yield from self.iter_wrap(filter(RFMesh.fn_is_valid_revealed, self.bme.verts), wrap_fn=self._wrap_bmvert)
def iter_edges(self): yield from self.iter_wrap(filter(RFMesh.fn_is_valid_revealed, self.bme.edges), wrap_fn=self._wrap_bmedge)
def iter_faces(self): yield from self.iter_wrap(filter(RFMesh.fn_is_valid_revealed, self.bme.faces), wrap_fn=self._wrap_bmvert)
def get_verts(self): return list(self.iter_verts())
def get_edges(self): return list(self.iter_edges())
def get_faces(self): return list(self.iter_faces())
def get_vert_count(self): return len(self.bme.verts)
def get_edge_count(self): return len(self.bme.edges)
def get_face_count(self): return len(self.bme.faces)
# NOTE: self.bme.select_history does _NOT_ work
def get_selected_verts(self): return set(map(self._wrap_bmvert, filter(RFMesh.fn_is_selected_revealed, self.bme.verts)))
def get_selected_edges(self): return set(map(self._wrap_bmedge, filter(RFMesh.fn_is_selected_revealed, self.bme.edges)))
def get_selected_faces(self): return set(map(self._wrap_bmface, filter(RFMesh.fn_is_selected_revealed, self.bme.faces)))
def get_unselected_verts(self): return set(map(self._wrap_bmvert, filter(RFMesh.fn_is_unselected_revealed, self.bme.verts)))
def get_unselected_edges(self): return set(map(self._wrap_bmedge, filter(RFMesh.fn_is_unselected_revealed, self.bme.edges)))
def get_unselected_faces(self): return set(map(self._wrap_bmface, filter(RFMesh.fn_is_unselected_revealed, self.bme.faces)))
def get_hidden_verts(self): return set(map(self._wrap_bmvert, filter(RFMesh.fn_is_hidden, self.bme.verts)))
def get_hidden_edges(self): return set(map(self._wrap_bmedge, filter(RFMesh.fn_is_hidden, self.bme.edges)))
def get_hidden_faces(self): return set(map(self._wrap_bmface, filter(RFMesh.fn_is_hidden, self.bme.faces)))
def get_revealed_verts(self): return set(map(self._wrap_bmvert, filter(RFMesh.fn_is_valid_revealed, self.bme.verts)))
def get_revealed_edges(self): return set(map(self._wrap_bmedge, filter(RFMesh.fn_is_valid_revealed, self.bme.edges)))
def get_revealed_faces(self): return set(map(self._wrap_bmface, filter(RFMesh.fn_is_valid_revealed, self.bme.faces)))
def any_verts_selected(self): return any(bmv.select for bmv in self.bme.verts if bmv.is_valid and not bmv.hide)
def any_edges_selected(self): return any(bme.select for bme in self.bme.edges if bme.is_valid and not bme.hide)
def any_faces_selected(self): return any(bmf.select for bmf in self.bme.faces if bmf.is_valid and not bmf.hide)
def any_selected(self): return self.any_verts_selected() or self.any_edges_selected() or self.any_faces_selected()
def get_selection_center(self):
v,c = Vector(),0
for bmv in self.bme.verts:
if not bmv.select or not bmv.is_valid: continue
v += bmv.co
c += 1
if c: self.selection_center = v / c
return self.xform.l2w_point(self.selection_center)
def get_selection_bbox(self):
l2w_point = self.xform.l2w_point
coords = [l2w_point(bmv.co) for bmv in self.bme.verts if bmv.is_valid and bmv.select]
#if not coords: return self.get_bbox()
return BBox(from_coords=coords)
def deselect_all(self):
for bmv in self.bme.verts: bmv.select = False
for bme in self.bme.edges: bme.select = False
for bmf in self.bme.faces: bmf.select = False
self.dirty(selectionOnly=True)
def deselect(self, elems, supparts=True, subparts=True):
if elems is None: return
if not hasattr(elems, '__len__'): elems = [elems]
elems = { e for e in elems if e and e.is_valid }
nelems = set(elems)
if supparts:
for elem in elems:
t = type(elem)
if t is BMVert or t is RFVert:
nelems.update(elem.link_edges)
nelems.update(elem.link_faces)
elif t is BMEdge or t is RFEdge:
nelems.update(elem.link_faces)
elif t is BMFace or t is RFFace:
pass
nelems = { e for e in nelems if e.select }
selems = set()
for elem in nelems-elems:
t = type(elem)
if t is BMEdge or t is RFEdge:
selems.update(elem.verts)
elif t is BMFace or t is RFFace:
selems.update(elem.verts)
selems.update(e for e in elem.edges if not (set(e.verts)&elems))
selems = selems - elems
selems = { e for e in selems if e.select }
for elem in nelems: elem.select = False
for elem in selems: elem.select = True
if subparts:
nelems = set()
for elem in elems:
t = type(elem)
if t is BMFace or t is RFFace:
for bme in elem.edges:
if not bme.select: continue
if any(f.select for f in bme.link_faces): continue
nelems.add(bme)
for bmv in elem.verts:
if not bmv.select: continue
if any(e.select for e in bmv.link_edges): continue
if any(f.select for f in bmv.link_faces): continue
nelems.add(bmv)
if t is BMEdge or t is RFEdge:
for bmv in elem.verts:
if not bmv.select: continue
if any(e.select for e in bmv.link_edges): continue
if any(f.select for f in bmv.link_faces): continue
nelems.add(bmv)
for elem in nelems:
elem.select = False
self.dirty(selectionOnly=True)
def select(self, elems, supparts=True, subparts=True, only=True):
if only: self.deselect_all()
if elems is None: return
if not hasattr(elems, '__len__'): elems = [elems]
elems = [e for e in elems if e and e.is_valid]
if subparts:
nelems = set(elems)
for elem in elems:
t = type(elem)
if t is BMVert or t is RFVert:
pass
elif t is BMEdge or t is RFEdge:
nelems.update(e for e in elem.verts)
elif t is BMFace or t is RFFace:
nelems.update(e for e in elem.verts)
nelems.update(e for e in elem.edges)
elems = nelems
for elem in elems: elem.select = True
if supparts:
for elem in elems:
t = type(elem)
if t is not BMVert and t is not RFVert: continue
for bme in elem.link_edges:
if all(bmv.select for bmv in bme.verts):
bme.select = True
for bmf in elem.link_faces:
if all(bmv.select for bmv in bmf.verts):
bmf.select = True
self.dirty(selectionOnly=True)
def get_quadwalk_edgesequence(self, edge):
bme = self._unwrap(edge)
touched = set()
edges = []
def crawl(bme0, bmv01):
nonlocal edges
if bme0 not in touched: edges.append(bme0)
if bmv01 in touched: return True # wrapped around the loop
touched.add(bmv01)
touched.add(bme0)
if len(bmv01.link_edges) > 4: return False
if len(bmv01.link_faces) > 4: return False
bmf0 = bme0.link_faces
for bme1 in bmv01.link_edges:
if any(f in bmf0 for f in bme1.link_faces): continue
bmv2 = bme1.other_vert(bmv01)
return crawl(bme1, bmv2)
return False
if not crawl(bme, bme.verts[0]):
# did not loop back around, so go other direction
edges.reverse()
crawl(bme, bme.verts[1])
return RFEdgeSequence(edges)
def _crawl_quadstrip_next(self, bme0, bmf0):
bmes = set(bmf0.edges) - { bme for bmv in bme0.verts for bme in bmv.link_edges }
if len(bmes) != 1: return (None,None)
bme1 = bmes.pop()
bmf1 = next(iter(set(bme1.link_faces) - { bmf0 }), None)
return (bme1, bmf1)
def _are_edges_flipped(self, bme0, bme1):
bmv00,bmv01 = bme0.verts
bmv10,bmv11 = bme1.verts
return ((bmv01.co - bmv00.co).dot(bmv11.co - bmv10.co)) < 0
def _crawl_quadstrip_to_loopend(self, bme_start, bmf_start=None):
'''
returns tuple (bme, flipped, bmf, looped) where bme is
1. at one end of a quad strip (looped == False), or
2. bme is bme0 because quad strip is loop (looped == True)
bmf is the next face going back (for retracing)
flipped indicates if bme is revered wrt to bme_start
'''
# choose one of the faces
if not bmf_start: bmf_start = next(iter(bme_start.link_faces), None)
if not bmf_start: return (None, False, None, False)
bme0,bmf0,flipped = bme_start,bmf_start,False
touched = set() # just in case!
'''
....
O--O
| |
O--O <- bme0
| | <- bmf0
O--O <- bme1
| | <- bmf1
O--O
....
O--O <- bme0'
| | <- bmf0'
O--O <- bme1', which is end of quad-strip!
'''
while bme0 not in touched:
touched.add(bme0)
bme1,bmf1 = self._crawl_quadstrip_next(bme0, bmf0)
if not bme1:
# bmf0 is not None, but couldn't find bme1, means that we bmf0 is not a quad
bmf_prev = next(iter(set(bme0.link_faces) - { bmf0 }), None)
return (bme0, flipped, bmf_prev, False)
if self._are_edges_flipped(bme0, bme1): flipped = not flipped
if not bmf1:
# hit end of quad-strip
return (bme1, flipped, bmf0, False)
if bme1 == bme_start:
# looped back around
return (bme_start, False, bmf_start, True)
bme0,bmf0 = bme1,bmf1
# we wrapped back around
return (bme0, flipped, bmf0, True)
def is_quadstrip_looped(self, edge):
edge = self._unwrap(edge)
_,_,_,looped = self._crawl_quadstrip_to_loopend(edge)
return looped
def iter_quadstrip(self, edge):
# crawl around until either 1) loop back around, or 2) hit end
# then, go back the other direction
# note: the bmesh may change while crawling!
edge = self._unwrap(edge)
bme,flipped,bmf,looped = self._crawl_quadstrip_to_loopend(edge)
if not bme: return
bme_start = bme
bmf_start = bmf
touched = set()
while bmf not in touched and bme not in touched:
touched.add(bmf)
touched.add(bme)
# find next bme and bmf, in case bmesh is edited!
if bmf: bme_next,bmf_next = self._crawl_quadstrip_next(bme, bmf)
yield (self._wrap_bmedge(bme), flipped)
if not bmf: break
if not bme_next: break
if bme_next == bme_start: break
if bmf_next == bmf_start: break
if self._are_edges_flipped(bme, bme_next): flipped = not flipped
bme,bmf = bme_next,bmf_next
def get_face_loop(self, edge):
r'''
+-- this diamond quad causes problems!
|
V
O-----O-----O
| / \ |
O---O O---O
/ \ / \
/ O \
/ / \ \
O---O O---O
\ \ / /
\ O /
\ | /
\ | /
\|/
O
'''
is_looped = self.is_quadstrip_looped(edge)
edges = list(bme for bme,_ in self.iter_quadstrip(edge))
return (edges, is_looped)
def get_edge_loop(self, edge):
touched = set()
edges = [edge]
r'''
description of crawl(bme0, bmv01) below...
given: bme0=A, bmv01=B
find: bme1=C, bmv12=D
O-----O-----O... O-----O-----O...
| | | | | |
O--A--B--C--D... O--A--B--C--O...
| ^0 | ^1 | | |\
O-----O-----O... O-----O O...
\|
O...
crawl dir: ======>
left : "normal" case, where B is part of 4 touching quads
right: here, find the edge with the direction most similarly
pointing in same direction
'''
def crawl(bme0, bmv01):
nonlocal edges, touched
while True:
bme1 = bme0.get_next_edge_in_strip(bmv01)
if not bme1:
# could not find next edge to continue crawling
# hit edge of mesh?
return False
if bme1 in touched:
# wrapped around (edge loop)!
# NOTE: should trim off any strip not part of loop? ex: P-shaped
return True
edges.append(bme1)
touched.add(bme1)
bmv01 = bme1.other_vert(bmv01)
bme0 = bme1
loop = crawl(edge, edge.verts[0])
if not loop:
# edge strip
edges.reverse()
loop = crawl(edge, edge.verts[1])
return (edges, loop)
def get_inner_edge_loop(self, edge):
# returns edge loop that follows the inside, boundary
bme = self._unwrap(edge)
if len(bme.link_faces) != 1: return ([], False)
touched = set()
edges = []
def crawl(bme0, bmv01):
nonlocal edges
if bme0 not in touched: edges.append(self._wrap_bmedge(bme0))
if bmv01 in touched: return True
touched.add(bmv01)
touched.add(bme0)
bmf0 = bme0.link_faces
for bme1 in bmv01.link_edges:
if bme1 == bme0: continue
if len(bme1.link_faces) != 1: continue
if any(f in bmf0 for f in bme1.link_faces): continue
bmv2 = bme1.other_vert(bmv01)
return crawl(bme1, bmv2)
return False
if crawl(bme, bme.verts[0]): return (edges, True)
edges.reverse()
crawl(bme, bme.verts[1])
return (edges, False)
def select_all(self):
for bmv in self.bme.verts: bmv.select = True
for bme in self.bme.edges: bme.select = True
for bmf in self.bme.faces: bmf.select = True
self.dirty(selectionOnly=True)
def select_toggle(self):
sel = self.any_verts_selected() or self.any_edges_selected() or self.any_faces_selected()
if sel: self.deselect_all()
else: self.select_all()
def select_invert(self):
if True:
sel_verts = [bmv for bmv in self.bme.verts if not bmv.select]
for bmf in self.bme.faces: bmf.select = all(bmv in sel_verts for bmv in bmf.verts)
for bme in self.bme.edges: bme.select = all(bmv in sel_verts for bmv in bme.verts)
for bmv in self.bme.verts: bmv.select = (bmv in sel_verts)
else:
for bmv in self.bme.verts: bmv.select = not bmv.select
for bme in self.bme.edges: bme.select = not bme.select
for bmf in self.bme.faces: bmf.select = not bmf.select
self.dirty()
def select_linked(self, *, select=True, connected_to=None):
if connected_to is None:
# if None, use current selection
working = set(self.get_selected_verts())
elif type(connected_to) is set or type(connected_to) is list:
working = set(connected_to)
elif isinstance(connected_to, RFVert) or isinstance(connected_to, RFEdge) or isinstance(connected_to, RFFace):
working = { connected_to }
else:
assert False, f'Unhandled type of connected_to: {connected_to}'
pworking, working = working, set()
for e in pworking:
if isinstance(e, RFVert) or isinstance(e, BMVert):
working.add(e)
else:
for v in e.verts:
working.add(v)
linked_verts = set(working)
while working:
bmv = working.pop()
for bme in bmv.link_edges:
bmvo = bme.other_vert(bmv)
if bmvo in linked_verts: continue
working.add(bmvo)
linked_verts.add(bmvo)
for bmv in linked_verts:
bmv.select = select
for bme in bmv.link_edges:
bme.select = select
for bmf in bmv.link_faces:
bmf.select = select
self.dirty()
class RFSource(RFMesh):
'''
RFSource is a source object for RetopoFlow. Source objects
are the high-resolution meshes being retopologized.
'''
__cache = {}
@staticmethod
# @profiler.function
def new(obj:bpy.types.Object):
# TODO: REIMPLEMENT CACHING!!
# HAD TO DISABLE THIS BECAUSE 2.83 AND 2.90 WOULD CRASH
# WHEN RESTARTING RF. PROBABLY DUE TO HOLDING REFS TO
# OLD DATA (CRASH DUE TO FREEING INVALID DATA??)
assert type(obj) is bpy.types.Object and type(obj.data) is bpy.types.Mesh, 'obj must be mesh object'
# check cache
rfsource = None
if False:
if obj.data.name in RFSource.__cache:
# does cache match current state?
rfsource = RFSource.__cache[obj.data.name]
hashed = hash_object(obj)
if rfsource.hash != hashed:
rfsource = None
if not rfsource:
# need to (re)generate RFSource object
RFSource.creating = True
rfsource = RFSource()
del RFSource.creating
rfsource.__setup__(obj)
RFSource.__cache[obj.data.name] = rfsource
else:
rfsource = RFSource.__cache[obj.data.name]
else:
RFSource.creating = True
rfsource = RFSource()
del RFSource.creating
rfsource.__setup__(obj)
return rfsource
def __init__(self):
assert hasattr(RFSource, 'creating'), 'Do not create new RFSource directly! Use RFSource.new()'
RFMesh.create_count += 1
# print('RFSource.__init__', RFMesh.create_count, RFMesh.delete_count)
def __setup__(self, obj:bpy.types.Object):
super().__setup__(obj, deform=True, triangulate=True, selection=False, keepeme=True)
self.mirror_mod = None
self.ensure_lookup_tables()
def __str__(self):
return '<RFSource %s>' % self.obj.name
@property
def layer_pin(self):
return None
class RFTarget(RFMesh):
'''
RFTarget is a target object for RetopoFlow. Target objects
are the low-resolution, retopologized meshes.
'''
@staticmethod
# @profiler.function
def new(obj:bpy.types.Object, unit_scaling_factor):
assert type(obj) is bpy.types.Object and type(obj.data) is bpy.types.Mesh, f'{obj} must be mesh object'
RFTarget.creating = True
rftarget = RFTarget()
del RFTarget.creating
rftarget.__setup__(obj, unit_scaling_factor=unit_scaling_factor)
rftarget.rewrap()
return rftarget
def __init__(self):
assert hasattr(RFTarget, 'creating'), 'Do not create new RFTarget directly! Use RFTarget.new()'
RFMesh.create_count += 1
# print('RFTarget.__init__', RFMesh.create_count, RFMesh.delete_count)
def __str__(self):
return '<RFTarget %s>' % self.obj.name
def __setup__(self, obj:bpy.types.Object, unit_scaling_factor:float, rftarget_copy=None):
bme = rftarget_copy.bme.copy() if rftarget_copy else None
xy_symmetry_accel = rftarget_copy.xy_symmetry_accel if rftarget_copy else None
xz_symmetry_accel = rftarget_copy.xz_symmetry_accel if rftarget_copy else None
yz_symmetry_accel = rftarget_copy.yz_symmetry_accel if rftarget_copy else None
super().__setup__(obj, bme=bme, deform=False)
# if Mirror modifier is attached, set up symmetry to match
self.setup_mirror()
self.setup_displace()
self.editmesh_version = None
self.xy_symmetry_accel = xy_symmetry_accel
self.xz_symmetry_accel = xz_symmetry_accel
self.yz_symmetry_accel = yz_symmetry_accel
self.unit_scaling_factor = unit_scaling_factor
@property
def layer_pin(self):
il = self.bme.verts.layers.int
return il['pin'] if 'pin' in il else il.new('pin')
def setup_mirror(self):
self.mirror_mod = ModifierWrapper_Mirror.get_from_object(self.obj)
if not self.mirror_mod:
self.mirror_mod = ModifierWrapper_Mirror.create_new(self.obj)
def setup_displace(self):
self.displace_mod = None
self.displace_strength = 0.020
for mod in self.obj.modifiers:
if mod.type == 'DISPLACE':
self.displace_mod = mod
self.displace_strength = mod.strength
if not self.displace_mod:
bpy.ops.object.modifier_add(type='DISPLACE')
self.displace_mod = self.obj.modifiers[-1]
self.displace_mod.show_expanded = False
self.displace_mod.strength = self.displace_strength
self.displace_mod.show_render = False
self.displace_mod.show_viewport = False
def set_symmetry_accel(self, xy_symmetry_accel, xz_symmetry_accel, yz_symmetry_accel):
self.xy_symmetry_accel = xy_symmetry_accel
self.xz_symmetry_accel = xz_symmetry_accel
self.yz_symmetry_accel = yz_symmetry_accel
def get_point_symmetry(self, point, from_world=True):
if from_world: point = self.xform.w2l_point(point)
px,py,pz = point
threshold = self.mirror_mod.symmetry_threshold * self.unit_scaling_factor / 2.0
symmetry = set()
if self.mirror_mod.x and px <= threshold: symmetry.add('x')
if self.mirror_mod.y and -py <= threshold: symmetry.add('y')
if self.mirror_mod.z and pz <= threshold: symmetry.add('z')
return symmetry
def check_symmetry(self):
threshold = self.mirror_mod.symmetry_threshold * self.unit_scaling_factor / 2.0
ret = list()
if self.mirror_mod.x and any(bmv.co.x < -threshold for bmv in self.bme.verts): ret.append('X')
if self.mirror_mod.y and any(bmv.co.y > threshold for bmv in self.bme.verts): ret.append('Y')
if self.mirror_mod.z and any(bmv.co.z < -threshold for bmv in self.bme.verts): ret.append('Z')
return ret
def select_bad_symmetry(self):
threshold = self.mirror_mod.symmetry_threshold * self.unit_scaling_factor / 2.0
for bmv in self.bme.verts:
if self.mirror_mod.x and bmv.co.x < -threshold: bmv.select = True
if self.mirror_mod.y and bmv.co.y > threshold: bmv.select = True
if self.mirror_mod.z and bmv.co.z < -threshold: bmv.select = True
def snap_to_symmetry(self, point, symmetry, from_world=True, to_world=True):
if not symmetry and from_world == to_world: return point
if from_world: point = self.xform.w2l_point(point)
if symmetry:
dist = lambda p: (p - point).length_squared
px,py,pz = point
if 'x' in symmetry:
edges = self.yz_symmetry_accel.get_edges(Point2D((py, pz)), -px)
point = min((e.closest(point) for e in edges), key=dist, default=Point((0, py, pz)))
px,py,pz = point
if 'y' in symmetry:
edges = self.xz_symmetry_accel.get_edges(Point2D((px, pz)), py)
point = min((e.closest(point) for e in edges), key=dist, default=Point((px, 0, pz)))
px,py,pz = point
if 'z' in symmetry:
edges = self.xy_symmetry_accel.get_edges(Point2D((px, py)), -pz)
point = min((e.closest(point) for e in edges), key=dist, default=Point((px, py, 0)))
px,py,pz = point
if to_world: point = self.xform.l2w_point(point)
return point
def symmetry_real(self, point:Point, from_world=True, to_world=True):
if from_world: point = self.xform.w2l_point(point)
dist = lambda p: (p - point).length_squared
px,py,pz = point
threshold = self.mirror_mod.symmetry_threshold * self.unit_scaling_factor / 2.0
if self.mirror_mod.x and px <= threshold:
edges = self.yz_symmetry_accel.get_edges(Point2D((py, pz)), -px)
point = min((e.closest(point) for e in edges), key=dist, default=Point((0, py, pz)))
px,py,pz = point
if self.mirror_mod.y and py >= threshold:
edges = self.xz_symmetry_accel.get_edges(Point2D((px, pz)), py)
point = min((e.closest(point) for e in edges), key=dist, default=Point((px, 0, pz)))
px,py,pz = point
if self.mirror_mod.z and pz <= threshold:
edges = self.xy_symmetry_accel.get_edges(Point2D((px, py)), -pz)
point = min((e.closest(point) for e in edges), key=dist, default=Point((px, py, 0)))
px,py,pz = point
if to_world: point = self.xform.l2w_point(point)
return point
def __deepcopy__(self, memo):
'''
custom deepcopy method, because BMesh and BVHTree are not copyable
'''
rftarget = RFTarget.__new__(RFTarget)
memo[id(self)] = rftarget
rftarget.__setup__(self.obj, self.unit_scaling_factor, rftarget_copy=self)
# deepcopy all remaining settings
for k,v in self.__dict__.items():
if k not in {'prev_state'} and k in rftarget.__dict__: continue
setattr(rftarget, k, copy.deepcopy(v, memo))
return rftarget
def to_json(self):
data = {
'verts': None,
'edges': None,
'faces': None,
'symmetry': list(self.mirror_mod.xyz)
}
self.bme.verts.ensure_lookup_table()
data['verts'] = [list(bmv.co) for bmv in self.bme.verts]
data['edges'] = [list(bmv.index for bmv in bme.verts) for bme in self.bme.edges]
data['faces'] = [list(bmv.index for bmv in bmf.verts) for bmf in self.bme.faces]
return data
def rewrap(self):
BMElemWrapper.wrap(self)
def commit(self):
self.restore_state()
def cancel(self):
self.restore_state()
def clean(self):
super().clean()
version = self.get_version()
if self.editmesh_version == version: return
self.editmesh_version = version
try:
self._clean_mesh()
self._clean_selection()
self._clean_mirror()
self._clean_displace()
except Exception as e:
print(f'Caught Exception while trying to clean RFTarget: {e}')
self.handle_exception(e)
def _clean_mesh(self):
prev_mesh = self.obj.data
prev_mesh_name = prev_mesh.name
new_mesh = self.obj.data.copy()
self.bme.to_mesh(new_mesh)
self.obj.data = new_mesh
bpy.data.meshes.remove(prev_mesh)
new_mesh.name = prev_mesh_name
def _clean_selection(self):
for bmv,emv in zip(self.bme.verts, self.obj.data.vertices):
emv.select = bmv.select
for bme,eme in zip(self.bme.edges, self.obj.data.edges):
eme.select = bme.select
for bmf,emf in zip(self.bme.faces, self.obj.data.polygons):
emf.select = bmf.select
def _clean_mirror(self):
self.mirror_mod.write()
def _clean_displace(self):
self.displace_mod.strength = self.displace_strength
def enable_symmetry(self, axis): self.mirror_mod.enable_axis(axis)
def disable_symmetry(self, axis): self.mirror_mod.disable_axis(axis)
def has_symmetry(self, axis): return self.mirror_mod.is_enabled_axis(axis)
def apply_mirror_symmetry(self, nearest):
out = []
def apply_mirror_and_return_geom(axis):
return mirror(
self.bme,
geom=list(self.bme.verts) + list(self.bme.edges) + list(self.bme.faces),
merge_dist=self.mirror_mod.symmetry_threshold,
axis=axis,
)['geom']
if self.mirror_mod.x: out += apply_mirror_and_return_geom('X')
if self.mirror_mod.y: out += apply_mirror_and_return_geom('Y')
if self.mirror_mod.z: out += apply_mirror_and_return_geom('Z')
self.mirror_mod.x = False
self.mirror_mod.y = False
self.mirror_mod.z = False
for bmv in (e for e in out if type(e) is BMVert):
rfvert = self._wrap_bmvert(bmv)
xyz, norm, _, _ = nearest(rfvert.co)
if xyz is None: continue
rfvert.co = xyz
rfvert.normal = norm
self.recalculate_face_normals(verts=[e for e in out if type(e) is BMVert], faces=[e for e in out if type(e) is BMFace])
def flip_symmetry_verts_to_correct_side(self):
for bmv in self.bme.verts:
if self.mirror_mod.x and bmv.co.x < 0:
bmv.co.x = -bmv.co.x
bmv.normal.x = -bmv.normal.x
if self.mirror_mod.y and bmv.co.y > 0:
bmv.co.y = -bmv.co.y
bmv.normal.y = -bmv.normal.y
if self.mirror_mod.z and bmv.co.z < 0:
bmv.co.z = -bmv.co.z
bmv.normal.z = -bmv.normal.z
def new_vert(self, co, norm):
# assuming co and norm are in world space!
# so, do not set co directly; need to xform to local first.
bmv = self.bme.verts.new((0,0,0))
rfv = self._wrap_bmvert(bmv)
rfv.co = co
rfv.normal = norm
return rfv
def new_edge(self, verts):
verts = [self._unwrap(v) for v in verts]
bme = self.bme.edges.new(verts)
return self._wrap_bmedge(bme)
def new_face(self, verts):
# see if a face happens to exist already...
verts = [v for v in verts if v]
face_in_common = accumulate_last(
(
set(f for f in v.link_faces if f.is_valid)
for v in verts if v.is_valid
), lambda s0,s1: s0 & s1
)
if face_in_common: return next(iter(face_in_common))
verts = [self._unwrap(v) for v in verts]
# make sure there are no duplicate verts (issue #957)
# however, this _could_ reduce vert count < 3
nverts = deduplicate_list(verts)
if len(nverts) < 3: return None
bmf = self.bme.faces.new(nverts)
self.update_face_normal(bmf)
return self._wrap_bmface(bmf)
def holes_fill(self, edges, sides):
edges = list(map(self._unwrap, edges))
ret = holes_fill(self.bme, edges=edges, sides=sides)
print('RetopoFlow holes_fill', ret)
def merge_at_center(self, nearest):
rfvs = list(self.get_selected_verts())
co, norm, _, _ = nearest(Point.average(v.co for v in rfvs))
if not co or not norm: return None
bmvs = [self._unwrap(v) for v in rfvs]
pointmerge(self.bme, verts=bmvs)
rfv = self._wrap_bmvert(bmvs[0])
rfv.co = co
rfv.normal = norm
rfv.select = True
self.update_verts_faces([rfv])
return rfv
def collapse_edges_faces(self, nearest):
# find all connected components
# for each component:
# compute average vert position
# merge all selected verts to point located at average
verts = set(self.get_selected_verts())
edges = set(self.get_selected_edges())
faces = set(self.get_selected_faces())
remaining = set(verts)
while remaining:
working = set()
working_next = set([ next(iter(remaining)) ])
while working_next:
v = working_next.pop()
if v not in remaining: continue
remaining.remove(v)
working.add(v)
for e in v.link_edges:
if e not in edges: continue
working_next |= {v_ for v_ in e.verts if v_ in remaining}
for f in v.link_faces:
if f not in faces: continue
working_next |= {v_ for v_ in f.verts if v_ in remaining}
average = Point.average(v.co for v in working)
p, n, _, _ = nearest(average)
rfv = self.new_vert(p, n)
for v in working:
rfv = rfv.merge_robust(v)
rfv.co = p
rfv.normal = n
rfv.select = True
def delete_selection(self, del_empty_edges=True, del_empty_verts=True, del_verts=True, del_edges=True, del_faces=True):
if del_faces:
faces = { f for f in self.bme.faces if f.select }
self.delete_faces(faces, del_empty_edges=del_empty_edges, del_empty_verts=del_empty_verts)
if del_edges:
edges = { e for e in self.bme.edges if e.select }
self.delete_edges(edges, del_empty_verts=del_empty_verts)
if del_verts:
verts = { v for v in self.bme.verts if v.select }
self.delete_verts(verts)
def delete_verts(self, verts):
for bmv in map(self._unwrap, verts):
if bmv.is_valid and not bmv.hide: self.bme.verts.remove(bmv)
def delete_edges(self, edges, del_empty_verts=True):
edges = { self._unwrap(e) for e in edges if e.is_valid and not e.hide }
verts = { v for e in edges for v in e.verts }
for bme in edges: self.bme.edges.remove(bme)
if del_empty_verts:
for bmv in verts:
if len(bmv.link_edges) == 0: self.bme.verts.remove(bmv)
def delete_faces(self, faces, del_empty_edges=True, del_empty_verts=True):
faces = { self._unwrap(f) for f in faces if f.is_valid and not f.hide }
edges = { e for f in faces for e in f.edges }
verts = { v for f in faces for v in f.verts }
for bmf in faces: self.bme.faces.remove(bmf)
if del_empty_edges:
for bme in edges:
if len(bme.link_faces) == 0: self.bme.edges.remove(bme)
if del_empty_verts:
for bmv in verts:
if len(bmv.link_faces) == 0: self.bme.verts.remove(bmv)
def dissolve_verts(self, verts, use_face_split=False, use_boundary_tear=False):
verts = [ self._unwrap(v) for v in verts if v.is_valid and not v.hide ]
dissolve_verts(self.bme, verts=verts, use_face_split=use_face_split, use_boundary_tear=use_boundary_tear)
def dissolve_edges(self, edges, use_verts=True, use_face_split=False):
edges = [ self._unwrap(e) for e in edges if e.is_valid and not e.hide ]
dissolve_edges(self.bme, edges=edges, use_verts=use_verts, use_face_split=use_face_split)
def dissolve_faces(self, faces, use_verts=True):
faces = [ self._unwrap(f) for f in faces if f.is_valid and not f.hide ]
dissolve_faces(self.bme, faces=faces, use_verts=use_verts)
def update_verts_faces(self, verts):
faces = { f for v in verts if v.is_valid for f in self._unwrap(v).link_faces }
for bmf in faces:
n = compute_normal(v.co for v in bmf.verts)
vnorm = sum((v.normal for v in bmf.verts), Vector())
if n.dot(vnorm) < 0:
bmf.normal_flip()
bmf.normal_update()
def update_face_normal(self, face):
bmf = self._unwrap(face)
n = compute_normal(v.co for v in bmf.verts)
vnorm = sum((v.normal for v in bmf.verts), Vector())
if n.dot(vnorm) < 0:
bmf.normal_flip()
bmf.normal_update()
def clean_duplicate_bmedges(self, vert):
if not vert.is_valid: return {}
bmv = self._unwrap(vert)
# search for two edges between the same pair of verts
lbme = list(bmv.link_edges)
lbme_dup = []
for i0,bme0 in enumerate(lbme):
for i1,bme1 in enumerate(lbme):
if i1 <= i0: continue
if bme0.other_vert(bmv) == bme1.other_vert(bmv):
lbme_dup.append((bme0,bme1))
mapping = {}
for bme0,bme1 in lbme_dup:
if not bme0.is_valid or not bme1.is_valid: continue
l0,l1 = len(bme0.link_faces), len(bme1.link_faces)
bme0.select |= bme1.select
bme1.select |= bme0.select
handled = False
if l0 == 0:
self.bme.edges.remove(bme0)
handled = True
if l1 == 0:
self.bme.edges.remove(bme1)
handled = True
if l0 == 1 and l1 == 1:
# remove bme1 and recreate attached faces
lbmv = list(bme1.link_faces[0].verts)
bmf = self._wrap_bmface(bme1.link_faces[0])
s = bmf.select
self.bme.edges.remove(bme1)
nf = self.new_face(lbmv)
if not nf:
print(f'clean_duplicate_bmedges: could not create new bmface: {lbmv}')
else:
mapping[bmf] = nf
mapping[bmf].select = s
handled = True
if not handled:
# assert handled, 'unhandled count of linked faces %d, %d' % (l0,l1)
print('clean_duplicate_bmedges: unhandled count of linked faces %d, %d' % (l0,l1))
return mapping
def remove_duplicate_bmfaces(self, vert):
bmv = self._unwrap(vert)
mapping = {}
check = True
while check:
check = False
bmfs = list(bmv.link_faces)
for i0,bmf0 in enumerate(bmfs):
for i1,bmf1 in enumerate(bmfs):
if i1 <= i0: continue
if set(bmf0.verts) ^ set(bmf1.verts): continue
# bmf0 and bmf1 have exactly the same verts! delete one!
mapping[bmf1] = bmf0
self.delete_faces([bmf1])
check = True
break
if check: break
return mapping
def snap_verts_filter(self, nearest, fn_filter):
'''
snap verts when fn_filter returns True
'''
for rfv in self.iter_verts():
if not fn_filter(rfv): continue
xyz,norm,_,_ = nearest(rfv.co)
rfv.co = xyz
rfv.normal = norm
self.dirty()
# def snap_all_verts(self, nearest):
# self.snap_verts_filter(nearest, lambda _: True)
def snap_all_nonhidden_verts(self, nearest):
self.snap_verts_filter(nearest, lambda v: not v.hide)
def snap_selected_verts(self, nearest):
self.snap_verts_filter(nearest, lambda v: v.select)
# def snap_unselected_verts(self, nearest):
# self.snap_verts_filter(nearest, lambda v: v.unselect)
def pin_selected(self):
for v in self.iter_verts():
if v.select: v.pinned = True
def unpin_selected(self):
for v in self.iter_verts():
if v.select: v.pinned = False
def unpin_all(self):
for v in self.iter_verts():
v.pinned = False
def mark_seam_selected(self):
for v in self.iter_edges():
if v.select: v.seam = True
def clear_seam_selected(self):
for v in self.iter_edges():
if v.select: v.seam = False
def remove_all_doubles(self, dist):
bmv = [v for v in self.bme.verts if not v.hide]
remove_doubles(self.bme, verts=bmv, dist=dist)
self.dirty()
def remove_selected_doubles(self, dist):
remove_doubles(self.bme, verts=[bmv for bmv in self.bme.verts if bmv.select], dist=dist)
self.dirty()
def flip_face_normals(self):
verts = set()
for bmf in self.get_selected_faces():
bmf.normal_flip()
for bmv in bmf.verts: verts.add(bmv)
for bmv in verts:
if not bmv.is_wire:
bmv.normal_update()
self.dirty()
def recalculate_face_normals(self, *, verts=None, faces=None):
if faces is None: faces = { bmf for bmf in self.bme.faces if bmf.select }
else: faces = { self._unwrap(bmf) for bmf in faces }
if verts: faces |= { self._unwrap(bmf) for bmv in verts for bmf in bmv.link_faces}
recalc_face_normals(self.bme, faces=list(faces))
for bmv in (bmv for bmf in faces for bmv in bmf.verts): bmv.normal_update()
self.dirty()