Update
This commit is contained in:
219
2023/scripts/rigging_tools/ngskintools2/ui/widgets.py
Normal file
219
2023/scripts/rigging_tools/ngskintools2/ui/widgets.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from ngSkinTools2.api import PaintMode
|
||||
from ngSkinTools2.api.pyside import QtCore, QtGui, QtWidgets
|
||||
from ngSkinTools2.api.python_compatibility import Object
|
||||
from ngSkinTools2.signal import Signal
|
||||
from ngSkinTools2.ui import qt
|
||||
from ngSkinTools2.ui.layout import scale_multiplier
|
||||
|
||||
|
||||
def curve_mapping(x, s, t):
|
||||
"""
|
||||
provides a linear-to smooth curve mapping
|
||||
|
||||
based on a paper https://arxiv.org/abs/2010.09714
|
||||
"""
|
||||
epsilon = 0.000001
|
||||
|
||||
if x < 0:
|
||||
return 0
|
||||
if x > 1:
|
||||
return 1
|
||||
if x < t:
|
||||
return (t * x) / (x + s * (t - x) + epsilon)
|
||||
|
||||
return ((1 - t) * (x - 1)) / (1 - x - s * (t - x) + epsilon) + 1
|
||||
|
||||
|
||||
class NumberSliderGroup(Object):
|
||||
"""
|
||||
float spinner is the "main control" while the slider acts as complementary way to change value
|
||||
"""
|
||||
|
||||
slider_resolution = 1000.0
|
||||
infinity_max = 65535
|
||||
|
||||
def __init__(self, value_type=float, min_value=0, max_value=1, soft_max=True, tooltip="", expo=None, decimals=3):
|
||||
self.value_range = 0
|
||||
self.min_value = 0
|
||||
self.max_value = 0
|
||||
|
||||
self.float_mode = value_type == float
|
||||
|
||||
self.__layout = layout = QtWidgets.QHBoxLayout()
|
||||
self.valueChanged = Signal("sliderGroupValueChanged")
|
||||
|
||||
self.spinner = spinner = QtWidgets.QDoubleSpinBox() if self.float_mode else QtWidgets.QSpinBox()
|
||||
spinner.setKeyboardTracking(False)
|
||||
|
||||
self.expo = expo
|
||||
self.expo_coefficient = 1.0
|
||||
|
||||
spinner.setFixedWidth(80 * scale_multiplier)
|
||||
if self.float_mode:
|
||||
spinner.setDecimals(decimals)
|
||||
|
||||
spinner.setToolTip(tooltip)
|
||||
|
||||
self.slider = slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
||||
slider.setMinimum(0)
|
||||
slider.setMaximum(self.slider_resolution)
|
||||
slider.setToolTip(tooltip)
|
||||
|
||||
layout.addWidget(spinner)
|
||||
layout.addWidget(slider)
|
||||
|
||||
self.set_range(min_value, max_value, soft_max=soft_max)
|
||||
|
||||
@qt.on(spinner.valueChanged)
|
||||
def update_slider():
|
||||
slider_value = self.__to_slider_value(spinner.value())
|
||||
if slider.value() == slider_value:
|
||||
return
|
||||
with qt.signals_blocked(slider):
|
||||
slider.setValue(slider_value)
|
||||
self.valueChanged.emit()
|
||||
|
||||
@qt.on(slider.valueChanged)
|
||||
def slider_dragging():
|
||||
slider_value = self.__from_slider_value(slider.value())
|
||||
with qt.signals_blocked(spinner):
|
||||
spinner.setValue(slider_value)
|
||||
|
||||
@qt.on(slider.sliderReleased)
|
||||
def slider_drag_finished():
|
||||
self.valueChanged.emit()
|
||||
|
||||
self.update_slider = update_slider
|
||||
|
||||
def set_range(self, min_value, max_value, soft_max=True):
|
||||
with qt.signals_blocked(self.spinner):
|
||||
self.spinner.setMaximum(self.infinity_max if soft_max else max_value)
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.value_range = max_value - min_value
|
||||
single_step = self.value_range / 100.0
|
||||
if not self.float_mode and single_step < 1:
|
||||
single_step = 1
|
||||
self.spinner.setSingleStep(single_step)
|
||||
|
||||
def __to_slider_value(self, v):
|
||||
# formulas: https://www.desmos.com/calculator/gjwk5t3wmn
|
||||
|
||||
x = float(v - self.min_value) / self.value_range
|
||||
|
||||
y = x
|
||||
if self.expo == 'start':
|
||||
y = curve_mapping(x, self.expo_coefficient, 0)
|
||||
if self.expo == 'end':
|
||||
y = curve_mapping(x, self.expo_coefficient, 1)
|
||||
|
||||
return y * self.slider_resolution
|
||||
|
||||
def __from_slider_value(self, v):
|
||||
x = v / self.slider_resolution
|
||||
if self.expo == 'start':
|
||||
x = curve_mapping(x, self.expo_coefficient, 1)
|
||||
if self.expo == 'end':
|
||||
x = curve_mapping(x, self.expo_coefficient, 0)
|
||||
|
||||
return self.min_value + self.value_range * x
|
||||
|
||||
def layout(self):
|
||||
return self.__layout
|
||||
|
||||
def value(self):
|
||||
return self.spinner.value()
|
||||
|
||||
def value_trimmed(self):
|
||||
value = self.value()
|
||||
if self.min_value is not None and value < self.min_value:
|
||||
return self.min_value
|
||||
if self.max_value is not None and value > self.max_value:
|
||||
return self.max_value
|
||||
return value
|
||||
|
||||
def set_value(self, value):
|
||||
if self.value == value:
|
||||
return
|
||||
self.spinner.setValue(value)
|
||||
self.update_slider()
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self.spinner.setEnabled(enabled)
|
||||
self.slider.setEnabled(enabled)
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
def blockSignals(self, block):
|
||||
"""
|
||||
a mimic of qt's blockSignals for both inner widgets
|
||||
"""
|
||||
result = self.spinner.blockSignals(block)
|
||||
self.slider.blockSignals(block)
|
||||
self.valueChanged.enabled = not block
|
||||
return result
|
||||
|
||||
def set_expo(self, expo, coefficient=3):
|
||||
self.expo = expo
|
||||
self.expo_coefficient = coefficient
|
||||
self.update_slider()
|
||||
|
||||
|
||||
def set_paint_expo(number_group, paint_mode):
|
||||
"""
|
||||
Sets number slider group expo according to paint mode.
|
||||
|
||||
:type paint_mode: int
|
||||
:type number_group: NumberSliderGroup
|
||||
"""
|
||||
intensity_expo = {
|
||||
PaintMode.add: ("start", 3),
|
||||
PaintMode.scale: ("end", 8),
|
||||
PaintMode.smooth: ("start", 3),
|
||||
PaintMode.sharpen: ("start", 3),
|
||||
}
|
||||
expo, c = intensity_expo.get(paint_mode, (None, 1))
|
||||
if number_group.expo == expo and number_group.expo_coefficient == c:
|
||||
return
|
||||
|
||||
number_group.set_expo(expo=expo, coefficient=c)
|
||||
|
||||
|
||||
def button_row(button_defs, side_menu=None):
|
||||
result = QtWidgets.QHBoxLayout()
|
||||
|
||||
stretch_marker = "Marker"
|
||||
|
||||
for i in (side_menu or []) + [stretch_marker] + button_defs:
|
||||
if i == stretch_marker:
|
||||
result.addStretch()
|
||||
continue
|
||||
label, handler = i
|
||||
btn = QtWidgets.QPushButton(label, minimumWidth=100)
|
||||
qt.on(btn.clicked)(handler)
|
||||
result.addWidget(btn)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ColorButton(QtWidgets.QPushButton):
|
||||
def __init__(self):
|
||||
QtWidgets.QPushButton.__init__(self)
|
||||
self.color = None
|
||||
qt.on(self.clicked)(self.__pick_color)
|
||||
|
||||
self.color_changed = Signal("color changed")
|
||||
|
||||
def set_color(self, color):
|
||||
if isinstance(color, (list, tuple)):
|
||||
color = QtGui.QColor.fromRgb(color[0] * 255, color[1] * 255, color[2] * 255, 255)
|
||||
self.color = color
|
||||
self.setStyleSheet("background-color: %s;" % color.name())
|
||||
self.color_changed.emit()
|
||||
|
||||
def get_color_3f(self):
|
||||
return [i / 255.0 for i in self.color.getRgb()[:3]]
|
||||
|
||||
def __pick_color(self):
|
||||
color = QtWidgets.QColorDialog.getColor(initial=self.color, options=QtWidgets.QColorDialog.DontUseNativeDialog)
|
||||
if color.isValid():
|
||||
self.set_color(color)
|
||||
Reference in New Issue
Block a user