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

1031 lines
44 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'''
Copyright (C) 2023 CG Cookie
http://cgcookie.com
hello@cgcookie.com
Created by Jonathan Denning, Jonathan Williamson
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 copy
import json
import time
import shelve
import platform
import tempfile
from datetime import datetime
from contextlib import contextmanager
from collections.abc import Iterable
import bpy
from ..addon_common.common import gpustate
from ..addon_common.common.blender import get_path_from_addon_root
from ..addon_common.common.boundvar import BoundBool, BoundInt, BoundFloat, BoundString
from ..addon_common.common.debug import Debugger, dprint
from ..addon_common.common.decorators import run
from ..addon_common.common.drawing import Drawing
from ..addon_common.common.logger import Logger
from ..addon_common.common.maths import Color
from ..addon_common.common.profiler import Profiler
from ..addon_common.common.ui_document import UI_Document
from ..addon_common.common.utils import normalize_triplequote
from ..addon_common.hive.hive import Hive
###########################################
# RetopoFlow Configurations
# important: update Makefile and root/__init__.py, too!
# TODO: make Makefile pull version from here or some other file?
# TODO: make __init__.py pull version from here or some other file?
retopoflow_product = {
# all the values are filled in by code below...
'hive': None, # based on `hive.json` contents
'version': None, # based on hive above
'version tuple': None, # based on hive above
'git version': None, # looks for `.git` folder
'cgcookie built': None, # looks for `.cgcookie` file
'github': None, # depends on `.cgcookie` contents
'blender market': None, # depends on `.cgcookie` contents
}
retopoflow_urls = {
'blender market': 'https://blendermarket.com/products/retopoflow',
'github issues': 'https://github.com/CGCookie/retopoflow/issues',
'new github issue': 'https://github.com/CGCookie/retopoflow/issues/new',
'help docs': 'https://docs.retopoflow.com',
'help doc': lambda fn: f'https://docs.retopoflow.com/{fn}.html',
'tip': 'https://paypal.me/gfxcoder/', # TODO: REPLACE WITH COOKIE-RELATED ACCOUNT!! :)
# note: can add number to URL to set a default, ex: https://paypal.me/retopoflow/5
}
# files created by retopoflow, all located at the root of RetopoFlow add-on
retopoflow_files = {
'options filename': 'RetopoFlow_options.json',
'screenshot filename': 'RetopoFlow_screenshot.png',
'instrument filename': 'RetopoFlow_instrument.txt',
'log filename': 'RetopoFlow_log.txt',
# 'debug filename': 'RetopoFlow_debug.txt', # hard-coded in __init__.py
'backup filename': 'RetopoFlow_backup.blend', # if working on unsaved blend file
'profiler filename': 'RetopoFlow_profiler.txt',
'keymaps filename': 'RetopoFlow_keymaps.json',
}
# objects / blender data created by retopoflow
retopoflow_datablocks = {
'rotate object': 'RetopoFlow_Rotate', # name of rotate object used for setting view
'blender state': 'RetopoFlow Session Data', # name of text block that contains data about blender state
}
# the following enables / disables profiler code, overriding the options['profiler']
# TODO: make this False before shipping!
# TODO: make Makefile check this value!
retopoflow_profiler = False
# convert version string to tuple
@run
def set_version_info():
global retopoflow_product
release_short = {
'alpha': 'α',
'beta': 'β',
'official': '',
}
retopoflow_product['hive'] = Hive()
version = retopoflow_product['hive']['version']
release = retopoflow_product['hive']['release']
retopoflow_product['version'] = f'{version}{release_short[release]}'
retopoflow_product['version tuple'] = tuple(int(i) for i in version.split('.'))
@run
def set_git_info():
global retopoflow_product
try:
path_git = get_path_from_addon_root('.git')
git_head_path = os.path.join(path_git, 'HEAD')
if not os.path.exists(git_head_path): return
git_ref_path = open(git_head_path).read().split()[1]
assert git_ref_path.startswith('refs/heads/')
git_ref_path = git_ref_path[len('refs/heads/'):]
git_ref_fullpath = os.path.join(path_git, 'logs', 'refs', 'heads', git_ref_path)
if not os.path.exists(git_ref_fullpath): return
log = open(git_ref_fullpath).read().splitlines()
commit = log[-1].split()[1]
retopoflow_product['git version'] = f'{git_ref_path} {commit}'
except Exception as e:
print('An exception occurred while checking git info')
print(e)
@run
def set_build_info():
global retopoflow_product
try:
cgcookie_built_path = get_path_from_addon_root('.cgcookie')
cgcookie_built = (
open(cgcookie_built_path, 'rt').read()
if os.path.exists(cgcookie_built_path)
else ''
)
retopoflow_product['cgcookie built'] = cgcookie_built != ''
retopoflow_product['github'] = 'GitHub' in cgcookie_built
retopoflow_product['blender market'] = 'Blender Market' in cgcookie_built
except Exception as e:
print('An exception occurred while getting build info')
print(e)
# @run(git=None, cgcookie_built=True, blendermarket=True)
def override_version_settings(**kwargs):
# use kwargs defined below to override product info for testing purposes
global retopoflow_product
if 'git' in kwargs: retopoflow_product['git version'] = kwargs['git']
if 'cgcookie_built' in kwargs: retopoflow_product['cgcookie built'] = kwargs['cgcookie_built']
if 'github' in kwargs: retopoflow_product['github'] = kwargs['github']
if 'blendermarket' in kwargs: retopoflow_product['blender market'] = kwargs['blendermarket']
print(f'RetopoFlow git: {retopoflow_product["git version"]}')
###########################################
# Get system info
build_platform = bpy.app.build_platform.decode('utf-8')
(
platform_system,
platform_node,
platform_release,
platform_version,
platform_machine,
platform_processor,
) = platform.uname()
gpu_info = gpustate.gpu_info()
class Options:
default_options = { # all the default settings for unset or reset
'rf version': None, # if versions differ, flush stored options
'version update': False,
# WARNING THRESHOLDS
'warning max target': '20k', # can specify as 20000, 20_000, '20k', '20000', see convert_numstr_num() in addon_common.common.maths
'warning max sources': '1m',
'warning normal check': True,
'show experimental': False, # should show experimental tools?
'preload help images': False,
'async mesh loading': True, # True: load source meshes asynchronously
'async image loading': True,
# AUTO SAVE
'last auto save path': '', # file path of last auto save (used for recover)
# STARTUP
'check auto save': True, # give warning about disabled auto save at start
'check unsaved': True, # give warning about unsaved blend file at start
'welcome': True, # show welcome message?
'starting tool': 'PolyPen', # which tool to start with when clicking diamond
# BLENDER PANEL
'expand advanced panel': False,
'expand help panel': True,
# BLENDER UI
'hide panels no overlap': True, # hide panels even when region overlap is disabled
'hide header panel': True, # hide header panel (where RF menu shows)
# DIALOGS
'show main window': True, # True: show main window; False: show tiny
'show options window': True, # show options window
'show geometry window': True, # show geometry counts window
'tools autohide': True, # should tool's options auto-hide/-show when switching tools?
# DEBUG, PROFILE, INSTRUMENT SETTINGS
'profiler': False, # enable profiler?
'instrument': False, # enable instrumentation?
'debug level': 0, # debug level, 0--5 (for printing to console). 0=no print; 5=print all
'debug actions': False, # print actions (except MOUSEMOVE) to console
# UNDO SETTINGS
'undo change tool': False, # should undo change the selected tool?
'undo depth': 100, # size of undo stack
'select dist': 10, # pixels away to select
'action dist': 20, # pixels away to allow action
'move dist': 10, # pixels away until mousedrag grabs
'remove doubles dist': 0.001,
'push and snap distance': 0.1, # distance to push vertices out along normal before snapping back to source surface
# VISIBILITY TEST TUNING PARAMETERS
'visible bbox factor': 0.01, # rf_sources.visibility_preset_*
'visible dist offset': 0.1, # rf_sources.visibility_preset_*
'selection occlusion test': True, # True: do not select occluded geometry
'selection backface test': True, # True: do not select geometry that is facing away
'accel recompute delay': 0.125, # seconds to wait to prevent recomputing accel structs too quickly after navigation
'view change delay': 0.250, # seconds to wait before calling view change callbacks (> accel recompute delay)
'target change delay': 0.010, # seconds to wait before calling target change callbacks
'move rotate object if no selection': True,
####################################################
# VISUALIZATION SETTINGS
# UX SETTINGS
'show tooltips': True,
'tooltip delay': 0.75,
'escape to quit': False, # True:ESC is action for quitting
'confirm tab quit': True, # True:pressing TAB to quit is confirmed (prevents accidentally leaving when pie menu was intended)
'hide cursor on tweak': True, # True: cursor is hidden when tweaking geometry
'hide overlays': True, # hide overlays (wireframe, grid, axes, etc.)
'override shading': 'dark', # light, dark, or off. Sets optimal values for backface culling, xray, shadows, cavity, outline, and matcap
'shading view': 'SOLID',
'shading light': 'MATCAP',
'shading matcap light': 'retopoflow_light.exr', # found under matcaps/
'shading matcap dark': 'retopoflow_dark.exr', # found under matcaps/
'shading colortype': 'SINGLE',
'shading color light': [1.0, 1.0, 1.0],
'shading color dark': [1.0, 1.0, 1.0],
'shading backface culling': True,
'shading xray': False,
'shading shadows': False,
'shading cavity': False,
'shading outline': False,
'color theme': 'Green',
'symmetry view': 'Edge',
'symmetry effect': 0.5,
'symmetry mirror input': False, # True: input is mirrored to correct side of symmetry. False: input is clamped
'normal offset multiplier': 1.0,
'constrain offset': False, # when False, symmetry viz looks good. do we still need this???
'ui scale': 1.0,
'clip auto adjust': True, # True: clip settings are automatically adjusted based on view distance and source bbox
'clip auto start mult': 0.0010, # factor for clip_start
'clip auto start min': 0.0010, # absolute minimum for clip_start
'clip auto end mult': 100.00, # factor for clip_end
'clip auto end max': 500.0, # absolute maximum for clip_end
'clip override': True, # True: override with below values; False: scale by unit scale factor
'clip start override': 0.0500,
'clip end override': 200.00,
# TARGET VISUALIZATION SETTINGS
# 'pin enabled' and 'pin seam' are in TARGET PINNING SETTINGS
'warn non-manifold': True, # visualize non-manifold warnings
'show pinned': True, # visualize pinned geometry
'show seam': True,
'target vert size': 4.0,
'target edge size': 1.0,
'target alpha': 1.00,
'target hidden alpha': 0.2,
'target alpha backface': 0.1,
'target cull backfaces': False,
'target alpha poly': 0.65,
'target alpha poly selected': 0.75,
'target alpha poly warning': 0.25,
'target alpha poly pinned': 0.75,
'target alpha poly seam': 0.75,
'target alpha poly mirror': 0.25,
'target alpha poly mirror selected': 0.25,
'target alpha poly mirror warning': 0.15,
'target alpha poly mirror pinned': 0.25,
'target alpha poly mirror seam': 0.25,
'target alpha line': 0.10,
'target alpha line selected': 1.00,
'target alpha line warning': 0.75,
'target alpha line pinned': 0.75,
'target alpha line seam': 0.75,
'target alpha line mirror': 0.10,
'target alpha line mirror selected': 0.50,
'target alpha line mirror warning': 0.15,
'target alpha line mirror pinned': 0.15,
'target alpha line mirror seam': 0.15,
'target alpha point': 0.25,
'target alpha point selected': 1.00,
'target alpha point warning': 0.75,
'target alpha point pinned': 0.95,
'target alpha point seam': 0.95,
'target alpha point mirror': 0.00,
'target alpha point mirror selected': 0.50,
'target alpha point mirror warning': 0.15,
'target alpha point mirror pinned': 0.15,
'target alpha point mirror seam': 0.15,
'target alpha point highlight': 1.00,
'target alpha mirror': 1.00,
# TARGET PINNING SETTINGS
# 'show pinned' and 'show seam' are in TARGET VISUALIZATION SETTINGS
'pin enabled': True,
'pin seam': True,
# ADDON UPDATER SETTINGS
'updater auto check update': True,
'updater interval months': 0,
'updater interval days': 1,
'updater interval hours': 0,
'updater interval minutes': 0,
#######################################
# GENERAL SETTINGS
'smooth edge flow iterations': 10,
'automerge': True,
'merge dist': 10, # pixels away to merge
#######################################
# TOOL SETTINGS
'contours count': 16,
'contours uniform': True, # should new cuts be made uniformly about circumference?
'contours non-manifold check': True,
'polystrips radius': 40,
'polystrips below alpha': 0.6,
'polystrips scale falloff': 0.93,
'polystrips draw curve': False,
'polystrips max strips': 10, # PS will not show handles if knot count is above max
'polystrips arrows': False,
'polystrips handle inner size': 15,
'polystrips handle outer size': 20,
'polystrips handle border': 3,
'strokes radius': 40,
'strokes below alpha': 0.6,
'strokes span insert mode': 'Brush Size',
'strokes span count': 1,
'strokes snap stroke': True, # should stroke snap to unselected geometry?
'strokes snap dist': 10, # pixels away to snap
'strokes automerge': True,
'strokes merge dist': 10, # pixels away to merge
'knife automerge': True,
'knife merge dist': 10, # pixels away to merge
'knife snap dist': 5, # pixels away to snap
'polypen automerge': True,
'polypen merge dist': 10, # pixels away to merge
'polypen insert dist': 15, # pixels away for inserting new vertex in existing geo
'polypen insert mode': 'Tri/Quad',
'brush min alpha': 0.10,
'brush max alpha': 0.70,
'relax radius': 50,
'relax falloff': 1.5,
'relax strength': 0.5,
'relax below alpha': 0.6,
'relax algorithm': '3D',
'relax mask boundary': 'include',
'relax mask symmetry': 'maintain',
'relax mask occluded': 'exclude',
'relax mask selected': 'all',
'relax steps': 2,
'relax force multiplier': 1.5,
'relax edge length': True,
'relax face radius': True,
'relax face sides': False,
'relax face angles': True,
'relax correct flipped faces': False,
'relax straight edges': True,
'relax preset 1 name': 'Preset 1',
'relax preset 1 radius': 50,
'relax preset 1 falloff': 1.5,
'relax preset 1 strength': 0.5,
'relax preset 2 name': 'Preset 2',
'relax preset 2 radius': 50,
'relax preset 2 falloff': 1.5,
'relax preset 2 strength': 0.5,
'relax preset 3 name': 'Preset 3',
'relax preset 3 radius': 50,
'relax preset 3 falloff': 1.5,
'relax preset 3 strength': 0.5,
'relax preset 4 name': 'Preset 4',
'relax preset 4 radius': 50,
'relax preset 4 falloff': 1.5,
'relax preset 4 strength': 0.5,
'tweak mode': 'raycast', # mode to move tweaked vert back to surface of source: snap or raycast
'tweak radius': 50,
'tweak falloff': 1.5,
'tweak strength': 0.5,
'tweak below alpha': 0.6,
'tweak mask boundary': 'include',
'tweak mask symmetry': 'maintain',
'tweak mask occluded': 'exclude',
'tweak mask selected': 'all',
'tweak preset 1 name': 'Preset 1',
'tweak preset 1 radius': 50,
'tweak preset 1 falloff': 1.5,
'tweak preset 1 strength': 0.5,
'tweak preset 2 name': 'Preset 2',
'tweak preset 2 radius': 50,
'tweak preset 2 falloff': 1.5,
'tweak preset 2 strength': 0.5,
'tweak preset 3 name': 'Preset 3',
'tweak preset 3 radius': 50,
'tweak preset 3 falloff': 1.5,
'tweak preset 3 strength': 0.5,
'tweak preset 4 name': 'Preset 4',
'tweak preset 4 radius': 50,
'tweak preset 4 falloff': 1.5,
'tweak preset 4 strength': 0.5,
'patches angle': 120,
'select geometry': 'Verts',
'select merge dist': 10, # pixels away to merge
'select automerge': True,
}
db = None # current options dict
fndb = None # name of file in which to store db (set up in __init__)
is_dirty = False # does the internal db differ from db stored in file? (need writing)
last_change = 0 # when did we last changed an option?
write_delay = 1.0 # seconds to wait before writing db to file
write_error = False # True when we failed to write options to file
def __init__(self):
self._callbacks = []
self._calling = False
if not Options.fndb:
Options.fndb = get_path_from_addon_root(retopoflow_files['options filename'])
# Options.fndb = self.get_path('options filename')
print(f'RetopoFlow options path: {Options.fndb}')
self.read()
self['version update'] = (self['rf version'] != retopoflow_product['version'])
self['rf version'] = retopoflow_product['version']
self.update_external_vars()
def __getitem__(self, key):
return Options.db[key] if key in Options.db else Options.default_options[key]
def __setitem__(self, key, val):
assert key in Options.default_options, f'Attempting to write "{key}":"{val}" to options, but key does not exist'
assert not self._calling, f'Attempting to change option "{key}" to "{val}" while calling callbacks'
if self[key] == val: return
oldval = self[key]
Options.db[key] = val
self.dirty()
self.clean()
def add_callback(self, callback):
self._callbacks += [callback]
def remove_callback(self, callback):
self._callbacks = [cb for cb in self._callbacks if cb != callback]
def clear_callbacks(self):
self._callbacks = []
def call_callbacks(self):
self._calling = True
for callback in self._callbacks: callback()
self._calling = False
def get_path(self, key):
return get_path_from_addon_root(retopoflow_files[key])
def get_path_incremented(self, key):
p = self.get_path(key)
if os.path.exists(p):
i = 0
p0,p1 = os.path.splitext(p)
while os.path.exists('%s.%03d.%s' % (p0, i, p1)): i += 1
p = '%s.%03d.%s' % (p0, i, p1)
return p
def update_external_vars(self):
Debugger.set_error_level(self['debug level'])
Logger.set_log_filename(retopoflow_files['log filename'])
# Profiler.set_profiler_enabled(self['profiler'] and retopoflow_profiler)
Profiler.set_profiler_filename(self.get_path('profiler filename'))
Drawing.set_custom_dpi_mult(self['ui scale'])
UI_Document.show_tooltips = self['show tooltips']
UI_Document.tooltip_delay = self['tooltip delay']
self.call_callbacks()
def dirty(self):
Options.is_dirty = True
Options.last_change = time.time()
self.update_external_vars()
def clean(self, force=False, raise_exception=True, retry=True):
if not Options.is_dirty:
# nothing has changed
return
if not force and time.time() < Options.last_change + Options.write_delay:
# we haven't waited long enough before storing db
if retry: bpy.app.timers.register(self.clean, first_interval=Options.write_delay)
return
# dprint('Writing options:', Options.db)
pass
try:
json.dump(
Options.db,
open(Options.fndb, 'wt'),
indent=2,
sort_keys=True,
)
Options.is_dirty = False
except PermissionError as e:
self.write_error = True
if raise_exception: raise e
except Exception as e:
self.write_error = True
if raise_exception: raise e
def read(self):
Options.db = {}
if os.path.exists(Options.fndb):
try:
Options.db = json.load(open(Options.fndb, 'rt'))
except Exception as e:
print('Exception caught while trying to read options from file')
print(str(e))
# remove options that are not in default options
for k in set(Options.db.keys()) - set(Options.default_options.keys()):
print(f'Deleting key "{k}" from options')
del Options.db[k]
else:
print('No options file')
self.update_external_vars()
Options.is_dirty = False
def keys(self):
return Options.db.keys()
def reset(self, keys=None, version=True):
if keys is None:
keys = list(Options.db.keys())
for key in keys:
if key in Options.db:
del Options.db[key]
if version:
Options.db['rf version'] = retopoflow_product['version']
self.dirty()
self.clean()
def set_default(self, key, val):
# does not dirty nor invoke write!
assert key in Options.default_options, f'Attempting to write "{key}":"{val}" to options, but key does not exist'
if key not in Options.db:
Options.db[key] = val
def set_defaults(self, d_key_vals):
# does not dirty nor invoke write!
for key in d_key_vals:
self.set_default(key, d_key_vals[key])
def getter(self, key, getwrap=None):
if not getwrap: getwrap = lambda v: v
def _getter(): return getwrap(options[key])
return _getter
def setter(self, key, setwrap=None, setcallback=None):
if not setwrap: setwrap = lambda v: v
if not setcallback:
def nop(v): pass
setcallback = nop
def _setter(v):
options[key] = setwrap(v)
setcallback(options[key])
return _setter
def gettersetter(self, key, getwrap=None, setwrap=None, setcallback=None):
return (self.getter(key, getwrap=getwrap), self.setter(key, setwrap=setwrap, setcallback=setcallback))
def get_auto_save_filepath(self, *, suffix=None, emergency=False):
suffix = f'_{suffix}' if suffix else ''
if emergency or not getattr(bpy.data, 'filepath', None):
# not working on a saved .blend file, yet!
path = os.path.expanduser('~')
# path = bpy.context.preferences.filepaths.temporary_directory
# if not path: path = tempfile.gettempdir()
filename = retopoflow_files['backup filename']
else:
fullpath = os.path.abspath(bpy.data.filepath)
path, filename = os.path.split(fullpath)
suffix = f'_RetopoFlow_AutoSave{suffix}'
base, ext = os.path.splitext(filename)
return os.path.join(path, f'{base}{suffix}{ext}')
class Themes:
# fallback color for when specified key is not found
error = Color.from_ints(255, 64, 255, 255)
common = {
'mesh': Color.from_ints(255, 255, 255, 255),
'warning': Color.from_ints(182, 31, 0, 128),
'stroke': Color.from_ints(255, 255, 0, 255),
'highlight': Color.from_ints(255, 255, 25, 255),
'set select': Color.from_ints(255, 255, 255, 192),
'add select': Color.from_ints(128, 255, 128, 192),
'del select': Color.from_ints(255, 128, 128, 192),
# RFTools
'polystrips': Color.from_ints( 0, 100, 25, 150),
'strokes': Color.from_ints( 0, 100, 90, 150),
'tweak': Color.from_ints(229, 137, 26, 255), # Opacity is set by brush strength
'relax': Color.from_ints( 0, 135, 255, 255), # Opacity is set by brush strength
# Target Geometry
'warn': Color((0.43, 0.072, 0.03)), #.from_ints(182, 31, 0),
'seam': Color((0.859, 0.145, 0.071)), #.from_ints(255, 160, 255),
'pin': Color.from_ints(217, 200, 18), #.from_ints(255, 41, 255),
}
themes = {
'Green': {
'select': Color.from_ints( 78, 207, 81),
'new': Color.from_ints( 40, 255, 40),
},
'Blue': {
'select': Color.from_ints( 55, 160, 255),
'new': Color.from_ints( 40, 40, 255),
},
'Orange': {
'select': Color.from_ints(255, 135, 54),
'new': Color.from_ints(255, 128, 64),
},
}
# themes['Blue'] = {
# key: color.rotated_hue((209 - 121) / 360) for (key, color) in themes['Green'].items()
# }
# themes['Orange'] = {
# key: color.rotated_hue((24 - 121) / 360) for (key, color) in themes['Green'].items()
# }
@property
def theme(self):
return self.themes[options['color theme']]
def __getitem__(self, key):
return self.theme.get(key, self.common.get(key, self.error))
class Visualization_Settings:
def __init__(self):
self._last = {}
self.update_settings()
def update_settings(self):
watch = [
'color theme',
'normal offset multiplier',
'constrain offset',
'target vert size',
'target edge size',
*[f'target alpha poly {p}' for p in ['', 'selected', 'warning', 'pinned', 'seam']],
*[f'target alpha poly mirror {p}' for p in ['', 'selected', 'warning', 'pinned', 'seam']],
*[f'target alpha line {p}' for p in ['', 'selected', 'warning', 'pinned', 'seam']],
*[f'target alpha line mirror {p}' for p in ['', 'selected', 'warning', 'pinned', 'seam']],
*[f'target alpha point {p}' for p in ['', 'selected', 'warning', 'pinned', 'seam']],
*[f'target alpha point mirror {p}' for p in ['', 'selected', 'warning', 'pinned', 'seam']],
'target alpha point highlight',
'target alpha mirror',
]
watch = [w.strip() for w in watch] # strip watched properties to remove trailing spaces
if all(getattr(self._last, key, None) == options[key] for key in watch): return
for key in watch: self._last[key] = options[key]
color_mesh = themes['mesh']
color_select = themes['select']
color_warn = themes['warn']
color_pin = themes['pin']
color_seam = themes['seam']
color_hilight = themes['highlight']
normal_offset_multiplier = options['normal offset multiplier']
constrain_offset = options['constrain offset']
vert_size = options['target vert size']
edge_size = options['target edge size']
self._source_settings = {
'poly color': (0.0, 0.0, 0.0, 0.0),
'poly offset': 0.000008,
'poly dotoffset': 1.0,
'line width': 0.0,
'point size': 0.0,
'load edges': False,
'load verts': False,
'no selection': True,
'no warning': True,
'no pinned': True,
'no seam': True,
'no below': True,
'triangles only': True, # source bmeshes are triangles only!
'cull backfaces': True,
'focus mult': 0.01,
'normal offset': 0.0005 * normal_offset_multiplier, # pushes vertices out along normal
'constrain offset': constrain_offset,
}
mirror_alpha_factor = options['target alpha mirror']
self._target_settings = {
'poly color': (*color_mesh[:3], options['target alpha poly']),
'poly color selected': (*color_select[:3], options['target alpha poly selected']),
'poly color warning': (*color_warn[:3], options['target alpha poly warning']),
'poly color pinned': (*color_pin[:3], options['target alpha poly pinned']),
'poly color seam': (*color_seam[:3], options['target alpha poly seam']),
'poly offset': 0.000010,
'poly dotoffset': 1.0,
'poly mirror color': (*color_mesh[:3], options['target alpha poly mirror'] * mirror_alpha_factor),
'poly mirror color selected': (*color_select[:3], options['target alpha poly mirror selected'] * mirror_alpha_factor),
'poly mirror color warning': (*color_warn[:3], options['target alpha poly mirror warning'] * mirror_alpha_factor),
'poly mirror color pinned': (*color_pin[:3], options['target alpha poly mirror pinned'] * mirror_alpha_factor),
'poly mirror color seam': (*color_seam[:3], options['target alpha poly mirror seam'] * mirror_alpha_factor),
'poly mirror offset': 0.000010,
'poly mirror dotoffset': 1.0,
'line color': (*color_mesh[:3], options['target alpha line']),
'line color selected': (*color_select[:3], options['target alpha line selected']),
'line color warning': (*color_warn[:3], options['target alpha line warning']),
'line color pinned': (*color_pin[:3], options['target alpha line pinned']),
'line color seam': (*color_seam[:3], options['target alpha line seam']),
'line width': edge_size,
'line offset': 0.000012,
'line dotoffset': 1.0,
'line mirror color': (*color_mesh[:3], options['target alpha line mirror'] * mirror_alpha_factor),
'line mirror color selected': (*color_select[:3], options['target alpha line mirror selected'] * mirror_alpha_factor),
'line mirror color warning': (*color_warn[:3], options['target alpha line mirror warning'] * mirror_alpha_factor),
'line mirror color pinned': (*color_pin[:3], options['target alpha line mirror pinned'] * mirror_alpha_factor),
'line mirror color seam': (*color_seam[:3], options['target alpha line mirror seam'] * mirror_alpha_factor),
'line mirror width': 1.5,
'line mirror offset': 0.000012,
'line mirror dotoffset': 1.0,
'point color': (*color_mesh[:3], options['target alpha point']),
'point color selected': (*color_select[:3], options['target alpha point selected']),
'point color warning': (*color_warn[:3], options['target alpha point warning']),
'point color pinned': (*color_pin[:3], options['target alpha point pinned']),
'point color seam': (*color_seam[:3], options['target alpha point seam']),
'point color highlight': (*color_hilight[:3],options['target alpha point highlight']),
'point size': vert_size,
'point size highlight': 10.0,
'point offset': 0.000015,
'point dotoffset': 1.0,
'point mirror color': (*color_mesh[:3], options['target alpha point mirror'] * mirror_alpha_factor),
'point mirror color selected': (*color_select[:3], options['target alpha point mirror selected'] * mirror_alpha_factor),
'point mirror color warning': (*color_warn[:3], options['target alpha point mirror warning'] * mirror_alpha_factor),
'point mirror color pinned': (*color_pin[:3], options['target alpha point mirror pinned'] * mirror_alpha_factor),
'point mirror color seam': (*color_seam[:3], options['target alpha point mirror seam'] * mirror_alpha_factor),
'point mirror size': 3.0,
'point mirror offset': 0.000015,
'point mirror dotoffset': 1.0,
'focus mult': 0.0, #1.0,
'normal offset': 0.001 * normal_offset_multiplier, # pushes vertices out along normal
'constrain offset': constrain_offset,
}
def get_source_settings(self):
self.update_settings()
return self._source_settings
def get_target_settings(self):
self.update_settings()
return self._target_settings
def source(self, key):
self.update_settings()
return self._source_settings[key]
def target(self, key):
self.update_settings()
return self._target_settings[key]
def __getitem__(self, key):
return self.target(key)
def __setitem__(self, key, val):
assert key in Options.default_options, 'Attempting to write "%s":"%s" to options, but key does not exist' % (str(key),str(val))
if self[key] == val: return
Options.db[key] = val
self.dirty()
self.clean()
class SessionOptions:
'''
options/settings that are specific to this particular .blend file.
useful for storing current state and restoring in case of failure.
data is stored in bpy.data.texts[textblockname]['data'].
'''
textblockname = retopoflow_datablocks['blender state']
userfriendlytext = normalize_triplequote('''
RetopoFlow customizes several aspects of Blender for optimal retopology
experience by overriding viewport settings, rendering settings, mesh sizes,
and so on. This text block is used to store the previous options and settings
that RetopoFlow overrides when it starts, so that they can be restored when
RetopoFlow quits.
Normally, this text block is never seen. However, if Blender happens to crash or
is closed before RetopoFlow was able to restore the Blender options and settings,
these changes will remain in the last saved .blend file. RetopoFlow will use
this information to restore everything back to normal the next time the .blend
file is opened.
If you see this text block, RetopoFlow has not finished restoring the Blender
settings. In the 3D View, click RetopoFlow > Recover: Finish Auto Save Recovery.
''')
default = {
'retopoflow': {
'version': retopoflow_product['version'],
'timestamp': None, # automatically filled out when getting session data
'target': None, # automatically filled out when starting RF
},
'disabled': False,
'normalize': {
'unit scaling factor': None,
'mesh scaling factor': 1.0,
'view scaling factor': 1.0,
'clip distances': {
'start': None,
'end': None,
},
'view': {
'distance': None,
'location': None,
},
},
'blender': {
# to be filled in by CookieCutter_Blender and RetopoFlow_Normalize
},
}
@classmethod
def _get_data_as_pydata(cls):
if cls.textblockname not in bpy.data.texts: return None
def convert(d):
# print(f'{d=} {type(d)=}')
if type(d) in {bool, int, float, str}:
return d
if hasattr(d, 'keys'):
return { k: convert(d[k]) for k in d.keys() }
# ASSUMING it is a list!
return [ convert(v) for v in d ]
assert False, f'Unknown type: {d} ({type(d)})'
return convert(bpy.data.texts[cls.textblockname]['data'])
@classmethod
@contextmanager
def temp_disable(cls):
if not cls.has_session_data():
yield None
return
cls.set('disabled', True)
yield None
cls.set('disabled', False)
@classmethod
def has_active_session_data(cls):
if not cls.has_session_data(): return False
data = bpy.data.texts[cls.textblockname]['data']
return data['disabled'] if 'disabled' in data else True
@classmethod
def has_session_data(cls):
if cls.textblockname not in bpy.data.texts: return False
return True
@classmethod
def _get_data(cls):
if not cls.has_session_data():
# create text block for storing state
textblock = bpy.data.texts.new(SessionOptions.textblockname)
# set user-friendly message
textblock.from_string(SessionOptions.userfriendlytext)
textblock.cursor_set(0, character=0)
# assignment below will create deep copy of default
textblock['data'] = SessionOptions.default
cls.set('retopoflow', 'timestamp', str(datetime.now()))
#cls.set('retopoflow', 'timestamp', timestamp)
else:
textblock = bpy.data.texts[SessionOptions.textblockname]
return textblock['data']
class Walker:
def __init__(self, *path):
if len(path) == 1 and type(path[0]) is str:
path = [path]
self.__dict__['path'] = path
@property
def path(self):
return self.__dict__['path']
def __truediv__(self, key):
return SessionOptions.Walker(*self.path, key)
def __getattr__(self, key):
return SessionOptions.get(*self.path, key)
def __setattr__(self, key, value):
SessionOptions.set(*self.path, key, val)
return val
@property
def value(self):
return SessionOptions.get(*self.path)
@value.setter
def value(self, val):
SessionOptions.set(*self.path, val)
@classmethod
def __truediv__(cls, key):
return SessionOptions.Walker([key])
@classmethod
@property
def data(cls):
return SessionOptions.Walker()
@classmethod
def get(cls, *keys):
data = cls._get_data()
if len(keys) == 1 and type(keys[0]) is not str:
keys = keys[0]
for key in keys: data = data[key]
return data
@classmethod
def _get_default(cls, *keys):
data = cls.default
if len(keys) == 1 and type(keys[0]) is not str:
keys = keys[0]
for key in keys: data = data[key]
return data
@classmethod
def set(cls, *args):
if len(args) == 1:
# `args` contains a dictionary
dict_keys_vals = args[0]
assert type(dict_keys_vals) is dict, f'SessionOptions.set expects dictionary ({dict_keys_vals=})'
def s(*args):
*path, dkv = args
if type(dkv) is dict and type(cls._get_default(*path)) is dict:
for k,v in dkv.items():
s(*path, k, v)
else:
cls.set(*path, dkv)
s(dict_keys_vals)
else:
# `args` is a list, where all but last are keys into SessionOptions and last is the value to set
keys_then_value = args
assert len(keys_then_value) >= 2, f'SessionOptions.set expects at least 2 arguments ({keys_then_value=})'
*keys, value = keys_then_value
data = cls.get(keys[:-1])
data[keys[-1]] = value
def __getitem__(self, keys):
if type(keys) is str: keys = (keys,)
return self.get(*keys)
def __setitem__(self, keys, value):
if type(keys) is str: keys = (keys,)
return self.set(*keys, value)
@classmethod
def clear(cls):
if not cls.has_session_data(): return
textblock = bpy.data.texts[cls.textblockname]
bpy.data.texts.remove(textblock)
# set all the default values!
options = Options()
themes = Themes()
visualization = Visualization_Settings()
sessionoptions = SessionOptions()