Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/blenderregister.py
T
2026-03-17 14:30:01 -06:00

1106 lines
46 KiB
Python

'''
Copyright (C) 2023 CG Cookie
http://cgcookie.com
hello@cgcookie.com
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
'''
import os
import re
import sys
import json
import time
import textwrap
import importlib
from pathlib import Path
import bpy
from bpy.types import Menu, Operator, Panel
from bpy.props import BoolProperty
from bpy_extras import object_utils
from bpy.app.handlers import persistent
from ..addon_common.hive.hive import Hive
from ..addon_common.common.decorators import add_cache
from ..addon_common.cookiecutter.cookiecutter import CookieCutter
import_succeeded = False
try:
if "retopoflow" in locals():
print('RetopoFlow: RELOADING!')
# reloading RF modules
importlib.reload(retopoflow)
importlib.reload(image_preloader)
importlib.reload(helpsystem)
importlib.reload(updatersystem)
importlib.reload(keymapsystem)
importlib.reload(configoptions)
importlib.reload(updater)
importlib.reload(cookiecutter)
importlib.reload(rftool)
else:
print('RetopoFlow: Initial load')
from ..config import options as configoptions
from . import retopoflow
from . import helpsystem
from . import updatersystem
from . import keymapsystem
from . import updater
from . import rftool
from ..addon_common.cookiecutter import cookiecutter
from ..addon_common.common.maths import convert_numstr_num, has_inverse
from ..addon_common.common.blender import get_active_object, BlenderIcon, get_path_from_addon_root, show_blender_popup, show_blender_text
from ..addon_common.common.boundvar import BoundBool
from ..addon_common.common.image_preloader import ImagePreloader
from ..addon_common.terminal.deepdebug import DeepDebug
options = configoptions.options
rfurls = configoptions.retopoflow_urls
import_succeeded = True
RFTool = rftool.RFTool
except ModuleNotFoundError as e:
print('RetopoFlow: ModuleNotFoundError caught when trying to enable add-on!')
print(e)
except Exception as e:
print('RetopoFlow: Unexpected Exception caught when trying to enable add-on!')
print(e)
from .addon_common.common.debug import Debugger
message,h = Debugger.get_exception_info_and_hash()
message = '\n'.join('- %s'%l for l in message.splitlines())
print(message)
# the classes to register/unregister
RF_classes = []
def add_to_registry(cls_or_list):
if isinstance(cls_or_list, list):
RF_classes.extend(cls_or_list)
else:
# could be used as class decorator, so return class
RF_classes.append(cls_or_list)
return cls_or_list
if import_succeeded:
# point BlenderIcon to correct icon path
BlenderIcon.path_icons = get_path_from_addon_root('icons')
if options['preload help images']:
# start preloading images
ImagePreloader.start([
('help'),
('icons'),
('addon_common', 'common', 'images'),
])
##################################################################################
# Blender Operator Factories
@add_cache('_cache', {})
def create_help_builtin_operator(label, filename):
key = (label, filename)
if key not in create_help_builtin_operator._cache:
idname = label.replace(' ', '')
class VIEW3D_OT_RetopoFlow_Help(helpsystem.RetopoFlow_OpenHelpSystem):
"""Open RetopoFlow Help System"""
bl_idname = f'cgcookie.retopoflow_help_{idname.lower()}'
bl_label = f'RF Help: {label}'
bl_description = f'Open RetopoFlow Help System: {label}'
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = set()
rf_startdoc = f'{filename}.md'
VIEW3D_OT_RetopoFlow_Help.__name__ = f'VIEW3D_OT_RetopoFlow_Help_{idname}'
add_to_registry(VIEW3D_OT_RetopoFlow_Help)
create_help_builtin_operator._cache[key] = VIEW3D_OT_RetopoFlow_Help
return create_help_builtin_operator._cache[key]
@add_cache('_cache', {})
def create_help_online_operator(label, filename):
key = (label, filename)
if key not in create_help_online_operator._cache:
idname = label.replace(' ', '')
class VIEW3D_OT_RetopoFlow_Online(Operator):
"""Open RetopoFlow Help Online"""
bl_idname = f'cgcookie.retopoflow_online_{idname.lower()}'
bl_label = f'RF Online: {label}'
bl_description = f'Open RetopoFlow Help Online: {label}'
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = set()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
bpy.ops.wm.url_open(url=rfurls['help doc'](filename))
return {'FINISHED'}
VIEW3D_OT_RetopoFlow_Online.__name__ = f'VIEW3D_OT_RetopoFlow_Online_{idname}'
add_to_registry(VIEW3D_OT_RetopoFlow_Online)
create_help_online_operator._cache[key] = VIEW3D_OT_RetopoFlow_Online
return create_help_online_operator._cache[key]
@add_cache('_cache', {})
def create_webpage_operator(name, label, description, url):
key = (name, label, description, url)
if key not in create_webpage_operator._cache:
idname = name.lower()
class VIEW3D_OT_RetopoFlow_Web(Operator):
bl_idname = f'cgcookie.retopoflow_web_{idname}'
bl_label = f'{label}'
bl_description = f'Open {description} in the default browser'
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
bpy.ops.wm.url_open(url=url)
return {'FINISHED'}
VIEW3D_OT_RetopoFlow_Web.__name__ = f'VIEW3D_OT_RetopoFlow_Web_{name}'
add_to_registry(VIEW3D_OT_RetopoFlow_Web)
create_webpage_operator._cache[key] = VIEW3D_OT_RetopoFlow_Web
return create_webpage_operator._cache[key]
def create_toggle_operator(name, label, description, boundbool):
idname = label.replace(' ', '')
class VIEW3D_OT_RetopoFlow_Toggle(Operator):
bl_idname = f'cgcookie.retopoflow_toggle_{idname.lower()}'
bl_label = f'RF Toggle: {label}'
bl_description = f'Toggle RetopoFlow value: {label}'
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = set()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
boundbool.checked = not boundbool.checked
return {'FINISHED'}
VIEW3D_OT_RetopoFlow_Toggle.__name__ = f'VIEW3D_OT_RetopoFlow_Toggle_{name}'
add_to_registry(VIEW3D_OT_RetopoFlow_Toggle)
return VIEW3D_OT_RetopoFlow_Toggle
##################################################################################
create_webpage_operator(
'BlenderMarket',
'Visit Blender Market',
'Blender Market RetopoFlow',
rfurls['blender market'],
)
create_webpage_operator(
'GitHub_NewIssue',
'Create a new issue on GitHub',
'RetopoFlow GitHub New Issue Page',
rfurls['new github issue'],
)
create_webpage_operator(
'Online_Main',
'Online documentation',
'RetopoFlow Online Documentation',
rfurls['help docs'],
)
@add_to_registry
class VIEW3D_OT_RetopoFlow_EnableDebugging(Operator):
bl_idname = "cgcookie.retopoflow_enabledebugging"
bl_label = "RetopoFlow: Enable Debugging"
bl_description = "Enables deep debugging (requires restarting Blender)"
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
@classmethod
def poll(cls, context):
return DeepDebug.can_be_enabled() and not DeepDebug.is_enabled()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
DeepDebug.enable()
show_blender_popup('You must restart Blender to finish enabling deep debugging', title='Restart Blender')
return {'FINISHED'}
@add_to_registry
class VIEW3D_OT_RetopoFlow_DisableDebugging(Operator):
bl_idname = "cgcookie.retopoflow_disabledebugging"
bl_label = "RetopoFlow: Disable Debugging"
bl_description = "Disables deep debugging (requires restarting Blender)"
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
@classmethod
def poll(cls, context):
return DeepDebug.is_enabled()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
DeepDebug.disable()
show_blender_popup('You must restart Blender to finish disabling deep debugging', title='Restart Blender')
return {'FINISHED'}
@add_to_registry
class VIEW3D_OT_RetopoFlow_OpenDebugging(Operator):
bl_idname = "cgcookie.retopoflow_opendebugging"
bl_label = "RetopoFlow: Open Debugging Info"
bl_description = "Opens deep debugging info in a text editor"
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
@classmethod
def poll(cls, context):
path = next((str(p) for p in [DeepDebug.path_debug(), DeepDebug.path_debug_backup()] if p.exists()), None)
return path is not None
# return DeepDebug.is_enabled()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
path = next((str(p) for p in [DeepDebug.path_debug(), DeepDebug.path_debug_backup()] if p.exists()), None)
if not path: return {'CANCELLED'}
def get_debug_textblock():
return next((t for t in bpy.data.texts if t.filepath == path), None)
sys.stdout.flush()
sys.stderr.flush()
t = get_debug_textblock()
if t: bpy.data.texts.remove(t)
bpy.ops.text.open(filepath=path)
t = get_debug_textblock()
show_blender_text(t.name)
return {'FINISHED'}
@add_to_registry
class VIEW3D_OT_RetopoFlow_ClearDebugging(Operator):
bl_idname = "cgcookie.retopoflow_cleardebugging"
bl_label = "RetopoFlow: Clear Debugging Info"
bl_description = "Deletes any deep debugging info"
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
@classmethod
def poll(cls, context):
path = next((str(p) for p in [DeepDebug.path_debug(), DeepDebug.path_debug_backup()] if p.exists()), None)
return path is not None
# return DeepDebug.is_enabled()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
for path in [DeepDebug.path_debug(), DeepDebug.path_debug_backup()]:
if path.exists(): path.unlink()
return {'FINISHED'}
if import_succeeded:
# create operators for viewing RetopoFlow help documents
for (label, filename) in [
('Quick Start Guide', 'quick_start'),
('Welcome Message', 'welcome'),
('Table of Contents', 'table_of_contents'),
('FAQ', 'faq'),
('Keymap Editor', 'keymap_editor'),
('Updater System', 'addon_updater'),
('Warning Details', 'warnings'),
('Debugging', 'debugging')
]:
create_help_builtin_operator(label, filename),
create_help_online_operator(label, filename),
@add_to_registry
class VIEW3D_OT_RetopoFlow_UpdaterSystem(updatersystem.RetopoFlow_OpenUpdaterSystem):
"""Open RetopoFlow Updater System"""
bl_idname = "cgcookie.retopoflow_updater"
bl_label = "Updater"
bl_description = "Open RetopoFlow Updater"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = set()
@add_to_registry
class VIEW3D_OT_RetopoFlow_KeymapEditor(keymapsystem.RetopoFlow_OpenKeymapSystem):
"""Open RetopoFlow Keymap Editor"""
bl_idname = "cgcookie.retopoflow_keymapeditor"
bl_label = "Keymap Editor"
bl_description = "Open RetopoFlow Keymap Editor"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = set()
if import_succeeded:
'''
create operators to start RetopoFlow
'''
@add_to_registry
class VIEW3D_OT_RetopoFlow_NewTarget_Cursor(Operator):
"""Create new target object+mesh at the 3D Cursor and start RetopoFlow"""
bl_idname = "cgcookie.retopoflow_newtarget_cursor"
bl_label = "RF: New target at Cursor"
bl_description = "A suite of retopology tools for Blender through a unified retopology mode.\nCreate new target mesh based on the cursor and start RetopoFlow"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
@classmethod
def poll(cls, context):
if not context.region or context.region.type != 'WINDOW': return False
if not context.space_data or context.space_data.type != 'VIEW_3D': return False
# check we are not in mesh editmode
if context.mode == 'EDIT_MESH': return False
# make sure we have source meshes
if not retopoflow.RetopoFlow.get_sources(): return False
# all seems good!
return True
def invoke(self, context, event):
retopoflow.RetopoFlow.create_new_target(context)
return bpy.ops.cgcookie.retopoflow('INVOKE_DEFAULT')
@add_to_registry
class VIEW3D_OT_RetopoFlow_NewTarget_Active(Operator):
"""Create new target object+mesh at the active source and start RetopoFlow"""
bl_idname = "cgcookie.retopoflow_newtarget_active"
bl_label = "RF: New target at Active"
bl_description = "A suite of retopology tools for Blender through a unified retopology mode.\nCreate new target mesh based on the active source and start RetopoFlow"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
@classmethod
def poll(cls, context):
if not context.region or context.region.type != 'WINDOW': return False
if not context.space_data or context.space_data.type != 'VIEW_3D': return False
# check we are not in mesh editmode
if context.mode == 'EDIT_MESH': return False
# make sure we have source meshes
if not retopoflow.RetopoFlow.get_sources(): return False
o = get_active_object()
if not o: return False
if not retopoflow.RetopoFlow.is_valid_source(o, test_poly_count=False): return False
# all seems good!
return True
def invoke(self, context, event):
o = get_active_object()
retopoflow.RetopoFlow.create_new_target(context, matrix_world=o.matrix_world)
return bpy.ops.cgcookie.retopoflow('INVOKE_DEFAULT')
@add_to_registry
class VIEW3D_OT_RetopoFlow_Continue_Active(Operator):
"""Continue with active target object+mesh and start RetopoFlow"""
bl_idname = "cgcookie.retopoflow_continue_active"
bl_label = "RF: Continue with Active Target"
bl_description = "A suite of retopology tools for Blender through a unified retopology mode.\nContinue editing with active target"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
@classmethod
def poll(cls, context):
if not context.region or context.region.type != 'WINDOW': return False
if not context.space_data or context.space_data.type != 'VIEW_3D': return False
# check we are not in mesh editmode
if context.mode != 'OBJECT': return False
# make sure we have source meshes
if not retopoflow.RetopoFlow.get_sources(ignore_active=True): return False
o = get_active_object()
if not o: return False
if not retopoflow.RetopoFlow.is_valid_target(o, ignore_edit_mode=True): return False
# all seems good!
return True
def invoke(self, context, event):
bpy.ops.object.mode_set(mode='EDIT')
# o = get_active_object()
# retopoflow.RetopoFlow.create_new_target(context, matrix_world=o.matrix_world)
return bpy.ops.cgcookie.retopoflow('INVOKE_DEFAULT')
@add_to_registry
class VIEW3D_OT_RetopoFlow_LastTool(retopoflow.RetopoFlow):
"""Start RetopoFlow"""
bl_idname = "cgcookie.retopoflow"
bl_label = "Start RetopoFlow"
bl_description = "A suite of retopology tools for Blender through a unified retopology mode.\nStart with last used tool"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
@add_to_registry
class VIEW3D_OT_RetopoFlow_Warnings(retopoflow.RetopoFlow):
"""Start RetopoFlow"""
bl_idname = "cgcookie.retopoflow_warnings"
bl_label = "Start RetopoFlow (with warnings)"
bl_description = "\nWARNINGS were detected!\n\nA suite of retopology tools for Blender through a unified retopology mode.\nStart with last used tool"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
def VIEW3D_OT_RetopoFlow_Tool_Factory(rftool):
name = rftool.name
description = rftool.description
class VIEW3D_OT_RetopoFlow_Tool(retopoflow.RetopoFlow):
"""Start RetopoFlow with a specific tool"""
bl_idname = f'cgcookie.retopoflow_{name.lower()}'
bl_label = f'RF: {name}'
bl_description = f'A suite of retopology tools for Blender through a unified retopology mode.\nStart with {name}: {description}'
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}
rf_starting_tool = name
icon_id = rftool.icon_id
# just in case: remove spaces, so that class name is proper
VIEW3D_OT_RetopoFlow_Tool.__name__ = f'VIEW3D_OT_RetopoFlow_{name.replace(" ", "")}'
return VIEW3D_OT_RetopoFlow_Tool
RF_tool_classes = [
VIEW3D_OT_RetopoFlow_Tool_Factory(rftool)
for rftool in RFTool.registry
]
add_to_registry(RF_tool_classes)
if import_succeeded:
'''
create operator for recovering auto save
'''
@add_to_registry
class VIEW3D_OT_RetopoFlow_RecoverOpen(Operator):
bl_idname = 'cgcookie.retopoflow_recover_open'
bl_label = 'Recover: Open Last Auto Save'
bl_description = 'Recover by opening last file automatically saved by RetopoFlow'
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
rf_icon = 'rf_recover_icon'
@classmethod
def poll(cls, context):
return retopoflow.RetopoFlow.has_auto_save()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
retopoflow.RetopoFlow.recover_auto_save()
return {'FINISHED'}
@add_to_registry
class VIEW3D_OT_RetopoFlow_RecoverFolder(Operator):
bl_idname = 'cgcookie.retopoflow_recover_folder'
bl_label = 'Recover: Open Folder With Last Auto Save'
bl_description = 'Open folder containing last file automatically saved by RetopoFlow'
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
rf_icon = 'rf_recover_icon'
# FILE_FOLDER
@classmethod
def poll(cls, context):
return retopoflow.RetopoFlow.has_auto_save()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
filename = retopoflow.RetopoFlow.get_auto_save_filename()
bpy.ops.wm.path_open(filepath=os.path.dirname(filename))
return {'FINISHED'}
@add_to_registry
class VIEW3D_OT_RetopoFlow_RecoverDelete(Operator):
bl_idname = 'cgcookie.retopoflow_recover_delete'
bl_label = 'Permanently Delete Last Auto Save'
bl_description = 'Delete last file automatically saved by RetopoFlow'
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
rf_icon = 'rf_recover_icon'
@classmethod
def poll(cls, context):
return retopoflow.RetopoFlow.has_auto_save()
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
# return self.execute(context)
def execute(self, context):
retopoflow.RetopoFlow.delete_auto_save()
return {'FINISHED'}
@add_to_registry
class VIEW3D_OT_RetopoFlow_RecoverRevert(Operator):
bl_idname = 'cgcookie.retopoflow_recover_finish'
bl_label = 'Recover: Finish Auto Save Recovery'
bl_description = 'Finish recovering open file'
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_options = set()
rf_icon = 'rf_recover_icon'
@classmethod
def poll(cls, context):
return retopoflow.RetopoFlow.can_recover()
def invoke(self, context, event):
return self.execute(context)
def execute(self, context):
retopoflow.RetopoFlow.recovery_revert()
return {'FINISHED'}
if import_succeeded:
'''
create panel for showing tools in Blender
'''
# some common checker fns
def has_sources(context):
return retopoflow.RetopoFlow.has_valid_source()
def is_editing_target(context):
obj = context.active_object
mode_string = context.mode
edit_object = context.edit_object
gp_edit = obj and obj.mode in {'EDIT_GPENCIL', 'PAINT_GPENCIL', 'SCULPT_GPENCIL', 'WEIGHT_GPENCIL'}
return not gp_edit and edit_object and mode_string == 'EDIT_MESH'
def are_sources_too_big(context):
# take a look at https://github.com/CoDEmanX/blend_stats/blob/master/blend_stats.py#L98
total = 0
for src in retopoflow.RetopoFlow.get_sources():
total += len(src.data.polygons)
m = convert_numstr_num(options['warning max sources'])
return total > m
def is_target_too_big(context):
# take a look at https://github.com/CoDEmanX/blend_stats/blob/master/blend_stats.py#L98
tar = retopoflow.RetopoFlow.get_target()
if not tar: return False
m = convert_numstr_num(options['warning max target'])
return len(tar.data.polygons) > m
def multiple_3dviews(context):
views = [area for area in context.window.screen.areas if area.type == 'VIEW_3D']
return len(views) > 1
def is_local_view(context):
return context.space_data.local_view is not None
def in_quadview(context):
for area in context.window.screen.areas:
if area.type != 'VIEW_3D': continue
for space in area.spaces:
if space.type != 'VIEW_3D': continue
if bool(space.region_quadviews): return True
return False
def is_addon_folder_valid(context):
# remove .retopoflow
path = re.sub(r'\.retopoflow$', '', __package__)
bad_chars = set(re.sub(r'[a-zA-Z0-9_]', '', path))
if not bad_chars: return True
# print(f'Bad characters found in add-on: {bad_chars}')
return False
rf_label_extra = " (?)"
if configoptions.retopoflow_product['git version']: rf_label_extra = " (git)"
elif not configoptions.retopoflow_product['cgcookie built']: rf_label_extra = " (self)"
elif configoptions.retopoflow_product['github']: rf_label_extra = " (github)"
elif configoptions.retopoflow_product['blender market']: rf_label_extra = ""
@add_to_registry
class VIEW3D_PT_RetopoFlow(Panel):
"""RetopoFlow Blender Menu"""
bl_label = 'RetopoFlow'
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
# bl_ui_units_x = 12
@staticmethod
def draw_popover(self, context):
if retopoflow.RetopoFlow.instance: return
if CookieCutter.is_running: return
if context.mode == 'EDIT_MESH' or context.mode == 'OBJECT':
self.layout.separator()
if is_editing_target(context):
if VIEW3D_PT_RetopoFlow_Warnings.has_warnings(context):
# self.layout.operator('cgcookie.retopoflow_warnings', text="", icon='ERROR')
pass
else:
self.layout.operator('cgcookie.retopoflow', text="", icon='MOD_DATA_TRANSFER')
if cookiecutter.is_broken:
self.layout.popover('VIEW3D_PT_RetopoFlow', text='RetopoFlow BROKEN')
else:
self.layout.popover('VIEW3D_PT_RetopoFlow')
def draw(self, context):
layout = self.layout
layout.label(text=f'RetopoFlow {configoptions.retopoflow_product["version"]}{rf_label_extra}')
if cookiecutter.is_broken:
layout.label(text=f'BROKEN')
@add_to_registry
class VIEW3D_PT_RetopoFlow_Warnings(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_parent_id = 'VIEW3D_PT_RetopoFlow'
bl_label = 'WARNINGS!'
debug_all_warnings = False
@classmethod
def has_warnings(cls, context):
return any(v for v in cls.get_warnings(context).values())
@classmethod
def get_warnings(cls, context):
minv, maxv = Hive.get_version('blender minimum version'), Hive.get_version('blender maximum version')
sources = retopoflow.RetopoFlow.get_sources()
target = retopoflow.RetopoFlow.get_target()
warnings = {
# install checks
'install: invalid add-on folder': not is_addon_folder_valid(context),
'install: unexpected runtime error occurred': cookiecutter.is_broken,
'install: invalid version': bpy.app.version < minv or (maxv and bpy.app.version > maxv),
# setup checks
'setup: local view': is_local_view(context),
'setup: no sources': not sources,
'setup: source has non-invertible matrix': not all(has_inverse(source.matrix_local) for source in sources),
'setup: source has armature': any(mod.type == 'ARMATURE' and mod.object and mod.show_viewport for source in sources for mod in source.modifiers),
'setup: no target': is_editing_target(context) and not target,
'setup: target has non-invertible matrix': target and not has_inverse(target.matrix_local),
# performance checks
'performance: target too big': is_target_too_big(context),
'performance: source too big': are_sources_too_big(context),
# layout checks
'layout: multiple 3d views': multiple_3dviews(context),
'layout: in quad view': in_quadview(context),
'layout: view is locked to cursor': any(space.lock_cursor for space in context.area.spaces if space.type == 'VIEW_3D'),
'layout: view is locked to object': any(space.lock_object for space in context.area.spaces if space.type == 'VIEW_3D'),
# auto save / unsaved checks
'save: auto save is disabled': not retopoflow.RetopoFlow.get_auto_save_settings(context)['auto save'],
'save: unsaved blender file': not retopoflow.RetopoFlow.get_auto_save_settings(context)['saved'],
'save: can recover auto save': retopoflow.RetopoFlow.can_recover(), # user directly opened an auto save file
'save: has auto save': retopoflow.RetopoFlow.has_auto_save(), # auto save file detected
}
return warnings if not cls.debug_all_warnings else { k: True for k in warnings }
@classmethod
def poll(cls, context):
return cls.has_warnings(context)
def draw(self, context):
layout = self.layout
class WarningSection:
_boxes = {}
''' creates exactly one warning subbox per label _only_ when needed '''
def __init__(self, label):
self._label = label
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def subbox(self):
if self._label not in WarningSection._boxes:
box = layout.box().column(align=True)
box.label(text=self._label, icon='ERROR')
WarningSection._boxes[self._label] = box
return WarningSection._boxes[self._label]
def label(self, *args, **kwargs):
box = self.subbox()
box.label(*args, **kwargs)
return box
warnings = self.get_warnings(context)
with WarningSection('Installation') as section:
if warnings['install: invalid add-on folder']:
section.label(text=f'Invalid add-on folder name', icon='DOT')
if warnings['install: unexpected runtime error occurred']:
section.label(text=f'Unexpected runtime error', icon='DOT')
if warnings['install: invalid version']:
box = section.subbox()
def neatver(v): return f'{v[0]}.{v[1]}'
box.label(text=f'Incorrect versions', icon='DOT')
tab = box.row(align=True)
tab.label(icon='BLANK1')
minv, maxv = Hive.get_version('blender minimum version'), Hive.get_version('blender maximum version')
if not maxv:
tab.label(text=f'Require Blender {neatver(minv)}+', icon='BLENDER')
else:
tab.label(text=f'Require Blender {neatver(minv)}--{neatver(maxv)}', icon='BLENDER')
with WarningSection('Setup Issue') as section:
if warnings['setup: local view']:
section.label(text=f'Currently in local view', icon='DOT')
if warnings['setup: no sources']:
section.label(text=f'No sources detected', icon='DOT')
if warnings['setup: source has non-invertible matrix']:
section.label(text=f'A source has non-invertible matrix', icon='DOT')
if warnings['setup: source has armature']:
section.label(text=f'A source has an armature', icon='DOT')
if warnings['setup: no target']:
section.label(text=f'No target detected', icon='DOT')
if warnings['setup: target has non-invertible matrix']:
section.label(text=f'Target has non-invertible matrix', icon='DOT')
with WarningSection('Performance Issue') as section:
if warnings['performance: target too big']:
section.label(text=f'Target is too large (>{options["warning max target"]})', icon='DOT')
if warnings['performance: source too big']:
section.label(text=f'Sources are too large (>{options["warning max sources"]})', icon='DOT')
with WarningSection('Layout Issue') as section:
if warnings['layout: multiple 3d views']:
section.label(text='Multiple 3D Views', icon='DOT')
if warnings['layout: in quad view']:
section.label(text='Quad View will be disabled', icon='DOT')
if warnings['layout: view is locked to cursor']:
section.label(text='View is locked to cursor', icon='DOT')
if warnings['layout: view is locked to object']:
section.label(text='View is locked to object', icon='DOT')
with WarningSection('Auto Save / Save') as section:
if warnings['save: auto save is disabled']:
section.label(text='Auto Save is disabled', icon='DOT')
if warnings['save: unsaved blender file']:
section.label(text='Unsaved Blender file', icon='DOT')
if warnings['save: can recover auto save']:
box = section.subbox()
box.label(text=f'Auto Save file opened', icon='DOT')
tab = box.row(align=True)
tab.label(icon='BLANK1')
tab.operator('cgcookie.retopoflow_recover_finish', text='Finish Auto Save Recovery', icon='RECOVER_LAST')
if warnings['save: has auto save']:
box = section.subbox()
box.label(text=f'Found RetopoFlow auto save', icon='DOT')
tab = box.row(align=True)
tab.label(icon='BLANK1')
tab.label(text=bpy.path.basename(retopoflow.RetopoFlow.get_auto_save_filename()))
tab = box.row(align=True)
tab.label(icon='BLANK1')
col = tab.column(align=True)
col.operator('cgcookie.retopoflow_recover_open', text='Open', icon='RECOVER_LAST')
col.operator('cgcookie.retopoflow_recover_folder', text='Open Folder', icon='FILE_FOLDER')
col.operator('cgcookie.retopoflow_recover_delete', text='Delete', icon='X')
# show button for more warning details
row = layout.row(align=True)
row.label(text='See details on these warnings')
row.operator('cgcookie.retopoflow_help_warningdetails', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_warningdetails', text='', icon='URL')
@add_to_registry
class VIEW3D_PT_RetopoFlow_EditMesh(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_parent_id = 'VIEW3D_PT_RetopoFlow'
bl_label = 'Continue Editing Target'
@classmethod
def poll(cls, context):
return is_editing_target(context)
def draw(self, context):
layout = self.layout
if False:
# currently editing target, so show RF tools
for c in RF_tool_classes:
layout.operator(c.bl_idname, text=c.rf_starting_tool, icon_value=c.icon_id)
else:
col = layout.column(align=True)
col.operator('cgcookie.retopoflow')
buttons = col.grid_flow(
row_major=True,
columns=int(len(RF_tool_classes) / 2),
even_columns=True, even_rows=True,
align=True,
)
for c in RF_tool_classes:
buttons.operator(c.bl_idname, text='', icon_value=c.icon_id)
@add_to_registry
class VIEW3D_PT_ReteopoFlow_ObjectMode(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_parent_id = 'VIEW3D_PT_RetopoFlow'
bl_label = 'Start RetopoFlow'
@classmethod
def poll(cls, context):
return not is_editing_target(context)
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text='Continue')
row.operator('cgcookie.retopoflow_continue_active', text='Edit Active', icon='MOD_DATA_TRANSFER') # icon='EDITMODE_HLT')
row = layout.row(align=True)
row.label(text='New')
row.operator('cgcookie.retopoflow_newtarget_cursor', text='Cursor', icon='ADD')
row.operator('cgcookie.retopoflow_newtarget_active', text='Active', icon='ADD')
expand_help_op = create_toggle_operator(
'expand_help',
'Expand Help and Support',
'Expand Help and Support Panel',
BoundBool('''options['expand help panel']'''),
)
@add_to_registry
class VIEW3D_PT_RetopoFlow_HelpAndSupport(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_parent_id = 'VIEW3D_PT_RetopoFlow'
bl_label = 'Help and Support'
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text='Expand...')
row.operator(expand_help_op.bl_idname, text='', icon='DOWNARROW_HLT', depress=options['expand help panel'])
if not options['expand help panel']: return
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.label(text='Quick Start Guide')
row.operator('cgcookie.retopoflow_help_quickstartguide', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_quickstartguide', text='', icon='URL')
row = col.row(align=True)
row.label(text='Welcome Message')
row.operator('cgcookie.retopoflow_help_welcomemessage', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_welcomemessage', text='', icon='URL')
row = col.row(align=True)
row.label(text='Table of Contents')
row.operator('cgcookie.retopoflow_help_tableofcontents', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_tableofcontents', text='', icon='URL')
row = col.row(align=True)
row.label(text='FAQ')
row.operator('cgcookie.retopoflow_help_faq', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_faq', text='', icon='URL')
# col.separator()
# col.operator('cgcookie.retopoflow_web_online_main', icon='HELP')
col.separator()
col.operator('cgcookie.retopoflow_web_blendermarket', icon_value=BlenderIcon.icon_id('blendermarket.png')) # icon='URL'
expand_advanced_op = create_toggle_operator(
'expand_advanced',
'Expand Advanced',
'Expand Advanced RetopoFlow Panel',
BoundBool('''options['expand advanced panel']'''),
)
@add_to_registry
class VIEW3D_PT_RetopoFlow_Advanced(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
bl_parent_id = 'VIEW3D_PT_RetopoFlow'
bl_label = 'Advanced'
def draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text='Expand...')
row.operator(expand_advanced_op.bl_idname, text='', icon='DOWNARROW_HLT', depress=options['expand advanced panel'])
if not options['expand advanced panel']: return
box = layout.box()
# KEYMAP EDITOR
row = box.row(align=True)
row.label(text='Keymap Editor')
row.operator('cgcookie.retopoflow_keymapeditor', text='', icon='PREFERENCES')
row.operator('cgcookie.retopoflow_help_keymapeditor', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_keymapeditor', text='', icon='URL')
# DEEP DEBUGGER
if DeepDebug.can_be_enabled():
col = box.column()
row = col.row(align=True)
row.label(text='Deep Debugging')
if DeepDebug.is_enabled():
row.operator('cgcookie.retopoflow_disabledebugging', text='', icon='CHECKBOX_HLT') #'X')
else:
row.operator('cgcookie.retopoflow_enabledebugging', text='', icon='CHECKBOX_DEHLT') #'X')
row.operator('cgcookie.retopoflow_help_debugging', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_debugging', text='', icon='URL')
if DeepDebug.needs_restart():
col.label(text='Restart Blender to finish', icon='BLENDER')
elif DeepDebug.is_enabled():
row = col.row(align=True)
row.label(text='', icon='DOT')
row.operator('cgcookie.retopoflow_opendebugging', text='Open', icon='TEXT')
elif DeepDebug.path_debug_backup().exists():
row = col.row(align=True)
row.label(text='', icon='DOT')
row.operator('cgcookie.retopoflow_opendebugging', text='Open', icon='TEXT')
row.operator('cgcookie.retopoflow_cleardebugging', text='Clear', icon='X')
# ADDON UPDATER
col = box.column(align=True)
row = col.row(align=True)
row.label(text='Updater')
if configoptions.retopoflow_product['git version']:
col.label(text='Use Git to Pull latest updates', icon='DOT')
else:
row.operator('cgcookie.retopoflow_updater_check_now', text='', icon='FILE_REFRESH')
row.operator('cgcookie.retopoflow_updater_update_now', text='', icon="IMPORT")
row.operator('cgcookie.retopoflow_updater', text='', icon='SETTINGS')
row.operator('cgcookie.retopoflow_help_updatersystem', text='', icon='HELP')
row.operator('cgcookie.retopoflow_online_updatersystem', text='', icon='URL')
# @add_to_registry
# class VIEW3D_PT_RetopoFlow_AutoSave(Panel):
# bl_space_type = 'VIEW_3D'
# bl_region_type = 'HEADER'
# bl_parent_id = 'VIEW3D_PT_RetopoFlow'
# bl_label = 'Auto Save'
# def draw(self, context):
# layout = self.layout
# layout.operator(
# 'cgcookie.retopoflow_recover_open',
# text='Open Last Auto Save',
# icon='RECOVER_LAST',
# )
# # if retopoflow.RetopoFlow.has_backup():
# # box.label(text=options['last auto save path'])
# @add_to_registry
# class VIEW3D_PT_RetopoFlow_Updater(Panel):
# bl_space_type = 'VIEW_3D'
# bl_region_type = 'HEADER'
# bl_parent_id = 'VIEW3D_PT_RetopoFlow'
# bl_label = 'Updater'
# def draw(self, context):
# layout = self.layout
# if configoptions.retopoflow_product['git version']:
# box = layout.box().column(align=True)
# box.label(text='RetopoFlow under Git control') #, icon='DOT')
# box.label(text='Use Git to Pull latest updates') #, icon='DOT')
# # col.operator('cgcookie.retopoflow_updater', text='Updater System', icon='SETTINGS')
# else:
# col = layout.column(align=True)
# col.operator('cgcookie.retopoflow_updater_check_now', text='Check for updates', icon='FILE_REFRESH')
# col.operator('cgcookie.retopoflow_updater_update_now', text='Update now', icon="IMPORT")
# col.separator()
# row = col.row(align=True)
# row.operator('cgcookie.retopoflow_updater', text='Updater System', icon='SETTINGS')
# row.operator('cgcookie.retopoflow_help_updatersystem', text='', icon='HELP')
# row.operator('cgcookie.retopoflow_online_updatersystem', text='', icon='URL')
if not import_succeeded:
'''
importing failed. show this to the user!
'''
from .addon_common.common.utils import normalize_triplequote
@add_to_registry
class VIEW3D_PT_RetopoFlow(Panel):
"""RetopoFlow Blender Menu"""
bl_label = "RetopoFlow (broken)"
bl_space_type = 'VIEW_3D'
bl_region_type = 'HEADER'
@staticmethod
def draw_popover(self, context):
self.layout.popover('VIEW3D_PT_RetopoFlow')
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text='RetopoFlow cannot start.', icon='ERROR')
box = layout.box()
tw_p = textwrap.TextWrapper(width=36)
tw_ul = textwrap.TextWrapper(width=30)
report_lines = normalize_triplequote('''
This is likely due to an incorrect installation of the add-on.
Please try restarting Blender.
If that does not work, please try:
- remove RetopoFlow from Blender,
- restart Blender,
- download the latest version from the Blender Market, then
- install RetopoFlow in Blender.
If you continue to see this error, contact us through the Blender Market Inbox, and we will work to get it fixed!
''')
for paragraph in report_lines.split('\n\n'):
lines = paragraph.split('\n')
icons = ('NONE', 'NONE')
tw = tw_p
if lines[0].startswith('- '):
nlines = []
for line in lines:
line = line.strip()
if not line.startswith('- '):
nlines[-1] += f' {line}'
else:
nlines += [line[2:].strip()]
lines = nlines
icons = ('DOT', 'BLANK1')
tw = tw_ul
col = box.column(align=True)
for line in lines:
for i, l in enumerate(tw.wrap(text=line)):
col.label(text=l, icon=icons[0 if i==0 else 1])
box = layout.box()
box.operator('cgcookie.retopoflow_web_blendermarket', icon='URL')
def register(bl_info):
for cls in RF_classes: bpy.utils.register_class(cls)
if import_succeeded: updater.register(bl_info)
bpy.types.VIEW3D_MT_editor_menus.append(VIEW3D_PT_RetopoFlow.draw_popover)
def unregister(bl_info):
if import_succeeded: ImagePreloader.quit()
bpy.types.VIEW3D_MT_editor_menus.remove(VIEW3D_PT_RetopoFlow.draw_popover)
if import_succeeded: updater.unregister()
for cls in reversed(RF_classes): bpy.utils.unregister_class(cls)