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

191 lines
7.1 KiB
Python

"""
## Event handling brainstorm
# Usecase: when selection changes, handlers need to update to this.
Handlers are interested in same data (what's the selected mesh, are layers available, etc). When even is received
by handler, all data to handle the even is there. Data is mostly pre-fetched (assuming that someone will eventually
need it anyway), but for some events lazy-loading might be needed.
# Usecase: event handlers need to respond only when data actually changes (state goes from "layers available"
to "layers unavailable")
Even handlers store that information on heir side. Signal has no way of knowing prevous state of the handler.
# Usecase: event can be fired as a source of multiple other events (layer availability changed: could come from
data transformation or undo/redo event)
Events have their own hierarchy, "layers availability changed" signal stores information about it's previous state
and emits if state changes.
## Events hierarchy complexity
Whenever possible, keep event tree localized in single place for easier refactoring.
"""
from maya import cmds
from ngSkinTools2 import api, cleanup, signal
from ngSkinTools2.api import target_info
from ngSkinTools2.api.log import getLogger
from ngSkinTools2.api.python_compatibility import Object
from ngSkinTools2.signal import Signal
log = getLogger("events")
class ConditionalEmit(Object):
def __init__(self, name, check):
self.signal = Signal(name)
self.check = check
def emitIfChanged(self):
if self.check():
self.signal.emit()
def addHandler(self, handler, **kwargs):
self.signal.addHandler(handler, **kwargs)
def removeHandler(self, handler):
self.signal.removeHandler(handler)
def script_job(*args, **kwargs):
"""
a proxy on top of cmds.scriptJob for scriptJob creation;
will register an automatic cleanup procedure to kill the job
"""
job = cmds.scriptJob(*args, **kwargs)
def kill():
# noinspection PyBroadException
try:
cmds.scriptJob(kill=job)
except:
# should be no issue if we cannot kill the job anymore (e.g., killing from the
# import traceback; traceback.print_exc()
pass
cleanup.registerCleanupHandler(kill)
return job
class Events(Object):
"""
root tree of events signaling each other
"""
def __init__(self, state):
"""
:type state: ngSkinTools2.api.session.State
"""
def script_job_signal(name):
result = Signal(name + "_scriptJob")
script_job(e=[name, result.emit])
return result
self.mayaDeleteAll = script_job_signal('deleteAll')
self.nodeSelectionChanged = script_job_signal('SelectionChanged')
self.undoExecuted = script_job_signal('Undo')
self.redoExecuted = script_job_signal('Redo')
self.undoRedoExecuted = Signal('undoRedoExecuted')
self.undoExecuted.addHandler(self.undoRedoExecuted.emit)
self.redoExecuted.addHandler(self.undoRedoExecuted.emit)
self.toolChanged = script_job_signal('ToolChanged')
self.quitApplication = script_job_signal('quitApplication')
def check_target_changed():
"""
verify that currently selected mesh is changed, and this means a change in LayersManager.
"""
selection = cmds.ls(selection=True, objectsOnly=True) or []
selected_skin_cluster = None if not selection else target_info.get_related_skin_cluster(selection[-1])
if selected_skin_cluster is not None:
layers_available = api.get_layers_enabled(selected_skin_cluster)
else:
layers_available = False
if state.selectedSkinCluster == selected_skin_cluster and state.layersAvailable == layers_available:
return False
state.selection = selection
state.set_skin_cluster(selected_skin_cluster)
state.skin_cluster_dq_channel_used = (
False if selected_skin_cluster is None else cmds.getAttr(selected_skin_cluster + ".skinningMethod") == 2
)
state.layersAvailable = layers_available
state.all_layers = [] # reset when target has actually changed
log.info("target changed, layers available: %s", state.layersAvailable)
return True
self.targetChanged = event = ConditionalEmit("targetChanged", check_target_changed)
for source in [self.mayaDeleteAll, self.undoRedoExecuted, self.nodeSelectionChanged]:
source.addHandler(event.emitIfChanged)
def check_layers_list_changed():
state.all_layers = [] if not state.layersAvailable else api.Layers(state.selectedSkinCluster).list()
return True
self.layerListChanged = ConditionalEmit("layerListChanged", check_layers_list_changed)
signal.on(self.targetChanged, self.undoRedoExecuted)(self.layerListChanged.emitIfChanged)
def check_current_layer_changed():
# current layer changed if current mesh changed,
# or id within the mesh changed
current_layer = None
if state.selectedSkinCluster is not None and state.layersAvailable:
current_layer = api.Layers(state.selectedSkinCluster).current_layer()
if state.selectedSkinCluster == state.currentLayer.selectedSkinCluster and state.currentLayer.layer == current_layer:
return False
state.currentLayer.selectedSkinCluster = state.selectedSkinCluster
state.currentLayer.layer = current_layer
return True
self.currentLayerChanged = event = ConditionalEmit("currentLayerChanged", check_current_layer_changed)
self.targetChanged.addHandler(event.emitIfChanged)
self.undoRedoExecuted.addHandler(event.emitIfChanged)
def check_current_paint_target_changed():
skin_cluster = state.selectedSkinCluster
new_layer = state.currentLayer.layer
new_targets = None
if new_layer is not None:
new_targets = new_layer.paint_targets
log.info("[%s] checking current influence changed to %s %r", skin_cluster, new_layer, new_targets)
if (
skin_cluster == state.currentInfluence.skinCluster
and new_layer == state.currentInfluence.layer
and new_targets == state.currentInfluence.targets
):
return False
log.info("[%s] current influence changed to %s %r", skin_cluster, new_layer, new_targets)
state.currentInfluence.skinCluster = skin_cluster
state.currentInfluence.layer = new_layer
state.currentInfluence.targets = new_targets
return True
self.currentInfluenceChanged = event = ConditionalEmit("currentInfluenceChanged", check_current_paint_target_changed)
self.currentLayerChanged.addHandler(event.emitIfChanged)
self.influencesListUpdated = Signal("influencesListUpdated")
# now get initial state
self.targetChanged.emitIfChanged()