2025-07-01
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
http://cgcookie.com
|
||||
hello@cgcookie.com
|
||||
|
||||
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
__all__ = ['cookiecutter', 'test']
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 sys
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..common.blender import perform_redraw_all
|
||||
from ..common.debug import debugger, tprint
|
||||
from ..common.profiler import profiler
|
||||
|
||||
from .cookiecutter_actions import CookieCutter_Actions
|
||||
from .cookiecutter_blender import CookieCutter_Blender
|
||||
from .cookiecutter_debug import CookieCutter_Debug
|
||||
from .cookiecutter_exceptions import CookieCutter_Exceptions
|
||||
from .cookiecutter_fsm import CookieCutter_FSM
|
||||
from .cookiecutter_modal import CookieCutter_Modal
|
||||
from .cookiecutter_ui import CookieCutter_UI
|
||||
|
||||
|
||||
is_broken = False
|
||||
|
||||
class CookieCutter(
|
||||
Operator,
|
||||
CookieCutter_UI,
|
||||
CookieCutter_Actions,
|
||||
CookieCutter_FSM,
|
||||
CookieCutter_Blender,
|
||||
CookieCutter_Exceptions,
|
||||
CookieCutter_Debug,
|
||||
CookieCutter_Modal,
|
||||
):
|
||||
'''
|
||||
CookieCutter is used to create advanced operators very quickly!
|
||||
|
||||
To use:
|
||||
|
||||
- specify CookieCutter as a subclass
|
||||
- provide appropriate values for Blender class attributes: bl_idname, bl_label, etc.
|
||||
- provide appropriate dictionary that maps user action labels to keyboard and mouse actions
|
||||
- override the start function
|
||||
- register finite state machine state callbacks with the FSM.on_state(state) function decorator
|
||||
- state can be any string that is a state in your FSM
|
||||
- Must provide at least a 'main' state
|
||||
- return values of each on_state decorated function tell FSM which state to switch into
|
||||
- None, '', or no return: stay in same state
|
||||
- register drawing callbacks with the CookieCutter.Draw(mode) function decorator
|
||||
- mode: 'pre3d', 'post3d', 'post2d'
|
||||
|
||||
'''
|
||||
|
||||
# registry = []
|
||||
# def __init_subclass__(cls, *args, **kwargs):
|
||||
# super().__init_subclass__(*args, **kwargs)
|
||||
# if not hasattr(cls, '_cookiecutter_index'):
|
||||
# # add cls to registry (might get updated later) and add FSM,Draw
|
||||
# cls._rfwidget_index = len(CookieCutter.registry)
|
||||
# CookieCutter.registry.append(cls)
|
||||
# cls.fsm = FSM()
|
||||
# cls.drawcallbacks = DrawCallbacks()
|
||||
# else:
|
||||
# # update registry, but do not add new FSM
|
||||
# CookieCutter.registry[cls._cookiecutter_index] = cls
|
||||
|
||||
|
||||
############################################################################
|
||||
# override the following values and functions
|
||||
|
||||
bl_idname = "view3d.cookiecutter_unnamed"
|
||||
bl_label = "CookieCutter Unnamed"
|
||||
|
||||
is_running = False
|
||||
|
||||
@classmethod
|
||||
def can_start(cls, context): return True
|
||||
|
||||
def prestart(self): pass
|
||||
def is_ready_to_start(self): return True
|
||||
def start(self): pass
|
||||
def update(self): pass
|
||||
def end_commit(self): pass
|
||||
def end_cancel(self): pass
|
||||
def end(self): pass
|
||||
def should_pass_through(self, context, event): return False
|
||||
|
||||
############################################################################
|
||||
|
||||
@staticmethod
|
||||
def cc_break():
|
||||
global is_broken
|
||||
is_broken = True
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
global is_broken
|
||||
if is_broken: return False
|
||||
with cls.try_exception('call can_start()'):
|
||||
return cls.can_start(context)
|
||||
print('BREAKING COOKIECUTTER')
|
||||
print(f'{cls.bl_idname}')
|
||||
cls.cc_break()
|
||||
return False
|
||||
|
||||
def invoke(self, context, event):
|
||||
CookieCutter.is_running = True
|
||||
self._cc_stage = 'prestart'
|
||||
self.context = context
|
||||
self.event = event
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def stop_running(self):
|
||||
CookieCutter.is_running = False
|
||||
|
||||
def done(self, *, cancel=False, emergency_bail=False):
|
||||
if emergency_bail:
|
||||
self._done = 'bail'
|
||||
else:
|
||||
self._done = 'commit' if not cancel else 'cancel'
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 sys
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.useractions import ActionHandler
|
||||
|
||||
|
||||
class CookieCutter_Actions:
|
||||
def _cc_actions_init(self):
|
||||
self._cc_actions = ActionHandler(self.context)
|
||||
self._timer = self._cc_actions.start_timer(10)
|
||||
|
||||
def _cc_actions_update(self):
|
||||
self._cc_actions.update(self.context, self.event, fn_debug=self.debug_print_actions)
|
||||
|
||||
def _cc_actions_end(self):
|
||||
self._timer.done()
|
||||
self._cc_actions.done()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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
|
||||
from inspect import ismethod, isfunction, signature
|
||||
from contextlib import contextmanager
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.blender import region_label_to_data, create_simple_context, StoreRestore, BlenderSettings
|
||||
from ..common.decorators import blender_version_wrapper, ignore_exceptions
|
||||
from ..common.functools import find_fns, self_wrapper
|
||||
from ..common.debug import debugger
|
||||
from ..common.blender_cursors import Cursors
|
||||
from ..common.utils import iter_head
|
||||
|
||||
|
||||
|
||||
class CookieCutter_Blender(BlenderSettings):
|
||||
def _cc_blenderui_init(self):
|
||||
self.storerestore_init()
|
||||
for _,fn in find_fns(self, '_blender_change_callback'):
|
||||
self.register_blender_change_callback(self_wrapper(self, fn))
|
||||
self._storerestore.store_all()
|
||||
|
||||
@staticmethod
|
||||
def blender_change_callback(fn):
|
||||
fn._blender_change_callback = True
|
||||
return fn
|
||||
def register_blender_change_callback(self, fn):
|
||||
self._storerestore.register_storage_change_callback(fn)
|
||||
def blender_change_init(self, storage):
|
||||
self._storerestore.init_storage(storage)
|
||||
|
||||
def _cc_blenderui_end(self, ignore=None):
|
||||
self._storerestore.restore_all(ignore=ignore)
|
||||
|
||||
self.header_text_restore()
|
||||
self.statusbar_text_restore()
|
||||
self.cursor_restore()
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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/>.
|
||||
'''
|
||||
|
||||
from ..common.blender import get_text_block
|
||||
|
||||
|
||||
class CookieCutter_Debug:
|
||||
cc_debug_print_to = 'CookieCutter_Debug'
|
||||
cc_debug_all_enabled = False
|
||||
cc_debug_actions_enabled = False
|
||||
|
||||
def debug_print(self, label, *args, override_enabled=False, **kwargs):
|
||||
if not override_enabled and not self.cc_debug_all_enabled: return
|
||||
|
||||
text_block = get_text_block(self.cc_debug_print_to)
|
||||
assert text_block
|
||||
text_block.cursor_set(0x7fffffff) # move cursor to last line
|
||||
text_block.write(f'{label}: {", ".join(args)}\n')
|
||||
for k,v in kwargs.items():
|
||||
text_block.write(f' {k} = {v}\n')
|
||||
text_block.write('\n')
|
||||
|
||||
def debug_print_actions(self, *args, **kwargs):
|
||||
self.debug_print(
|
||||
'Action',
|
||||
*args,
|
||||
override_enabled=self.cc_debug_actions_enabled,
|
||||
**kwargs
|
||||
)
|
||||
@@ -0,0 +1,72 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
https://github.com/CGCookie/retopoflow
|
||||
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 contextlib
|
||||
|
||||
from ..common.fsm import FSM
|
||||
from ..common.debug import debugger, ExceptionHandler
|
||||
from ..common.functools import find_fns
|
||||
|
||||
class CookieCutter_Exceptions:
|
||||
@staticmethod
|
||||
def _handle_exception(e, action, fatal=False):
|
||||
print(f'CookieCutter_Exceptions: handling caught exception')
|
||||
print(f' action: {action}')
|
||||
debugger.print_exception()
|
||||
if fatal: assert False
|
||||
if hasattr(CookieCutter_Exceptions, '_instance'):
|
||||
CookieCutter_Exceptions._instance._callback_exception_callbacks(e)
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def try_exception(action, *, fatal=False, fn_succeed=None, fn_exception=None, fn_finally=None):
|
||||
try:
|
||||
yield
|
||||
if fn_succeed: fn_succeed()
|
||||
except Exception as e:
|
||||
CookieCutter_Exceptions._handle_exception(e, action, fatal=fatal)
|
||||
if fn_exception: fn_exception(e)
|
||||
finally:
|
||||
if fn_finally: fn_finally()
|
||||
|
||||
@staticmethod
|
||||
def _exception_callback_wrapper(fn):
|
||||
fn._cc_exception_callback = True
|
||||
return fn
|
||||
Exception_Callback = _exception_callback_wrapper
|
||||
|
||||
@ExceptionHandler.on_exception
|
||||
def _callback_exception_callbacks(self, e):
|
||||
print(f'CookieCutter_Exceptions._callback_exception_callbacks: {e}')
|
||||
# debugger.dcallstack(0)
|
||||
for fn_name in self._exception_callbacks:
|
||||
try:
|
||||
fn = getattr(self, fn_name)
|
||||
fn(e)
|
||||
except Exception as e2:
|
||||
print(f'CookieCutter caught exception while calling back exception callbacks: {fn.__name__}')
|
||||
debugger.print_exception()
|
||||
|
||||
def _cc_exception_init(self):
|
||||
self._exception_callbacks = [fn.__name__ for (_,fn) in find_fns(self, '_cc_exception_callback')]
|
||||
self._exceptionhandler = ExceptionHandler(self)
|
||||
#self._exceptionhandler.add_callback(self._callback_exception_callbacks, universal=True)
|
||||
CookieCutter_Exceptions._instance = self
|
||||
|
||||
def _cc_exception_done(self):
|
||||
del self._exceptionhandler
|
||||
del CookieCutter_Exceptions._instance
|
||||
self._exceptionhandler = None
|
||||
ExceptionHandler.clear_universal_callbacks()
|
||||
@@ -0,0 +1,55 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
https://github.com/CGCookie/retopoflow
|
||||
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 bpy
|
||||
|
||||
from ..common.debug import debugger
|
||||
from ..common.fsm import FSM
|
||||
from ..common.timerhandler import TimerHandler
|
||||
|
||||
class CookieCutter_FSM:
|
||||
def _cc_fsm_init(self):
|
||||
self.fsm = FSM(self, start='main')
|
||||
self.fsm.add_exception_callback(lambda e: self._handle_exception(e, 'handle exception caught by FSM'))
|
||||
# def callback(e): self._handle_exception(e, 'handle exception caught by FSM')
|
||||
# self.fsm.add_exception_callback(callback)
|
||||
|
||||
def _cc_fsm_update(self):
|
||||
self.fsm.update()
|
||||
|
||||
def _cc_fsm_force_event(self):
|
||||
# add some NOP event to event queue to force modal operator to be called again right away
|
||||
|
||||
# # warp cursor to same spot
|
||||
# # DOES NOT WORK: (event.mouse_x, event.mouse_y) might be incorrect!!!
|
||||
# self.context.window.cursor_warp(self.event.mouse_x, self.event.mouse_y)
|
||||
|
||||
# # simulate an event
|
||||
# # DOES NOT WORK: only works with `--enable-event-simulate`, but then Blender cannot accept any input!!!
|
||||
# self.context.window.event_simulate(type='NONE', value='NOTHING')
|
||||
|
||||
# # register a short-lived timer (only returns `None`)
|
||||
# # DOES NOT WORK: these timers do NOT cause modal operator to be called for some reason :(
|
||||
# bpy.app.timers.register(lambda:None, first_interval=0.01)
|
||||
|
||||
# create a short-lived WindowManager timer
|
||||
# self.actions might not yet be created!
|
||||
if not hasattr(self, '_cc_force_event_handler'):
|
||||
self._cc_force_event_handler = TimerHandler(120, context=self.context, enabled=False)
|
||||
self._cc_force_event_handler.start()
|
||||
|
||||
def _cc_fsm_stop_force_event(self):
|
||||
if hasattr(self, '_cc_force_event_handler'):
|
||||
self._cc_force_event_handler.stop()
|
||||
@@ -0,0 +1,166 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 sys
|
||||
import copy
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..common.blender import perform_redraw_all, get_view3d_area
|
||||
from ..common.debug import debugger, tprint
|
||||
from ..common.profiler import profiler
|
||||
|
||||
|
||||
class CookieCutter_Modal:
|
||||
def modal(self, context, event):
|
||||
self.context = context
|
||||
self.event = event
|
||||
|
||||
if self._cc_stage == 'quit': return {'FINISHED'}
|
||||
|
||||
# if we're not yet in the main loop, create a NOP event so that we can
|
||||
# work our way through the initialization stuff as quickly as possible!
|
||||
if self._cc_stage != 'main loop': self._cc_fsm_force_event()
|
||||
else: self._cc_fsm_stop_force_event()
|
||||
|
||||
# get the method corresponding to the current stage
|
||||
fn_modal = {
|
||||
'prestart': self.modal_prestart,
|
||||
'start when ready': self.modal_start_when_ready,
|
||||
'init CC internals': self.modal_init_cc_internals,
|
||||
'init CC start': self.modal_init_cc_start,
|
||||
'init CC UI': self.modal_init_cc_ui,
|
||||
'main loop': self.modal_mainloop,
|
||||
}.get(self._cc_stage, None)
|
||||
assert fn_modal, f"Unhandled CC stage: '{self._cc_stage}'"
|
||||
|
||||
ret = fn_modal()
|
||||
# if ret == {'PASS_THROUGH'}:
|
||||
# print(f'passing through {random.random()}')
|
||||
return ret
|
||||
|
||||
def modal_prestart(self):
|
||||
with self.try_exception('prestarting'):
|
||||
self.prestart()
|
||||
self._cc_stage = 'start when ready'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_start_when_ready(self):
|
||||
with self.try_exception('waiting for start readiness'):
|
||||
if self.is_ready_to_start():
|
||||
self._cc_stage = 'init CC internals'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_init_cc_internals(self):
|
||||
with self.try_exception('initialize internals (Exception Callbacks, FSM, UI, Actions)'):
|
||||
self._nav = False
|
||||
self._nav_time = 0
|
||||
self._done = False
|
||||
self._start_time = time.time()
|
||||
self._tmp_time = self._start_time
|
||||
self._cc_exception_init()
|
||||
self._cc_fsm_init()
|
||||
self._cc_ui_init()
|
||||
self._cc_actions_init()
|
||||
self._cc_stage = 'init CC start'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_init_cc_start(self):
|
||||
with self.try_exception('initialize start'):
|
||||
self.start()
|
||||
self._cc_stage = 'init CC UI'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_init_cc_ui(self):
|
||||
with self.try_exception('initialize ui'):
|
||||
self._cc_ui_start()
|
||||
self._cc_stage = 'main loop'
|
||||
return {'RUNNING_MODAL'}
|
||||
self.cc_break()
|
||||
return {'CANCELLED'}
|
||||
|
||||
def modal_mainloop(self):
|
||||
self.drawcallbacks.reset_pre()
|
||||
|
||||
if time.time() - self._tmp_time >= 1:
|
||||
self._tmp_time = time.time()
|
||||
# print('--- %d ---' % int(self._tmp_time - self._start_time))
|
||||
profiler.printfile()
|
||||
|
||||
if self._done:
|
||||
self.modal_maindone()
|
||||
self._cc_ui_end()
|
||||
self._cc_actions_end()
|
||||
self._cc_exception_done()
|
||||
return {'FINISHED'} if self._done=='finish' else {'CANCELLED'}
|
||||
|
||||
ret = None
|
||||
|
||||
if self._nav:
|
||||
self._nav = False
|
||||
self._nav_time = time.time()
|
||||
self._cc_actions_update()
|
||||
|
||||
if self._cc_ui_update():
|
||||
# UI handled the action
|
||||
ret = {'RUNNING_MODAL'}
|
||||
elif self._cc_actions.using('blender window action'):
|
||||
# allow window actions to pass through to Blender
|
||||
ret = {'PASS_THROUGH'}
|
||||
elif self._cc_actions.is_navigating or (self._cc_actions.timer and self._nav):
|
||||
self._nav = True
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
with self.try_exception('call update'):
|
||||
self.update()
|
||||
if self.should_pass_through(self.context, self.event):
|
||||
ret = {'PASS_THROUGH'}
|
||||
|
||||
if not ret:
|
||||
self._cc_fsm_update()
|
||||
ret = {'RUNNING_MODAL'}
|
||||
|
||||
perform_redraw_all(only_area=get_view3d_area(self.context))
|
||||
return ret
|
||||
|
||||
def modal_maindone(self):
|
||||
if self._done == 'bail':
|
||||
return
|
||||
|
||||
try:
|
||||
fn_end = self.end_commit if self._done == 'commit' else self.end_cancel
|
||||
fn_end()
|
||||
self.end()
|
||||
self.stop_running()
|
||||
except Exception as e:
|
||||
self._handle_exception(e, 'call end() with %s' % self._done)
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
'''
|
||||
Copyright (C) 2023 CG Cookie
|
||||
|
||||
https://github.com/CGCookie/retopoflow
|
||||
|
||||
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 random
|
||||
|
||||
import bpy
|
||||
from bpy.types import SpaceView3D
|
||||
from mathutils import Matrix
|
||||
|
||||
from ..common import gpustate
|
||||
from ..common.globals import Globals
|
||||
from ..common.gpustate import ScissorStack
|
||||
from ..common.blender import bversion, tag_redraw_all, get_view3d_area, get_view3d_region, get_view3d_space
|
||||
from ..common.decorators import blender_version_wrapper
|
||||
from ..common.debug import debugger, tprint
|
||||
from ..common.drawing import Drawing, DrawCallbacks
|
||||
from ..common.ui_core_images import preload_image
|
||||
from ..common.ui_document import UI_Document
|
||||
|
||||
|
||||
if not bpy.app.background:
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
# https://docs.blender.org/api/blender2.8/gpu.html#triangle-with-custom-shader
|
||||
cover_vshader = '''
|
||||
in vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0f, 1.0f);
|
||||
}
|
||||
'''
|
||||
cover_fshader = '''
|
||||
uniform float darken;
|
||||
out vec4 outColor;
|
||||
void main() {
|
||||
// float r = length(gl_FragCoord.xy - vec2(0.5, 0.5));
|
||||
if(mod(floor(gl_FragCoord.x+gl_FragCoord.y), 2.0) == 0) {
|
||||
outColor = vec4(0.0,0.0,0.0,1.0);
|
||||
} else {
|
||||
outColor = vec4(0.0f, 0.0f, 0.0f, darken);
|
||||
}
|
||||
}
|
||||
'''
|
||||
Drawing.glCheckError(f'Pre-compile check: cover shader')
|
||||
shader, _ = gpustate.gpu_shader(f'blender ui cover', cover_vshader, cover_fshader)
|
||||
Drawing.glCheckError(f'Post-compile check: cover shader')
|
||||
|
||||
# create batch to draw large triangle that covers entire clip space (-1,-1)--(+1,+1)
|
||||
batch_full = batch_for_shader(shader, 'TRIS', {"position": [(-100, -100), (300, -100), (-100, 300)]})
|
||||
|
||||
|
||||
|
||||
class CookieCutter_UI:
|
||||
'''
|
||||
Assumes that direct subclass will have singleton instance (shared CookieCutter among all instances of that subclass and any subclasses)
|
||||
'''
|
||||
|
||||
def _cc_ui_init(self):
|
||||
# preload images
|
||||
preload_image(
|
||||
'checkmark.png', 'close.png', 'collapse_close.png', 'collapse_open.png', 'radio.png'
|
||||
)
|
||||
self.document = Globals.ui_document # UI_Document(self.context)
|
||||
self.document.init(self.context)
|
||||
self.document.add_exception_callback(lambda e: self._handle_exception(e, 'handle exception caught by UI'))
|
||||
self.drawing = Globals.drawing
|
||||
area = get_view3d_area()
|
||||
space = get_view3d_space()
|
||||
region = get_view3d_region()
|
||||
self.drawing.set_region(area, space, region, space.region_3d, bpy.context.window)
|
||||
self.drawcallbacks = DrawCallbacks(self)
|
||||
self._cc_blenderui_init()
|
||||
self._ignore_ui_events = False
|
||||
self._hover_ui = False
|
||||
tag_redraw_all('CC ui_init', only_tag=False)
|
||||
|
||||
@property
|
||||
def ignore_ui_events(self):
|
||||
return self._ignore_ui_events
|
||||
@ignore_ui_events.setter
|
||||
def ignore_ui_events(self, v):
|
||||
self._ignore_ui_events = bool(v)
|
||||
|
||||
def _cc_ui_start(self):
|
||||
def preview():
|
||||
try: self.drawcallbacks.pre3d()
|
||||
except Exception as e:
|
||||
self._handle_exception(e, 'draw pre3d')
|
||||
ScissorStack.end(force=True)
|
||||
def postview():
|
||||
# print('***** postview')
|
||||
try: self.drawcallbacks.post3d()
|
||||
except Exception as e:
|
||||
self._handle_exception(e, 'draw post3d')
|
||||
ScissorStack.end(force=True)
|
||||
def postpixel():
|
||||
# print('***** postpixel')
|
||||
gpustate.blend('ALPHA')
|
||||
try: self.drawcallbacks.post2d()
|
||||
except Exception as e:
|
||||
self._handle_exception(e, 'draw post2d')
|
||||
ScissorStack.end(force=True)
|
||||
try: self.document.draw(self.context)
|
||||
except Exception as e:
|
||||
self._handle_exception(e, 'draw window UI')
|
||||
ScissorStack.end(force=True)
|
||||
self._done = True # consider this a fatal failure
|
||||
|
||||
space = bpy.types.SpaceView3D
|
||||
self._handle_preview = space.draw_handler_add(preview, tuple(), 'WINDOW', 'PRE_VIEW')
|
||||
self._handle_postview = space.draw_handler_add(postview, tuple(), 'WINDOW', 'POST_VIEW')
|
||||
self._handle_postpixel = space.draw_handler_add(postpixel, tuple(), 'WINDOW', 'POST_PIXEL')
|
||||
tag_redraw_all('CC ui_start', only_tag=False)
|
||||
|
||||
def _cc_ui_update(self):
|
||||
self.drawing.update_dpi()
|
||||
if self.ignore_ui_events: return False
|
||||
ret = self.document.update(self.context, self.event)
|
||||
self._hover_ui = ret and 'hover' in ret
|
||||
return self._hover_ui
|
||||
|
||||
def _cc_ui_end(self):
|
||||
self._cc_blenderui_end()
|
||||
space = bpy.types.SpaceView3D
|
||||
space.draw_handler_remove(self._handle_preview, 'WINDOW')
|
||||
space.draw_handler_remove(self._handle_postview, 'WINDOW')
|
||||
space.draw_handler_remove(self._handle_postpixel, 'WINDOW')
|
||||
self.region_restore()
|
||||
self.context.workspace.status_text_set(None)
|
||||
tag_redraw_all('CC ui_end', only_tag=False)
|
||||
|
||||
|
||||
#########################################
|
||||
# Region Darkening
|
||||
|
||||
def _cc_region_draw_cover(self, a):
|
||||
gpustate.blend('ALPHA')
|
||||
gpustate.depth_test('NONE')
|
||||
shader.bind()
|
||||
shader.uniform_float("darken", 0.50)
|
||||
batch_full.draw(shader)
|
||||
gpu.shader.unbind()
|
||||
|
||||
def region_darken(self):
|
||||
if hasattr(self, '_region_darkened'): return # already darkened!
|
||||
self._region_darkened = True
|
||||
self._postpixel_callbacks = []
|
||||
|
||||
# darken all spaces
|
||||
spaces = [(getattr(bpy.types, n), n) for n in dir(bpy.types) if n.startswith('Space')]
|
||||
spaces = [(s,n) for (s,n) in spaces if hasattr(s, 'draw_handler_add')]
|
||||
|
||||
# https://docs.blender.org/api/blender2.8/bpy.types.Region.html#bpy.types.Region.type
|
||||
# ['WINDOW', 'HEADER', 'CHANNELS', 'TEMPORARY', 'UI', 'TOOLS', 'TOOL_PROPS', 'PREVIEW', 'NAVIGATION_BAR', 'EXECUTE']
|
||||
# NOTE: b280 has no TOOL_PROPS region for SpaceView3D!
|
||||
# handling SpaceView3D differently!
|
||||
general_areas = ['WINDOW', 'HEADER', 'CHANNELS', 'TEMPORARY', 'UI', 'TOOLS', 'TOOL_PROPS', 'PREVIEW', 'HUD', 'NAVIGATION_BAR', 'EXECUTE', 'FOOTER', 'TOOL_HEADER'] #['WINDOW', 'HEADER', 'UI', 'TOOLS', 'NAVIGATION_BAR']
|
||||
SpaceView3D_areas = ['TOOLS', 'UI', 'HEADER', 'TOOL_PROPS']
|
||||
|
||||
for (s,n) in spaces:
|
||||
areas = SpaceView3D_areas if n == 'SpaceView3D' else general_areas
|
||||
for a in areas:
|
||||
try:
|
||||
cb = s.draw_handler_add(self._cc_region_draw_cover, (a,), a, 'POST_PIXEL')
|
||||
self._postpixel_callbacks += [(s, a, cb)]
|
||||
except:
|
||||
pass
|
||||
|
||||
tag_redraw_all('CC region_darken', only_tag=False)
|
||||
|
||||
def region_restore(self):
|
||||
# remove callback handlers
|
||||
if hasattr(self, '_postpixel_callbacks'):
|
||||
for (s,a,cb) in self._postpixel_callbacks: s.draw_handler_remove(cb, a)
|
||||
del self._postpixel_callbacks
|
||||
if hasattr(self, '_region_darkened'):
|
||||
del self._region_darkened
|
||||
tag_redraw_all('CC region_restore', only_tag=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user