1280 lines
52 KiB
Python
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]
|