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

251 lines
9.1 KiB
Python

'''
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 re
import copy
import math
import inspect
class IgnoreChange(Exception): pass
class BoundVar:
def __init__(self, value_str, *, on_change=None, frame_depth=1, frames_deep=1, f_globals=None, f_locals=None, callbacks=None, validators=None, disabled=False, pre_wrap=None, post_wrap=None, wrap=None):
assert type(value_str) is str, f'BoundVar: constructor needs value as string, but received {value_str} instead!'
if f_globals is None or f_locals is None:
frame = inspect.currentframe()
ff_globals, ff_locals = {}, {}
for i in range(frame_depth + frames_deep):
if i >= frame_depth:
ff_globals = frame.f_globals | ff_globals
ff_locals = frame.f_locals | ff_locals
frame = frame.f_back
self._f_globals = f_globals or ff_globals
self._f_locals = dict(f_locals or ff_locals)
else:
self._f_globals = f_globals
self._f_locals = dict(f_locals)
try:
exec(value_str, self._f_globals, self._f_locals)
except Exception as e:
print(f'Caught exception when trying to bind to variable')
print(f'exception: {e}')
print(f'globals: {self._f_globals}')
print(f'locals: {self._f_locals}')
assert False, f'BoundVar: value string ("{value_str}") must be a valid variable!'
self._f_locals.update({'boundvar_interface': self._boundvar_interface})
self._value_str = value_str
self._callbacks = callbacks or []
self._validators = validators or []
self._disabled = disabled
self._pre_wrap = wrap if wrap is not None else pre_wrap if pre_wrap is not None else ''
self._post_wrap = wrap if wrap is not None else post_wrap if post_wrap is not None else ''
if on_change: self.on_change(on_change)
def clone_with_overrides(self, **overrides):
# perform SHALLOW copy (shared attribs, such as _callbacks!) and override attribs as given
other = copy.copy(self)
for k, v in overiddes.iteritems():
try:
setattr(other, k, v)
except AttributeError:
setattr(other, f'_{k}', v)
return other
def _boundvar_interface(self, v): self._v = v
def _call_callbacks(self):
for cb in self._callbacks: cb()
def __str__(self): return str(self.value)
def get(self):
return self.value
def set(self, value):
self.value = value
@property
def disabled(self):
return self._disabled
@disabled.setter
def disabled(self, v):
self._disabled = bool(v)
self._call_callbacks()
def get_value(self):
exec(f'boundvar_interface({self._value_str})', self._f_globals, self._f_locals)
return self._v
def set_value(self, value):
try:
for validator in self._validators: value = validator(value)
except IgnoreChange:
return
if self.value == value: return
exec(f'{self._value_str} = {self._pre_wrap}{value}{self._post_wrap}', self._f_globals, self._f_locals)
self._call_callbacks()
@property
def value(self): return self.get_value()
@value.setter
def value(self, value): self.set_value(value)
@property
def value_as_str(self): return str(self)
@property
def is_bounded(self): return False
def on_change(self, fn): self._callbacks.append(fn)
def add_validator(self, fn): self._validators.append(fn)
class BoundString(BoundVar):
def __init__(self, value_str, *, frame_depth=2, **kwargs):
super().__init__(value_str, frame_depth=frame_depth, wrap='"', **kwargs)
class BoundStringToBool(BoundVar):
def __init__(self, value_str, true_str, *, frame_depth=2, **kwargs):
self._true_str = true_str
super().__init__(value_str, frame_depth=frame_depth, wrap='"', **kwargs)
@property
def value(self):
return self.get_value() == self._true_str
@value.setter
def value(self, v):
if bool(v): self.set_value(self._true_str)
@property
def checked(self):
return self.get_value() == self._true_str
@checked.setter
def checked(self, v):
# sets to a true str iff v is True
# assuming that some other BoundStringToBool will get set to True
if bool(v): self.value = self._true_str
class BoundBool(BoundVar):
def __init__(self, value_str, *, frame_depth=2, **kwargs):
super().__init__(value_str, frame_depth=frame_depth, **kwargs)
@property
def checked(self): return self.get_value()
@checked.setter
def checked(self,v): self.set_value(v)
class BoundInt(BoundVar):
def __init__(self, value_str, *, min_value=None, max_value=None, step_size=None, frame_depth=2, **kwargs):
super().__init__(value_str, frame_depth=frame_depth, **kwargs)
self._min_value = min_value
self._max_value = max_value
self._step_size = step_size or 0
self.add_validator(self.int_validator)
@property
def min_value(self): return self._min_value
@property
def max_value(self): return self._max_value
@property
def step_size(self): return self._step_size
@property
def is_bounded(self):
return self._min_value is not None and self._max_value is not None
@property
def bounded_ratio(self):
assert self.is_bounded, f'Cannot compute bounded_ratio of unbounded BoundInt'
return (self.value - self.min_value) / (self.max_value - self.min_value)
def int_validator(self, value):
try:
t = type(value)
if t is str: nv = int(re.sub(r'[^\d.-]', '', value))
elif t is int: nv = value
elif t is float: nv = int(value)
else: assert False, 'Unhandled type of value: %s (%s)' % (str(value), str(t))
if self._min_value is not None: nv = max(nv, self._min_value)
if self._max_value is not None: nv = min(nv, self._max_value)
if self._step_size and self._min_value is not None:
nv = math.floor((nv - self._min_value) / self._step_size) * self._step_size + self._min_value
return nv
except ValueError as e:
raise IgnoreChange()
except Exception:
# ignoring all exceptions?
raise IgnoreChange()
def add_delta(self, scale):
self.value += self.step_size * scale
class BoundFloat(BoundVar):
def __init__(self, value_str, *, min_value=None, max_value=None, step_size=None, frame_depth=2, format_str=None, **kwargs):
super().__init__(value_str, frame_depth=frame_depth, **kwargs)
self._min_value = min_value
self._max_value = max_value
self._step_size = step_size or 0
self.add_validator(self.float_validator)
self._format_str = '%0.5f' if format_str is None else format_str
def __str__(self):
return self._format_str % self.value
@property
def min_value(self): return self._min_value
@property
def max_value(self): return self._max_value
@property
def step_size(self): return self._step_size
@property
def is_bounded(self):
return self._min_value is not None and self._max_value is not None
@property
def bounded_ratio(self):
assert self.is_bounded, f'Cannot compute bounded_ratio of unbounded BoundFloat'
return (self.value - self.min_value) / (self.max_value - self.min_value)
def float_validator(self, value):
try:
t = type(value)
if t is str: nv = float(re.sub(r'[^\d.-]', '', value))
elif t is int: nv = float(value)
elif t is float: nv = value
else: assert False, 'Unhandled type of value: %s (%s)' % (str(value), str(t))
if self._min_value is not None: nv = max(nv, self._min_value)
if self._max_value is not None: nv = min(nv, self._max_value)
if self._step_size and self._min_value is not None:
nv = math.floor((nv - self._min_value) / self._step_size) * self._step_size + self._min_value
return nv
except ValueError as e:
raise IgnoreChange()
except Exception:
# ignoring all exceptions?
raise IgnoreChange()
def add_delta(self, scale):
self.value += self.step_size * scale