2025-07-01
This commit is contained in:
@@ -0,0 +1,852 @@
|
||||
'''
|
||||
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 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'<RFEdgeSequence: {len(self.verts)}, {self.loop}, {e}>'
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user