2025-07-01
This commit is contained in:
@@ -0,0 +1,960 @@
|
||||
'''
|
||||
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 math
|
||||
import time
|
||||
import random
|
||||
from itertools import chain
|
||||
from functools import partial
|
||||
from collections import deque
|
||||
|
||||
from ..rfmesh.rfmesh_wrapper import RFVert, RFEdge, RFFace
|
||||
|
||||
from ...addon_common.common import gpustate
|
||||
from ...addon_common.cookiecutter.cookiecutter import CookieCutter
|
||||
from ...addon_common.common.blender import tag_redraw_all
|
||||
from ...addon_common.common.decorators import timed_call
|
||||
from ...addon_common.common.drawing import Cursors, DrawCallbacks, CC_DRAW, CC_2D_LINES
|
||||
from ...addon_common.common.fsm import FSM
|
||||
from ...addon_common.common.globals import Globals
|
||||
from ...addon_common.common.maths import Vec2D, Point2D, RelPoint2D, Direction2D
|
||||
from ...addon_common.common.profiler import profiler
|
||||
from ...addon_common.common.ui_core import UI_Element
|
||||
from ...addon_common.common.utils import normalize_triplequote, Dict
|
||||
from ...config.options import options, retopoflow_files
|
||||
from ...addon_common.common.timerhandler import StopwatchHandler, CallGovernor
|
||||
|
||||
class RetopoFlow_FSM(CookieCutter): # CookieCutter must be here in order to override fns
|
||||
def setup_states(self):
|
||||
self.view_version = None
|
||||
self._last_rfwidget = None
|
||||
self.fast_update_timer = self.actions.start_timer(120.0, enabled=False)
|
||||
|
||||
def update(self, timer=True):
|
||||
if not self.loading_done:
|
||||
# calling self.fsm.update() in case mouse is hovering over ui
|
||||
self.fsm.update()
|
||||
return
|
||||
|
||||
options.clean(raise_exception=False)
|
||||
if options.write_error and not hasattr(self, '_write_error_reported'):
|
||||
# could not write options to file for some reason
|
||||
# issue #1070
|
||||
self._write_error_reported = True
|
||||
message = normalize_triplequote(f'''
|
||||
Could not write options to file (incorrect permissions).
|
||||
|
||||
Check that you have permission to write to `{retopoflow_files["options filename"]}` to the RetopoFlow add-on folder.
|
||||
|
||||
Or, try: uninstall RetopoFlow from Blender, restart Blender, then install the latest version of RetopoFlow from the Blender Market.
|
||||
|
||||
Note: You can continue using RetopoFlow, but any changes to options will not be saved.
|
||||
This error will not be reported again during the current RetopoFlow session.
|
||||
''')
|
||||
self.alert_user(message, level='error')
|
||||
|
||||
if timer:
|
||||
self.rftool._callback('timer')
|
||||
if self.rftool.rfwidget:
|
||||
self.rftool.rfwidget._callback_widget('timer')
|
||||
|
||||
if self.rftool.rfwidget != self._last_rfwidget:
|
||||
# force redraw when widget changes to clear out any widget drawing
|
||||
self._last_rfwidget = self.rftool.rfwidget
|
||||
tag_redraw_all('RFWidget change')
|
||||
|
||||
rftarget_version = self.rftarget.get_version()
|
||||
if self.rftarget_version != rftarget_version:
|
||||
self.rftarget_version = rftarget_version
|
||||
self.update_rot_object()
|
||||
self.callback_target_change()
|
||||
tag_redraw_all('RF_FSM target change')
|
||||
|
||||
view_version = self.get_view_version()
|
||||
if self.view_version != view_version:
|
||||
self.update_view_sessionoptions(self.context)
|
||||
self.update_clip_settings(rescale=False)
|
||||
self.view_version = view_version
|
||||
self.callback_view_change()
|
||||
tag_redraw_all('RF_FSM view change')
|
||||
|
||||
self.actions.hit_pos,self.actions.hit_norm,_,_ = self.raycast_sources_mouse()
|
||||
fpsdiv = self.document.body.getElementById('fpsdiv')
|
||||
if fpsdiv: fpsdiv.innerText = f'UI FPS: {self.document._draw_fps:.2f}'
|
||||
|
||||
# @CallGovernor.limit(fn_delay=lambda:options['target change delay'])
|
||||
def callback_target_change(self):
|
||||
# throttling this fn will cause target_change and draw callbacks to get out-of-sync
|
||||
# ex: contours depends on data collected in target change callback!
|
||||
self.rftool._callback('target change')
|
||||
if self.rftool.rfwidget:
|
||||
self.rftool.rfwidget._callback_widget('target change')
|
||||
self.update_ui_geometry()
|
||||
tag_redraw_all('RF_FSM target change')
|
||||
|
||||
@CallGovernor.limit(fn_delay=lambda:options['view change delay'])
|
||||
def callback_view_change(self):
|
||||
self.rftool._callback('view change')
|
||||
if self.rftool.rfwidget:
|
||||
self.rftool.rfwidget._callback_widget('view change')
|
||||
tag_redraw_all('RF_FSM view change')
|
||||
|
||||
def should_pass_through(self, context, event):
|
||||
return self.actions.using('blender passthrough')
|
||||
|
||||
@FSM.on_state('main')
|
||||
def modal_main(self):
|
||||
# if self.actions.just_pressed: print('modal_main', self.actions.just_pressed)
|
||||
if self.rftool._fsm_in_main() and (not self.rftool.rfwidget or self.rftool.rfwidget._fsm_in_main()):
|
||||
# handle exit
|
||||
if self.actions.pressed('done'):
|
||||
if options['confirm tab quit']:
|
||||
self.show_quit_dialog()
|
||||
else:
|
||||
self.done()
|
||||
return
|
||||
if options['escape to quit'] and self.actions.pressed('done alt0'):
|
||||
self.done()
|
||||
return
|
||||
|
||||
# handle help actions
|
||||
if self.actions.pressed('all help'):
|
||||
self.helpsystem_open('table_of_contents.md')
|
||||
return
|
||||
if self.actions.pressed('general help'):
|
||||
self.helpsystem_open('general.md')
|
||||
return
|
||||
if self.actions.pressed('tool help'):
|
||||
self.helpsystem_open(self.rftool.help)
|
||||
return
|
||||
|
||||
# user wants to save?
|
||||
if self.actions.pressed('blender save'):
|
||||
self.save_normal()
|
||||
return
|
||||
|
||||
# toggle ui
|
||||
if self.actions.pressed('toggle ui'):
|
||||
# hide ui if main (or minimized main, tiny) is visible
|
||||
ui_hide = self.ui_main.is_visible or self.ui_tiny.is_visible
|
||||
if ui_hide:
|
||||
self.ui_hide = True
|
||||
self.ui_main.is_visible = False
|
||||
self.ui_tiny.is_visible = False
|
||||
self.ui_options.is_visible = False
|
||||
self.ui_options_min.is_visible = False
|
||||
self.ui_geometry.is_visible = False
|
||||
self.ui_geometry_min.is_visible = False
|
||||
else:
|
||||
self.ui_main.is_visible = options['show main window']
|
||||
self.ui_tiny.is_visible = not options['show main window']
|
||||
self.ui_options.is_visible = options['show options window']
|
||||
self.ui_options_min.is_visible = not options['show options window']
|
||||
self.ui_geometry.is_visible = options['show geometry window']
|
||||
self.ui_geometry_min.is_visible = not options['show geometry window']
|
||||
self.ui_hide = False
|
||||
return
|
||||
|
||||
# handle pie menu
|
||||
if self.actions.pressed('pie menu'):
|
||||
self.show_pie_menu([
|
||||
{'text':rftool.name, 'image':rftool.icon, 'value':rftool}
|
||||
for rftool in self.rftools
|
||||
], self.select_rftool, highlighted=self.rftool)
|
||||
return
|
||||
|
||||
# debugging
|
||||
if False:
|
||||
if self.actions.pressed('SHIFT+F5'): breakit = 42 / 0
|
||||
if self.actions.pressed('SHIFT+F6'): assert False
|
||||
if self.actions.pressed('SHIFT+F7'): self.alert_user(message='Foo', level='exception', msghash='2ec5e386ae05c1abeb66dce8e1f1cb95')
|
||||
if self.actions.pressed('F7'):
|
||||
assert False, 'test exception throwing'
|
||||
# self.alert_user(title='Test', message='foo bar', level='warning', msghash=None)
|
||||
return
|
||||
# if self.actions.just_pressed: print('modal_main', self.actions.just_pressed)
|
||||
|
||||
# profiler
|
||||
if False:
|
||||
if self.actions.pressed('SHIFT+F10'):
|
||||
profiler.clear()
|
||||
return
|
||||
if self.actions.pressed('SHIFT+F11'):
|
||||
profiler.printout()
|
||||
self.document.debug_print()
|
||||
return
|
||||
|
||||
# reload CSS
|
||||
if self.actions.pressed('reload css'):
|
||||
print('RetopoFlow: Reloading stylings')
|
||||
self.reload_stylings()
|
||||
return
|
||||
|
||||
# handle tool switching
|
||||
for rftool in self.rftools:
|
||||
if rftool == self.rftool: continue
|
||||
if self.actions.pressed(rftool.shortcut):
|
||||
self.select_rftool(rftool)
|
||||
return
|
||||
if self.actions.pressed(rftool.quick_shortcut, unpress=False):
|
||||
self.quick_select_rftool(rftool)
|
||||
return 'quick switch'
|
||||
|
||||
# handle undo/redo
|
||||
if self.actions.pressed('blender undo'):
|
||||
self.undo_pop()
|
||||
if self.rftool: self.rftool._reset()
|
||||
return
|
||||
if self.actions.pressed('blender redo'):
|
||||
self.redo_pop()
|
||||
if self.rftool: self.rftool._reset()
|
||||
return
|
||||
|
||||
# handle general selection (each tool will handle specific selection / selection painting)
|
||||
if self.actions.pressed('select all'):
|
||||
# print('modal_main:selecting all toggle')
|
||||
self.undo_push('select all')
|
||||
self.select_toggle()
|
||||
return
|
||||
if self.actions.pressed('deselect all'):
|
||||
self.undo_push('deselect all')
|
||||
self.deselect_all()
|
||||
return
|
||||
if self.actions.pressed('select invert'):
|
||||
self.undo_push('select invert')
|
||||
self.select_invert()
|
||||
return
|
||||
if self.actions.pressed('select linked'):
|
||||
self.undo_push('select linked')
|
||||
self.select_linked()
|
||||
return
|
||||
if self.actions.pressed({'select linked mouse', 'deselect linked mouse'}, unpress=False):
|
||||
select = self.actions.pressed('select linked mouse')
|
||||
self.actions.unpress()
|
||||
bmv,_ = self.accel_nearest2D_vert(max_dist=options['select dist'])
|
||||
bme,_ = self.accel_nearest2D_edge(max_dist=options['select dist'])
|
||||
bmf,_ = self.accel_nearest2D_face(max_dist=options['select dist'])
|
||||
connected_to = bmv or bme or bmf
|
||||
if connected_to:
|
||||
self.undo_push('select linked mouse')
|
||||
self.select_linked(connected_to=connected_to, select=select)
|
||||
return
|
||||
|
||||
# hide/reveal
|
||||
if self.actions.pressed('hide selected'):
|
||||
self.hide_selected()
|
||||
return
|
||||
if self.actions.pressed('hide unselected'):
|
||||
self.hide_unselected()
|
||||
return
|
||||
if self.actions.pressed('reveal hidden'):
|
||||
self.reveal_hidden()
|
||||
return
|
||||
|
||||
# delete
|
||||
if self.actions.pressed('delete'):
|
||||
self.show_delete_dialog()
|
||||
return
|
||||
if self.actions.pressed('delete pie menu'):
|
||||
def callback(option):
|
||||
if not option: return
|
||||
self.delete_dissolve_collapse_option(option)
|
||||
self.show_pie_menu([
|
||||
('Delete Verts', ('Delete', 'Vertices')),
|
||||
('Delete Edges', ('Delete', 'Edges')),
|
||||
('Delete Faces', ('Delete', 'Faces')),
|
||||
('Dissolve Faces', ('Dissolve', 'Faces')),
|
||||
('Dissolve Edges', ('Dissolve', 'Edges')),
|
||||
('Dissolve Verts', ('Dissolve', 'Vertices')),
|
||||
('Merge at Center', ('Merge', 'At Center')),
|
||||
('Merge by Distance', ('Merge', 'By Distance')),
|
||||
# ('Collapse Edges & Faces', ('Collapse', 'Edges & Faces')),
|
||||
#'Dissolve Loops',
|
||||
], callback, release='delete pie menu', always_callback=True, rotate=-60)
|
||||
return
|
||||
|
||||
# merge
|
||||
if self.actions.pressed('merge'):
|
||||
self.show_merge_dialog()
|
||||
return
|
||||
|
||||
# smoothing
|
||||
if self.actions.pressed('smooth edge flow'):
|
||||
self.smooth_edge_flow(iterations=options['smooth edge flow iterations'])
|
||||
return
|
||||
|
||||
# pin/unpin
|
||||
if self.actions.pressed('pin'):
|
||||
self.pin_selected()
|
||||
return
|
||||
if self.actions.pressed('unpin'):
|
||||
self.unpin_selected()
|
||||
return
|
||||
if self.actions.pressed('unpin all'):
|
||||
self.unpin_all()
|
||||
return
|
||||
|
||||
# seams
|
||||
if self.actions.pressed('mark seam'):
|
||||
self.mark_seam_selected()
|
||||
return
|
||||
if self.actions.pressed('clear seam'):
|
||||
self.clear_seam_selected()
|
||||
return
|
||||
|
||||
return self.modal_main_rest()
|
||||
|
||||
def modal_main_rest(self):
|
||||
self.ignore_ui_events = False
|
||||
|
||||
self.normal_check() # this call is governed!
|
||||
|
||||
if not self.actions.is_navigating: self.rftool._done_navigating()
|
||||
|
||||
if self.rftool.rfwidget:
|
||||
Cursors.set(self.rftool.rfwidget.rfw_cursor)
|
||||
if self.rftool.rfwidget.redraw_on_mouse and self.actions.mousemove:
|
||||
tag_redraw_all('RFTool.RFWidget.redraw_on_mouse')
|
||||
ret = self.rftool.rfwidget._fsm_update()
|
||||
if self.fsm.is_state(ret):
|
||||
return ret
|
||||
if self.rftool.rfwidget._fsm.state != 'main':
|
||||
self.ignore_ui_events = True
|
||||
return
|
||||
|
||||
ret = self.rftool._fsm_update()
|
||||
if self.fsm.is_state(ret):
|
||||
self.ignore_ui_events = True
|
||||
return ret
|
||||
if self.fsm.state != 'main':
|
||||
self.ignore_ui_events = True
|
||||
|
||||
if not self.ignore_ui_events:
|
||||
self.handle_auto_save()
|
||||
|
||||
if self.actions.pressed('rotate'):
|
||||
return 'rotate selected'
|
||||
|
||||
if self.actions.pressed('scale'):
|
||||
return 'scale selected'
|
||||
|
||||
if self.actions.pressed('rip'):
|
||||
return self.rip(fill=False)
|
||||
if self.actions.pressed('rip fill'):
|
||||
return self.rip(fill=True)
|
||||
|
||||
@FSM.on_state('quick switch', 'enter')
|
||||
def quick_switch_enter(self):
|
||||
self._quick_switch_wait = 2
|
||||
|
||||
@FSM.on_state('quick switch')
|
||||
def quick_switch(self):
|
||||
self._quick_switch_wait -= 1
|
||||
if self.rftool._fsm.state == 'main' and (not self.rftool.rfwidget or self.rftool.rfwidget._fsm.state == 'main'):
|
||||
if self._quick_switch_wait < 0 and self.actions.released(self.rftool.quick_shortcut):
|
||||
return 'main'
|
||||
self.modal_main_rest()
|
||||
|
||||
@FSM.on_state('quick switch', 'exit')
|
||||
def quick_switch_exit(self):
|
||||
self.quick_restore_rftool()
|
||||
|
||||
|
||||
def setup_action(self, pt0, pt1, fn_callback, done_pressed=None, done_released=None, cancel_pressed=None):
|
||||
v01 = pt1 - pt0
|
||||
self.action_data = {
|
||||
'p0': pt0, 'p1': pt1, 'v01': v01,
|
||||
'fn callback': fn_callback,
|
||||
'done pressed': done_pressed, 'done released': done_released, 'cancel pressed': cancel_pressed,
|
||||
'val': lambda p: v01.dot(p - pt0),
|
||||
}
|
||||
return 'action handler'
|
||||
|
||||
@FSM.on_state('action handler', 'enter')
|
||||
def action_handler_enter(self):
|
||||
assert self.action_data
|
||||
self.undo_push('action handler')
|
||||
self.fast_update_timer.start()
|
||||
self.action_data['mouse'] = self.actions.mouse
|
||||
self.action_data['val start'] = self.action_data['val'](self.actions.mouse)
|
||||
|
||||
@FSM.on_state('action handler')
|
||||
def action_handler(self):
|
||||
d = self.action_data
|
||||
if self.actions.pressed(d['done pressed']) or self.actions.released(d['done released']):
|
||||
self.actions_data = None
|
||||
return 'main'
|
||||
if self.actions.released(d['cancel pressed']):
|
||||
self.undo_pop()
|
||||
self.dirty()
|
||||
return 'main'
|
||||
if not self.actions.mousemove: return
|
||||
val = self.action_data['val'](self.actions.mouse)
|
||||
self.action_data['fn callback'](val - self.action_data['val start'])
|
||||
self.dirty()
|
||||
|
||||
@FSM.on_state('action handler', 'exit')
|
||||
def action_handler_exit(self):
|
||||
self.fast_update_timer.stop()
|
||||
|
||||
|
||||
|
||||
@FSM.on_state('rotate selected', 'can enter')
|
||||
# @profiler.function
|
||||
def rotate_selected_canenter(self):
|
||||
if not self.get_selected_verts(): return False
|
||||
|
||||
@FSM.on_state('rotate selected', 'enter')
|
||||
def rotate_selected_enter(self):
|
||||
bmverts = self.get_selected_verts()
|
||||
opts = {}
|
||||
opts['bmverts'] = [(bmv, self.Point_to_Point2D(bmv.co)) for bmv in bmverts]
|
||||
opts['center'] = RelPoint2D.average(co for _,co in opts['bmverts'])
|
||||
opts['rotate_x'] = Direction2D(self.actions.mouse - opts['center'])
|
||||
opts['rotate_y'] = Direction2D((-opts['rotate_x'].y, opts['rotate_x'].x))
|
||||
opts['move_done_pressed'] = 'confirm'
|
||||
opts['move_done_released'] = None
|
||||
opts['move_cancelled'] = 'cancel'
|
||||
opts['mouselast'] = self.actions.mouse
|
||||
opts['mousedown'] = self.actions.mouse
|
||||
opts['lasttime'] = 0
|
||||
self.rotate_selected_opts = opts
|
||||
self.undo_push('rotate')
|
||||
|
||||
statusbar = self.substitute_keymaps('{{confirm}} Confirm\t{{cancel}} Cancel', wrap='', pre='', post=':', separator='/', onlyfirst=2)
|
||||
statusbar = statusbar.replace('\t', ' ')
|
||||
self.context.workspace.status_text_set(f'Rotating selected: {statusbar}')
|
||||
|
||||
self.fast_update_timer.start()
|
||||
self.split_target_visualization_selected()
|
||||
self.set_accel_defer(True)
|
||||
tag_redraw_all('rotate init')
|
||||
|
||||
@FSM.on_state('rotate selected')
|
||||
# @profiler.function
|
||||
def rotate_selected(self):
|
||||
opts = self.rotate_selected_opts
|
||||
if self.actions.pressed(opts['move_done_pressed']):
|
||||
return 'main'
|
||||
if self.actions.released(opts['move_done_released']):
|
||||
return 'main'
|
||||
if self.actions.pressed(opts['move_cancelled']):
|
||||
self.undo_cancel()
|
||||
self.actions.unuse(opts['move_done_released'], ignoremods=True, ignoremulti=True)
|
||||
return 'main'
|
||||
|
||||
if (self.actions.mouse - opts['mouselast']).length == 0: return
|
||||
if time.time() < opts['lasttime'] + 0.05: return
|
||||
opts['mouselast'] = self.actions.mouse
|
||||
opts['lasttime'] = time.time()
|
||||
|
||||
delta = Direction2D(self.actions.mouse - opts['center'])
|
||||
dx,dy = opts['rotate_x'].dot(delta),opts['rotate_y'].dot(delta)
|
||||
theta = math.atan2(dy, dx)
|
||||
|
||||
set2D_vert = self.set2D_vert
|
||||
for bmv,xy in opts['bmverts']:
|
||||
if not bmv.is_valid: continue
|
||||
dxy = xy - opts['center']
|
||||
nx = dxy.x * math.cos(theta) - dxy.y * math.sin(theta)
|
||||
ny = dxy.x * math.sin(theta) + dxy.y * math.cos(theta)
|
||||
nxy = Point2D((nx, ny)) + opts['center']
|
||||
set2D_vert(bmv, nxy)
|
||||
self.update_verts_faces(v for v,_ in opts['bmverts'])
|
||||
self.dirty()
|
||||
tag_redraw_all('rotate mouse move')
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('rotate selected')
|
||||
def draw_rotate_post2d(self):
|
||||
opts = self.rotate_selected_opts
|
||||
|
||||
gpustate.blend('ALPHA')
|
||||
Globals.drawing.draw2D_line(
|
||||
opts['mousedown'],
|
||||
opts['center'],
|
||||
(0.1, 1.0, 1.0, 1.0), color1=(0.1, 1.0, 1.0, 0.0),
|
||||
width=2, stipple=[2, 2]
|
||||
)
|
||||
Globals.drawing.draw2D_line(
|
||||
opts['center'],
|
||||
self.actions.mouse,
|
||||
(1.0, 1.0, 0.1, 1.0), color1=(1.0, 1.0, 0.1, 0.0),
|
||||
width=2, stipple=[2, 2]
|
||||
)
|
||||
|
||||
|
||||
@FSM.on_state('rotate selected', 'exit')
|
||||
def rotate_selected_exit(self):
|
||||
self.fast_update_timer.stop()
|
||||
self.clear_split_target_visualization()
|
||||
self.set_accel_defer(False)
|
||||
self._update_rftool_ui()
|
||||
|
||||
|
||||
|
||||
|
||||
@FSM.on_state('scale selected', 'can enter')
|
||||
# @profiler.function
|
||||
def scale_selected_canenter(self):
|
||||
if not self.get_selected_verts(): return False
|
||||
|
||||
@FSM.on_state('scale selected', 'enter')
|
||||
def scale_selected_enter(self):
|
||||
bmverts = self.get_selected_verts()
|
||||
opts = {}
|
||||
opts['bmverts'] = [(bmv, self.Point_to_Point2D(bmv.co)) for bmv in bmverts]
|
||||
opts['center'] = RelPoint2D.average(co for _,co in opts['bmverts'])
|
||||
opts['start_dist'] = (self.actions.mouse - opts['center']).length
|
||||
opts['move_done_pressed'] = 'confirm'
|
||||
opts['move_done_released'] = None
|
||||
opts['move_cancelled'] = 'cancel'
|
||||
opts['mouselast'] = self.actions.mouse
|
||||
opts['mousedown'] = self.actions.mouse
|
||||
opts['lasttime'] = 0
|
||||
self.scale_selected_opts = opts
|
||||
self.undo_push('scale')
|
||||
|
||||
statusbar = self.substitute_keymaps('{{confirm}} Confirm\t{{cancel}} Cancel', wrap='', pre='', post=':', separator='/', onlyfirst=2)
|
||||
statusbar = statusbar.replace('\t', ' ')
|
||||
self.context.workspace.status_text_set(f'Scaling selected: {statusbar}')
|
||||
|
||||
self.fast_update_timer.start()
|
||||
self.split_target_visualization_selected()
|
||||
self.set_accel_defer(True)
|
||||
tag_redraw_all('scale init')
|
||||
|
||||
@FSM.on_state('scale selected')
|
||||
# @profiler.function
|
||||
def scale_selected(self):
|
||||
opts = self.scale_selected_opts
|
||||
if self.actions.pressed(opts['move_done_pressed']):
|
||||
return 'main'
|
||||
if self.actions.released(opts['move_done_released']):
|
||||
return 'main'
|
||||
if self.actions.pressed(opts['move_cancelled']):
|
||||
self.undo_cancel()
|
||||
self.actions.unuse(opts['move_done_released'], ignoremods=True, ignoremulti=True)
|
||||
return 'main'
|
||||
|
||||
if (self.actions.mouse - opts['mouselast']).length == 0: return
|
||||
if time.time() < opts['lasttime'] + 0.05: return
|
||||
opts['mouselast'] = self.actions.mouse
|
||||
opts['lasttime'] = time.time()
|
||||
|
||||
dist = (self.actions.mouse - opts['center']).length
|
||||
|
||||
set2D_vert = self.set2D_vert
|
||||
for bmv,xy in opts['bmverts']:
|
||||
if not bmv.is_valid: continue
|
||||
dxy = xy - opts['center']
|
||||
nxy = dxy * dist / opts['start_dist'] + opts['center']
|
||||
set2D_vert(bmv, nxy)
|
||||
self.update_verts_faces(v for v,_ in opts['bmverts'])
|
||||
self.dirty()
|
||||
tag_redraw_all('scale mouse move')
|
||||
|
||||
@DrawCallbacks.on_draw('post2d')
|
||||
@FSM.onlyinstate('scale selected')
|
||||
def draw_rotate_post2d(self):
|
||||
opts = self.scale_selected_opts
|
||||
|
||||
gpustate.blend('ALPHA')
|
||||
Globals.drawing.draw2D_line(
|
||||
opts['mousedown'],
|
||||
opts['center'],
|
||||
(0.1, 1.0, 1.0, 1.0), color1=(0.1, 1.0, 1.0, 0.0),
|
||||
width=2, stipple=[2, 2]
|
||||
)
|
||||
Globals.drawing.draw2D_line(
|
||||
opts['center'],
|
||||
self.actions.mouse,
|
||||
(1.0, 1.0, 0.1, 1.0), color1=(1.0, 1.0, 0.1, 0.0),
|
||||
width=2, stipple=[2, 2]
|
||||
)
|
||||
|
||||
|
||||
@FSM.on_state('scale selected', 'exit')
|
||||
def scale_selected_exit(self):
|
||||
self.fast_update_timer.stop()
|
||||
self.clear_split_target_visualization()
|
||||
self.set_accel_defer(False)
|
||||
self._update_rftool_ui()
|
||||
|
||||
|
||||
def select_path(self, bmelem_types, fn_filter_bmelem=None, kwargs_select=None, kwargs_filter=None, **kwargs):
|
||||
vis_accel = self.get_accel_visible()
|
||||
nearest2D_vert = self.accel_nearest2D_vert
|
||||
nearest2D_edge = self.accel_nearest2D_edge
|
||||
nearest2D_face = self.accel_nearest2D_face
|
||||
|
||||
kwargs_filter = kwargs_filter or {}
|
||||
def fn_filter(bmelem):
|
||||
if not bmelem: return False
|
||||
if not fn_filter_bmelem: return True
|
||||
return fn_filter_bmelem(bmelem, **kwargs_filter)
|
||||
def get_bmelem(*args, **kwargs):
|
||||
if 'vert' in bmelem_types:
|
||||
bmelem, _ = nearest2D_vert(*args, vis_accel=vis_accel, **kwargs)
|
||||
if fn_filter(bmelem): return bmelem
|
||||
if 'edge' in bmelem_types:
|
||||
bmelem, _ = nearest2D_edge(*args, vis_accel=vis_accel, **kwargs)
|
||||
if fn_filter(bmelem): return bmelem
|
||||
if 'face' in bmelem_types:
|
||||
bmelem, _ = nearest2D_face(*args, vis_accel=vis_accel, **kwargs)
|
||||
if fn_filter(bmelem): return bmelem
|
||||
return None
|
||||
|
||||
bmelem = get_bmelem(max_dist=options['select dist']) # find what's under the mouse
|
||||
if not bmelem:
|
||||
# print('found nothing under mouse')
|
||||
return # nothing there; leave!
|
||||
|
||||
bmelem_types = { RFVert: {'vert'}, RFEdge: {'edge'}, RFFace: {'face'} }[type(bmelem)]
|
||||
kwargs_select = kwargs_select or {}
|
||||
kwargs.update(kwargs_select)
|
||||
kwargs['only'] = False
|
||||
|
||||
# find all other visible elements
|
||||
vis_elems = self.accel_vis_verts | self.accel_vis_edges | self.accel_vis_faces
|
||||
|
||||
# walk from bmelem to all other connected visible geometry
|
||||
path = {}
|
||||
working = deque()
|
||||
working.append((bmelem, None))
|
||||
def add(o, bme):
|
||||
nonlocal vis_elems, path, working
|
||||
if o not in vis_elems or o in path: return
|
||||
if not fn_filter(o): return
|
||||
working.append((o, bme))
|
||||
closest = None
|
||||
while working:
|
||||
bme, from_bme = working.popleft()
|
||||
if bme in path: continue
|
||||
path[bme] = from_bme
|
||||
if bme.select:
|
||||
# found closest!
|
||||
closest = bme
|
||||
break
|
||||
if 'vert' in bmelem_types:
|
||||
for c in bme.link_edges:
|
||||
o = c.other_vert(bme)
|
||||
add(o, bme)
|
||||
if 'edge' in bmelem_types:
|
||||
for c in bme.verts:
|
||||
for o in c.link_edges:
|
||||
add(o, bme)
|
||||
if 'face' in bmelem_types:
|
||||
for c in bme.edges:
|
||||
for o in c.link_faces:
|
||||
add(o, bme)
|
||||
|
||||
if not closest:
|
||||
# print('could not find closest element')
|
||||
return
|
||||
|
||||
self.undo_push('select path')
|
||||
while closest:
|
||||
self.select(closest, **kwargs)
|
||||
closest = path[closest]
|
||||
|
||||
|
||||
def setup_smart_selection_painting(self, bmelem_types, *, use_select_tool=False, selecting=True, deselect_all=False, fn_filter_bmelem=None, kwargs_select=None, kwargs_deselect=None, kwargs_filter=None, **kwargs):
|
||||
vis_accel = self.get_accel_visible()
|
||||
nearest2D_vert = self.accel_nearest2D_vert
|
||||
nearest2D_edge = self.accel_nearest2D_edge
|
||||
nearest2D_face = self.accel_nearest2D_face
|
||||
|
||||
kwargs_filter = kwargs_filter or {}
|
||||
kwargs_select = kwargs_select or {}
|
||||
kwargs_deselect = kwargs_deselect or {}
|
||||
|
||||
def fn_filter(bmelem):
|
||||
if not bmelem: return False
|
||||
if not fn_filter_bmelem: return True
|
||||
return fn_filter_bmelem(bmelem, **kwargs_filter)
|
||||
def get_bmelem(*args, **kwargs):
|
||||
if 'vert' in bmelem_types:
|
||||
bmelem, _ = nearest2D_vert(*args, vis_accel=vis_accel, **kwargs)
|
||||
if fn_filter(bmelem): return bmelem
|
||||
if 'edge' in bmelem_types:
|
||||
bmelem, _ = nearest2D_edge(*args, vis_accel=vis_accel, **kwargs)
|
||||
if fn_filter(bmelem): return bmelem
|
||||
if 'face' in bmelem_types:
|
||||
bmelem, _ = nearest2D_face(*args, vis_accel=vis_accel, **kwargs)
|
||||
if fn_filter(bmelem): return bmelem
|
||||
return None
|
||||
|
||||
bmelem_first = get_bmelem(max_dist=options['select dist']) # find what's under the mouse
|
||||
if not bmelem_first:
|
||||
# nothing there; either leave or use select tool
|
||||
if not use_select_tool:
|
||||
return
|
||||
rftool_select = next(rftool for rftool in self.rftools if rftool.name=='Select')
|
||||
self.quick_select_rftool(rftool_select)
|
||||
rftool_select._callback('quickselect start')
|
||||
return 'quick switch'
|
||||
|
||||
bmelem_type, vis_elems = {
|
||||
RFVert: ('vert', self.accel_vis_verts),
|
||||
RFEdge: ('edge', self.accel_vis_edges),
|
||||
RFFace: ('face', self.accel_vis_faces),
|
||||
}[type(bmelem_first)]
|
||||
bmelem_types = { bmelem_type } # needed so get_bmelem returns correct type
|
||||
|
||||
selecting |= not bmelem_first.select # if not explicitly selecting, start selecting only if elem under mouse is not selected
|
||||
kwargs.update(kwargs_select if selecting else kwargs_deselect)
|
||||
if selecting: kwargs['only'] = False
|
||||
|
||||
# walk from bmelem_first to all other connected visible geometry
|
||||
path_to_first = {}
|
||||
working = deque()
|
||||
def add_to_working(from_bmelem, to_bmelem):
|
||||
if to_bmelem not in vis_elems or to_bmelem in path_to_first: return
|
||||
if not fn_filter(to_bmelem): return
|
||||
working.append((from_bmelem, to_bmelem))
|
||||
add_to_working(None, bmelem_first)
|
||||
while working:
|
||||
from_bmelem, bmelem = working.popleft()
|
||||
if bmelem in path_to_first: continue
|
||||
path_to_first[bmelem] = from_bmelem
|
||||
match bmelem_type:
|
||||
case 'vert':
|
||||
for edge in bmelem.link_edges:
|
||||
for vert in edge.verts:
|
||||
add_to_working(bmelem, vert)
|
||||
case 'edge':
|
||||
for vert in bmelem.verts:
|
||||
for edge in vert.link_edges:
|
||||
add_to_working(bmelem, edge)
|
||||
case 'face':
|
||||
for edge in bmelem.edges:
|
||||
for face in edge.link_faces:
|
||||
add_to_working(bmelem, face)
|
||||
|
||||
fn_select = partial((self.select if selecting else self.deselect), **kwargs)
|
||||
|
||||
self.selection_painting_opts = Dict(
|
||||
fn_get_bmelem = get_bmelem,
|
||||
path_to_first = path_to_first,
|
||||
fn_select = fn_select,
|
||||
previous_selection = [],
|
||||
last_bmelem = bmelem_first,
|
||||
)
|
||||
|
||||
self.undo_push('smart select' if selecting else 'smart deselect')
|
||||
if deselect_all: self.deselect_all()
|
||||
fn_select(bmelem_first)
|
||||
|
||||
return 'smart selection painting'
|
||||
|
||||
@FSM.on_state('smart selection painting', 'enter')
|
||||
def smart_selection_painting_enter(self):
|
||||
self.fast_update_timer.start()
|
||||
self.split_target_visualization_visible()
|
||||
self.set_accel_defer(True)
|
||||
|
||||
|
||||
@DrawCallbacks.on_draw('predraw')
|
||||
@FSM.onlyinstate('smart selection painting')
|
||||
def unpause_smart_selection_painting_update(self):
|
||||
self.smart_selection_painting_update.unpause()
|
||||
|
||||
@CallGovernor.limit(pause_after_call=True)
|
||||
def smart_selection_painting_update(self):
|
||||
opts = self.selection_painting_opts
|
||||
|
||||
bmelem = opts.fn_get_bmelem()
|
||||
if not bmelem or bmelem not in opts.path_to_first: return
|
||||
|
||||
# hovering over same bmelem
|
||||
if bmelem == opts.last_bmelem: return
|
||||
opts.last_bmelem = bmelem
|
||||
|
||||
# reset to previous selection
|
||||
for (bme, s) in opts.previous_selection: bme.select = s
|
||||
|
||||
# get bmelems from hovered back to first
|
||||
current_selection = []
|
||||
while bmelem:
|
||||
current_selection.append(bmelem)
|
||||
bmelem = opts.path_to_first[bmelem]
|
||||
opts.previous_selection = [(bmelem, bmelem.select) for bmelem in current_selection]
|
||||
opts.fn_select(current_selection)
|
||||
|
||||
|
||||
@FSM.on_state('smart selection painting')
|
||||
def smart_selection_painting(self):
|
||||
if self.actions.pressed('cancel'):
|
||||
self.undo_cancel()
|
||||
self.actions.unuse('select paint', ignoremods=True, ignoremulti=True)
|
||||
self.actions.unuse('select paint add', ignoremods=True, ignoremulti=True)
|
||||
return 'main'
|
||||
|
||||
if not self.actions.using({'select paint', 'select paint add'}, ignoremods=True):
|
||||
return 'main'
|
||||
|
||||
if self.actions.mousemove:
|
||||
self.smart_selection_painting_update()
|
||||
tag_redraw_all('RF selection_painting') # needed to force perform update
|
||||
|
||||
|
||||
@FSM.on_state('smart selection painting', 'exit')
|
||||
def smart_selection_painting_exit(self):
|
||||
self.selection_painting_opts = None
|
||||
self.fast_update_timer.stop()
|
||||
self.clear_split_target_visualization()
|
||||
self.set_accel_defer(False)
|
||||
|
||||
|
||||
def rip(self, *, fill=False):
|
||||
# find highest order geometry selected
|
||||
# - faces: error
|
||||
# - edges: for each selected edge, find nearest adjacent face to mouse cursor and rip edge from other face
|
||||
# - verts: for each selected vert, find nearest adjacent edge to mouse cursor and rip vert from faces not adjacent to that edge
|
||||
sel_verts, sel_edges, sel_faces = self.get_selected_geom()
|
||||
|
||||
if sel_faces:
|
||||
self.alert_user('Can only rip a single edge, but a face is selected')
|
||||
return
|
||||
|
||||
if not sel_edges and not sel_verts:
|
||||
self.alert_user('Can only rip a single edge, but none are selected')
|
||||
return
|
||||
|
||||
if sel_verts and not sel_edges:
|
||||
self.alert_user('Ripping vertices is not supported yet')
|
||||
return
|
||||
|
||||
if sel_edges and len(sel_edges) > 1:
|
||||
# a temporary limitation
|
||||
self.alert_user('Ripping more than one selected edge is not supported yet')
|
||||
return
|
||||
|
||||
if not sel_edges:
|
||||
self.alert_user('Must have exactly one edge selected at the moment')
|
||||
return
|
||||
|
||||
|
||||
# working with first selected edge (current implementation limitation)
|
||||
bme = next(iter(sel_edges))
|
||||
|
||||
adj_faces = set(bme.link_faces)
|
||||
if len(adj_faces) < 2:
|
||||
self.alert_user('Edge must have at least two adjacent faces')
|
||||
return
|
||||
|
||||
bmv0, bmv1 = bme.verts
|
||||
nearest_face, _ = self.accel_nearest2D_face(faces_only=adj_faces)
|
||||
other_face = next(iter({bmf for bmf in bme.link_faces if bmf != nearest_face}), None)
|
||||
|
||||
self.undo_push('rip edge')
|
||||
if True:
|
||||
bmv2 = bmv0.face_separate(nearest_face)
|
||||
bmv3 = bmv1.face_separate(nearest_face)
|
||||
move_verts = [bmv2, bmv3]
|
||||
else:
|
||||
bmv2 = bmv0.face_separate(other_face)
|
||||
bmv3 = bmv1.face_separate(other_face)
|
||||
move_verts = [bmv0, bmv1]
|
||||
self.select(move_verts, only=True)
|
||||
|
||||
if fill:
|
||||
# only implemented simple fill for now
|
||||
self.new_face([bmv0, bmv1, bmv3, bmv2])
|
||||
|
||||
# self.undo_push('move ripped edge')
|
||||
self.prep_move(
|
||||
bmverts=move_verts,
|
||||
action_confirm=(lambda: self.actions.pressed({'confirm', 'confirm drag'})),
|
||||
)
|
||||
return 'move'
|
||||
|
||||
|
||||
def prep_move(self, *, bmverts=None, action_confirm=None, action_cancel=None, defer_recomputing=True):
|
||||
Point_to_Point2D = self.Point_to_Point2D
|
||||
self.move_settings = Dict(
|
||||
bmverts_xys = [
|
||||
(bmv, xy)
|
||||
for bmv in (bmverts if bmverts is not None else self.get_selected_verts())
|
||||
if bmv and bmv.is_valid and (xy := Point_to_Point2D(bmv.co)) is not None
|
||||
],
|
||||
actions = Dict(
|
||||
confirm=action_confirm or (lambda: self.actions.pressed('confirm')),
|
||||
cancel=action_cancel or (lambda: self.actions.pressed('cancel')),
|
||||
),
|
||||
mousedown = self.actions.mouse,
|
||||
last_delta = None,
|
||||
vis_accel = self.get_accel_visible(selected_only=False),
|
||||
)
|
||||
self.move_settings.bmverts = [bmv for (bmv,_) in self.move_settings.bmverts_xys]
|
||||
|
||||
@FSM.on_state('move', 'enter')
|
||||
def move_enter(self):
|
||||
# if not self.move_done_released and options['hide cursor on tweak']: self.set_widget('hidden')
|
||||
if options['hide cursor on tweak']: Cursors.set('NONE')
|
||||
self.split_target_visualization_selected()
|
||||
self.fast_update_timer.start()
|
||||
self.set_accel_defer(True)
|
||||
|
||||
@FSM.on_state('move')
|
||||
def modal_move(self):
|
||||
if self.move_settings.actions['confirm']():
|
||||
if options['automerge']:
|
||||
self.merge_verts_by_dist(self.move_settings.bmverts, options['merge dist'])
|
||||
return 'main'
|
||||
|
||||
if self.move_settings.actions['cancel']():
|
||||
self.undo_cancel()
|
||||
return 'main'
|
||||
|
||||
delta = Vec2D(self.actions.mouse - self.move_settings.mousedown)
|
||||
if delta == self.move_settings.last_delta: return
|
||||
self.move_settings.last_delta = delta
|
||||
set2D_vert = self.set2D_vert
|
||||
|
||||
for bmv,xy in self.move_settings.bmverts_xys:
|
||||
xy_updated = xy + delta
|
||||
# check if xy_updated is "close" to any visible verts (in image plane)
|
||||
# if so, snap xy_updated to vert position (in image plane)
|
||||
if options['automerge']:
|
||||
bmv1,d = self.accel_nearest2D_vert(point=xy_updated, vis_accel=self.move_settings.vis_accel, max_dist=options['merge dist'])
|
||||
if bmv1 is None:
|
||||
set2D_vert(bmv, xy_updated)
|
||||
continue
|
||||
xy1 = self.Point_to_Point2D(bmv1.co)
|
||||
if not xy1:
|
||||
set2D_vert(bmv, xy_updated)
|
||||
continue
|
||||
set2D_vert(bmv, xy1)
|
||||
else:
|
||||
set2D_vert(bmv, xy_updated)
|
||||
|
||||
self.update_verts_faces(self.move_settings.bmverts)
|
||||
self.dirty()
|
||||
tag_redraw_all('move update')
|
||||
|
||||
@FSM.on_state('move', 'exit')
|
||||
def move_exit(self):
|
||||
self.fast_update_timer.stop()
|
||||
self.set_accel_defer(False)
|
||||
self.clear_split_target_visualization()
|
||||
|
||||
Reference in New Issue
Block a user