2025-07-01
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user