853 lines
28 KiB
Python
853 lines
28 KiB
Python
'''
|
|
Copyright (C) 2023 CG Cookie
|
|
http://cgcookie.com
|
|
hello@cgcookie.com
|
|
|
|
Created by Jonathan Denning, Jonathan Williamson
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
import math
|
|
|
|
import 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)
|