''' 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 bmesh from bmesh.types import BMesh, BMVert, BMEdge, BMFace from bmesh.utils import ( edge_split, vert_splice, face_split, vert_collapse_edge, vert_dissolve, face_join, face_vert_separate, ) from bmesh.ops import dissolve_verts, dissolve_edges, dissolve_faces from mathutils import Vector from ...addon_common.common.utils import iter_pairs from ...addon_common.common.debug import dprint from ...addon_common.common.profiler import profiler from ...addon_common.common.maths import ( triangle2D_det, triangle2D_area, segment2D_intersection, Vec2D, Point, Point2D, Vec, Direction, Normal, ) from ...config.options import options ''' BMElemWrapper wraps BMverts, BMEdges, BMFaces to automagically handle world-to-local and local-to-world transformations. Must override any property that can be set (TODO: find more elegant way to handle this!) and function that returns a BMVert, BMEdge, or BMFace. All functions and read-only properties are handled with __getattr__(). user-writable properties: BMVert: co, normal BMEdge: seam, smooth BMFace: material_index, normal, smooth common: hide, index. select, tag NOTE: RFVert, RFEdge, RFFace do NOT mark RFMesh as dirty! ''' class BMElemWrapper: @staticmethod def wrap(rftarget): BMElemWrapper.rftarget = rftarget BMElemWrapper.xform = rftarget.xform BMElemWrapper.l2w_point = rftarget.xform.l2w_point BMElemWrapper.w2l_point = rftarget.xform.w2l_point BMElemWrapper.l2w_normal = rftarget.xform.l2w_normal BMElemWrapper.w2l_normal = rftarget.xform.w2l_normal BMElemWrapper.symmetry_real = rftarget.symmetry_real BMElemWrapper.mirror_mod = rftarget.mirror_mod @staticmethod def _unwrap(bmelem): try: return bmelem.bmelem except: return bmelem def __init__(self, bmelem): self.bmelem = bmelem def __repr__(self): return f'<{"" if self.bmelem.is_valid else "XXX_"}{type(self).__name__}: {repr(self.bmelem)}>' def __hash__(self): return hash(self.bmelem) def __eq__(self, other): if other is None: return False if isinstance(other, BMElemWrapper): return self.bmelem == other.bmelem return self.bmelem == other def __ne__(self, other): return not self.__eq__(other) @property def hide(self): return self.bmelem.hide @hide.setter def hide(self, v): self.bmelem.hide = v @property def index(self): return self.bmelem.index @index.setter def index(self, v): self.bmelem.index = v @property def select(self): return self.bmelem.select and not self.bmelem.hide @select.setter def select(self, v): self.bmelem.select = v @property def unselect(self): return not self.bmelem.select and not self.bmelem.hide @property def tag(self): return self.bmelem.tag @tag.setter def tag(self, v): self.bmelem.tag = v def __getattr__(self, k): if k in self.__dict__: return getattr(self, k) return getattr(self.bmelem, k) class RFVert(BMElemWrapper): @staticmethod def get_link_edges(rfverts): return { RFEdge(bme) for bmv in rfverts for bme in bmv.bmelem.link_edges } @staticmethod def get_link_faces(rfverts): return { RFFace(bmf) for bmv in rfverts for bmf in bmv.bmelem.link_faces } @property def co(self): return self.l2w_point(self.bmelem.co) @co.setter def co(self, co): if not self.bmelem.is_valid: return if any(math.isnan(v) for v in co): return # assert not any(math.isnan(v) for v in co), f'Setting RFVert.co to {co}' if options['show pinned'] and options['pin enabled'] and self.pinned: return if options['show seam'] and options['pin seam'] and self.seam: return co = self.symmetry_real(co, to_world=False) # # the following does not work well, because new verts have co=(0,0,0) # mm = BMElemWrapper.mirror_mod # if mm.use_clip: # rft = BMElemWrapper.rftarget # th = mm.symmetry_threshold * rft.unit_scaling_factor / 2.0 # ox,oy,oz = self.bmelem.co # nx,ny,nz = (mm.x and abs(ox) <= th),(mm.y and abs(oy) <= th),(mm.z and abs(oz) <= th) # if nx or ny or nz: # co = rft.snap_to_symmetry(co, mm._symmetry, to_world=False, from_world=False) self.bmelem.co = co @property def pinned(self): return bool(self.bmelem[self.rftarget.layer_pin]) @pinned.setter def pinned(self, v): self.bmelem[self.rftarget.layer_pin] = 1 if bool(v) else 0 @property def seam(self): return any(e.seam for e in self.bmelem.link_edges) @property def normal(self): return self.l2w_normal(self.bmelem.normal) @normal.setter def normal(self, norm): self.bmelem.normal = self.w2l_normal(norm) @property def co_normal(self): return (self.co, self.normal) @co_normal.setter def co_normal(self, co_normal): self.co, self.normal = co_normal @property def link_edges(self): return [RFEdge(bme) for bme in self.bmelem.link_edges] @property def link_faces(self): return [RFFace(bmf) for bmf in self.bmelem.link_faces] def is_on_symmetry_plane(self): mm = BMElemWrapper.mirror_mod th = mm.symmetry_threshold * BMElemWrapper.rftarget.unit_scaling_factor / 2.0 x,y,z = self.bmelem.co if mm.x and abs(x) <= th: return True if mm.y and abs(y) <= th: return True if mm.z and abs(z) <= th: return True return False def is_on_boundary(self, symmetry_as_boundary=False): ''' similar to is_boundary property, but optionally discard symmetry boundaries ''' if not symmetry_as_boundary: if self.is_on_symmetry_plane(): return False return self.bmelem.is_boundary ############################################# def share_edge(self, other): if not self.is_valid or not other.is_valid: return False bmv0 = BMElemWrapper._unwrap(self) bmv1 = BMElemWrapper._unwrap(other) return any(bmv1 in bme.verts for bme in bmv0.link_edges if bme.is_valid) def shared_edge(self, other): if not self.is_valid or not other.is_valid: return False bmv0 = BMElemWrapper._unwrap(self) bmv1 = BMElemWrapper._unwrap(other) bme = next((bme for bme in bmv0.link_edges if bme.is_valid and bmv1 in bme.verts), None) return RFEdge(bme) if bme else None def share_face(self, other): if not self.is_valid or not other.is_valid: return False bmv0 = BMElemWrapper._unwrap(self) bmv1 = BMElemWrapper._unwrap(other) return any(bmv1 in bmf.verts for bmf in bmv0.link_faces if bmf.is_valid) def shared_faces(self, other): if not self.is_valid or not other.is_valid: return False bmv0 = BMElemWrapper._unwrap(self) bmv1 = BMElemWrapper._unwrap(other) return [RFFace(bmf) for bmf in bmv0.link_faces if bmf.is_valid and bmv1 in bmf.verts] def face_separate(self, f): if not (self.is_valid and f and f.is_valid): return None bmv = BMElemWrapper._unwrap(self) bmf = BMElemWrapper._unwrap(f) new_bmv = face_vert_separate(bmf, bmv) return RFVert(new_bmv) def merge(self, other): if not (self.is_valid and other.is_valid): if self.is_valid: return self if other.is_valid: return other return None try: bmv0 = BMElemWrapper._unwrap(self) bmv1 = BMElemWrapper._unwrap(other) vert_splice(bmv1, bmv0) return RFVert(bmv0) except Exception as e: print(f'Caught Exception while trying to merge') print(e) print(f'Will try more robust merge') return self.merge_robust(other) def merge_robust(self, other): if not (self.is_valid and other.is_valid): if self.is_valid: return self if other.is_valid: return other return None rftarget = self.rftarget if self.share_edge(other): bmv = self.shared_edge(other).collapse() rftarget.remove_duplicate_bmfaces(bmv) rftarget.clean_duplicate_bmedges(bmv) return bmv if not self.share_face(other): bmv = self.merge(other) rftarget.remove_duplicate_bmfaces(bmv) rftarget.clean_duplicate_bmedges(bmv) return bmv bmfs = self.shared_faces(other) for bmf in bmfs: bmf.split(self, other) rftarget.remove_duplicate_bmfaces(self) rftarget.clean_duplicate_bmedges(self) bmv = self.shared_edge(other).collapse() rftarget.remove_duplicate_bmfaces(bmv) rftarget.clean_duplicate_bmedges(bmv) return bmv def dissolve(self): bmv = BMElemWrapper._unwrap(self) vert_dissolve(bmv) def compute_normal(self): return Normal.average(f.compute_normal() for f in self.link_faces) class RFEdge(BMElemWrapper): @staticmethod def get_verts(rfedges): bmvs = { bmv for bme in rfedges for bmv in bme.bmelem.verts } return { RFVert(bmv) for bmv in bmvs } @property def seam(self): return self.bmelem.seam @seam.setter def seam(self, v): self.bmelem.seam = v @property def smooth(self): return self.bmelem.smooth @smooth.setter def smooth(self, v): self.bmelem.smooth = v def first_vert(self): return RFVert(self.bmelem.verts[0]) def other_vert(self, bmv): bmv = self._unwrap(bmv) o = self.bmelem.other_vert(bmv) if o is None: return None return RFVert(o) def share_vert(self, bme): if not self.is_valid or not bme.is_valid: return False bme = self._unwrap(bme) return any(v in bme.verts for v in self.bmelem.verts if v.is_valid) def shared_vert(self, bme): if not self.is_valid or not bme.is_valid: return None bme = self._unwrap(bme) verts = [v for v in self.bmelem.verts if v.is_valid and v in bme.verts] if not verts: return None return RFVert(verts[0]) def nonshared_vert(self, bme): if not self.is_valid or not bme.is_valid: return None bme = self._unwrap(bme) verts = [v for v in self.bmelem.verts if v.is_valid and v not in bme.verts] if len(verts) != 1: return None return RFVert(verts[0]) def share_face(self, bme): if not self.is_valid or not bme.is_valid: return False bme = self._unwrap(bme) return any(f in bme.link_faces for f in self.bmelem.link_faces) def shared_faces(self, bme): if not self.is_valid or not bme.is_valid: return set() bme = self._unwrap(bme) return { RFFace(f) for f in (set(self.bmelem.link_faces) & set(bme.link_faces)) if f.is_valid } @property def verts(self): bmv0, bmv1 = self.bmelem.verts return (RFVert(bmv0), RFVert(bmv1)) @property def link_faces(self): return [RFFace(bmf) for bmf in self.bmelem.link_faces] def get_left_right_link_faces(self): v0, v1 = self.bmelem.verts bmfl, bmfr = None, None if len(self.bmelem.link_faces) == 2: bmfl, bmfr = self.bmelem.link_faces elif len(self.bmelem.link_faces) == 1: bmfl = next(iter(self.bmelem.link_faces)) else: return (None, None) for lv0, lv1 in iter_pairs(bmfl.verts, True): if lv0 == v0 and lv1 == v1: # correct orientation! break else: # swap left and right faces bmfl, bmfr = bmfr, bmfl if bmfl: bmfl = RFFace(bmfl) if bmfr: bmfr = RFFace(bmfr) return (bmfl, bmfr) ############################################# def compute_normal(self): return Normal.average(bmf.normal for bmf in self.link_faces) def calc_length(self): v0, v1 = self.bmelem.verts return (self.l2w_point(v0.co) - self.l2w_point(v1.co)).length @property def length(self): return self.calc_length() def calc_center(self): v0, v1 = self.bmelem.verts return self.l2w_point((v0.co + v1.co) / 2) def vector(self, from_vert=None, to_vert=None): v0, v1 = self.verts if from_vert: if v1 == from_vert: v0, v1 = v1, v0 assert v0 == from_vert elif to_vert: if v0 == to_vert: v0, v1 = v1, v0 assert v1 == to_vert return v1.co - v0.co def vector2D(self, Point_to_Point2D, from_vert=None, to_vert=None): v0, v1 = self.verts if from_vert: if v1 == from_vert: v0, v1 = v1, v0 assert v0 == from_vert elif to_vert: if v0 == to_vert: v0, v1 = v1, v0 assert v1 == to_vert return Point_to_Point2D(v1.co) - Point_to_Point2D(v0.co) def direction(self, from_vert=None, to_vert=None): return Direction(self.vector(from_vert=from_vert, to_vert=to_vert)) def perpendicular(self): d = self.vector() n = self.normal() return Direction(d.cross(n)) @staticmethod def get_direction(bme): v0, v1 = bme.verts return Direction(v1.co - v0.co) ############################################# def get_next_edge_in_strip(self, rfvert): r''' given self=A and bmv=B, return C o-----o-----o... o-----o-----o... | | | | | | o--A--B--C--o... o--A--B--C--o... | | | | |\ 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 ''' bmv = self._unwrap(rfvert) assert bmv in self.bmelem.verts, "Vert not part of Edge" link_faces = list(self.bmelem.link_faces) link_edges = [bme for bme in bmv.link_edges if bme != self.bmelem] # for details, see: https://github.com/CGCookie/retopoflow/issues/554#issuecomment-408185805 if len(link_faces) == 0: if len(link_edges) != 1: return None bme = link_edges[0] if len(bme.link_faces) != 0: return None return RFEdge(bme) if len(link_faces) == 1: bmf0 = link_faces[0] lbme = [bme for bme in link_edges if len(bme.link_faces) == 1] lbme = [bme for bme in lbme if bmf0 not in bme.link_faces] lbme = [bme for bme in lbme if any(bme0 == bme1 for bme0 in bmf0.edges for bmf1 in bme.link_faces for bme1 in bmf1.edges)] if len(lbme) != 1: return None return RFEdge(lbme[0]) if len(link_faces) == 2 and len(bmv.link_faces) == 4 and len(bmv.link_edges) == 4: # bmv is part of 4 touching quads and all quads are touching # (left figure above) # find bme that does not share a face with self for bme in rfvert.link_edges: if len(bme.link_faces) != 2: continue if not (set(bme.link_faces) & set(link_faces)): return bme return None return None ############################################# def split(self, vert=None, fac=0.5): bme = BMElemWrapper._unwrap(self) bmv = BMElemWrapper._unwrap(vert) or bme.verts[0] bme_new, bmv_new = edge_split(bme, bmv, fac) return RFEdge(bme_new), RFVert(bmv_new) def collapse(self): bme = BMElemWrapper._unwrap(self) bmv0, bmv1 = bme.verts del_faces = [f for f in bme.link_faces if len(f.verts) == 3] for bmf in del_faces: self.rftarget.bme.faces.remove(bmf) bmesh.ops.collapse(self.rftarget.bme, edges=[bme], uvs=True) return RFVert(bmv0 if bmv0.is_valid else bmv1) # # not working # def separate(self, face): # bme = BMElemWrapper._unwrap(self) # bmf = BMElemWrapper._unwrap(face) # loops = list(bme.link_loops) # floops = [loop for loop in loops if loop.face == bmf] # print(f'{bmf=} {loops=} {floops=}') # loop = next(iter(floops)) # bmv0 = bmesh.utils.loop_separate(loop) # return RFVert(bmv0) class RFFace(BMElemWrapper): @staticmethod def get_verts(rffaces): bmvs = { bmv for bmf in rffaces for bmv in bmf.bmelem.verts } return { RFVert(bmv) for bmv in bmvs } @property def material_index(self): return self.bmelem.material_index @material_index.setter def material_index(self, v): self.bmelem.material_index = v @property def normal(self): return self.l2w_normal(self.bmelem.normal) @normal.setter def normal(self, v): self.bmelem.normal = self.w2l_normal(v) @property def smooth(self): return self.bmelem.smooth @smooth.setter def smooth(self, v): self.bmelem.smooth = v @property def edges(self): return [RFEdge(bme) for bme in self.bmelem.edges] def share_edge(self, other): bmes = set(self._unwrap(other).edges) return any(e in bmes for e in self.bmelem.edges) def shared_edge(self, other): edges = set(self.bmelem.edges) for bme in other.bmelem.edges: if bme in edges: return RFEdge(bme) return None def opposite_edge(self, e): if len(self.bmelem.edges) != 4: return None e = self._unwrap(e) for i, bme in enumerate(self.bmelem.edges): if bme == e: return RFEdge(self.bmelem.edges[(i + 2) % 4]) return None def neighbor_edges(self, e): e = self._unwrap(e) l = len(self.bmelem.edges) for i, bme in enumerate(self.bmelem.edges): if bme == e: return ( RFEdge(self.bmelem.edges[(i - 1) % l]), RFEdge(self.bmelem.edges[(i + 1) % l]) ) return None @property def verts(self): return [RFVert(bmv) for bmv in self.bmelem.verts] def get_vert_co(self): return [self.l2w_point(bmv.co) for bmv in self.bmelem.verts] def get_vert_normal(self): return [self.l2w_normal(bmv.normal) for bmv in self.bmelem.verts] def is_quad(self): return len(self.bmelem.verts) == 4 def is_triangle(self): return len(self.bmelem.verts) == 3 def center(self): return Point.average(self.l2w_point(bmv.co) for bmv in self.bmelem.verts) ############################################# def compute_normal(self): ''' computes normal based on verts ''' # TODO: should use loop rather than verts? an = Vec((0,0,0)) vs = list(self.bmelem.verts) bmv1,bmv2 = vs[-2],vs[-1] v1 = bmv2.co - bmv1.co for bmv in vs: bmv0,bmv1,bmv2 = bmv1,bmv2,bmv v0,v1 = -v1,bmv2.co-bmv1.co an = an + v0.cross(v1) return self.l2w_normal(Normal(an)) def is_flipped(self): fn = self.w2l_normal(self.compute_normal()) vs = list(self.bmelem.verts) return any(v.normal.dot(fn) <= 0 for v in vs) def overlap2D(self, other, Point_to_Point2D): return self.overlap2D_center(other, Point_to_Point2D) def overlap2D_center(self, other, Point_to_Point2D): verts0 = list(map(Point_to_Point2D, [v.co for v in self.bmelem.verts])) verts1 = list( map(Point_to_Point2D, [v.co for v in self._unwrap(other).verts])) center0 = sum(map(Vec2D, verts0), Vec2D((0, 0))) / len(verts0) center1 = sum(map(Vec2D, verts1), Vec2D((0, 0))) / len(verts1) radius0 = sum((v - center0).length for v in verts0) / len(verts0) radius1 = sum((v - center1).length for v in verts1) / len(verts1) ratio = 1 - (center0 - center1).length / (radius0 + radius1) return max(0, ratio) def overlap2D_Sutherland_Hodgman(self, other, Point_to_Point2D): ''' computes area in image space of overlap between self and other this is done by clipping other to self by iterating through all of edges in self and clipping to the "inside" half-space. Sutherland-Hodgman Algorithm: https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm ''' # NOTE: assumes self and other are convex! (not a terrible assumption) verts0 = list(map(Point_to_Point2D, [v.co for v in self.bmelem.verts])) verts1 = list( map(Point_to_Point2D, [v.co for v in self._unwrap(other).verts])) for v00, v01 in zip(verts0, verts0[1:] + verts0[:1]): # other polygon (verts1) by line v00-v01 len1 = len(verts1) sides = [triangle2D_det(v00, v01, v1) <= 0 for v1 in verts1] intersections = [ segment2D_intersection(v00, v01, v10, v11) for v10, v11 in zip(verts1, verts1[1:] + verts1[:1]) ] nverts1 = [] for i0 in range(len1): i1 = (i0 + 1) % len1 v10, v11 = verts1[i0], verts1[i1] s10, s11 = sides[i0], sides[i1] if s10 and s11: # both outside. might intersect if intersections[i0]: nverts1 += [intersections[i0]] elif not s11: if s10: # v10 is outside, v11 is inside if intersections[i0]: nverts1 += [intersections[i0]] nverts1 += [v11] verts1 = nverts1 if len(verts1) < 3: return 0 v0 = verts1[0] return sum( triangle2D_area(v0, v1, v2) for v1, v2 in zip(verts1[1:-1], verts1[2:]) ) def merge(self, other): # find vert of other that is closest to self's v0 verts0, verts1 = list(self.bmelem.verts), list(other.bmelem.verts) l = len(verts0) assert l == len(verts1), 'RFFaces must have same vert count' self.rftarget.bme.faces.remove(self._unwrap(other)) offset = min(range(l), key=lambda i: ( verts1[i].co - verts0[0].co).length) # assuming verts are in same rotational order (should be) for i0 in range(l): i1 = (i0 + offset) % l bme = next(( bme for bme in verts0[i0].link_edges if verts1[i1] in bme.verts ), None) if bme: # issue #372 # TODO: handle better # dprint('bme: ' + str(bme)) pass pass else: vert_splice(verts1[i1], verts0[i0]) # for v in verts0: # self.rftarget.clean_duplicate_bmedges(v) ############################################# def split(self, vert_a, vert_b, coords=[]): bmf = BMElemWrapper._unwrap(self) bmva = BMElemWrapper._unwrap(vert_a) bmvb = BMElemWrapper._unwrap(vert_b) coords = [BMElemWrapper.w2l_point(c) for c in coords] bmf_new, bml_new = face_split(bmf, bmva, bmvb, coords=coords) return RFFace(bmf_new) def shatter(self): working = [ self ] ret = set() while working: bmf = working.pop() if not bmf.is_valid: continue ret.add(bmf) # see if one bmv connects to another touched_bmvs, path = set(), [] def find_exit(bmv0): nonlocal touched_bmvs, path, bmf touched_bmvs.add(bmv0) path.append(bmv0) for bme in bmv0.link_edges: if bme.link_faces: continue # not a potential edge bmv1 = bme.other_vert(bmv0) if bmv1 in touched_bmvs: continue # already seen bmv1 (loop?) if bmf in bmv1.link_faces: path += [bmv1] return True if find_exit(bmv1): return True path.pop() # working with bmv0 does not work, so remove bmv0 from path # find bmvs around perimeter of bmf that could possibly be an entrance for shatter for bmv in bmf.verts: if not any(len(bme.link_faces)==0 for bme in bmv.link_edges): continue if find_exit(bmv): break else: # could not shatter current bmf continue # found a path to shatter bmf try: nbmf = bmf.split(path[0], path[-1], coords=[bmv.co for bmv in path[1:-1]]) except Exception as e: print(f'shatter: Caught exception while trying to split {bmf} along {path}') print(e) continue for bmv_old in path[1:-1]: bmv_new,_ = min(((bmv,(bmv.co-bmv_old.co).length) for bmv in nbmf.verts), key=lambda d:d[1]) if bmv_old.select: bmv_new.select = True bmv_new.merge(bmv_old) for bmv in bmf.verts + nbmf.verts: self.rftarget.clean_duplicate_bmedges(bmv) working.append(bmf) # check bmf again! working.append(nbmf) # check new bmf return ret class RFEdgeSequence: def __init__(self, sequence): if not sequence: self.verts = [] self.edges = [] self.loop = False return seq = list(BMElemWrapper._unwrap(elem) for elem in sequence) if type(seq[0]) is BMVert: self.verts = seq self.loop = ( len(seq) > 1 and len(set(seq[0].link_edges) & set(seq[-1].link_edges)) != 0 ) self.edges = [next(iter(set(v0.link_edges) & set(v1.link_edges))) for v0, v1 in iter_pairs(seq, self.loop)] elif type(seq[0]) is BMEdge: self.edges = seq self.loop = len(seq) > 2 and len( set(seq[0].verts) & set(seq[-1].verts)) != 0 if len(seq) == 1 and not self.loop: self.verts = seq[0].verts else: self.verts = [next(iter(set(e0.verts) & set(e1.verts))) for e0, e1 in iter_pairs(seq, self.loop)] else: assert False, 'unhandled type: %s' % str(type(seq[0])) def __repr__(self): e = min(map(repr, self.edges)) if self.edges else None return f'' def __len__(self): return len(self.edges) def get_verts(self): return [RFVert(bmv) for bmv in self.verts] def get_edges(self): return [RFEdge(bme) for bme in self.edges] def is_loop(self): return self.loop def iter_vert_pairs(self): return iter_pairs(self.get_verts(), self.loop) def iter_edge_pairs(self): return iter_pairs(self.get_edges(), self.loop)