2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,486 @@
'''
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 sys
import math
import copy
import json
import time
import random
from itertools import chain
from queue import Queue
from concurrent.futures import ThreadPoolExecutor
import bpy
import gpu
import bmesh
from bmesh.types import BMesh, BMVert, BMEdge, BMFace
from mathutils import Matrix, Vector
from mathutils.bvhtree import BVHTree
from mathutils.kdtree import KDTree
from mathutils.geometry import normal as compute_normal, intersect_point_tri
from ...addon_common.common import gpustate
from ...addon_common.common import bmesh_render as bmegl
from ...addon_common.common.blender import tag_redraw_all
from ...addon_common.common.bmesh_render import triangulateFace, BufferedRender_Batch
from ...addon_common.common.debug import dprint, Debugger
from ...addon_common.common.decorators import stats_wrapper
from ...addon_common.common.globals import Globals
from ...addon_common.common.hasher import hash_object, hash_bmesh
from ...addon_common.common.profiler import profiler
from ...addon_common.common.maths import (
Point, Direction, Normal, Frame,
Point2D, Vec2D, Direction2D,
Ray, XForm, BBox, Plane,
)
from ...addon_common.common.utils import min_index
from ...config.options import options
from .rfmesh_wrapper import (
BMElemWrapper, RFVert, RFEdge, RFFace, RFEdgeSequence,
)
class RFMeshRender():
'''
RFMeshRender handles rendering RFMeshes.
'''
cache = {}
create_count = 0
delete_count = 0
@staticmethod
# @profiler.function
def new(rfmesh, opts, always_dirty=False):
# 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??)
if False:
if True: # with profiler.code('hashing object'):
ho = hash_object(rfmesh.obj)
if True: # with profiler.code('hashing bmesh'):
hb = hash_bmesh(rfmesh.bme)
h = (ho, hb)
if h not in RFMeshRender.cache:
RFMeshRender.creating = True
RFMeshRender.cache[h] = RFMeshRender(rfmesh, opts)
del RFMeshRender.creating
rfmrender = RFMeshRender.cache[h]
else:
RFMeshRender.creating = True
rfmrender = RFMeshRender(rfmesh, opts)
del RFMeshRender.creating
rfmrender.always_dirty = always_dirty
return rfmrender
# @profiler.function
def __init__(self, rfmesh, opts):
assert hasattr(RFMeshRender, 'creating'), (
'Do not create new RFMeshRender directly!'
'Use RFMeshRender.new()')
RFMeshRender.create_count += 1
# print('RFMeshRender.__init__', RFMeshRender.create_count, RFMeshRender.delete_count)
# initially loading asynchronously?
self.async_load = options['async mesh loading']
self._is_loading = False
self._is_loaded = False
self.load_verts = opts.get('load verts', True)
self.load_edges = opts.get('load edges', True)
self.load_faces = opts.get('load faces', True)
self.buf_data_queue = Queue()
self.buf_matrix_model = rfmesh.xform.to_gpubuffer_Model()
self.buf_matrix_inverse = rfmesh.xform.to_gpubuffer_Inverse()
self.buf_matrix_normal = rfmesh.xform.to_gpubuffer_Normal()
self.buffered_renders_static = []
self.buffered_renders_dynamic = []
self.split = None
self.drawing = Globals.drawing
self.opts = {}
self.replace_rfmesh(rfmesh)
self.replace_opts(opts)
def __del__(self):
RFMeshRender.delete_count += 1
# print('RFMeshRender.__del__', self.rfmesh, RFMeshRender.create_count, RFMeshRender.delete_count)
self.bmesh.free()
if hasattr(self, 'buf_matrix_model'): del self.buf_matrix_model
if hasattr(self, 'buf_matrix_inverse'): del self.buf_matrix_inverse
if hasattr(self, 'buf_matrix_normal'): del self.buf_matrix_normal
if hasattr(self, 'buffered_renders_static'): del self.buffered_renders_static
if hasattr(self, 'buffered_renders_dynamic'): del self.buffered_renders_dynamic
if hasattr(self, 'bmesh'): del self.bmesh
if hasattr(self, 'rfmesh'): del self.rfmesh
# @profiler.function
def replace_opts(self, opts):
opts = dict(opts)
opts['dpi mult'] = self.drawing.get_dpi_mult()
if opts == self.opts: return
self.opts = opts
self.rfmesh_version = None
# @profiler.function
def replace_rfmesh(self, rfmesh):
self.rfmesh = rfmesh
self.bmesh = rfmesh.bme
self.rfmesh_version = None
def dirty(self):
self.rfmesh_version = None
# @profiler.function
def add_buffered_render(self, draw_type, data, static):
batch = BufferedRender_Batch(draw_type)
batch.buffer(data['vco'], data['vno'], data['sel'], data['warn'], data['pin'], data['seam'])
if static: self.buffered_renders_static.append(batch)
else: self.buffered_renders_dynamic.append(batch)
def split_visualization(self, verts=None, edges=None, faces=None):
if not verts and not edges and not faces:
self.split = None
else:
unwrap = BMElemWrapper._unwrap
verts = { unwrap(v) for v in verts } if verts else set()
edges = { unwrap(e) for e in edges } if edges else set()
faces = { unwrap(f) for f in faces } if faces else set()
edges.update(e for v in verts for e in v.link_edges)
faces.update(f for e in edges for f in e.link_faces)
verts.update(v for e in edges for v in e.verts)
verts.update(v for f in faces for v in f.verts)
edges.update(e for f in faces for e in f.edges)
self.split = {
'gathered static': False,
'static verts': { v for v in self.bmesh.verts if v not in verts },
'static edges': { e for e in self.bmesh.edges if e not in edges },
'static faces': { f for f in self.bmesh.faces if f not in faces },
'gathered dynamic': False,
'dynamic verts': verts,
'dynamic edges': edges,
'dynamic faces': faces,
}
self.dirty()
# @profiler.function
def _gather_data(self):
if not self.split:
self.buffered_renders_static = []
self.buffered_renders_dynamic = []
else:
if not self.split['gathered dynamic']:
self.buffered_renders_static = []
self.split['gathered dynamic'] = True
self.buffered_renders_dynamic = []
mirror_axes = self.rfmesh.mirror_mod.xyz if self.rfmesh.mirror_mod else []
mirror_x = 'x' in mirror_axes
mirror_y = 'y' in mirror_axes
mirror_z = 'z' in mirror_axes
layer_pin = self.rfmesh.layer_pin
def gather(verts, edges, faces, static):
vert_count = 100_000
edge_count = 50_000
face_count = 10_000
'''
IMPORTANT NOTE: DO NOT USE PROFILER INSIDE THIS FUNCTION IF LOADING ASYNCHRONOUSLY!
'''
def sel(g):
return 1.0 if g.select else 0.0
def warn_vert(g):
if mirror_x and g.co.x <= 0.0001: return 0.0
if mirror_y and g.co.y >= -0.0001: return 0.0
if mirror_z and g.co.z <= 0.0001: return 0.0
return 0.0 if g.is_manifold and not g.is_boundary else 1.0
def warn_edge(g):
v0,v1 = g.verts
if mirror_x and v0.co.x <= 0.0001 and v1.co.x <= 0.0001: return 0.0
if mirror_y and v0.co.y >= -0.0001 and v1.co.y >= -0.0001: return 0.0
if mirror_z and v0.co.z <= 0.0001 and v1.co.z <= 0.0001: return 0.0
return 0.0 if g.is_manifold else 1.0
def warn_face(g):
return 1.0
def pin_vert(g):
if not layer_pin: return 0.0
return 1.0 if g[layer_pin] else 0.0
def pin_edge(g):
return 1.0 if all(pin_vert(v) for v in g.verts) else 0.0
def pin_face(g):
return 1.0 if all(pin_vert(v) for v in g.verts) else 0.0
def seam_vert(g):
return 1.0 if any(e.seam for e in g.link_edges) else 0.0
def seam_edge(g):
return 1.0 if g.seam else 0.0
def seam_face(g):
return 0.0
try:
time_start = time.time()
# NOTE: duplicating data rather than using indexing, otherwise
# selection will bleed
if True: # with profiler.code('gathering', enabled=not self.async_load):
if self.load_faces:
tri_faces = [(bmf, list(bmvs))
for bmf in faces
if bmf.is_valid and not bmf.hide
for bmvs in triangulateFace(bmf.verts)
]
l = len(tri_faces)
for i0 in range(0, l, face_count):
i1 = min(l, i0 + face_count)
face_data = {
'vco': [ tuple(bmv.co) for bmf, verts in tri_faces[i0:i1] for bmv in verts ],
'vno': [ tuple(bmv.normal) for bmf, verts in tri_faces[i0:i1] for bmv in verts ],
'sel': [ sel(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ],
'warn': [ warn_face(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ],
'pin': [ pin_face(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ],
'seam': [ seam_face(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ],
'idx': None, # list(range(len(tri_faces)*3)),
}
if self.async_load:
self.buf_data_queue.put((BufferedRender_Batch.TRIANGLES, face_data, static))
tag_redraw_all('buffer update')
else:
self.add_buffered_render(BufferedRender_Batch.TRIANGLES, face_data, static)
if self.load_edges:
edges = [bme for bme in edges if bme.is_valid and not bme.hide]
l = len(edges)
for i0 in range(0, l, edge_count):
i1 = min(l, i0 + edge_count)
edge_data = {
'vco': [ tuple(bmv.co) for bme in edges[i0:i1] for bmv in bme.verts ],
'vno': [ tuple(bmv.normal) for bme in edges[i0:i1] for bmv in bme.verts ],
'sel': [ sel(bme) for bme in edges[i0:i1] for _ in bme.verts ],
'warn': [ warn_edge(bme) for bme in edges[i0:i1] for _ in bme.verts ],
'pin': [ pin_edge(bme) for bme in edges[i0:i1] for _ in bme.verts ],
'seam': [ seam_edge(bme) for bme in edges[i0:i1] for _ in bme.verts ],
'idx': None, # list(range(len(self.bmesh.edges)*2)),
}
if self.async_load:
self.buf_data_queue.put((BufferedRender_Batch.LINES, edge_data, static))
tag_redraw_all('buffer update')
else:
self.add_buffered_render(BufferedRender_Batch.LINES, edge_data, static)
if self.load_verts:
verts = [bmv for bmv in verts if bmv.is_valid and not bmv.hide]
l = len(verts)
for i0 in range(0, l, vert_count):
i1 = min(l, i0 + vert_count)
vert_data = {
'vco': [ tuple(bmv.co) for bmv in verts[i0:i1] ],
'vno': [ tuple(bmv.normal) for bmv in verts[i0:i1] ],
'sel': [ sel(bmv) for bmv in verts[i0:i1] ],
'warn': [ warn_vert(bmv) for bmv in verts[i0:i1] ],
'pin': [ pin_vert(bmv) for bmv in verts[i0:i1] ],
'seam': [ seam_vert(bmv) for bmv in verts[i0:i1] ],
'idx': None, # list(range(len(self.bmesh.verts))),
}
if self.async_load:
self.buf_data_queue.put((BufferedRender_Batch.POINTS, vert_data, static))
tag_redraw_all('buffer update')
else:
self.add_buffered_render(BufferedRender_Batch.POINTS, vert_data, static)
if self.async_load:
self.buf_data_queue.put('done')
time_end = time.time()
# print('RFMeshRender: Gather time: %0.2f' % (time_end - time_start))
except Exception as e:
print('EXCEPTION WHILE GATHERING: ' + str(e))
raise e
# self.bmesh.verts.ensure_lookup_table()
for bmv in self.bmesh.verts:
if bmv.link_faces:
bmv.normal_update()
# for bmelem in chain(self.bmesh.faces, self.bmesh.edges):
# bmelem.normal_update()
self._is_loading = True
self._is_loaded = False
# with profiler.code('Gathering data for RFMesh (%ssync)' % ('a' if self.async_load else '')):
if not self.async_load:
#print(f'RFMeshRender._gather: synchronous')
#profiler.function(gather)()
if not self.split:
#print(f' v={len(self.bmesh.verts)} e={len(self.bmesh.edges)} f={len(self.bmesh.faces)}')
gather(self.bmesh.verts, self.bmesh.edges, self.bmesh.faces, True)
else:
if not self.split['gathered static']:
#print(f' sv={len(self.split["static verts"])} se={len(self.split["static edges"])} sf={len(self.split["static faces"])}')
gather(self.split['static verts'], self.split['static edges'], self.split['static faces'], True)
self.split['gathered static'] = True
#print(f' dv={len(self.split["dynamic verts"])} de={len(self.split["dynamic edges"])} df={len(self.split["dynamic faces"])}')
gather(self.split['dynamic verts'], self.split['dynamic edges'], self.split['dynamic faces'], False)
else:
#print(f'RFMeshRender._gather: asynchronous')
#self._gather_submit = ThreadPoolExecutor.submit(gather)
e = ThreadPoolExecutor()
if not self.split:
#print(f' v={len(self.bmesh.verts)} e={len(self.bmesh.edges)} f={len(self.bmesh.faces)}')
e.submit(lambda : gather(self.bmesh.verts, self.bmesh.edges, self.bmesh.faces, True))
else:
if not self.split['gathered static']:
#print(f' sv={len(self.split["static verts"])} se={len(self.split["static edges"])} sf={len(self.split["static faces"])}')
e.submit(lambda : gather(self.split['static verts'], self.split['static edges'], self.split['static faces'], True))
self.split['gathered static'] = True
#print(f' dv={len(self.split["dynamic verts"])} de={len(self.split["dynamic edges"])} df={len(self.split["dynamic faces"])}')
e.submit(lambda : gather(self.split['dynamic verts'], self.split['dynamic edges'], self.split['dynamic faces'], False))
# @profiler.function
def clean(self):
if not self.buf_data_queue.empty():
tag_redraw_all('buffer update')
while not self.buf_data_queue.empty():
data = self.buf_data_queue.get()
if data == 'done':
self._is_loading = False
self._is_loaded = True
self.async_load = False
else:
self.add_buffered_render(*data)
try:
# return if rfmesh hasn't changed
self.rfmesh.clean()
ver = self.rfmesh.get_version() if not self.always_dirty else None
if self.rfmesh_version == ver:
# profiler.add_note('--> is clean')
return
# profiler.add_note(
# '--> versions: "%s",
# "%s"' % (str(self.rfmesh_version),
# str(ver))
# )
# make not dirty first in case bad things happen while drawing
self.rfmesh_version = ver
self._gather_data()
except:
Debugger.print_exception()
# profiler.add_note('--> exception')
pass
# profiler.add_note('--> passed through')
# @profiler.function
def draw(
self,
view_forward, unit_scaling_factor,
buf_matrix_target, buf_matrix_target_inv,
buf_matrix_view, buf_matrix_view_invtrans,
buf_matrix_proj,
alpha_above, alpha_below,
cull_backfaces, alpha_backface,
draw_mirrored,
symmetry=None, symmetry_view=None,
symmetry_effect=0.0, symmetry_frame: Frame=None
):
self.clean()
if not self.buffered_renders_static and not self.buffered_renders_dynamic: return
try:
gpustate.depth_test('LESS_EQUAL')
gpustate.depth_mask(False) # do not overwrite the depth buffer
opts = dict(self.opts)
opts['matrix model'] = self.rfmesh.xform.mx_p
opts['matrix normal'] = self.rfmesh.xform.mx_n
opts['matrix target'] = buf_matrix_target
opts['matrix target inverse'] = buf_matrix_target_inv
opts['matrix view'] = buf_matrix_view
opts['matrix view normal'] = buf_matrix_view_invtrans
opts['matrix projection'] = buf_matrix_proj
opts['forward direction'] = view_forward
opts['unit scaling factor'] = unit_scaling_factor
opts['symmetry'] = symmetry
opts['symmetry frame'] = symmetry_frame
opts['symmetry view'] = symmetry_view
opts['symmetry effect'] = symmetry_effect
opts['draw mirrored'] = draw_mirrored
bmegl.glSetDefaultOptions()
opts['no warning'] = not options['warn non-manifold']
opts['no pinned'] = not options['show pinned']
opts['no seam'] = not options['show seam']
opts['cull backfaces'] = cull_backfaces
opts['alpha backface'] = alpha_backface
opts['dpi mult'] = self.drawing.get_dpi_mult()
mirror_axes = self.rfmesh.mirror_mod.xyz if self.rfmesh.mirror_mod else []
for axis in mirror_axes: opts['mirror %s' % axis] = True
if not opts.get('no below', False):
# draw geometry hidden behind
# geometry below
opts['depth test'] = 'GREATER'
# opts['depth mask'] = False
opts['poly hidden'] = 1 - alpha_below
opts['poly mirror hidden'] = 1 - alpha_below
opts['line hidden'] = 1 - alpha_below
opts['line mirror hidden'] = 1 - alpha_below
opts['point hidden'] = 1 - alpha_below
opts['point mirror hidden'] = 1 - alpha_below
for buffered_render in chain(self.buffered_renders_static, self.buffered_renders_dynamic):
buffered_render.draw(opts)
# geometry above
opts['depth test'] = 'LESS_EQUAL'
# opts['depth mask'] = False
opts['poly hidden'] = 1 - alpha_above
opts['poly mirror hidden'] = 1 - alpha_above
opts['line hidden'] = 1 - alpha_above
opts['line mirror hidden'] = 1 - alpha_above
opts['point hidden'] = 1 - alpha_above
opts['point mirror hidden'] = 1 - alpha_above
for buffered_render in chain(self.buffered_renders_static, self.buffered_renders_dynamic):
buffered_render.draw(opts)
gpustate.depth_test('LESS_EQUAL')
gpustate.depth_mask(True)
except:
Debugger.print_exception()
pass
@@ -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)