758 lines
32 KiB
Python
758 lines
32 KiB
Python
'''
|
|
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))
|
|
|