''' 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 . ''' 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 '' % 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 '' % 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()