''' 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 . ''' 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()