Files
Nexus/2023/scripts/rigging_tools/ngskintools2/api/paint.py
2025-11-24 08:27:50 +08:00

467 lines
15 KiB
Python

import copy
import json
from maya import cmds
from ngSkinTools2.api import internals, plugin
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.pyside import QtCore
from ngSkinTools2.api.python_compatibility import Object
log = getLogger("api/paint")
# noinspection PyClassHasNoInit
class BrushProjectionMode(Object):
surface = 0
screen = 1
# noinspection PyClassHasNoInit
class PaintMode(Object):
"""
Constants for paint mode
"""
replace = 1
add = 2
scale = 3
smooth = 4
sharpen = 5
@classmethod
def all(cls):
return cls.replace, cls.smooth, cls.add, cls.scale, cls.sharpen
# noinspection PyClassHasNoInit
class TabletMode(Object):
unused = 0
multiplyIntensity = 1
multiplyOpacity = 2
multiplyRadius = 3
# noinspection PyClassHasNoInit
class WeightsDisplayMode(Object):
allInfluences = 0
currentInfluence = 1
currentInfluenceColored = 2
# noinspection PyClassHasNoInit
class MaskDisplayMode(Object):
default_ = 0
color_ramp = 1
# noinspection PyClassHasNoInit
class BrushShape(Object):
solid = 0 # 1.0 for whole brush size
smooth = 1 # feathered edges
gaus = 2 # very smooth from center
# noinspection PyClassHasNoInit
class PaintModeSettings(Object):
"""
Brush/Flood settings
"""
__property_map = {
"brush_projection_mode": 'brushProjectionMode',
"mode": 'paintMode',
"brush_radius": 'brushRadius',
"brush_shape": 'brushShape',
"intensity": 'brushIntensity',
"iterations": 'brushIterations',
"tablet_mode": 'tabletMode',
"mirror": 'interactiveMirror',
"influences_limit": 'influencesLimit',
"fixed_influences_per_vertex": 'fixedInfluencesPerVertex',
"limit_to_component_selection": 'limitToComponentSelection',
"use_volume_neighbours": 'useVolumeNeighbours',
"distribute_to_other_influences": 'redistributeRemovedWeight',
"sample_joint_on_stroke_start": 'sampleJointOnStrokeStart',
}
mode = PaintMode.replace #: Tool mode. One of the :py:class:`PaintMode` values.
# varies by mode
intensity = 1.0 #: tool intensity;
iterations = 1
"""
iterations; repeats the same smooth operation given number of times -
using this parameter instead of calling `flood_weights` multiple times.
"""
brush_shape = BrushShape.solid
# varies by screen mode
# can be modified inside the plugin
brush_radius = 10
mirror = False #: is automatic mirroring on or off
distribute_to_other_influences = False
influences_limit = 0 #: influences limit per vertex to ensure while smoothing
brush_projection_mode = BrushProjectionMode.surface #: brush projection mode, one of :py:class:`BrushProjectionMode` values.
sample_joint_on_stroke_start = False
tablet_mode = TabletMode.unused
use_volume_neighbours = False
limit_to_component_selection = False
fixed_influences_per_vertex = False
"""
only applicable for smooth mode; when set to True, smoothing will not add additional influences to a vertex.
"""
def apply_primary_brush(self):
self.__apply(1)
def apply_alternative_brush(self):
self.__apply(2)
def apply_inverted_brush(self):
self.__apply(3)
def __apply(self, settings_type):
"""
apply settings to C++ plugin side.
:param settings_type:
"""
kwargs = {v: getattr(self, k) for k, v in self.__property_map.items()}
kwargs['paintType'] = settings_type
plugin.ngst2PaintSettingsCmd(**kwargs)
def from_dict(self, values):
# type: (dict) -> PaintModeSettings
for k in self.__property_map.keys():
if k in values:
setattr(self, k, values[k])
return self
def to_dict(self):
# type: () -> dict
return {k: getattr(self, k) for k in self.__property_map.keys()}
def __make_common_property__(property_name):
def setval(self, val):
setattr(self.primary_settings, property_name, val)
self.apply_settings()
return property(lambda self: getattr(self.primary_settings, property_name), setval)
def __make_mode_property__(property_name):
return __make_dimensional_property__(lambda self: self.mode, lambda self: self.mode_settings[self.mode], property_name)
def __make_projection_property__(property_name):
return __make_dimensional_property__(
lambda self: self.brush_projection_mode, lambda self: self.projection_settings[self.brush_projection_mode], property_name
)
def __make_dimensional_property__(name, get_dimension_func, property_name):
def setval(self, val):
log.debug("setting dimensional property %s/%s: %r", name(self), property_name, val)
dimension = get_dimension_func(self)
dimension[property_name] = val
setattr(self.primary_settings, property_name, val)
self.apply_settings()
def getval(self):
return get_dimension_func(self).get(property_name, getattr(self.primary_settings, property_name))
return property(getval, setval)
class PaintSettingsModel(Object):
"""
Paint settings model manages paint settings persistence and storage on CPP side;
in CPP side three states need to be maintained:
* primary settings
* alternative settings (used when shift is pressed)
* inverse settings (used when ctrl is pressed)
"""
projection_settings = None # type: dict
mode_settings = None # type: dict
primary_settings = None # type: PaintModeSettings
paint_mode = __make_common_property__("mode")
mode = __make_common_property__("mode")
intensity = __make_mode_property__("intensity")
iterations = __make_mode_property__("iterations")
brush_shape = __make_mode_property__("brush_shape")
brush_radius = __make_projection_property__("brush_radius")
mirror = __make_common_property__("mirror")
distribute_to_other_influences = __make_common_property__("distribute_to_other_influences")
influences_limit = __make_common_property__("influences_limit")
brush_projection_mode = __make_common_property__("brush_projection_mode")
sample_joint_on_stroke_start = __make_common_property__("sample_joint_on_stroke_start")
tablet_mode = __make_common_property__("tablet_mode")
use_volume_neighbours = __make_common_property__("use_volume_neighbours")
limit_to_component_selection = __make_common_property__("limit_to_component_selection")
fixed_influences_per_vertex = __make_common_property__("fixed_influences_per_vertex")
def __init__(self):
self.projection_settings = None
self.mode_settings = None
self.primary_settings = None
self.storage_func_save = lambda data: None
self.storage_func_load = lambda: ""
self.apply_settings_func = self.apply_plugin_settings
self.setup_maya_option_var_persistence()
def __save_settings(self):
data = {
"common": self.primary_settings.to_dict(),
"mode_settings": self.mode_settings,
"projection_settings": self.projection_settings,
}
serialized_data = json.dumps(data)
log.info("saving brush settings: %s", serialized_data)
self.storage_func_save(serialized_data)
def load_settings(self):
def to_int_keys(d):
return {int(k): v for k, v in d.items()}
try:
saved_data = self.storage_func_load()
log.info("loading brush settings from %s", saved_data)
if saved_data is None:
self.initialize_defaults()
return
data = json.loads(saved_data)
self.primary_settings = PaintModeSettings().from_dict(data['common'])
self.mode_settings = to_int_keys(data['mode_settings'])
self.projection_settings = to_int_keys(data['projection_settings'])
self.apply_settings()
except Exception as err:
log.info(err)
def setup_maya_option_var_persistence(self):
from ngSkinTools2.ui import options
val = options.PersistentValue(options.VAR_OPTION_PREFIX + "_brush_settings")
self.storage_func_load = val.get
self.storage_func_save = val.set
self.load_settings()
def __bake_settings(self, mode):
result = copy.copy(self.primary_settings)
result.mode = mode
for k, v in self.mode_settings[mode].items():
setattr(result, k, v)
for k, v in self.projection_settings[self.brush_projection_mode].items():
setattr(result, k, v)
return result
def apply_settings(self):
# TODO: very inelegant here; we should not have to re-bake and re-set all settings at once
# if we're switching any of dimensional settings, we need to reflect this
self.primary_settings = self.__bake_settings(self.mode)
primary = self.primary_settings
inverted_modes = {
PaintMode.replace: PaintMode.replace,
PaintMode.add: PaintMode.scale,
PaintMode.scale: PaintMode.add,
PaintMode.smooth: PaintMode.sharpen,
PaintMode.sharpen: PaintMode.smooth,
}
alternative = self.__bake_settings(PaintMode.smooth)
inverted = self.__bake_settings(inverted_modes.get(self.mode, PaintMode.replace))
if self.mode == PaintMode.replace:
inverted.intensity = 0
log.debug("normal mode intensity: %r", primary.intensity)
self.apply_settings_func(primary, alternative, inverted)
self.__save_settings()
def initialize_defaults(self):
self.primary_settings = PaintModeSettings()
self.primary_settings.mode = PaintMode.replace
self.primary_settings.brush_radius = 2
self.primary_settings.intensity = 1.0
self.primary_settings.tablet_mode = TabletMode.unused
self.primary_settings.brush_shape = BrushShape.solid
self.primary_settings.distribute_to_other_influences = False
self.mode_settings = {
PaintMode.replace: {
"intensity": 1.0,
"brush_shape": BrushShape.solid,
},
PaintMode.add: {
"intensity": 0.1,
"brush_shape": BrushShape.solid,
},
PaintMode.scale: {
"intensity": 0.95,
"brush_shape": BrushShape.solid,
},
PaintMode.smooth: {
"intensity": 0.2,
"iterations": 5,
"brush_shape": BrushShape.smooth,
},
PaintMode.sharpen: {
"intensity": 0.2,
"iterations": 5,
"brush_shape": BrushShape.smooth,
},
}
self.projection_settings = {
BrushProjectionMode.surface: {
"brush_radius": 2,
},
BrushProjectionMode.screen: {
"brush_radius": 100,
},
}
self.apply_settings()
# noinspection PyMethodMayBeStatic
def apply_plugin_settings(self, primary, alternative, inverted):
# type: (PaintModeSettings, PaintModeSettings, PaintModeSettings) -> None
primary.apply_primary_brush()
alternative.apply_alternative_brush()
inverted.apply_inverted_brush()
class DisplaySettings(Object):
def __init__(self):
from ngSkinTools2.ui import options
self.persistence = options.PersistentDict("paint_display_settings")
weights_display_mode = internals.make_editable_property('weightsDisplayMode')
mask_display_mode = internals.make_editable_property('maskDisplayMode')
layer_effects_display = internals.make_editable_property('layerEffectsDisplay')
display_masked = internals.make_editable_property('displayMasked')
show_selected_verts_only = internals.make_editable_property('showSelectedVertsOnly')
wireframe_color = internals.make_editable_property('wireframeColor')
wireframe_color_single_influence = internals.make_editable_property('wireframeColorSingleInfluence')
# noinspection PyMethodMayBeStatic
def __edit__(self, **kwargs):
plugin.ngst2PaintSettingsCmd(**kwargs)
for k, v in kwargs.items():
self.persistence[k] = v
@property
def display_node_visible(self):
"""
gets/sets visibility of temporary node that displays weight colors. when set to false, displays original mesh instead.
"""
return plugin.ngst2PaintSettingsCmd(q=True, displayNodeVisible=True)
@display_node_visible.setter
def display_node_visible(self, value):
plugin.ngst2PaintSettingsCmd(displayNodeVisible=value)
# noinspection PyMethodMayBeStatic
def __query__(self, **kwargs):
for k in kwargs:
persisted = self.persistence[k]
if persisted is not None:
return persisted
return plugin.ngst2PaintSettingsCmd(q=True, **kwargs)
class PaintTool(PaintSettingsModel):
__paint_context = None
def __init__(self):
PaintSettingsModel.__init__(self)
self.display_settings = DisplaySettings()
def update_plugin_brush_radius(self):
new_value = plugin.ngst2PaintSettingsCmd(q=True, brushRadius=True)
if self.brush_radius != new_value:
self.brush_radius = new_value
def update_plugin_brush_intensity(self):
new_value = plugin.ngst2PaintSettingsCmd(q=True, brushIntensity=True)
if self.intensity != new_value:
self.intensity = new_value
@classmethod
def start(cls):
if cls.__paint_context is None:
cls.__paint_context = plugin.ngst2PaintContext()
cmds.setToolTo(cls.__paint_context)
def flood(self, layer, influence=None, influences=None):
from ngSkinTools2.api import tools
tools.flood_weights(target=layer, influence=influence, influences=influences, settings=self.primary_settings)
@classmethod
def is_painting(cls):
return cmds.contextInfo(cmds.currentCtx(), c=True) == 'ngst2PaintContext'
class Popups(Object):
def __init__(self):
self.windows = []
def add(self, w):
self.windows.append(w)
w.destroyed.connect(lambda *args: self.remove(w))
def remove(self, w):
self.windows = [i for i in self.windows if i != w]
def close_all(self):
for i in self.windows:
i.close()
self.windows = []
popups = Popups()
class TabletEventFilter(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
self.pressure = 1.0
def eventFilter(self, obj, event):
if event.type() in [QtCore.QEvent.TabletPress, QtCore.QEvent.TabletMove]:
self.pressure = event.pressure()
# log.info("tablet pressure: %r", self.pressure)
return QtCore.QObject.eventFilter(self, obj, event)
def install(self):
from ngSkinTools2.ui import qt
log.info("installing event filter...")
qt.mainWindow.installEventFilter(self)
log.info("...done")
def uninstall(self):
from ngSkinTools2.ui import qt
qt.mainWindow.removeEventFilter(self)
log.info("event filter uninstalled")
tabletEventFilter = TabletEventFilter()