2025-07-01
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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/>.
|
||||
'''
|
||||
|
||||
__all__ = ['contours']
|
||||
@@ -0,0 +1,757 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 os
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
|
||||
from mathutils import Matrix
|
||||
|
||||
from ..rftool import RFTool
|
||||
from ..rfwidget import RFWidget
|
||||
|
||||
from ...addon_common.common import gpustate
|
||||
from ...addon_common.common.globals import Globals
|
||||
from ...addon_common.common.debug import dprint
|
||||
from ...addon_common.common.fsm import FSM
|
||||
from ...addon_common.common.drawing import Drawing, DrawCallbacks
|
||||
from ...addon_common.common.maths import Point, Normal, Vec2D, Plane, Vec
|
||||
from ...addon_common.common.profiler import profiler
|
||||
from ...addon_common.common.timerhandler import CallGovernor, StopwatchHandler
|
||||
from ...addon_common.common.utils import iter_pairs
|
||||
from ...addon_common.common import blender_preferences as bprefs
|
||||
from ...addon_common.common.blender import tag_redraw_all
|
||||
|
||||
from ...config.options import options
|
||||
|
||||
from .contours_ops import Contours_Ops
|
||||
from .contours_props import Contours_Props
|
||||
from .contours_utils import (
|
||||
find_loops,
|
||||
find_strings,
|
||||
loop_plane, loop_radius,
|
||||
Contours_Loop,
|
||||
Contours_Utils,
|
||||
)
|
||||
|
||||
from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory
|
||||
from ..rfwidgets.rfwidget_linecut import RFWidget_LineCut_Factory
|
||||
|
||||
|
||||
class Contours(RFTool, Contours_Ops, Contours_Props, Contours_Utils):
|
||||
name = 'Contours'
|
||||
description = 'Retopologize cylindrical forms, like arms and legs'
|
||||
icon = 'contours-icon.png'
|
||||
help = 'contours.md'
|
||||
shortcut = 'contours tool'
|
||||
statusbar = '{{insert}} Insert contour\t{{increase count}} Increase segments\t{{decrease count}} Decrease segments\t{{fill}} Bridge'
|
||||
ui_config = 'contours_options.html'
|
||||
|
||||
RFWidget_Default = RFWidget_Default_Factory.create()
|
||||
RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND')
|
||||
RFWidget_LineCut = RFWidget_LineCut_Factory.create('Contours line cut')
|
||||
|
||||
@RFTool.on_init
|
||||
def init(self):
|
||||
self.rfwidgets = {
|
||||
'default': self.RFWidget_Default(self),
|
||||
'cut': self.RFWidget_LineCut(self),
|
||||
'hover': self.RFWidget_Move(self),
|
||||
}
|
||||
self.clear_widget()
|
||||
|
||||
@RFTool.on_reset
|
||||
def reset(self):
|
||||
self.show_cut = False
|
||||
self.show_arrows = False
|
||||
self.pts = []
|
||||
self.cut_pts = []
|
||||
self.connected = False
|
||||
self.cuts = []
|
||||
self.crawl_viz = [] # for debugging
|
||||
self.hovering_sel_edge = None
|
||||
self.ui_initial_count = None
|
||||
|
||||
@RFTool.on_target_change
|
||||
#@FSM.onlyinstate('main')
|
||||
def update_target(self):
|
||||
self.sel_edges = set(self.rfcontext.get_selected_edges())
|
||||
#sel_faces = self.rfcontext.get_selected_faces()
|
||||
|
||||
# disable initial count input box if anything is selected
|
||||
if not self.ui_initial_count:
|
||||
self.ui_initial_count = self.document.body.getElementById('contours-initial-count')
|
||||
if self.ui_initial_count:
|
||||
self.ui_initial_count.disabled = bool(self.sel_edges)
|
||||
|
||||
# find verts along selected loops and strings
|
||||
sel_loops = find_loops(self.sel_edges)
|
||||
sel_strings = find_strings(self.sel_edges)
|
||||
|
||||
# filter out any loops or strings that are in the middle of a selected patch
|
||||
def in_middle(bmvs, is_loop):
|
||||
return any(len(bmv0.shared_edge(bmv1).link_faces) > 1 for bmv0,bmv1 in iter_pairs(bmvs, is_loop))
|
||||
sel_loops = [loop for loop in sel_loops if not in_middle(loop, True)]
|
||||
sel_strings = [string for string in sel_strings if not in_middle(string, False)]
|
||||
|
||||
# filter out long loops that wrap around patches, sharing edges with other strings
|
||||
bmes = {bmv0.shared_edge(bmv1) for string in sel_strings for bmv0,bmv1 in iter_pairs(string,False)}
|
||||
sel_loops = [loop for loop in sel_loops if not any(bmv0.shared_edge(bmv1) in bmes for bmv0,bmv1 in iter_pairs(loop,True))]
|
||||
|
||||
mirror_mod = self.rfcontext.rftarget.mirror_mod
|
||||
symmetry_threshold = mirror_mod.symmetry_threshold
|
||||
def get_string_length(string):
|
||||
nonlocal mirror_mod, symmetry_threshold
|
||||
c = len(string)
|
||||
if c == 0: return 0
|
||||
touches_mirror = False
|
||||
(x0,y0,z0),(x1,y1,z1) = string[0].co,string[-1].co
|
||||
if mirror_mod.x:
|
||||
if abs(x0) < symmetry_threshold or abs(x1) < symmetry_threshold:
|
||||
c = (c - 1) * 2
|
||||
touches_mirror = True
|
||||
if mirror_mod.y:
|
||||
if abs(y0) < symmetry_threshold or abs(y1) < symmetry_threshold:
|
||||
c = (c - 1) * 2
|
||||
touches_mirror = True
|
||||
if mirror_mod.z:
|
||||
if abs(z0) < symmetry_threshold or abs(z1) < symmetry_threshold:
|
||||
c = (c - 1) * 2
|
||||
touches_mirror = True
|
||||
if not touches_mirror: c -= 1
|
||||
return c
|
||||
|
||||
self.loops_data = [{
|
||||
'loop': loop,
|
||||
'plane': loop_plane(loop),
|
||||
'count': len(loop),
|
||||
'radius': loop_radius(loop),
|
||||
'cl': Contours_Loop(loop, True),
|
||||
} for loop in sel_loops]
|
||||
self.strings_data = [{
|
||||
'string': string,
|
||||
'plane': loop_plane(string),
|
||||
'count': get_string_length(string),
|
||||
'cl': Contours_Loop(string, False),
|
||||
} for string in sel_strings]
|
||||
self.sel_loops = [Contours_Loop(loop, True) for loop in sel_loops]
|
||||
|
||||
self._var_cut_count.disabled = True
|
||||
if len(self.loops_data) == 1 and len(self.strings_data) == 0:
|
||||
self._var_cut_count_value = self.loops_data[0]['count']
|
||||
self._var_cut_count.disabled = any(len(e.link_edges)!=2 for e in self.loops_data[0]['loop'])
|
||||
if len(self.strings_data) == 1 and len(self.loops_data) == 0:
|
||||
self._var_cut_count_value = self.strings_data[0]['count']
|
||||
self._var_cut_count.disabled = False
|
||||
|
||||
@FSM.on_state('main')
|
||||
def main(self):
|
||||
if not self.actions.using('action', ignoredrag=True):
|
||||
# only update while not pressing action, because action includes drag, and
|
||||
# the artist might move mouse off selected edge before drag kicks in!
|
||||
self.hovering_sel_edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['action dist'], selected_only=True)
|
||||
|
||||
if self.actions.using_onlymods('insert'):
|
||||
self.set_widget('cut')
|
||||
elif self.hovering_sel_edge:
|
||||
self.set_widget('hover')
|
||||
else:
|
||||
self.set_widget('default')
|
||||
|
||||
if self.handle_inactive_passthrough(): return
|
||||
|
||||
if self.actions.pressed('grab'):
|
||||
''' grab for translations '''
|
||||
self.move_done_pressed = 'confirm'
|
||||
self.move_done_released = None
|
||||
return 'grab'
|
||||
|
||||
if self.hovering_sel_edge:
|
||||
if self.actions.pressed('action'):
|
||||
# return self.action_setup()
|
||||
self.move_done_pressed = None
|
||||
self.move_done_released = 'action'
|
||||
return 'grab'
|
||||
|
||||
if self.rfcontext.actions.pressed('rotate plane'):
|
||||
''' rotation of loops (NOT strips) about plane normal '''
|
||||
return 'rotate plane'
|
||||
|
||||
if self.rfcontext.actions.pressed('rotate screen'):
|
||||
''' screen-space rotation of loops about plane origin '''
|
||||
return 'rotate screen'
|
||||
|
||||
if self.rfcontext.actions.pressed('fill'):
|
||||
self.fill()
|
||||
return
|
||||
|
||||
if self.rfcontext.actions.pressed({'increase count', 'decrease count'}, unpress=False):
|
||||
delta = 1 if self.rfcontext.actions.pressed('increase count') else -1
|
||||
self.rfcontext.undo_push('change segment count', repeatable=True)
|
||||
self.change_count(delta=delta)
|
||||
return
|
||||
|
||||
if self.actions.pressed({'select paint', 'select paint add'}, unpress=False):
|
||||
sel_only = self.actions.pressed('select paint')
|
||||
return self.rfcontext.setup_smart_selection_painting(
|
||||
{'edge'},
|
||||
use_select_tool=True,
|
||||
selecting=not sel_only,
|
||||
deselect_all=sel_only,
|
||||
# fn_filter_bmelem=self.filter_edge_selection,
|
||||
kwargs_select={'supparts': False},
|
||||
kwargs_deselect={'subparts': False},
|
||||
)
|
||||
|
||||
if self.actions.pressed({'select path add'}):
|
||||
return self.rfcontext.select_path(
|
||||
{'edge'},
|
||||
fn_filter_bmelem=self.filter_edge_selection,
|
||||
kwargs_select={'supparts': False},
|
||||
)
|
||||
|
||||
if self.actions.pressed({'select single', 'select single add'}, unpress=False):
|
||||
self.sel_only = self.actions.pressed('select single')
|
||||
self.actions.unpress()
|
||||
self.select_single()
|
||||
return
|
||||
|
||||
if self.rfcontext.actions.pressed({'select smart', 'select smart add'}, unpress=False):
|
||||
self.select_single.cancel()
|
||||
sel_only = self.rfcontext.actions.pressed('select smart')
|
||||
self.rfcontext.actions.unpress()
|
||||
|
||||
self.rfcontext.undo_push('select smart')
|
||||
edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=10)
|
||||
if not edge:
|
||||
if sel_only: self.rfcontext.deselect_all()
|
||||
return
|
||||
self.rfcontext.select_edge_loop(edge, only=sel_only, supparts=False)
|
||||
return
|
||||
|
||||
@StopwatchHandler.delayed(time_delay=0.1)
|
||||
def select_single(self):
|
||||
bme,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['select dist'])
|
||||
if not bme and not self.sel_only: return
|
||||
|
||||
self.rfcontext.undo_push('select')
|
||||
if self.sel_only: self.rfcontext.deselect_all()
|
||||
if not bme: return
|
||||
|
||||
if bme.select: self.rfcontext.deselect(bme, subparts=False)
|
||||
else: self.rfcontext.select(bme, supparts=False, only=self.sel_only)
|
||||
self.rfcontext.dirty(selectionOnly=True)
|
||||
|
||||
@FSM.on_state('rotate plane', 'can enter')
|
||||
def rotateplane_can_enter(self):
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
sel_loops = find_loops(sel_edges)
|
||||
if not sel_loops:
|
||||
if self.strings_data:
|
||||
self.rfcontext.alert_user('Can only plane-rotate complete loops that do not cross the symmetry plane')
|
||||
else:
|
||||
self.rfcontext.alert_user('Could not find valid loops to plane-rotate')
|
||||
return False
|
||||
|
||||
self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
|
||||
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
|
||||
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
|
||||
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
|
||||
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
|
||||
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
|
||||
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
|
||||
|
||||
self.move_cuts = []
|
||||
for cloop in self.move_cloops:
|
||||
xy = self.rfcontext.Point_to_Point2D(cloop.plane.o)
|
||||
ray = self.rfcontext.Point2D_to_Ray(xy)
|
||||
crawl = self.rfcontext.plane_intersection_crawl(ray, cloop.plane, walk_to_plane=True)
|
||||
if not crawl:
|
||||
# dprint('could not crawl around sources for loop')
|
||||
pass
|
||||
self.move_cuts += [None]
|
||||
continue
|
||||
crawl_pts = [c for _,c,_ in crawl]
|
||||
connected = cloop.connected # XXX why was `crawl[0][0] is not None` here?
|
||||
crawl_pts,connected = self.rfcontext.clip_pointloop(crawl_pts, connected)
|
||||
if not crawl_pts or connected != cloop.connected:
|
||||
# dprint('could not clip loop to symmetry')
|
||||
pass
|
||||
self.move_cuts += [None]
|
||||
continue
|
||||
cl_cut = Contours_Loop(crawl_pts, connected)
|
||||
cl_cut.align_to(cloop)
|
||||
self.move_cuts += [cl_cut]
|
||||
if not any(self.move_cuts):
|
||||
self.rfcontext.alert_user('Could not find valid loops to plane-rotate')
|
||||
# dprint('Found no loops to shift')
|
||||
pass
|
||||
return False
|
||||
|
||||
@FSM.on_state('rotate plane', 'enter')
|
||||
def rotateplane_enter(self):
|
||||
self.rot_axis = Vec((0,0,0))
|
||||
self.rot_origin = Point.average(cut.get_origin() for cut in self.move_cuts if cut)
|
||||
self.shift_about = self.rfcontext.Point_to_Point2D(self.rot_origin)
|
||||
for cut in self.move_cuts:
|
||||
if not cut: continue
|
||||
a = cut.get_normal()
|
||||
o = cut.get_origin()
|
||||
if self.rot_axis.dot(a) < 0: a = -a
|
||||
self.rot_axis += a
|
||||
self.rot_axis.normalize()
|
||||
p0 = next(iter(cut.get_origin() for cut in self.move_cuts if cut))
|
||||
p1 = p0 + self.rot_axis * 0.001
|
||||
self.rot_axis2D = (self.rfcontext.Point_to_Point2D(p1) - self.rfcontext.Point_to_Point2D(p0))
|
||||
self.rot_axis2D.normalize()
|
||||
self.rot_perp2D = Vec2D((self.rot_axis2D.y, -self.rot_axis2D.x))
|
||||
# print(self.rot_axis, self.rot_axis2D, self.rot_perp2D)
|
||||
|
||||
self.rfcontext.undo_push('rotate plane contours')
|
||||
|
||||
self.mousedown = self.rfcontext.actions.mouse
|
||||
|
||||
self._timer = self.actions.start_timer(120.0)
|
||||
self.rfcontext.split_target_visualization(verts=[v for vs in self.move_verts for v in vs])
|
||||
self.rfcontext.set_accel_defer(True)
|
||||
|
||||
@FSM.on_state('rotate plane')
|
||||
# @profiler.function
|
||||
def rotateplane_main(self):
|
||||
if self.rfcontext.actions.pressed('confirm'):
|
||||
return 'main'
|
||||
if self.rfcontext.actions.pressed('cancel'):
|
||||
self.rfcontext.undo_cancel()
|
||||
return 'main'
|
||||
|
||||
if self.rfcontext.actions.pressed('rotate screen'):
|
||||
self.rfcontext.undo_cancel()
|
||||
return 'rotate screen'
|
||||
|
||||
if not self.actions.mousemove_stop: return
|
||||
# # only update cut on timer events and when mouse has moved
|
||||
# if not self.rfcontext.actions.timer: return
|
||||
|
||||
delta = Vec2D(self.rfcontext.actions.mouse - self.mousedown)
|
||||
shift_offset = self.rfcontext.drawing.unscale(self.rot_perp2D.dot(delta)) / 1000
|
||||
up_dir = self.rfcontext.Vec_up()
|
||||
|
||||
raycast,project = self.rfcontext.raycast_sources_Point2D,self.rfcontext.Point_to_Point2D
|
||||
for i_cloop in range(len(self.move_cloops)):
|
||||
cloop = self.move_cloops[i_cloop]
|
||||
cl_cut = self.move_cuts[i_cloop]
|
||||
if not cl_cut: continue
|
||||
shift_dir = 1 if cl_cut.get_normal().dot(self.rot_axis) > 0 else -1
|
||||
|
||||
verts = self.move_verts[i_cloop]
|
||||
dists = self.move_dists[i_cloop]
|
||||
proj_dists = self.move_proj_dists[i_cloop]
|
||||
circumference = self.move_circumferences[i_cloop]
|
||||
|
||||
lc = cl_cut.circumference
|
||||
shft = (cl_cut.offset + shift_offset * shift_dir * lc) % lc
|
||||
ndists = [shft] + [0.999 * lc * (d/circumference) for d in dists]
|
||||
i,dist = 0,ndists[0]
|
||||
l = len(ndists)-1 if cloop.connected else len(ndists)
|
||||
for c0,c1 in cl_cut.iter_pts(repeat=True):
|
||||
d = (c1-c0).length
|
||||
while dist - d <= 0:
|
||||
# create new vert between c0 and c1
|
||||
p = c0 + (c1 - c0) * (dist / d) + (cloop.plane.n * proj_dists[i])
|
||||
p,n,_,_ = self.rfcontext.nearest_sources_Point(p)
|
||||
verts[i].co = p
|
||||
verts[i].normal = n
|
||||
i += 1
|
||||
if i == l: break
|
||||
dist += ndists[i]
|
||||
dist -= d
|
||||
if i == l: break
|
||||
|
||||
self.rfcontext.update_verts_faces(verts)
|
||||
self.rfcontext.dirty()
|
||||
|
||||
@FSM.on_state('rotate plane', 'exit')
|
||||
def rotateplane_exit(self):
|
||||
self._timer.done()
|
||||
self.rfcontext.clear_split_target_visualization()
|
||||
self.rfcontext.set_accel_defer(False)
|
||||
tag_redraw_all('Contours finish rotate')
|
||||
|
||||
|
||||
def action_setup(self):
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
sel_loops = find_loops(sel_edges)
|
||||
sel_strings = find_strings(sel_edges, min_length=2)
|
||||
if not sel_loops or sel_strings: return
|
||||
|
||||
# prefer to move loops over strings
|
||||
if sel_loops: self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
|
||||
else: self.move_cloops = [Contours_Loop(string, False) for string in sel_strings]
|
||||
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
|
||||
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
|
||||
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
|
||||
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
|
||||
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
|
||||
self.move_orig_origins = [Point(p) for p in self.move_origins]
|
||||
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
|
||||
|
||||
#self.grab_along = self.rfcontext.Point_to_Point2D(sum(self.move_origins, Vec((0,0,0))) / len(self.move_origins))
|
||||
#self.rotate_start = math.atan2(self.rotate_about.y - self.mousedown.y, self.rotate_about.x - self.mousedown.x)
|
||||
|
||||
self.mousedown = self.actions.mouse
|
||||
self.move_prevmouse = None
|
||||
|
||||
return self.rfcontext.setup_action()
|
||||
|
||||
def action_callback(self, val):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@FSM.on_state('grab', 'can enter')
|
||||
def grab_can_enter(self):
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
sel_loops = find_loops(sel_edges)
|
||||
sel_strings = find_strings(sel_edges, min_length=2)
|
||||
return bool(sel_loops or sel_strings)
|
||||
|
||||
@FSM.on_state('grab', 'enter')
|
||||
def grab_enter(self):
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
sel_loops = find_loops(sel_edges)
|
||||
sel_strings = find_strings(sel_edges, min_length=2)
|
||||
|
||||
# prefer to move loops over strings
|
||||
if sel_loops: self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
|
||||
else: self.move_cloops = [Contours_Loop(string, False) for string in sel_strings]
|
||||
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
|
||||
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
|
||||
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
|
||||
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
|
||||
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
|
||||
self.move_orig_origins = [Point(p) for p in self.move_origins]
|
||||
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
|
||||
|
||||
self.rfcontext.undo_push('grab contours')
|
||||
|
||||
#self.grab_along = self.rfcontext.Point_to_Point2D(sum(self.move_origins, Vec((0,0,0))) / len(self.move_origins))
|
||||
#self.rotate_start = math.atan2(self.rotate_about.y - self.mousedown.y, self.rotate_about.x - self.mousedown.x)
|
||||
|
||||
self.grab_opts = {
|
||||
'mousedown': self.actions.mouse,
|
||||
'timer': self.actions.start_timer(120.0),
|
||||
}
|
||||
self.rfcontext.split_target_visualization(verts=[v for vs in self.move_verts for v in vs])
|
||||
self.rfcontext.set_accel_defer(True)
|
||||
|
||||
|
||||
@FSM.on_state('grab')
|
||||
# @profiler.function
|
||||
def grab(self):
|
||||
opts = self.grab_opts
|
||||
|
||||
if self.rfcontext.actions.pressed(self.move_done_pressed):
|
||||
return 'main'
|
||||
if self.rfcontext.actions.released(self.move_done_released, ignoredrag=True):
|
||||
return 'main'
|
||||
if self.rfcontext.actions.pressed('cancel'):
|
||||
self.rfcontext.actions.unuse(self.move_done_released, ignoremods=True, ignoremulti=True)
|
||||
self.rfcontext.undo_cancel()
|
||||
return 'main'
|
||||
|
||||
if not self.actions.mousemove_stop: return
|
||||
# # only update cut on timer events and when mouse has moved
|
||||
# if not self.rfcontext.actions.timer: return
|
||||
|
||||
delta = Vec2D(self.actions.mouse - opts['mousedown'])
|
||||
# print(f'contours.grab: {delta}')
|
||||
# self.crawl_viz = []
|
||||
|
||||
raycast,project = self.rfcontext.raycast_sources_Point2D,self.rfcontext.Point_to_Point2D
|
||||
for i_cloop in range(len(self.move_cloops)):
|
||||
cloop = self.move_cloops[i_cloop]
|
||||
verts = self.move_verts[i_cloop]
|
||||
pts = self.move_pts[i_cloop]
|
||||
dists = self.move_dists[i_cloop]
|
||||
origin = self.move_origins[i_cloop]
|
||||
proj_dists = self.move_proj_dists[i_cloop]
|
||||
circumference = self.move_circumferences[i_cloop]
|
||||
|
||||
depth = self.rfcontext.Point_to_depth(origin)
|
||||
if depth is None: continue
|
||||
origin2D_new = self.rfcontext.Point_to_Point2D(origin) + delta
|
||||
origin_new = self.rfcontext.Point2D_to_Point(origin2D_new, depth)
|
||||
plane_new = Plane(origin_new, cloop.plane.n)
|
||||
ray_new = self.rfcontext.Point2D_to_Ray(origin2D_new)
|
||||
crawl = self.rfcontext.plane_intersection_crawl(ray_new, plane_new, walk_to_plane=True)
|
||||
if not crawl: continue
|
||||
crawl_pts = [c for _,c,_ in crawl]
|
||||
# self.crawl_viz += [crawl_pts]
|
||||
connected = crawl[0][0] is not None
|
||||
crawl_pts,nconnected = self.rfcontext.clip_pointloop(crawl_pts, connected)
|
||||
connected = nconnected
|
||||
if not crawl_pts or connected != cloop.connected: continue
|
||||
cl_cut = Contours_Loop(crawl_pts, connected)
|
||||
|
||||
cl_cut.align_to(cloop)
|
||||
lc = cl_cut.circumference
|
||||
ndists = [cl_cut.offset] + [0.999 * lc * (d/circumference) for d in dists]
|
||||
i,dist = 0,ndists[0]
|
||||
l = len(ndists)-1 if cloop.connected else len(ndists)
|
||||
for c0,c1 in cl_cut.iter_pts(repeat=True):
|
||||
d = max(0.000001, (c1-c0).length)
|
||||
while dist - d <= 0:
|
||||
# create new vert between c0 and c1
|
||||
p = c0 + (c1 - c0) * (dist / d) + (cloop.plane.n * proj_dists[i])
|
||||
p,_,_,_ = self.rfcontext.nearest_sources_Point(p)
|
||||
verts[i].co = p
|
||||
i += 1
|
||||
if i == l: break
|
||||
dist += ndists[i]
|
||||
dist -= d
|
||||
if i == l: break
|
||||
|
||||
self.rfcontext.update_verts_faces(verts)
|
||||
self.rfcontext.dirty()
|
||||
|
||||
@FSM.on_state('grab', 'exit')
|
||||
def grab_exit(self):
|
||||
self.grab_opts['timer'].done()
|
||||
self.rfcontext.set_accel_defer(False)
|
||||
self.rfcontext.clear_split_target_visualization()
|
||||
tag_redraw_all('Contours finish grab')
|
||||
|
||||
|
||||
@FSM.on_state('rotate screen', 'can enter')
|
||||
def rotatescreen_can_enter(self):
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
sel_loops = find_loops(sel_edges)
|
||||
sel_strings = find_strings(sel_edges, min_length=2)
|
||||
return sel_loops or sel_strings
|
||||
|
||||
@FSM.on_state('rotate screen', 'enter')
|
||||
def rotatescreen_enter(self):
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
sel_loops = find_loops(sel_edges)
|
||||
sel_strings = find_strings(sel_edges, min_length=2)
|
||||
|
||||
# prefer to move loops over strings
|
||||
if sel_loops: self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
|
||||
else: self.move_cloops = [Contours_Loop(string, False) for string in sel_strings]
|
||||
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
|
||||
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
|
||||
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
|
||||
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
|
||||
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
|
||||
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
|
||||
|
||||
self.rfcontext.undo_push('rotate screen contours')
|
||||
|
||||
self.mousedown = self.rfcontext.actions.mouse
|
||||
|
||||
self.rotate_about = self.rfcontext.Point_to_Point2D(sum(self.move_origins, Vec((0,0,0))) / len(self.move_origins))
|
||||
self.rotate_start = math.atan2(self.rotate_about.y - self.mousedown.y, self.rotate_about.x - self.mousedown.x)
|
||||
|
||||
self._timer = self.actions.start_timer(120.0)
|
||||
self.rfcontext.split_target_visualization(verts=[v for vs in self.move_verts for v in vs])
|
||||
self.rfcontext.set_accel_defer(True)
|
||||
|
||||
@FSM.on_state('rotate screen')
|
||||
# @profiler.function
|
||||
def rotatescreen_main(self):
|
||||
if self.rfcontext.actions.pressed('confirm'):
|
||||
return 'main'
|
||||
if self.rfcontext.actions.pressed('cancel'):
|
||||
self.rfcontext.undo_cancel()
|
||||
return 'main'
|
||||
|
||||
if self.rfcontext.actions.pressed('rotate plane'):
|
||||
self.rfcontext.undo_cancel()
|
||||
return 'rotate plane'
|
||||
|
||||
if not self.actions.mousemove_stop: return
|
||||
# # only update cut on timer events and when mouse has moved
|
||||
# if not self.rfcontext.actions.timer: return
|
||||
|
||||
delta = Vec2D(self.rfcontext.actions.mouse - self.rotate_about)
|
||||
rotate = (math.atan2(delta.y, delta.x) - self.rotate_start + math.pi) % (math.pi * 2)
|
||||
|
||||
raycast,project = self.rfcontext.raycast_sources_Point2D,self.rfcontext.Point_to_Point2D
|
||||
for i_cloop in range(len(self.move_cloops)):
|
||||
cloop = self.move_cloops[i_cloop]
|
||||
verts = self.move_verts[i_cloop]
|
||||
pts = self.move_pts[i_cloop]
|
||||
dists = self.move_dists[i_cloop]
|
||||
origin = self.move_origins[i_cloop]
|
||||
proj_dists = self.move_proj_dists[i_cloop]
|
||||
circumference = self.move_circumferences[i_cloop]
|
||||
|
||||
origin2D = self.rfcontext.Point_to_Point2D(origin)
|
||||
ray = self.rfcontext.Point_to_Ray(origin)
|
||||
rmat = Matrix.Rotation(rotate, 4, -ray.d)
|
||||
normal = rmat @ cloop.plane.n
|
||||
plane = Plane(cloop.plane.o, normal)
|
||||
ray = self.rfcontext.Point2D_to_Ray(origin2D)
|
||||
crawl = self.rfcontext.plane_intersection_crawl(ray, plane, walk_to_plane=True)
|
||||
if not crawl: continue
|
||||
crawl_pts = [c for _,c,_ in crawl]
|
||||
connected = crawl[0][0] is not None
|
||||
crawl_pts,connected = self.rfcontext.clip_pointloop(crawl_pts, connected)
|
||||
if not crawl_pts or connected != cloop.connected: continue
|
||||
cl_cut = Contours_Loop(crawl_pts, connected)
|
||||
|
||||
cl_cut.align_to(cloop)
|
||||
lc = cl_cut.circumference
|
||||
ndists = [cl_cut.offset] + [0.999 * lc * (d/circumference) for d in dists]
|
||||
i,dist = 0,ndists[0]
|
||||
l = len(ndists)-1 if cloop.connected else len(ndists)
|
||||
for c0,c1 in cl_cut.iter_pts(repeat=True):
|
||||
d = (c1-c0).length
|
||||
d = max(0.00000001, d)
|
||||
while dist - d <= 0:
|
||||
# create new vert between c0 and c1
|
||||
p = c0 + (c1 - c0) * (dist / d) + (cloop.plane.n * proj_dists[i])
|
||||
p,_,_,_ = self.rfcontext.nearest_sources_Point(p)
|
||||
verts[i].co = p
|
||||
i += 1
|
||||
if i == l: break
|
||||
dist += ndists[i]
|
||||
dist -= d
|
||||
if i == l: break
|
||||
|
||||
self.rfcontext.update_verts_faces(verts)
|
||||
self.rfcontext.dirty()
|
||||
|
||||
@FSM.on_state('rotate screen', 'exit')
|
||||
def rotatescreen_exit(self):
|
||||
self._timer.done()
|
||||
self.rfcontext.clear_split_target_visualization()
|
||||
self.rfcontext.set_accel_defer(False)
|
||||
tag_redraw_all('Contours finish rotate')
|
||||
|
||||
|
||||
@RFWidget.on_action('Contours line cut')
|
||||
def new_line(self):
|
||||
xy0,xy1 = self.rfwidgets['cut'].line2D
|
||||
if not xy0 or not xy1: return
|
||||
if (xy1-xy0).length < 0.001: return
|
||||
xy01 = xy0 + (xy1-xy0) / 2
|
||||
plane = self.rfcontext.Point2D_to_Plane(xy0, xy1)
|
||||
ray = self.rfcontext.Point2D_to_Ray(xy01)
|
||||
self.new_cut(ray, plane, walk_to_plane=False, check_hit=xy01)
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('rotate screen')
|
||||
def draw_post2d_rotate_screenspace(self):
|
||||
gpustate.blend('ALPHA')
|
||||
Globals.drawing.draw2D_line(
|
||||
self.rotate_about,
|
||||
self.rfcontext.actions.mouse,
|
||||
(1.0, 1.0, 0.1, 1.0), color1=(1.0, 1.0, 0.1, 0.0),
|
||||
width=2, stipple=[2, 2]
|
||||
)
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('rotate plane')
|
||||
def draw_post2d_rotate_plane(self):
|
||||
gpustate.blend('ALPHA')
|
||||
Globals.drawing.draw2D_line(
|
||||
self.shift_about + self.rot_axis2D * 1000,
|
||||
self.shift_about - self.rot_axis2D * 1000,
|
||||
(0.1, 1.0, 1.0, 1.0), color1=(0.1, 1.0, 1.0, 0.0),
|
||||
width=2, stipple=[2,2],
|
||||
)
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('grab')
|
||||
def draw_post2d_grab(self):
|
||||
project = self.rfcontext.Point_to_Point2D
|
||||
intersect = self.rfcontext.raycast_sources_Point2D
|
||||
delta = Vec2D(self.actions.mouse - self.grab_opts['mousedown'])
|
||||
c0_good, c1_good = (1.0, 0.1, 1.0, 0.5), (1.0, 0.1, 1.0, 0.0)
|
||||
c0_bad, c1_bad = (1.0, 0.1, 0.1, 1.0), (1.0, 0.1, 0.1, 0.0)
|
||||
gpustate.blend('ALPHA')
|
||||
for o in self.move_origins:
|
||||
p0, p1 = project(o), project(o) + delta
|
||||
_p,_,_,_ = intersect(p1)
|
||||
Globals.drawing.draw2D_line(
|
||||
p0, p1,
|
||||
(c0_good if _p else c0_bad), color1=(c1_good if _p else c1_bad),
|
||||
width=2, stipple=[2,2],
|
||||
)
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
def draw_post2d(self):
|
||||
point_to_point2d = self.rfcontext.Point_to_Point2D
|
||||
is_visible = lambda p: self.rfcontext.is_visible(p, occlusion_test_override=True)
|
||||
up = self.rfcontext.Vec_up()
|
||||
size_to_size2D = self.rfcontext.size_to_size2D
|
||||
text_draw2D = self.rfcontext.drawing.text_draw2D
|
||||
self.rfcontext.drawing.set_font_size(12)
|
||||
|
||||
bmv_count = set()
|
||||
bmv_count_loops = {}
|
||||
bmv_count_strings = {}
|
||||
|
||||
for loop_data in self.loops_data:
|
||||
loop = loop_data['loop']
|
||||
radius = loop_data['radius']
|
||||
count = loop_data['count']
|
||||
plane = loop_data['plane']
|
||||
cl = loop_data['cl']
|
||||
|
||||
# draw segment count label
|
||||
loop = [vert for vert in loop if vert.is_valid]
|
||||
loop = [(vert, point_to_point2d(vert.co)) for vert in loop if is_visible(vert.co)]
|
||||
if loop:
|
||||
bmv = max(loop, key=lambda bmvp2d:bmvp2d[1].y)[0]
|
||||
if bmv not in bmv_count_loops: bmv_count_loops[bmv] = []
|
||||
bmv_count_loops[bmv].append(count)
|
||||
bmv_count.add(bmv)
|
||||
|
||||
for string_data in self.strings_data:
|
||||
string = string_data['string']
|
||||
count = string_data['count']
|
||||
plane = string_data['plane']
|
||||
|
||||
# draw segment count label
|
||||
string = [vert for vert in string if vert.is_valid]
|
||||
string = [(vert, point_to_point2d(vert.co)) for vert in string if is_visible(vert.co)]
|
||||
if string:
|
||||
bmv = max(string, key=lambda bmvp2d:bmvp2d[1].y)[0]
|
||||
if bmv not in bmv_count_strings: bmv_count_strings[bmv] = []
|
||||
bmv_count_strings[bmv].append(count)
|
||||
bmv_count.add(bmv)
|
||||
|
||||
for bmv in bmv_count:
|
||||
counts_loops = sorted(bmv_count_loops.get(bmv, []))
|
||||
counts_strings = sorted(bmv_count_strings.get(bmv, []))
|
||||
s_loops = ','.join(map(str, counts_loops))
|
||||
s_strings = ','.join(map(str, counts_strings))
|
||||
xy = point_to_point2d(bmv.co)
|
||||
xy.y += 10
|
||||
if s_loops:
|
||||
text_draw2D('O: ' + s_loops, xy, color=(1,1,0,1), dropshadow=(0,0,0,0.5))
|
||||
xy.y += 10
|
||||
if s_strings:
|
||||
text_draw2D('C: ' + s_strings, xy, color=(0,1,1,1), dropshadow=(0,0,0,0.5))
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 os
|
||||
import math
|
||||
from itertools import chain
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
|
||||
from ..rftool import RFTool
|
||||
|
||||
from ...addon_common.common.boundvar import BoundBool
|
||||
from ...addon_common.common.profiler import profiler
|
||||
from ...addon_common.common.utils import max_index, iter_pairs
|
||||
from ...addon_common.common.maths import Point,Point2D,Vec2D,Vec,Plane
|
||||
from ...config.options import options
|
||||
|
||||
from .contours_utils import (
|
||||
Contours_Loop,
|
||||
find_loops, find_strings, find_parallel_loops,
|
||||
loop_plane, loop_length, string_length,
|
||||
edges_between_loops,
|
||||
)
|
||||
|
||||
|
||||
class Contours_Ops:
|
||||
@RFTool.dirty_when_done
|
||||
def new_cut(self, ray, plane, count=None, walk_to_plane=True, check_hit=None, perform_nonmanifold_check=None):
|
||||
self.pts = []
|
||||
self.cut_pts = []
|
||||
self.cuts = []
|
||||
self.connected = False
|
||||
|
||||
crawl = self.rfcontext.plane_intersection_crawl(ray, plane, walk_to_plane=walk_to_plane)
|
||||
if not crawl: return
|
||||
|
||||
# get crawl data (over source)
|
||||
pts = [c for (f0,c,f1) in crawl]
|
||||
connected_preclip = crawl[0][0] is not None
|
||||
center = Point.average(pts)
|
||||
pts,connected = self.rfcontext.clip_pointloop(pts, connected_preclip)
|
||||
if not pts: return
|
||||
|
||||
self.rfcontext.undo_push('cut')
|
||||
|
||||
cl_cut = Contours_Loop(pts, connected)
|
||||
self.cuts = [cl_cut]
|
||||
self.cut_pts = pts
|
||||
self.connected = connected
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
|
||||
if check_hit:
|
||||
# if ray hits target, include the loops, too!
|
||||
visible_faces = self.rfcontext.visible_faces()
|
||||
hit_face,_ = self.rfcontext.nearest2D_face(point=check_hit, faces=visible_faces)
|
||||
if hit_face and hit_face.is_quad():
|
||||
# considering loops only at the moment
|
||||
edges = hit_face.edges
|
||||
eseqs = [self.rfcontext.get_quadwalk_edgesequence(edge) for edge in edges]
|
||||
eloops = [eseq.get_edges() if len(eseq) else None for eseq in eseqs]
|
||||
cloops = [Contours_Loop(eseq.get_verts(), eseq.is_loop()) if eseq else None for eseq in eseqs]
|
||||
|
||||
# use loop that is most parallel to cut
|
||||
norm = cl_cut.plane.n
|
||||
idx0 = max_index([abs(norm.dot(cloop.plane.n)) if cloop else -1 for cloop in cloops])
|
||||
idx1 = (idx0 + 2) % 4
|
||||
sel_edges |= set(eloops[idx0]) | set(eloops[idx1])
|
||||
|
||||
sel_loop_pos,sel_loop_neg = None,None
|
||||
sel_string_pos,sel_string_neg = None,None
|
||||
|
||||
if connected:
|
||||
# find two closest selected loops, one on each side
|
||||
sel_loops = find_loops(sel_edges)
|
||||
# find loops running parallel to selection
|
||||
par_loops = [ploop for loop in sel_loops for ploop in find_parallel_loops(loop)]
|
||||
|
||||
sel_loop_planes = [(loop, loop_plane(loop)) for loop in sel_loops]
|
||||
par_loop_planes = [(loop, loop_plane(loop)) for loop in par_loops]
|
||||
|
||||
def get_closest(loop_planes, positive):
|
||||
nonlocal center, plane
|
||||
mult = 1 if positive else -1
|
||||
loops = sorted([
|
||||
# loop, distance to loop, segment count of loop, loop circumference
|
||||
#(loop, plane.distance_to(p.o), len(loop), loop_length(loop))
|
||||
(loop, (loop_plane.o - center).length, len(loop), loop_length(loop))
|
||||
for loop,loop_plane in loop_planes if plane.side(loop_plane.o)*mult > 0
|
||||
], key=lambda data:data[1])
|
||||
return next(iter(loops), None)
|
||||
# (loop, plane.distance_to(p.o), len(loop), loop_length(loop))
|
||||
# for loop,p in zip(sel_loops, sel_loop_planes) if plane.side(p.o) > 0
|
||||
|
||||
sel_loop_pos = get_closest(sel_loop_planes, True)
|
||||
sel_loop_neg = get_closest(sel_loop_planes, False)
|
||||
par_loop_pos = get_closest(par_loop_planes, True)
|
||||
par_loop_neg = get_closest(par_loop_planes, False)
|
||||
|
||||
# if we've got only one selected loop, see if any parallel loops are closer
|
||||
if sel_loop_pos and par_loop_pos:
|
||||
if par_loop_pos[1] < sel_loop_pos[1]:
|
||||
sel_loop_pos = par_loop_pos
|
||||
if sel_loop_neg and par_loop_neg:
|
||||
if par_loop_neg[1] < sel_loop_neg[1]:
|
||||
sel_loop_neg = par_loop_neg
|
||||
|
||||
if sel_loop_pos and sel_loop_neg:
|
||||
if sel_loop_pos[2] != sel_loop_neg[2]:
|
||||
# selected loops do not have same count of vertices
|
||||
# choosing the closer loop
|
||||
if sel_loop_pos[1] < sel_loop_neg[1]:
|
||||
sel_loop_neg = None
|
||||
else:
|
||||
sel_loop_pos = None
|
||||
else:
|
||||
# find two closest selected strings, one on each side
|
||||
sel_strings = find_strings(sel_edges)
|
||||
parallel_strings = [pstring for string in sel_strings for pstring in find_parallel_loops(string, False)]
|
||||
sel_strings += parallel_strings
|
||||
|
||||
sel_string_planes = [loop_plane(string) for string in sel_strings]
|
||||
sel_strings_pos = sorted([
|
||||
(string, plane.distance_to(p.o), len(string), string_length(string))
|
||||
for string,p in zip(sel_strings, sel_string_planes) if plane.side(p.o) > 0
|
||||
], key=lambda data:data[1])
|
||||
sel_strings_neg = sorted([
|
||||
(string, plane.distance_to(p.o), len(string), string_length(string))
|
||||
for string,p in zip(sel_strings, sel_string_planes) if plane.side(p.o) < 0
|
||||
], key=lambda data:data[1])
|
||||
sel_string_pos = next(iter(sel_strings_pos), None)
|
||||
sel_string_neg = next(iter(sel_strings_neg), None)
|
||||
if sel_string_pos and sel_string_neg:
|
||||
if sel_string_pos[2] != sel_string_neg[2]:
|
||||
# selected strings do not have same count of vertices
|
||||
# choosing the closer string
|
||||
if sel_string_pos[1] < sel_string_neg[1]:
|
||||
sel_string_neg = None
|
||||
else:
|
||||
sel_string_pos = None
|
||||
|
||||
if not count:
|
||||
count = self._var_init_count.value
|
||||
if connected != connected_preclip:
|
||||
count = int(math.ceil(count / 2)) + 1
|
||||
#count = count or self.get_count()
|
||||
count = sel_loop_pos[2] if sel_loop_pos else sel_loop_neg[2] if sel_loop_neg else count
|
||||
count = sel_string_pos[2] if sel_string_pos else sel_string_neg[2] if sel_string_neg else count
|
||||
|
||||
if count <= 2:
|
||||
# too few verts for a cut! need at least 3
|
||||
# possible fix for issue #856
|
||||
return
|
||||
|
||||
if connected:
|
||||
cl_pos = Contours_Loop(sel_loop_pos[0], True) if sel_loop_pos else None
|
||||
cl_neg = Contours_Loop(sel_loop_neg[0], True) if sel_loop_neg else None
|
||||
else:
|
||||
cl_pos = Contours_Loop(sel_string_pos[0], False) if sel_string_pos else None
|
||||
cl_neg = Contours_Loop(sel_string_neg[0], False) if sel_string_neg else None
|
||||
|
||||
if cl_pos: self.cuts += [cl_pos]
|
||||
if cl_neg: self.cuts += [cl_neg]
|
||||
|
||||
if connected:
|
||||
if cl_pos and cl_neg:
|
||||
verts0 = list(cl_pos.verts)
|
||||
verts1 = list(cl_neg.verts)
|
||||
v0 = verts0[0]
|
||||
offset = None
|
||||
for i,v1 in enumerate(verts1):
|
||||
if v0.share_edge(v1): offset = i
|
||||
if offset is not None:
|
||||
verts1 = verts1[offset:] + verts1[:offset]
|
||||
if verts0[1].share_edge(verts1[-1]):
|
||||
verts1 = [verts1[0]] + list(reversed(verts1[1:]))
|
||||
|
||||
new_edges = []
|
||||
def split_face(v0, v1):
|
||||
nonlocal new_edges
|
||||
f0 = next(iter(v0.shared_faces(v1)), None)
|
||||
if not f0:
|
||||
self.rfcontext.alert_user('Something unexpected happened in trying to create a new cut', level='warning')
|
||||
self.rfcontext.undo_cancel()
|
||||
return
|
||||
f1 = f0.split(v0, v1)
|
||||
new_edges.append(f0.shared_edge(f1))
|
||||
|
||||
nvs = []
|
||||
for v0,v2 in zip(verts0, verts1):
|
||||
e1 = v0.shared_edge(v2)
|
||||
assert e1
|
||||
intersection = cl_cut.plane.line_intersection(v0.co, v2.co)
|
||||
v0,v2 = e1.verts
|
||||
e0,v1 = e1.split()
|
||||
assert v0 in e0.verts
|
||||
assert v2 in e1.verts
|
||||
v1.co = intersection
|
||||
self.rfcontext.snap_vert(v1)
|
||||
nvs.append(v1)
|
||||
|
||||
for v0,v1 in iter_pairs(nvs, wrap=True):
|
||||
split_face(v0, v1)
|
||||
|
||||
self.rfcontext.select(new_edges)
|
||||
#self.update()
|
||||
|
||||
return
|
||||
|
||||
cl_neg.align_to(cl_pos)
|
||||
cl_cut.align_to(cl_pos)
|
||||
if options['contours uniform']:
|
||||
step_size = cl_cut.circumference / count
|
||||
dists = [0] + [step_size for i in range(count-1)]
|
||||
else:
|
||||
lc,lp,ln = cl_cut.circumference,cl_pos.circumference,cl_neg.circumference
|
||||
dists = [0] + [lc * (d0/lp + d1/ln)/2 for d0,d1 in zip(cl_pos.dists,cl_neg.dists)]
|
||||
dists = dists[:-1]
|
||||
elif cl_pos:
|
||||
cl_cut.align_to(cl_pos)
|
||||
if options['contours uniform']:
|
||||
step_size = cl_cut.circumference / count
|
||||
dists = [0] + [step_size for i in range(count-1)]
|
||||
else:
|
||||
lc,lp = cl_cut.circumference,cl_pos.circumference
|
||||
dists = [0] + [lc * (d/lp) for d in cl_pos.dists]
|
||||
dists = dists[:-1]
|
||||
elif cl_neg:
|
||||
cl_cut.align_to(cl_neg)
|
||||
if options['contours uniform']:
|
||||
step_size = cl_cut.circumference / count
|
||||
dists = [0] + [step_size for i in range(count-1)]
|
||||
else:
|
||||
lc,ln = cl_cut.circumference,cl_neg.circumference
|
||||
dists = [0] + [lc * (d/ln) for d in cl_neg.dists]
|
||||
dists = dists[:-1]
|
||||
else:
|
||||
step_size = cl_cut.circumference / count
|
||||
dists = [0] + [step_size for i in range(count-1)]
|
||||
else:
|
||||
if cl_pos and cl_neg:
|
||||
cl_neg.align_to(cl_pos)
|
||||
cl_cut.align_to(cl_pos)
|
||||
lc,lp,ln = cl_cut.circumference,cl_pos.circumference,cl_neg.circumference
|
||||
dists = [0] + [0.999 * lc * (d0/lp + d1/ln)/2 for d0,d1 in zip(cl_pos.dists,cl_neg.dists)]
|
||||
elif cl_pos:
|
||||
cl_cut.align_to(cl_pos)
|
||||
lc,lp = cl_cut.circumference,cl_pos.circumference
|
||||
dists = [0] + [0.999 * lc * (d/lp) for d in cl_pos.dists]
|
||||
elif cl_neg:
|
||||
cl_cut.align_to(cl_neg)
|
||||
lc,ln = cl_cut.circumference,cl_neg.circumference
|
||||
dists = [0] + [0.999 * lc * (d/ln) for d in cl_neg.dists]
|
||||
else:
|
||||
step_size = cl_cut.circumference / (count-1)
|
||||
dists = [0] + [0.999 * step_size for i in range(count-1)]
|
||||
dists[0] = cl_cut.offset
|
||||
|
||||
# where new verts, edges, and faces are stored
|
||||
verts,edges,faces = [],[],[]
|
||||
|
||||
if sel_loop_pos and sel_loop_neg:
|
||||
edges_between = edges_between_loops(sel_loop_pos[0], sel_loop_neg[0])
|
||||
self.rfcontext.delete_edges(edges_between)
|
||||
if sel_string_pos and sel_string_neg:
|
||||
edges_between = edges_between_loops(sel_string_pos[0], sel_string_neg[0])
|
||||
self.rfcontext.delete_edges(edges_between)
|
||||
|
||||
i,dist = 0,dists[0]
|
||||
for c0,c1 in cl_cut.iter_pts(repeat=True):
|
||||
if c0 == c1: continue
|
||||
d = (c1 - c0).length
|
||||
while dist - d <= 0:
|
||||
# create new vert between c0 and c1
|
||||
p = c0 + (c1 - c0) * (dist / d)
|
||||
self.pts += [p]
|
||||
verts += [self.rfcontext.new_vert_point(p)]
|
||||
i += 1
|
||||
if i == len(dists): break
|
||||
dist += dists[i]
|
||||
dist -= d
|
||||
if i == len(dists): break
|
||||
assert len(dists)==len(verts), '%d != %d' % (len(dists), len(verts))
|
||||
for v0,v1 in iter_pairs(verts, connected):
|
||||
edges += [self.rfcontext.new_edge((v0, v1))]
|
||||
|
||||
if cl_pos: self.rfcontext.bridge_vertloop(verts, cl_pos.verts, connected)
|
||||
if cl_neg: self.rfcontext.bridge_vertloop(verts, cl_neg.verts, connected)
|
||||
|
||||
self.rfcontext.select(edges)
|
||||
|
||||
if perform_nonmanifold_check is None or perform_nonmanifold_check:
|
||||
if options['contours non-manifold check'] and not connected and (verts[0].co - verts[-1].co).length < 0.01:
|
||||
opt_nonmanifold = '''options['contours non-manifold check']'''
|
||||
self.rfcontext.alert_user('\n'.join([
|
||||
'The stroke has cut across a non-manifold edge in the source mesh and results may not be as expected. Please double check your source for duplicate vertices, un-merged symmetry, and holes.',
|
||||
'',
|
||||
'''<label><input type="checkbox" checked="BoundBool(opt_nonmanifold)">Perform this check</label>'''
|
||||
]), level='warning')
|
||||
|
||||
@RFTool.dirty_when_done
|
||||
def fill(self):
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
sel_loops = find_loops(sel_edges)
|
||||
|
||||
if len(sel_loops) != 2:
|
||||
self.rfcontext.alert_user('In order to fill, select exactly 2 loops of the same edge count')
|
||||
return
|
||||
loop0, loop1 = sel_loops
|
||||
if len(loop0) != len(loop1):
|
||||
self.rfcontext.alert_user('In order to fill, select exactly 2 loops of the same edge count')
|
||||
return
|
||||
if any(v0.share_edge(v1) for v0 in loop0 for v1 in loop1):
|
||||
self.rfcontext.alert_user('In order to fill, the 2 selected loops cannot share an edge')
|
||||
return
|
||||
|
||||
self.rfcontext.undo_push('fill')
|
||||
cl_pos = Contours_Loop(loop0, True)
|
||||
cl_neg = Contours_Loop(loop1, True)
|
||||
cl_neg.align_to(cl_pos)
|
||||
faces = self.rfcontext.bridge_vertloop(cl_neg.verts, cl_pos.verts, True)
|
||||
#self.dirty()
|
||||
#self.rfcontext.select(faces)
|
||||
|
||||
@RFTool.dirty_when_done
|
||||
def change_count(self, *, count=None, delta=None):
|
||||
assert count is not None or delta is not None, 'Contours.change_count: Must specify either count or delta!'
|
||||
sel_edges = self.rfcontext.get_selected_edges()
|
||||
loops = find_loops(sel_edges)
|
||||
strings = find_strings(sel_edges)
|
||||
if len(loops) == 1 and len(strings) == 0:
|
||||
self._change_loop_count(loops[0], count=count, delta=delta)
|
||||
elif len(strings) == 1 and len(loops) == 0:
|
||||
self._change_string_count(strings[0], count=count, delta=delta)
|
||||
else:
|
||||
print('Contours.change_count: expected either 1 loop+0 strings or 1 string+0 loops, but found %d loops and %d strings' % (len(loops), len(strings)))
|
||||
|
||||
def _change_loop_count(self, loop, *, count=None, delta=None):
|
||||
count_cur = len(loop)
|
||||
if count is not None: count_new = count
|
||||
else: count_new = count_cur + delta
|
||||
count_new = max(3, count_new)
|
||||
if count_cur == count_new: return
|
||||
if any(len(v.link_edges) != 2 for v in loop): return
|
||||
cl = Contours_Loop(loop, True)
|
||||
avg = Point.average(v.co for v in loop)
|
||||
plane = cl.plane
|
||||
ray = self.rfcontext.Point2D_to_Ray(self.rfcontext.Point_to_Point2D(avg))
|
||||
self.rfcontext.delete_edges(e for v in loop for e in v.link_edges)
|
||||
self.new_cut(ray, plane, walk_to_plane=True, count=count_new)
|
||||
|
||||
def _change_string_count(self, string, *, count=None, delta=None):
|
||||
count_cur = len(string)
|
||||
if count is not None: count_new = count
|
||||
else: count_new = count_cur + delta
|
||||
count_new = max(3, count_new)
|
||||
if count_cur == count_new: return
|
||||
if any(len(v.link_edges) != 2 for v in string[1:-1]):
|
||||
print('Contours._change_string_count: string is connected to other geometry')
|
||||
return
|
||||
if any(len(v.link_edges) != 1 for v in string[:1] + string[-1:]):
|
||||
print('Contours._change_string_count: string is connected to other geometry')
|
||||
return
|
||||
cl = Contours_Loop(string, False)
|
||||
avg = Point.average(v.co for v in string)
|
||||
plane = cl.plane
|
||||
ray = self.rfcontext.Point2D_to_Ray(self.rfcontext.Point_to_Point2D(avg))
|
||||
self.rfcontext.delete_edges(e for v in string for e in v.link_edges)
|
||||
self.new_cut(ray, plane, walk_to_plane=True, count=count_new, perform_nonmanifold_check=False)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<details>
|
||||
<summary>Contours</summary>
|
||||
<div class="contents">
|
||||
<label>
|
||||
<input type="checkbox" checked="BoundBool('''options['contours uniform']''')" title="If enabled, all new vertices will be spread uniformly (equal distance) around the circumference of the new cut. If disabled, new vertices will try to match distances between vertices of the extended cut.">
|
||||
Uniform Cut
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" checked="BoundBool('''options['contours non-manifold check']''')" title="If enabled, a warning message will appear if a new cut crosses a non-manifold edge in the source mesh. This check is useful when the source mesh is not water tight.">
|
||||
Non-manifold Check
|
||||
</label>
|
||||
<div class="labeled-input-text">
|
||||
<label for="contours-initial-count">
|
||||
Initial Count
|
||||
</label>
|
||||
<input id="contours-initial-count" type="number" value="BoundInt('''options['contours count']''', min_value=3, max_value=500)" title="Number of vertices to create in a new cut.">
|
||||
</div>
|
||||
<div class="labeled-input-text">
|
||||
<label for="contours-current-count">
|
||||
Cut Count
|
||||
</label>
|
||||
<input id="contours-current-count" type="number" value="self._var_cut_count" title="Number of vertices in currently selected cut.">
|
||||
</div>
|
||||
<!--<label title="Number of vertices in currently selected cut." class="input-text">
|
||||
Cut Count
|
||||
<input type="text" value="BoundInt('''self.var_cut_count''', min_value=3, max_value=500)">
|
||||
</label>-->
|
||||
</div>
|
||||
</details>
|
||||
@@ -0,0 +1,55 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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 os
|
||||
import re
|
||||
import math
|
||||
from itertools import chain
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix
|
||||
|
||||
from ..rftool import RFTool
|
||||
|
||||
from ...addon_common.common.boundvar import BoundBool, BoundInt
|
||||
from ...addon_common.common.utils import delay_exec
|
||||
from ...config.options import options
|
||||
|
||||
|
||||
class Contours_Props:
|
||||
@RFTool.on_init
|
||||
def init_props(self):
|
||||
self._var_init_count = BoundInt('''options['contours count']''', min_value=3, max_value=500)
|
||||
self._var_cut_count = BoundInt('''self.var_cut_count''', min_value=3, max_value=500)
|
||||
self._var_uniform_cut = BoundBool('''options['contours uniform']''')
|
||||
self._var_nonmanifold = BoundBool('''options['contours non-manifold check']''')
|
||||
|
||||
@property
|
||||
def var_cut_count(self):
|
||||
return getattr(self, '_var_cut_count_value', 0)
|
||||
@var_cut_count.setter
|
||||
def var_cut_count(self, v):
|
||||
if self.var_cut_count == v: return
|
||||
self._var_cut_count_value = v
|
||||
if self._var_cut_count.disabled: return
|
||||
self.rfcontext.undo_push('change segment count', repeatable=True)
|
||||
self.change_count(count=v)
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
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/>.
|
||||
'''
|
||||
|
||||
class Contours_Utils:
|
||||
def filter_edge_selection(self, bme, no_verts_select=True, ratio=0.33):
|
||||
if bme.select:
|
||||
# edge is already selected
|
||||
return True
|
||||
bmv0, bmv1 = bme.verts
|
||||
s0, s1 = bmv0.select, bmv1.select
|
||||
if s0 and s1:
|
||||
# both verts are selected, so return True
|
||||
return True
|
||||
if not s0 and not s1:
|
||||
if no_verts_select:
|
||||
# neither are selected, so return True by default
|
||||
return True
|
||||
else:
|
||||
# return True if none are selected; otherwise return False
|
||||
return self.rfcontext.none_selected()
|
||||
# if mouse is at least a ratio of the distance toward unselected vert, return True
|
||||
if s1: bmv0, bmv1 = bmv1, bmv0
|
||||
p = self.actions.mouse
|
||||
p0 = self.rfcontext.Point_to_Point2D(bmv0.co)
|
||||
p1 = self.rfcontext.Point_to_Point2D(bmv1.co)
|
||||
v01 = p1 - p0
|
||||
l01 = v01.length
|
||||
d01 = v01 / l01
|
||||
dot = d01.dot(p - p0)
|
||||
return dot / l01 > ratio
|
||||
|
||||
#def get_count(self): return
|
||||
|
||||
|
||||
import math
|
||||
from itertools import chain
|
||||
from mathutils import Vector, Quaternion
|
||||
|
||||
import bpy
|
||||
|
||||
from ..rfmesh.rfmesh import RFVert
|
||||
from ...addon_common.common.utils import iter_pairs, max_index
|
||||
from ...addon_common.common.hasher import hash_cycle
|
||||
from ...addon_common.common.maths import (
|
||||
Point, Vec, Normal, Direction,
|
||||
Point2D, Vec2D,
|
||||
Plane, Frame,
|
||||
)
|
||||
from ...addon_common.common.profiler import profiler
|
||||
|
||||
|
||||
def to_point(item):
|
||||
t = type(item)
|
||||
if t is RFVert: return item.co
|
||||
if t is Point or t is Vector or t is Vec: return item
|
||||
if t is tuple: return Point(item)
|
||||
return item.co
|
||||
|
||||
|
||||
def next_edge_in_string(edge0, vert01, ignore_two_faced=False):
|
||||
faces0 = edge0.link_faces
|
||||
edges1 = vert01.link_edges
|
||||
# ignore edge0
|
||||
edges1 = [edge for edge in edges1 if edge != edge0]
|
||||
if ignore_two_faced:
|
||||
# ignore edges that have two faces already
|
||||
edges1 = [edge for edge in edges1 if len(edge.link_faces) <= 1]
|
||||
# ignore edges that share face with previous edge
|
||||
edges1 = [edge for edge in edges1 if not faces0 or not any(f in faces0 for f in edge.link_faces)]
|
||||
return edges1[0] if len(edges1) == 1 else []
|
||||
|
||||
def find_loops(edges):
|
||||
if not edges: return []
|
||||
edges = set(edges)
|
||||
touched, loops = set(), []
|
||||
|
||||
def crawl(v0, edge01):
|
||||
nonlocal edges, touched, loops
|
||||
|
||||
vert_list = []
|
||||
while True:
|
||||
# ... -- 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]:
|
||||
# found a loop!
|
||||
loops.append(vert_list)
|
||||
return
|
||||
next_edges = [e for e in v1.link_edges if e in edges and e != edge01]
|
||||
if not next_edges:
|
||||
# could not find a loop
|
||||
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:
|
||||
# could not find a loop
|
||||
return
|
||||
v0, edge01 = v1, edge12
|
||||
|
||||
for edge in edges:
|
||||
if edge in touched: continue
|
||||
crawl(edge.verts[0], edge)
|
||||
|
||||
return loops
|
||||
|
||||
def find_parallel_loops(loop, wrap=True):
|
||||
def find_opposite_loop(loop, bmf):
|
||||
# find edge loop on opposite side of given face from given edge
|
||||
bmv0,bmv1 = loop[:2]
|
||||
bme01 = bmv0.shared_edge(bmv1)
|
||||
bme03 = next((bme for bme in bmf.neighbor_edges(bme01) if bmv0 in bme.verts), None)
|
||||
if not bme03: return None
|
||||
bmv_opposite = bme03.other_vert(bmv0)
|
||||
ploop = []
|
||||
for bmv0,bmv1 in iter_pairs(loop, wrap):
|
||||
if not bmf: return None
|
||||
if len(bmf.verts) != 4: return None
|
||||
ploop.append(bmv_opposite)
|
||||
bmv_opposite = next(iter(set(bmf.verts)-{bmv0,bmv1,bmv_opposite}), None)
|
||||
if not bmv_opposite: return None
|
||||
bme = bmv1.shared_edge(bmv_opposite)
|
||||
if not bme: return None
|
||||
bmf = next(iter(set(bme.link_faces) - {bmf}), None)
|
||||
if not ploop: return None
|
||||
if not wrap: ploop.append(bmv_opposite)
|
||||
return ploop
|
||||
|
||||
ploops = []
|
||||
|
||||
bmv0,bmv1 = loop[:2]
|
||||
bme01 = bmv0.shared_edge(bmv1)
|
||||
bmfs = [bmf for bmf in bme01.link_faces]
|
||||
touched = set()
|
||||
for bmf in bmfs:
|
||||
bme0 = bme01
|
||||
lloop = loop
|
||||
while bmf:
|
||||
if bmf in touched: break
|
||||
touched.add(bmf)
|
||||
ploop = find_opposite_loop(lloop, bmf)
|
||||
if not ploop: break
|
||||
ploops.append(ploop)
|
||||
bme1 = bmf.opposite_edge(bme0)
|
||||
if not bme1: break
|
||||
bmf = next((bmf_ for bmf_ in bme1.link_faces if bmf_ != bmf), None)
|
||||
bme0 = bme1
|
||||
lloop = ploop
|
||||
|
||||
return ploops
|
||||
|
||||
def find_strings(edges, min_length=3):
|
||||
if not edges: return []
|
||||
touched,strings = set(),[]
|
||||
|
||||
def crawl(v0, edge01, vert_list):
|
||||
nonlocal edges, touched
|
||||
# ... -- v0 -- edge01 -- v1 -- edge12 -- ...
|
||||
# came ^ from ^
|
||||
vert_list.append(v0)
|
||||
touched.add(edge01)
|
||||
v1 = edge01.other_vert(v0)
|
||||
if v1 == vert_list[0]: return []
|
||||
edge12 = next_edge_in_string(edge01, v1)
|
||||
if not edge12 or edge12 not in edges: return vert_list + [v1]
|
||||
return crawl(v1, edge12, vert_list)
|
||||
|
||||
for edge in edges:
|
||||
if edge in touched: continue
|
||||
vert_list0 = crawl(edge.verts[0], edge, [])
|
||||
vert_list1 = crawl(edge.verts[1], edge, [])
|
||||
vert_list = list(reversed(vert_list0)) + vert_list1[2:]
|
||||
if len(vert_list) >= min_length: strings.append(vert_list)
|
||||
|
||||
return strings
|
||||
|
||||
def find_cycles(edges, max_loops=10):
|
||||
# searches through edges to find loops
|
||||
# first, break into connected components
|
||||
# then, find all the junctions (verts with more than two connected edges)
|
||||
# sequence of edges between junctions can be reduced to single edge
|
||||
# find cycles in graph
|
||||
|
||||
if not edges: return []
|
||||
|
||||
vert_edges = {}
|
||||
for edge in edges:
|
||||
v0,v1 = edge.verts
|
||||
vert_edges[v0] = vert_edges.get(v0, []) + [(edge,v1)]
|
||||
vert_edges[v1] = vert_edges.get(v1, []) + [(edge,v0)]
|
||||
touched_edges = set()
|
||||
touched_verts = set()
|
||||
cycles = []
|
||||
cycle_hashes = set()
|
||||
def crawl(v0, vert_list):
|
||||
touched_verts.add(v0)
|
||||
vert_list.append(v0)
|
||||
for edge,v1 in vert_edges[v0]:
|
||||
if edge in touched_edges: continue
|
||||
touched_edges.add(edge)
|
||||
if v1 in vert_list:
|
||||
# found cycle!
|
||||
cycle = list(reversed(vert_list))
|
||||
while cycle[-1] != v1: cycle.pop()
|
||||
h = hash_cycle(cycle)
|
||||
if h not in cycle_hashes:
|
||||
cycle_hashes.add(h)
|
||||
cycles.append(cycle)
|
||||
else:
|
||||
crawl(v1, vert_list)
|
||||
touched_edges.remove(edge)
|
||||
if len(cycles) == max_loops: return
|
||||
vert_list.pop()
|
||||
for v in vert_edges.keys():
|
||||
if v in touched_verts: continue
|
||||
crawl(v, [])
|
||||
if len(cycles) == max_loops: print('max loop count reached')
|
||||
return cycles
|
||||
|
||||
def edges_of_loop(vert_loop):
|
||||
edges = []
|
||||
for v0,v1 in iter_pairs(vert_loop, True):
|
||||
e0 = set(v0.link_edges)
|
||||
e1 = set(v1.link_edges)
|
||||
edges += list(e0 & e1)
|
||||
return edges
|
||||
|
||||
def verts_of_loop(edge_loop):
|
||||
verts = []
|
||||
for e0,e1 in iter_pairs(edge_loop, False):
|
||||
if not verts:
|
||||
v0 = e0.shared_vert(e1)
|
||||
verts += [e0.other_vert(v0), v0]
|
||||
verts += [e1.other_vert(verts[-1])]
|
||||
if len(verts) > 1 and verts[0] == verts[-1]: return verts[:-1]
|
||||
return verts
|
||||
|
||||
def loop_plane(vert_loop):
|
||||
# average co is pt on plane
|
||||
# average cross product (point in same direction) is normal
|
||||
if not vert_loop: return None
|
||||
vert_loop = [to_point(v) for v in vert_loop]
|
||||
pt = sum((Vector(vert) for vert in vert_loop), Vector()) / len(vert_loop)
|
||||
n,cnt = None,0
|
||||
for vert0,vert1 in zip(vert_loop[:-1], vert_loop[1:]):
|
||||
c = Vec((vert0-pt).cross(vert1-pt)).normalize()
|
||||
n = n+c if n else c
|
||||
if not n: return Plane(pt, Normal())
|
||||
return Plane(pt, Normal(n).normalize())
|
||||
|
||||
def loop_radius(vert_loop):
|
||||
pt = sum((Vector(to_point(vert)) for vert in vert_loop), Vector()) / len(vert_loop)
|
||||
rad = sum((to_point(vert) - pt).length for vert in vert_loop) / len(vert_loop)
|
||||
return rad
|
||||
|
||||
def loop_length(vert_loop):
|
||||
return sum((to_point(v0)-to_point(v1)).length for v0,v1 in zip(vert_loop, chain(vert_loop[1:], vert_loop[:1])))
|
||||
|
||||
def loops_connected(vert_loop0, vert_loop1):
|
||||
if not vert_loop0 or not vert_loop1: return False
|
||||
v0 = vert_loop0
|
||||
v0_connected = { e.other_vert(v0) for e in v0.link_edges }
|
||||
return any(v1 in v0_connected for v1 in vert_loop1)
|
||||
|
||||
def edges_between_loops(vert_loop0, vert_loop1):
|
||||
loop1 = set(vert_loop1)
|
||||
return [e for v0 in vert_loop0 for e in v0.link_edges if e.other_vert(v0) in loop1]
|
||||
|
||||
def faces_between_loops(vert_loop0, vert_loop1):
|
||||
loop1 = set(vert_loop1)
|
||||
return [f for v0 in vert_loop0 for f in v0.link_faces if any(fv in loop1 for fv in f.verts)]
|
||||
|
||||
def string_length(vert_loop):
|
||||
return sum((to_point(v0)-to_point(v1)).length for v0,v1 in zip(vert_loop[:-1], vert_loop[1:]))
|
||||
|
||||
def project_loop_to_plane(vert_loop, plane):
|
||||
return [plane.project(to_point(v)) for v in vert_loop]
|
||||
|
||||
|
||||
|
||||
class Contours_Loop:
|
||||
def __init__(self, vert_loop, connected, offset=0):
|
||||
self.connected = connected
|
||||
self.set_vert_loop(vert_loop, offset)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Contours_Loop: %d,%s,%s>' % (len(self.verts), str(self.connected), str(self.verts))
|
||||
|
||||
# @profiler.function
|
||||
def set_vert_loop(self, vert_loop, offset):
|
||||
self.verts = vert_loop
|
||||
self.offset = offset
|
||||
self.pts = [to_point(bmv) for bmv in self.verts]
|
||||
self.count = len(self.pts)
|
||||
self.plane = loop_plane(self.pts)
|
||||
if not self.connected:
|
||||
self.plane.o = self.pts[0] + (self.pts[-1] - self.pts[0]) / 2
|
||||
self.up_dir = Direction(self.pts[0] - self.plane.o)
|
||||
self.frame = Frame.from_plane(self.plane, y=self.up_dir)
|
||||
|
||||
proj = self.plane.project
|
||||
self.dists = [(proj(p0)-proj(p1)).length for p0,p1 in iter_pairs(self.pts, self.connected)]
|
||||
self.proj_dists = [self.plane.signed_distance_to(p) for p in self.pts]
|
||||
self.circumference = sum(self.dists)
|
||||
self.radius = sum(self.w2l_point(pt).length for pt in self.pts) / self.count
|
||||
|
||||
def get_origin(self): return self.plane.o
|
||||
def get_normal(self): return self.plane.n
|
||||
def get_local_by_index(self, idx): return self.w2l_point(self.pts[idx])
|
||||
def w2l_point(self, co): return self.frame.w2l_point(to_point(co))
|
||||
def l2w_point(self, co): return self.frame.l2w_point(to_point(co))
|
||||
def get_index_of_top(self, pts):
|
||||
pts_local = [self.w2l_point(pt+self.frame.o) for pt in pts]
|
||||
idx = max_index(pts_local, key=lambda pt:pt.y)
|
||||
t = pts_local[idx]
|
||||
#print(pts_local, idx, t)
|
||||
offset = ((math.pi/2 - math.atan2(t.y, t.x)) * self.circumference / (math.pi*2)) % self.circumference
|
||||
return (idx,offset)
|
||||
|
||||
def align_to(self, other):
|
||||
n0, n1 = self.get_normal(), other.get_normal()
|
||||
is_opposite = n0.dot(n1) < 0
|
||||
vert_loop = list(reversed(self.verts)) if is_opposite else self.verts
|
||||
if not self.connected:
|
||||
self.set_vert_loop(vert_loop, 0)
|
||||
return
|
||||
if is_opposite: n0 = -n0
|
||||
|
||||
# issue #659
|
||||
angle = 0 if n0.length_squared == 0 or n1.length_squared == 0 else n0.angle(n1)
|
||||
q = Quaternion(n0.cross(n1), angle)
|
||||
|
||||
# rotate to align "topmost" vertex
|
||||
rel_pos = [Vec(q @ (to_point(p) - self.frame.o)) for p in vert_loop]
|
||||
rot_by,offset = other.get_index_of_top(rel_pos)
|
||||
vert_loop = vert_loop[rot_by:] + vert_loop[:rot_by]
|
||||
offset = (offset * self.circumference / other.circumference)
|
||||
self.set_vert_loop(vert_loop, offset)
|
||||
|
||||
def get_closest_point(self, point):
|
||||
point = to_point(point)
|
||||
cp,cd = None,None
|
||||
for p0,p1 in iter_pairs(self.pts, self.connected):
|
||||
diff = p1 - p0
|
||||
l = diff.length
|
||||
d = diff / l
|
||||
pp = p0 + d * max(0, min(l, (point - p0).dot(d)))
|
||||
dist = (point - pp).length
|
||||
if not cp or dist < cd: cp,cd = pp,dist
|
||||
return cp
|
||||
|
||||
def get_points_relative_to(self, other):
|
||||
scale = other.radius / self.radius
|
||||
return [other.l2w_point(Vector(self.w2l_point(pt)) * scale) for pt in self.pts]
|
||||
|
||||
def iter_pts(self, repeat=False):
|
||||
return iter_pairs(self.pts, self.connected, repeat=repeat)
|
||||
|
||||
def move_2D(self, xy_delta:Vec2D):
|
||||
pass
|
||||
Reference in New Issue
Block a user