467 lines
15 KiB
Python
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()
|