Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/rf/rf_target.py
T
2026-03-17 14:30:01 -06:00

1280 lines
52 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 time
import random
import traceback
from itertools import chain
import bpy
from mathutils import Vector
from mathutils.geometry import intersect_line_line_2d as intersect_segment_segment_2d
from ...config.options import visualization, options, retopoflow_datablocks
from ...addon_common.common.debug import dprint, Debugger
from ...addon_common.common.decorators import timed_call
from ...addon_common.common.profiler import profiler, time_it
from ...addon_common.common.utils import iter_pairs, Dict
from ...addon_common.common.maths import Point, Vec, Direction, Normal, Ray, XForm, BBox
from ...addon_common.common.maths import Point2D, Vec2D, Direction2D
from ...addon_common.common.maths_accel import Accel2D
from ...addon_common.common.text import fix_string
from ..rfmesh.rfmesh import RFMesh, RFVert, RFEdge, RFFace
from ..rfmesh.rfmesh import RFSource, RFTarget
from ..rfmesh.rfmesh_render import RFMeshRender
class RetopoFlow_Target:
'''
functions to work on target mesh (RFTarget)
'''
# @profiler.function
def setup_target(self):
''' target is the active object. must be selected and visible '''
tar_object = self.get_target()
assert tar_object, 'Could not find valid target?'
self.rftarget = RFTarget.new(tar_object, self.unit_scaling_factor)
opts = visualization.get_target_settings()
self.rftarget_draw = RFMeshRender.new(self.rftarget, opts)
self.rftarget_version = None
self.hide_target()
self.accel_defer_recomputing = False
self.accel_data_all = Dict(get_default=None)
self.accel_data_sel = Dict(get_default=None)
self.accel_data_unsel = Dict(get_default=None)
self.accel_recompute = True
self._draw_count = 0
@property
def accel_vis_verts(self): return self.accel_data_all.verts
@property
def accel_vis_edges(self): return self.accel_data_all.edges
@property
def accel_vis_faces(self): return self.accel_data_all.faces
@property
def accel_vis_accel(self): return self.accel_data_all.accel
@property
def accel_vis_recompute(self): return self.accel_data_all.recompute
@accel_vis_recompute.setter
def accel_vis_recompute(self, v): self.accel_data_all.recompute = v
@property
def accel_sel_verts(self): return self.accel_data_sel.verts
@property
def accel_sel_edges(self): return self.accel_data_sel.edges
@property
def accel_sel_faces(self): return self.accel_data_sel.faces
@property
def accel_sel_accel(self): return self.accel_data_sel.accel
@property
def accel_sel_recompute(self): return self.accel_data_sel.recompute
@accel_sel_recompute.setter
def accel_sel_recompute(self, v): self.accel_data_sel.recompute = v
@property
def accel_unsel_verts(self): return self.accel_data_unsel.verts
@property
def accel_unsel_edges(self): return self.accel_data_unsel.edges
@property
def accel_unsel_faces(self): return self.accel_data_unsel.faces
@property
def accel_unsel_accel(self): return self.accel_data_unsel.accel
@property
def accel_unsel_recompute(self): return self.accel_data_unsel.recompute
@accel_unsel_recompute.setter
def accel_unsel_recompute(self, v): self.accel_data_unsel.recompute = v
@property
def accel_recompute(self):
return any([ self.accel_data_all.recompute, self.accel_data_sel.recompute, self.accel_data_unsel.recompute ])
@accel_recompute.setter
def accel_recompute(self, v):
self.accel_data_all.recompute = v
self.accel_data_sel.recompute = v
self.accel_data_unsel.recompute = v
def hide_target(self):
self.rftarget.obj_viewport_hide()
self.rftarget.obj_render_hide()
def check_target_symmetry(self):
bad = self.rftarget.check_symmetry()
if not bad: return
a = ", ".join(bad) + (" axis" if len(bad)==1 else " axes")
p = "plane" if len(bad)==1 else "planes"
self.alert_user(
title='Bad Target Symmetry',
message=fix_string(f'''
Symmetry is enabled on the {a}, but vertices were found on the "wrong" side of the symmetry {p}.
Editing these vertices will cause them to snap to the symmetry plane.
(Editing vertices on the "correct" side of symmetry will work as expected)
You can see these vertices by clicking Select Bad Symmetry button
or flip these vertices by clicking Flip Bad Symmetry button.
Both buttons are under Target Cleaning > Symmetry
'''),
level='warning',
)
def select_bad_symmetry(self):
self.deselect_all()
self.rftarget.select_bad_symmetry()
def teardown_target(self):
# IMPORTANT: changes here should also go in rf_blender_save.backup_recover()
self.rftarget.obj_viewport_unhide()
self.rftarget.obj_render_unhide()
def done_target(self):
del self.rftarget_draw
del self.rftarget
self.get_target().to_mesh_clear()
#########################################
# split target visualization
def clear_split_target_visualization(self):
self.rftarget_draw.split_visualization()
def split_target_visualization(self, verts=None, edges=None, faces=None):
self.rftarget_draw.split_visualization(verts=verts, edges=edges, faces=faces)
def split_target_visualization_selected(self):
self.rftarget_draw.split_visualization(
verts=self.get_selected_verts(),
edges=self.get_selected_edges(),
faces=self.get_selected_faces(),
)
def split_target_visualization_visible(self):
self.rftarget_draw.split_visualization(
verts=self.accel_vis_verts,
edges=self.accel_vis_edges,
faces=self.accel_vis_faces,
)
#########################################
# acceleration structures
def set_accel_defer(self, defer): self.accel_defer_recomputing = defer
def get_accel_visible(self, **kwargs):
accel_data = self._generate_accel_data_struct(**kwargs)
return accel_data.accel
def _generate_accel_data_struct(self, *, selected_only=None, force=False):
target_version = self.get_target_version(selection=selected_only)
view_version = self.get_view_version()
mm = self.rftarget.mirror_mod
accel_data = {
None: self.accel_data_all,
True: self.accel_data_sel,
False: self.accel_data_unsel,
}[selected_only]
# force |= self.accel_recompute
needs_recomputed = any([
accel_data.recompute,
# missing acceleration data?
accel_data.verts is None,
accel_data.edges is None,
accel_data.faces is None,
accel_data.accel is None,
# did any important thing change since we last generated accel structure?
accel_data.target_version != target_version,
accel_data.view_version != view_version,
accel_data.visible_bbox_factor != options['visible bbox factor'],
accel_data.visible_dist_offset != options['visible dist offset'],
accel_data.selection_occlusion_test != options['selection occlusion test'],
accel_data.selection_backface_test != options['selection backface test'],
accel_data.ray_ignore_backface_sources != self.ray_ignore_backface_sources(),
accel_data.mirror_mod != (mm.x, mm.y, mm.z),
])
delay_recompute = ([
self.accel_defer_recomputing,
self._nav, # do not recompute while artist is navigating
(time.time() - self._nav_time) < options['accel recompute delay'], # wait just a small amount of time after artist finishes navigating
accel_data.draw_count == self._draw_count,
])
recompute = force or (needs_recomputed and not any(delay_recompute))
if not recompute:
# if needs_recomputed and any(delay_recompute):
# print(f'VIS ACCEL NEEDS RECOMPUTED, BUT DELAYED: {delay_recompute}')
if accel_data.verts: accel_data.verts = set(self.filter_is_valid(accel_data.verts))
if accel_data.edges: accel_data.edges = set(self.filter_is_valid(accel_data.edges))
if accel_data.faces: accel_data.faces = set(self.filter_is_valid(accel_data.faces))
return accel_data
accel_data.recompute = False
match selected_only:
case None:
verts, edges, faces = None, None, None
case True:
verts = self.get_selected_verts()
edges = self.get_selected_edges()
faces = self.get_selected_faces()
case False:
verts = self.get_unselected_verts()
edges = self.get_unselected_edges()
faces = self.get_unselected_faces()
with time_it('getting visible geometry', enabled=False):
accel_data.verts = self.visible_verts(verts=verts)
accel_data.edges = self.visible_edges(edges=edges, verts=accel_data.verts)
accel_data.faces = self.visible_faces(faces=faces, verts=accel_data.verts)
with time_it('building accel struct', enabled=False):
accel_data.accel = Accel2D(
f'RFTarget visible geometry ({selected_only=})',
accel_data.verts,
accel_data.edges,
accel_data.faces,
self.iter_point2D_symmetries
)
# remember important things that influence accel structure
accel_data.target_version = target_version
accel_data.view_version = view_version
accel_data.visible_bbox_factor = options['visible bbox factor']
accel_data.visible_dist_offset = options['visible dist offset']
accel_data.selection_occlusion_test = options['selection occlusion test']
accel_data.selection_backface_test = options['selection backface test']
accel_data.ray_ignore_backface_sources = self.ray_ignore_backface_sources()
accel_data.draw_count = self._draw_count
accel_data.mirror_mod = (mm.x, mm.y, mm.z)
return accel_data
@staticmethod
def filter_is_valid(bmelems): return filter(RFMesh.fn_is_valid, bmelems)
def get_vis_verts(self, **kwargs):
self._generate_accel_data_struct(**kwargs)
return self.accel_vis_verts
def get_vis_edges(self, **kwargs):
self._generate_accel_data_struct(**kwargs)
return self.accel_vis_edges
def get_vis_faces(self, **kwargs):
self._generate_accel_data_struct(**kwargs)
return self.accel_vis_faces
def get_vis_geom(self, **kwargs):
self._generate_accel_data_struct(**kwargs)
return self.accel_vis_verts, self.accel_vis_edges, self.accel_vis_faces
def get_custom_vis_accel(self, selection_only=None, include_verts=True, include_edges=True, include_faces=True, symmetry=True):
verts, edges, faces = self.visible_geom()
if selection_only is not None:
fn_select = lambda bmelem: bmelem.select == selection_only
verts, edges, faces = list(filter(fn_select, verts)), list(filter(fn_select, edges)), list(filter(fn_select, faces))
return Accel2D(
'RFTarget custom',
(verts if include_verts else []),
(edges if include_edges else []),
(faces if include_faces else []),
self.iter_point2D_symmetries if symmetry else self.iter_point2D_nosymmetry,
)
def accel_nearest2D_vert(self, point=None, max_dist=None, vis_accel=None, selected_only=None):
xy = self.get_point2D(point or self.actions.mouse)
if not vis_accel:
vis_accel = self.get_accel_visible(selected_only=selected_only)
if not vis_accel: return (None, None)
if not max_dist:
# no max_dist, so get _all_ visible vertices
verts = self.accel_vis_verts
else:
# get all visible vertices within max_dist from mouse
max_dist = self.drawing.scale(max_dist)
verts = vis_accel.get_verts(xy, max_dist)
if selected_only is not None:
verts = { bmv for bmv in verts if bmv.select == selected_only }
return self.rftarget.nearest2D_bmvert_Point2D(xy, self.iter_point2D_symmetries, verts=verts, max_dist=max_dist)
def accel_nearest2D_edge(self, point=None, max_dist=None, vis_accel=None, selected_only=None, edges_only=None):
xy = self.get_point2D(point or self.actions.mouse)
if not vis_accel:
vis_accel = self.get_accel_visible(selected_only=selected_only)
if not vis_accel: return (None, None)
if not max_dist:
edges = self.accel_vis_edges
else:
max_dist = self.drawing.scale(max_dist)
edges = vis_accel.get_edges(xy, max_dist)
if selected_only is not None:
edges = { bme for bme in edges if bme.select == selected_only }
if edges_only is not None:
edges = { bme for bme in edges if bme in edges_only }
return self.rftarget.nearest2D_bmedge_Point2D(xy, self.iter_point2D_symmetries, edges=edges, max_dist=max_dist)
def accel_nearest2D_face(self, point=None, max_dist=None, vis_accel=None, selected_only=None, faces_only=None):
xy = self.get_point2D(point or self.actions.mouse)
if not vis_accel:
vis_accel = self.get_accel_visible(selected_only=selected_only)
if not vis_accel: return (None, None)
if not max_dist:
faces = self.accel_vis_faces
else:
max_dist = self.drawing.scale(max_dist)
faces = vis_accel.get_faces(xy, max_dist)
if selected_only is not None:
faces = { bmf for bmf in faces if bmf.select == selected_only }
if faces_only is not None:
faces = { bmf for bmf in faces if bmf in faces_only }
return self.rftarget.nearest2D_bmface_Point2D(self.Vec_forward(), xy, self.iter_point2D_symmetries, faces=faces) #, max_dist=max_dist)
def accel_nearest2D_geom(self, **kwargs):
if (vert := self.accel_nearest2D_vert(**kwargs)[0]): return vert
if (edge := self.accel_nearest2D_edge(**kwargs)[0]): return edge
if (face := self.accel_nearest2D_face(**kwargs)[0]): return face
return None
#########################################
# find target entities in screen space
def get_point2D(self, point):
if not point: return None
if point.is_2D(): return point
return self.Point_to_Point2D(point)
def _iter_symmetry_points(self, point, normal):
mm = self.rftarget.mirror_mod
mx,my,mz = mm.x, mm.y, mm.z
yield ( point, normal )
if not mx and not my and not mz: return
px,py,pz = point
nx,ny,nz = normal
if mx: yield ( Point((-px, py, pz)), Normal((-nx, ny, nz)) )
if my: yield ( Point(( px, -py, pz)), Normal(( nx, -ny, nz)) )
if mz: yield ( Point(( px, py, -pz)), Normal(( nx, ny, -nz)) )
if mx and my: yield ( Point((-px, -py, pz)), Normal((-nx, -ny, nz)) )
if mx and mz: yield ( Point((-px, py, -pz)), Normal((-nx, ny, -nz)) )
if my and mz: yield ( Point(( px, -py, -pz)), Normal(( nx, -ny, -nz)) )
if mx and my and mz: yield ( Point((-px, -py, -pz)), Normal((-nx, -ny, -nz)) )
def iter_point2D_symmetries(self, co, normal, *, fwd=None):
if not fwd: fwd = self.Vec_forward()
yield from (
pt2D
for (pt3D, no3D) in self._iter_symmetry_points(co, normal)
if self.Point2D_in_area(pt2D := self.Point_to_Point2D(pt3D)) and no3D.dot(fwd) <= 0
)
def iter_point2D_nosymmetry(self, co, normal, *, fwd=None):
yield self.Point_to_Point2D(co)
# @profiler.function
def nearest2D_vert(self, point=None, max_dist=None, verts=None):
xy = self.get_point2D(point or self.actions.mouse)
if max_dist: max_dist = self.drawing.scale(max_dist)
return self.rftarget.nearest2D_bmvert_Point2D(xy, self.iter_point2D_symmetries, verts=verts, max_dist=max_dist, fwd=self.Vec_forward())
# @profiler.function
def nearest2D_verts(self, point=None, max_dist:float=10, verts=None):
xy = self.get_point2D(point or self.actions.mouse)
max_dist = self.drawing.scale(max_dist)
return self.rftarget.nearest2D_bmverts_Point2D(xy, max_dist, self.iter_point2D_symmetries, verts=verts, fwd=self.Vec_forward())
# @profiler.function
def nearest2D_edge(self, point=None, max_dist=None, edges=None):
xy = self.get_point2D(point or self.actions.mouse)
if max_dist: max_dist = self.drawing.scale(max_dist)
return self.rftarget.nearest2D_bmedge_Point2D(xy, self.iter_point2D_symmetries, edges=edges, max_dist=max_dist, fwd=self.Vec_forward())
# @profiler.function
def nearest2D_edges(self, point=None, max_dist:float=10, edges=None):
xy = self.get_point2D(point or self.actions.mouse)
if max_dist: max_dist = self.drawing.scale(max_dist)
return self.rftarget.nearest2D_bmedges_Point2D(xy, max_dist, self.iter_point2D_symmetries, edges=edges, fwd=self.Vec_forward())
# TODO: implement max_dist
# @profiler.function
def nearest2D_face(self, point=None, max_dist=None, faces=None):
xy = self.get_point2D(point or self.actions.mouse)
if max_dist: max_dist = self.drawing.scale(max_dist)
return self.rftarget.nearest2D_bmface_Point2D(self.Vec_forward(), xy, self.iter_point2D_symmetries, faces=faces, fwd=self.Vec_forward())
# TODO: fix this function! Izzza broken
# @profiler.function
def nearest2D_faces(self, point=None, max_dist:float=10, faces=None):
xy = self.get_point2D(point or self.actions.mouse)
if max_dist: max_dist = self.drawing.scale(max_dist)
return self.rftarget.nearest2D_bmfaces_Point2D(xy, self.iter_point2D_symmetries, faces=faces, fwd=self.Vec_forward())
########################################
# find target entities in world space
def get_point3D(self, point):
if point.is_3D(): return point
xyz,_,_,_ = self.raycast_sources_Point2D(point)
return xyz
def nearest_vert_point(self, point, verts=None):
xyz = self.get_point3D(point)
if xyz is None: return None
return self.rftarget.nearest_bmvert_Point(xyz, verts=verts)
def nearest_vert_mouse(self, verts=None):
return self.nearest_vert_point(self.actions.mouse, verts=verts)
def nearest_verts_point(self, point, max_dist:float, bmverts=None):
xyz = self.get_point3D(point)
if xyz is None: return None
return self.rftarget.nearest_bmverts_Point(xyz, max_dist, bmverts=bmverts)
def nearest_verts_mouse(self, max_dist:float):
return self.nearest_verts_point(self.actions.mouse, max_dist)
def nearest_edges_Point(self, point, max_dist:float):
max_dist = self.drawing.scale(max_dist)
return self.rftarget.nearest_bmedges_Point(point, max_dist)
def nearest_edge_Point(self, point:Point, edges=None):
return self.rftarget.nearest_bmedge_Point(point, edges=edges)
#######################################
# get visible geometry
def visible_verts(self, verts=None): return self.rftarget.visible_verts(self.gen_is_visible(), verts=verts)
def visible_edges(self, verts=None, edges=None): return self.rftarget.visible_edges(self.gen_is_visible(), verts=verts, edges=edges)
def visible_faces(self, verts=None, faces=None): return self.rftarget.visible_faces(self.gen_is_visible(), verts=verts, faces=faces)
def visible_geom(self): return (verts := self.visible_verts()), self.visible_edges(verts=verts), self.visible_faces(verts=verts)
def nonvisible_verts(self): return self.rftarget.visible_verts(self.gen_is_nonvisible())
def nonvisible_edges(self, verts=None): return self.rftarget.visible_edges(self.gen_is_nonvisible(), verts=verts)
def nonvisible_faces(self, verts=None): return self.rftarget.visible_faces(self.gen_is_nonvisible(), verts=verts)
def nonvisible_geom(self): return (verts := self.nonvisible_verts()), self.nonvisible_edges(verts=verts), self.nonvisible_faces(verts=verts)
def iter_verts(self): yield from self.rftarget.iter_verts()
def iter_edges(self): yield from self.rftarget.iter_edges()
def iter_faces(self): yield from self.rftarget.iter_faces()
########################################
# symmetry utils
def apply_mirror_symmetry(self):
self.undo_push('applying mirror symmetry')
self.rftarget.apply_mirror_symmetry(self.nearest_sources_Point)
self.dirty()
def flip_symmetry_verts_to_correct_side(self):
self.undo_push('flipping verts to correct side of symmetry')
self.rftarget.flip_symmetry_verts_to_correct_side()
self.dirty()
# @profiler.function
def clip_pointloop(self, pointloop, connected):
# assuming loop will cross symmetry line exactly zero or two times
l2w_point,w2l_point = self.rftarget.xform.l2w_point,self.rftarget.xform.w2l_point
pointloop = [w2l_point(pt) for pt in pointloop]
if self.rftarget.mirror_mod.x and any(p.x < 0 for p in pointloop):
if connected:
rot_idx = next(i for i,p in enumerate(pointloop) if p.x < 0)
pointloop = pointloop[rot_idx:] + pointloop[:rot_idx]
npl = []
for p0,p1 in iter_pairs(pointloop, connected):
if p0.x < 0 and p1.x < 0: continue
elif p0.x == 0: npl += [p0]
elif p0.x > 0 and p1.x > 0: npl += [p0]
else:
connected = False
npl += [p0 + (p1 - p0) * (p0.x / (p0.x - p1.x))]
if npl:
npl[0].x = 0
npl[-1].x = 0
pointloop = npl
if self.rftarget.mirror_mod.y and any(p.y > 0 for p in pointloop):
if connected:
rot_idx = next(i for i,p in enumerate(pointloop) if p.y > 0)
pointloop = pointloop[rot_idx:] + pointloop[:rot_idx]
npl = []
for p0,p1 in iter_pairs(pointloop, connected):
if p0.y > 0 and p1.y > 0: continue
elif p0.y == 0: npl += [p0]
elif p0.y < 0 and p1.y < 0: npl += [p0]
else:
connected = False
npl += [p0 + (p1 - p0) * (p0.y / (p0.y - p1.y))]
if npl:
npl[0].y = 0
npl[-1].y = 0
pointloop = npl
if self.rftarget.mirror_mod.z and any(p.z < 0 for p in pointloop):
if connected:
rot_idx = next(i for i,p in enumerate(pointloop) if p.z < 0)
pointloop = pointloop[rot_idx:] + pointloop[:rot_idx]
npl = []
for p0,p1 in iter_pairs(pointloop, connected):
if p0.z < 0 and p1.z < 0: continue
elif p0.z == 0: npl += [p0]
elif p0.z > 0 and p1.z > 0: npl += [p0]
else:
connected = False
npl += [p0 + (p1 - p0) * (p0.z / (p0.z - p1.z))]
if npl:
npl[0].z = 0
npl[-1].z = 0
pointloop = npl
pointloop = [l2w_point(pt) for pt in pointloop]
return (pointloop, connected)
def clamp_pointloop(self, pointloop, connected):
return (pointloop, connected)
def is_point_on_mirrored_side(self, point):
p = self.rftarget.xform.w2l_point(point)
if self.rftarget.mirror_mod.x and p.x < 0: return True
if self.rftarget.mirror_mod.y and p.y > 0: return True
if self.rftarget.mirror_mod.z and p.z < 0: return True
return False
def symmetry_planes_for_point(self, point):
point = self.rftarget.xform.w2l_point(point)
mm = self.rftarget.mirror_mod
th = mm.symmetry_threshold * self.rftarget.unit_scaling_factor / 2.0
planes = set()
if mm.x and abs(point.x) <= th: planes.add('x')
if mm.y and abs(point.y) <= th: planes.add('y')
if mm.z and abs(point.z) <= th: planes.add('z')
return planes
def mirror_point(self, point):
mm = self.rftarget.mirror_mod
if mm.x or mm.y or mm.z:
xform = self.rftarget.xform
p = xform.w2l_point(point)
if mm.x and p.x < 0: p.x = -p.x
if mm.y and p.y > 0: p.y = -p.y
if mm.z and p.z < 0: p.z = -p.z
point = xform.l2w_point(p)
return point
def mirror_point_normal(self, point, normal):
mm = self.rftarget.mirror_mod
if mm.x or mm.y or mm.z:
xform = self.rftarget.xform
p, n = xform.w2l_point(point), xform.w2l_normal(normal)
if mm.x and p.x < 0: p.x, n.x = -p.x, -n.x
if mm.y and p.y > 0: p.y, n.y = -p.y, -n.y
if mm.z and p.z < 0: p.z, n.z = -p.z, -n.z
point, normal = xform.l2w_point(p), xform.l2w_normal(n)
return (point, normal)
def get_point_symmetry(self, point):
return self.rftarget.get_point_symmetry(point)
def snap_to_symmetry(self, point, symmetry, to_world=True, from_world=True):
return self.rftarget.snap_to_symmetry(point, symmetry, to_world=to_world, from_world=from_world)
def clamp_point_to_symmetry(self, point):
return self.rftarget.symmetry_real(point)
def push_then_snap_all_verts(self):
self.undo_push('push then snap all non-hidden verts')
d = options['push and snap distance']
bmvs = [bmv for bmv in self.rftarget.get_verts() if not bmv.hide]
for bmv in bmvs: bmv.co += bmv.normal * d
self.rftarget.snap_all_nonhidden_verts(self.nearest_sources_Point)
self.recalculate_face_normals(verts=bmvs)
def push_then_snap_selected_verts(self):
self.undo_push('push then snap selected verts')
d = options['push and snap distance']
bmvs = self.rftarget.get_selected_verts()
for bmv in bmvs: bmv.co += bmv.normal * d
self.rftarget.snap_selected_verts(self.nearest_sources_Point)
self.recalculate_face_normals(verts=bmvs)
# def snap_verts_filter(self, fn_filter):
# self.undo_push('snap filtered verts')
# self.rftarget.snap_verts_filter(self.nearest_source_Point, fn_filter)
#
# def snap_all_verts(self):
# self.undo_push('snap all verts')
# self.rftarget.snap_all_verts(self.nearest_sources_Point)
#
# def snap_all_nonhidden_verts(self):
# self.undo_push('snap all visible verts')
# self.rftarget.snap_all_nonhidden_verts(self.nearest_sources_Point)
#
# def snap_selected_verts(self):
# self.undo_push('snap visible and selected verts')
# self.rftarget.snap_selected_verts(self.nearest_sources_Point)
#
# def snap_unselected_verts(self):
# self.undo_push('snap visible and unselected verts')
# self.rftarget.snap_unselected_verts(self.nearest_sources_Point)
#
# def snap_visible_verts(self):
# self.undo_push('snap visible verts')
# nonvisible_verts = self.nonvisible_verts()
# self.rftarget.snap_verts_filter(self.nearest_sources_Point, lambda v: not v.hide and v not in nonvisible_verts)
#
# def snap_nonvisible_verts(self):
# self.undo_push('snap non-visible verts')
# nonvisible_verts = self.nonvisible_verts()
# self.rftarget.snap_verts_filter(self.nearest_sources_Point, lambda v: not v.hide and v in nonvisible_verts)
def remove_all_doubles(self):
self.undo_push('remove all doubles')
self.rftarget.remove_all_doubles(options['remove doubles dist'])
def remove_selected_doubles(self):
self.undo_push('remove selected doubles')
self.rftarget.remove_selected_doubles(options['remove doubles dist'])
def flip_face_normals(self):
self.undo_push('flipping face normals')
self.rftarget.flip_face_normals()
def recalculate_face_normals(self, *, verts=None, faces=None):
self.undo_push('recalculating face normals')
self.rftarget.recalculate_face_normals(verts=verts, faces=faces)
#######################################
# target manipulation functions
#
# note: these do NOT dirty the target!
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
def snap_vert(self, vert:RFVert, *, snap_to_symmetry=None):
if not vert or not vert.is_valid: return
xyz,norm,_,_ = self.nearest_sources_Point(vert.co)
if snap_to_symmetry:
xyz = self.snap_to_symmetry(xyz, snap_to_symmetry)
vert.co = xyz
vert.normal = norm
def snap2D_vert(self, vert:RFVert):
if not vert or not vert.is_valid: return
xy = self.Point_to_Point2D(vert.co)
xyz,norm,_,_ = self.raycast_sources_Point2D(xy)
if xyz is None: return
vert.co = xyz
vert.normal = norm
def offset2D_vert(self, vert:RFVert, delta_xy:Vec2D):
if not vert or not vert.is_valid: return
xy = self.Point_to_Point2D(vert.co) + delta_xy
xyz,norm,_,_ = self.raycast_sources_Point2D(xy)
if xyz is None: return
vert.co = xyz
vert.normal = norm
def set2D_vert(self, vert:RFVert, xy:Point2D, snap_to_symmetry=None):
if not vert or not vert.is_valid: return
xyz,norm,_,_ = self.raycast_sources_Point2D(xy)
if xyz is None: return
if snap_to_symmetry:
xyz = self.snap_to_symmetry(xyz, snap_to_symmetry)
vert.co = xyz
vert.normal = norm
return xyz
def set2D_crawl_vert(self, vert:RFVert, xy:Point2D):
if not vert or not vert.is_valid: return
hits = self.raycast_sources_Point2D_all(xy)
if not hits: return
# find closest
co = vert.co
p,n,_,_ = min(hits, key=lambda hit:(hit[0]-co).length)
vert.co = p
vert.normal = n
def new_vert_point(self, xyz:Point, *, ignore_backface=None):
if not xyz: return None
xyz, norm, _, _ = self.nearest_sources_Point(xyz)
if not xyz or not norm: return None
rfvert = self.rftarget.new_vert(xyz, norm)
d = self.Point_to_Direction(xyz)
_, n, _, _ = self.raycast_sources_Point(xyz, ignore_backface=ignore_backface)
if d and n and n.dot(d) > 0.5: self._detected_bad_normals = True
# if (d is None or norm.dot(d) > 0.5) and self.is_visible(rfvert.co, bbox_factor_override=0, dist_offset_override=0):
# self._detected_bad_normals = True
return rfvert
def new2D_vert_point(self, xy:Point2D, *, ignore_backface=None):
xyz, norm, _, _ = self.raycast_sources_Point2D(xy, ignore_backface=ignore_backface)
if not xyz or not norm: return None
rfvert = self.rftarget.new_vert(xyz, norm)
if rfvert.normal.dot(self.Point2D_to_Direction(xy)) >= 0 and self.is_visible(rfvert.co):
self._detected_bad_normals = True
return rfvert
def new2D_vert_mouse(self, *, ignore_backface=None):
return self.new2D_vert_point(self.actions.mouse, ignore_backface=ignore_backface)
def new_edge(self, verts):
return self.rftarget.new_edge(verts)
def new_face(self, verts):
return self.rftarget.new_face(verts)
def bridge_vertloop(self, vloop0, vloop1, connected):
assert len(vloop0) == len(vloop1), "loops must have same vertex counts"
faces = []
for pair0,pair1 in zip(iter_pairs(vloop0, connected), iter_pairs(vloop1, connected)):
v00,v01 = pair0
v10,v11 = pair1
nf = self.new_face((v00,v01,v11,v10))
if nf: faces.append(nf)
return faces
def holes_fill(self, edges, sides):
self.rftarget.holes_fill(edges, sides)
def update_verts_faces(self, verts):
self.rftarget.update_verts_faces(verts)
def update_face_normal(self, face):
return self.rftarget.update_face_normal(face)
def clean_duplicate_bmedges(self, vert):
return self.rftarget.clean_duplicate_bmedges(vert)
def remove_duplicate_bmfaces(self, vert):
return self.rftarget.remove_duplicate_bmfaces(vert)
###################################################
def ensure_lookup_tables(self):
self.rftarget.ensure_lookup_tables()
def dirty(self, *, selectionOnly=False):
self.accel_recompute = True
self.rftarget.dirty(selectionOnly=selectionOnly)
def dirty_render(self):
self.rftarget_draw.dirty()
def get_target_version(self, selection=True):
return self.rftarget.get_version(selection=selection)
def get_target_geometry_counts(self):
return self.rftarget.get_geometry_counts()
###################################################
# determines if any of the edges cross
# uses face normal to compute 2D projection
# returns None if any of the points cannot project
def is_face_twisted(self, bmverts, Point_to_Point2D=None):
if not Point_to_Point2D:
# estimate a normal
v0, v1, v2 = bmverts[:3]
n = (v1.co-v0.co).cross(v2.co-v0.co)
t = Direction.uniform()
y = Direction(t.cross(n))
x = Direction(y.cross(n))
Point_to_Point2D = lambda point: Vec2D((x.dot(point), y.dot(point)))
pts = [Point_to_Point2D(bmv.co) for bmv in bmverts]
if not all(pts): return None
l = len(pts)
for i0 in range(l):
i1 = (i0 + 1) % l
p0, p1 = pts[i0], pts[i1]
for j0 in range(i1 + 1, l):
j1 = (j0 + 1) % l
p2, p3 = pts[j0], pts[j1]
if intersect_segment_segment_2d(p0, p1, p2, p3): return True
return False
###################################################
def get_quadwalk_edgesequence(self, edge):
return self.rftarget.get_quadwalk_edgesequence(edge)
def get_edge_loop(self, edge):
return self.rftarget.get_edge_loop(edge)
def get_inner_edge_loop(self, edge):
return self.rftarget.get_inner_edge_loop(edge)
def get_face_loop(self, edge):
return self.rftarget.get_face_loop(edge)
def is_quadstrip_looped(self, edge):
return self.rftarget.is_quadstrip_looped(edge)
def iter_quadstrip(self, edge):
yield from self.rftarget.iter_quadstrip(edge)
###################################################
def get_selected_verts(self): return self.rftarget.get_selected_verts()
def get_selected_edges(self): return self.rftarget.get_selected_edges()
def get_selected_faces(self): return self.rftarget.get_selected_faces()
def get_selected_geom(self): return self.get_selected_verts(), self.get_selected_edges(), self.get_selected_faces()
def get_unselected_verts(self): return self.rftarget.get_unselected_verts()
def get_unselected_edges(self): return self.rftarget.get_unselected_edges()
def get_unselected_faces(self): return self.rftarget.get_unselected_faces()
def get_unselected_geom(self): return self.get_unselected_verts(), self.get_unselected_edges(), self.get_unselected_faces()
def get_hidden_verts(self): return self.rftarget.get_hidden_verts()
def get_hidden_edges(self): return self.rftarget.get_hidden_edges()
def get_hidden_faces(self): return self.rftarget.get_hidden_faces()
def get_hidden_geom(self): return self.get_hidden_verts(), self.get_hidden_edges(), self.get_hidden_faces()
def get_revealed_verts(self): return self.rftarget.get_revealed_verts()
def get_revealed_edges(self): return self.rftarget.get_revealed_edges()
def get_revealed_faces(self): return self.rftarget.get_revealed_faces()
def any_verts_selected(self):
return self.rftarget.any_verts_selected()
def any_edges_selected(self):
return self.rftarget.any_edges_selected()
def any_faces_selected(self):
return self.rftarget.any_faces_selected()
def any_selected(self):
return self.rftarget.any_selected()
def none_selected(self):
return not self.any_selected()
def deselect_all(self):
self.rftarget.deselect_all()
def deselect(self, elems, supparts=True, subparts=True):
self.rftarget.deselect(elems, supparts=supparts, subparts=subparts)
def select(self, elems, supparts=True, subparts=True, only=True):
self.rftarget.select(elems, supparts=supparts, subparts=subparts, only=only)
def select_toggle(self):
self.rftarget.select_toggle()
def select_invert(self):
self.rftarget.select_invert()
def select_linked(self, *, select=True, connected_to=None):
self.rftarget.select_linked(select=select, connected_to=connected_to)
def select_edge_loop(self, edge, only=True, **kwargs):
eloop,connected = self.get_edge_loop(edge)
self.rftarget.select(eloop, only=only, **kwargs)
def select_inner_edge_loop(self, edge, **kwargs):
eloop,connected = self.get_inner_edge_loop(edge)
self.rftarget.select(eloop, **kwargs)
def pin_selected(self):
self.undo_push('pinning selected')
self.rftarget.pin_selected()
self.dirty()
def unpin_selected(self):
self.undo_push('unpinning selected')
self.rftarget.unpin_selected()
self.dirty()
def unpin_all(self):
self.undo_push('unpinning all')
self.rftarget.unpin_all()
self.dirty()
def mark_seam_selected(self):
self.undo_push('pinning selected')
self.rftarget.mark_seam_selected()
self.dirty()
def clear_seam_selected(self):
self.undo_push('unpinning selected')
self.rftarget.clear_seam_selected()
self.dirty()
def hide_selected(self):
self.undo_push('hide selected')
verts, edges, faces = self.get_selected_geom()
hide_elems = {
*verts, *(bmv for bme in edges for bmv in bme.verts), *(bmv for bmf in faces for bmv in bmf.verts),
*edges, *(bme for bmf in faces for bme in bmf.edges), *(bme for bmv in verts for bme in bmv.link_edges),
*faces, *(bmf for bmv in verts for bmf in bmv.link_faces), *(bmf for bme in edges for bmf in bme.link_faces),
}
for e in hide_elems: e.hide = True
self.dirty()
def hide_visible(self):
self.undo_push('hide visible')
verts, edges, faces = self.get_vis_geom()
hide_elems = {
*verts, *(bmv for bme in edges for bmv in bme.verts), *(bmv for bmf in faces for bmv in bmf.verts),
*edges, *(bme for bmf in faces for bme in bmf.edges), *(bme for bmv in verts for bme in bmv.link_edges),
*faces, *(bmf for bmv in verts for bmf in bmv.link_faces), *(bmf for bme in edges for bmf in bme.link_faces),
}
for e in hide_elems: e.hide = True
self.dirty()
def hide_nonvisible(self):
self.undo_push('hide visible')
verts, edges, faces = self.nonvisible_geom()
hide_elems = {
*verts, *(bmv for bme in edges for bmv in bme.verts), *(bmv for bmf in faces for bmv in bmf.verts),
*edges, *(bme for bmf in faces for bme in bmf.edges), *(bme for bmv in verts for bme in bmv.link_edges),
*faces, *(bmf for bmv in verts for bmf in bmv.link_faces), *(bmf for bme in edges for bmf in bme.link_faces),
}
for e in hide_elems: e.hide = True
self.dirty()
def hide_unselected(self):
self.undo_push('hide unselected')
for e in chain(*self.get_unselected_geom()): e.hide = True
self.dirty()
def reveal_hidden(self):
self.undo_push('reveal hidden')
for e in chain(*self.get_hidden_geom()): e.hide = False
self.dirty()
#######################################################
def get_verts_link_edges(self, verts):
return RFVert.get_link_edges(verts)
def get_verts_link_faces(self, verts):
return RFVert.get_link_faces(verts)
def get_edges_verts(self, edges):
return RFEdge.get_verts(edges)
def get_faces_verts(self, faces):
return RFFace.get_verts(faces)
#######################################################
def smooth_edge_flow(self, iterations=10):
self.undo_push(f'smooth edge flow')
# get connected loops/strips
all_edges = set(self.get_selected_edges())
edge_sets = []
while all_edges:
current_set = set()
working = { next(iter(all_edges)) }
while working:
e = working.pop()
if e not in all_edges: continue
all_edges.discard(e)
current_set.add(e)
v0,v1 = e.verts
working.update(o for o in v0.link_edges if o.select)
working.update(o for o in v1.link_edges if o.select)
edge_sets.append(current_set)
niters = 1 if len(edge_sets)==1 else iterations
for i in range(niters):
for current_set in edge_sets:
for e in current_set:
v0,v1 = e.verts
faces0 = e.shared_faces(v0)
edges0 = [edge for f in faces0 for edge in f.edges if not edge.select and edge != e and edge.share_vert(e)]
verts0 = [edge.other_vert(v0) for edge in edges0]
verts0 = [v for v in verts0 if v and v != v1]
faces1 = e.shared_faces(v1)
edges1 = [edge for f in faces1 for edge in f.edges if not edge.select and edge != e and edge.share_vert(e)]
verts1 = [edge.other_vert(v1) for edge in edges0]
verts1 = [v for v in verts1 if v and v != v0]
if len(verts0) > 1:
v0.co = Point.average([v.co for v in verts0])
self.snap_vert(v0)
if len(verts1) > 1:
v1.co = Point.average([v.co for v in verts1])
self.snap_vert(v1)
self.dirty()
#######################################################
def merge_verts_by_dist(self, bmverts, merge_dist, *, select_merged=True):
""" Merging colocated visible verts """
# TODO: remove colocated faces
if merge_dist is None: return
bmverts = set(bmverts)
accel_data = self.get_custom_vis_accel(
selection_only=False,
include_verts=True, include_edges=False, include_faces=False,
symmetry=False,
)
snappable_bmverts = { bmv for bmv in accel_data.verts if bmv not in bmverts }
kwargs = { 'max_dist': self.drawing.scale(merge_dist), 'vis_accel': accel_data }
update_verts = []
for bmv in bmverts:
if not (xy := self.Point_to_Point2D(bmv.co)): continue
if not (bmv1 := self.accel_nearest2D_vert(point=xy, **kwargs)[0]): continue
bmv1.merge_robust(bmv)
update_verts.append(bmv1)
self.update_verts_faces(update_verts)
if select_merged:
self.select(update_verts, only=False)
return update_verts
#######################################################
def update_rot_object(self):
bbox = self.rftarget.get_selection_bbox()
if bbox.min == None:
if not options['move rotate object if no selection']: return
#bbox = BBox.merge(src.get_bbox() for src in self.rfsources)
bboxes = []
for s in self.rfsources:
verts = [(s.obj.matrix_world @ Vector((v[0], v[1], v[2], 1))) for v in s.obj.bound_box]
verts = [(v[0]/v[3], v[1]/v[3], v[2]/v[3]) for v in verts]
bboxes.append(BBox(from_coords=verts))
bbox = BBox.merge(bboxes)
# print('update_rot_object', bbox)
diff = bbox.max - bbox.min
rot_object = bpy.data.objects[retopoflow_datablocks['rotate object']]
rot_object.location = bbox.min + diff / 2
rot_object.scale = diff / 2
#######################################################
# delete / dissolve
def delete_dissolve_collapse_option(self, opt):
actions = {
'Dissolve': self.dissolve_option,
'Delete': self.delete_option,
'Collapse': self.collapse_option,
'Merge': self.merge_option,
}
if opt is None or opt[0] not in actions: return
action = actions[opt[0]]
action(opt[1])
def dissolve_option(self, opt):
sel_verts = self.rftarget.get_selected_verts()
sel_edges = self.rftarget.get_selected_edges()
sel_faces = self.rftarget.get_selected_faces()
try:
self.undo_push('dissolve %s' % opt)
if opt == 'Vertices' and sel_verts:
self.dissolve_verts(sel_verts)
elif opt == 'Edges' and sel_edges:
self.dissolve_edges(sel_edges)
elif opt == 'Faces' and sel_faces:
self.dissolve_faces(sel_faces)
elif opt == 'Loops' and sel_edges:
self.dissolve_edges(sel_edges)
self.dissolve_verts(self.rftarget.get_selected_verts())
#self.dissolve_loops()
self.dirty()
except RuntimeError as e:
self.undo_cancel()
self.alert_user('Error while dissolving:\n' + '\n'.join(e.args))
def delete_option(self, opt):
del_empty_edges=True
del_empty_verts=True
del_verts=True
del_edges=True
del_faces=True
if opt == 'Vertices':
pass
elif opt == 'Edges':
del_verts = False
elif opt == 'Faces':
del_verts = False
del_edges = False
elif opt == 'Only Edges & Faces':
del_verts = False
del_empty_verts = False
elif opt == 'Only Faces':
del_verts = False
del_edges = False
del_empty_verts = False
del_empty_edges = False
try:
self.undo_push('delete %s' % opt)
self.delete_selection(del_empty_edges=del_empty_edges, del_empty_verts=del_empty_verts, del_verts=del_verts, del_edges=del_edges, del_faces=del_faces)
self.dirty()
except RuntimeError as e:
self.undo_cancel()
self.alert_user('Error while deleting:\n' + '\n'.join(e.args))
def collapse_option(self, opt):
del_empty_edges=True
del_empty_verts=True
del_verts=True
del_edges=True
del_faces=True
if opt == 'Edges & Faces':
pass
else:
return
try:
self.undo_push('collapse %s' % opt)
self.collapse_edges_faces()
self.dirty()
except RuntimeError as e:
self.undo_cancel()
self.alert_user('Error while collapsing:\n' + '\n'.join(e.args))
def merge_option(self, opt):
if opt == 'At Center':
pass
elif opt == 'By Distance':
pass
else:
return
try:
self.undo_push('merge %s' % opt)
if opt == 'At Center':
self.merge_at_center()
elif opt == 'By Distance':
self.remove_selected_doubles()
self.dirty()
except RuntimeError as e:
self.undo_cancel()
self.alert_user('Error while merging:\n' + '\n'.join(e.args))
def merge_at_center(self):
self.rftarget.merge_at_center(self.nearest_sources_Point)
def collapse_edges_faces(self):
self.rftarget.collapse_edges_faces(self.nearest_sources_Point)
def delete_selection(self, del_empty_edges=True, del_empty_verts=True, del_verts=True, del_edges=True, del_faces=True):
self.rftarget.delete_selection(del_empty_edges=del_empty_edges, del_empty_verts=del_empty_verts, del_verts=del_verts, del_edges=del_edges, del_faces=del_faces)
def delete_verts(self, verts):
self.rftarget.delete_verts(verts)
def delete_edges(self, edges):
self.rftarget.delete_edges(edges)
def delete_faces(self, faces, del_empty_edges=True, del_empty_verts=True):
self.rftarget.delete_faces(faces, del_empty_edges=del_empty_edges, del_empty_verts=del_empty_verts)
def dissolve_verts(self, verts, use_face_split=False, use_boundary_tear=False):
self.rftarget.dissolve_verts(verts, use_face_split, use_boundary_tear)
def dissolve_edges(self, edges, use_verts=True, use_face_split=False):
self.rftarget.dissolve_edges(edges, use_verts, use_face_split)
def dissolve_faces(self, faces, use_verts=True):
self.rftarget.dissolve_faces(faces, use_verts)
# def find_loops(self, edges):
# if not edges: return []
# touched,loops = set(),[]
# def crawl(v0, edge01, vert_list):
# nonlocal edges, touched
# # ... -- v0 -- edge01 -- v1 -- edge12 -- ...
# # > came-^-from-^ ^-going-^-to >
# vert_list.append(v0)
# touched.add(edge01)
# v1 = edge01.other_vert(v0)
# if v1 == vert_list[0]: return vert_list
# next_edges = [e for e in v1.link_edges if e in edges and e != edge01]
# if not next_edges: return []
# if len(next_edges) == 1: edge12 = next_edges[0]
# else: edge12 = next_edge_in_string(edge01, v1)
# if not edge12 or edge12 in touched or edge12 not in edges: return []
# return crawl(v1, edge12, vert_list)
# for edge in edges:
# if edge in touched: continue
# vert_list = crawl(edge.verts[0], edge, [])
# if vert_list:
# loops.append(vert_list)
# return loops
# def dissolve_loops(self):
# sel_edges = self.get_selected_edges()
# sel_loops = self.find_loops(sel_edges)
# if not sel_loops:
# dprint('Could not find any loops')
# return
# while sel_loops:
# ploop = None
# for loop in sel_loops:
# sloop = set(loop)
# # find a parallel loop next to loop
# adj_verts = {e.other_vert(v) for v in loop for e in v.link_edges} - sloop
# adj_verts = {v for v in adj_verts if v.is_valid}
# parallel_edges = [e for v in adj_verts for e in v.link_edges if e.other_vert(v) in adj_verts]
# parallel_loops = self.find_loops(parallel_edges)
# if len(parallel_loops) != 2: continue
# ploop = parallel_loops[0]
# break
# if not ploop: break
# # merge loop into ploop
# eloop = [v0.shared_edge(v1) for v0,v1 in iter_pairs(loop, wrap=True)]
# self.deselect(loop)
# self.deselect(eloop)
# self.deselect([f for e in eloop for f in e.link_faces])
# v01 = {v0:next(v1 for v1 in ploop if v0.share_edge(v1)) for v0 in loop}
# edges = [v0.shared_edge(v1) for v0,v1 in v01.items()]
# self.delete_edges(edges)
# touched = set()
# for v0,v1 in v01.items():
# v1.merge(v0)
# touched.add(v1)
# for v in touched:
# self.clean_duplicate_bmedges(v)
# # remove dissolved loop
# sel_loops = [l for l in sel_loops if l != loop]