Skip to content

Commit 33beb8b

Browse files
Attribute Curve Filter Pops
Adds a tool to detect and clean up pops in animation curves. GitHub issue #268.
1 parent 020bdcb commit 33beb8b

19 files changed

+1165
-146
lines changed

python/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,14 @@ if (MMSOLVER_BUILD_QT_UI)
159159
${CMAKE_CURRENT_BINARY_DIR}/mmSolver/tools/userprefswindow/ui/ui_pref_layout.py
160160
)
161161

162-
compile_qt_ui_to_python_file("fastbake"
162+
compile_qt_ui_to_python_file("attrbake"
163163
${CMAKE_CURRENT_SOURCE_DIR}/mmSolver/tools/attributebake/ui/attrbake_layout.ui
164164
${CMAKE_CURRENT_BINARY_DIR}/mmSolver/tools/attributebake/ui/ui_attrbake_layout.py
165+
)
166+
167+
compile_qt_ui_to_python_file("attributecurvefilterpops"
168+
${CMAKE_CURRENT_SOURCE_DIR}/mmSolver/tools/attributecurvefilterpops/ui/attrcurvefilterpops_layout.ui
169+
${CMAKE_CURRENT_BINARY_DIR}/mmSolver/tools/attributecurvefilterpops/ui/ui_attrcurvefilterpops_layout.py
165170
)
166171

167172
compile_qt_ui_to_python_file("createcontroller2"

python/mmSolver/tools/attributebake/tool.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,10 @@
3636
LOG = mmSolver.logger.get_logger()
3737

3838

39-
def _get_attributes(from_channelbox_state):
40-
attrs = []
41-
if from_channelbox_state is True:
42-
name = channelbox_utils.get_ui_name()
43-
attrs = maya.cmds.channelBox(name, query=True, selectedMainAttributes=True)
44-
return attrs or []
39+
def _get_selected_channelbox_attributes():
40+
name = channelbox_utils.get_ui_name()
41+
attrs = maya.cmds.channelBox(name, query=True, selectedMainAttributes=True) or []
42+
return attrs
4543

4644

4745
def main():
@@ -75,10 +73,12 @@ def main():
7573
frame_range_mode, custom_start_frame, custom_end_frame
7674
)
7775

78-
attrs = _get_attributes(from_channelbox_state)
79-
if from_channelbox_state is True and len(attrs) == 0:
80-
LOG.warn("Please select at least 1 attribute in the Channel Box.")
81-
return
76+
attrs = []
77+
if from_channelbox_state is True:
78+
attrs = _get_selected_channelbox_attributes()
79+
if len(attrs) == 0:
80+
LOG.warn("Please select at least 1 attribute in the Channel Box.")
81+
return
8282

8383
# Bake attributes
8484
s = time.time()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (C) 2025 David Cattermole.
2+
#
3+
# This file is part of mmSolver.
4+
#
5+
# mmSolver is free software: you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as
7+
# published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# mmSolver is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with mmSolver. If not, see <https://www.gnu.org/licenses/>.
17+
#
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright (C) 2025 David Cattermole.
2+
#
3+
# This file is part of mmSolver.
4+
#
5+
# mmSolver is free software: you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as
7+
# published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# mmSolver is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with mmSolver. If not, see <https://www.gnu.org/licenses/>.
17+
#
18+
"""
19+
Attribute Curve Filter constants.
20+
"""
21+
22+
WINDOW_TITLE = 'Attribute Curve Filter Pops'
23+
24+
# Constants for frame range mode.
25+
FRAME_RANGE_MODE_TIMELINE_INNER_VALUE = 'attrcurvefilterpops_timeline_inner'
26+
FRAME_RANGE_MODE_TIMELINE_OUTER_VALUE = 'attrcurvefilterpops_timeline_outer'
27+
FRAME_RANGE_MODE_CUSTOM_VALUE = 'attrcurvefilterpops_custom'
28+
FRAME_RANGE_MODE_VALUES = [
29+
FRAME_RANGE_MODE_TIMELINE_INNER_VALUE,
30+
FRAME_RANGE_MODE_TIMELINE_OUTER_VALUE,
31+
FRAME_RANGE_MODE_CUSTOM_VALUE,
32+
]
33+
34+
FRAME_RANGE_MODE_TIMELINE_INNER_LABEL = 'Timeline (Inner)'
35+
FRAME_RANGE_MODE_TIMELINE_OUTER_LABEL = 'Timeline (Outer)'
36+
FRAME_RANGE_MODE_CUSTOM_LABEL = 'Custom'
37+
FRAME_RANGE_MODE_LABELS = [
38+
FRAME_RANGE_MODE_TIMELINE_INNER_LABEL,
39+
FRAME_RANGE_MODE_TIMELINE_OUTER_LABEL,
40+
FRAME_RANGE_MODE_CUSTOM_LABEL,
41+
]
42+
43+
FRAME_RANGE_MODE_VALUE_LABEL_MAP = {
44+
FRAME_RANGE_MODE_TIMELINE_INNER_VALUE: FRAME_RANGE_MODE_TIMELINE_INNER_LABEL,
45+
FRAME_RANGE_MODE_TIMELINE_OUTER_VALUE: FRAME_RANGE_MODE_TIMELINE_OUTER_LABEL,
46+
FRAME_RANGE_MODE_CUSTOM_VALUE: FRAME_RANGE_MODE_CUSTOM_LABEL,
47+
}
48+
49+
50+
# Default Values
51+
DEFAULT_FRAME_START = 1
52+
DEFAULT_FRAME_END = 120
53+
DEFAULT_FRAME_RANGE_MODE = FRAME_RANGE_MODE_TIMELINE_INNER_VALUE
54+
DEFAULT_THRESHOLD = 1.0
55+
56+
# Config files
57+
CONFIG_FRAME_RANGE_MODE_KEY = 'mmSolver_attrcurvefilterpops_frame_range_mode'
58+
CONFIG_FRAME_START_KEY = 'mmSolver_attrcurvefilterpops_frame_start'
59+
CONFIG_FRAME_END_KEY = 'mmSolver_attrcurvefilterpops_frame_end'
60+
CONFIG_THRESHOLD_KEY = 'mmSolver_attrcurvefilterpops_threshold'
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright (C) 2025 David Cattermole.
2+
#
3+
# This file is part of mmSolver.
4+
#
5+
# mmSolver is free software: you can redistribute it and/or modify it
6+
# under the terms of the GNU Lesser General Public License as
7+
# published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# mmSolver is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with mmSolver. If not, see <https://www.gnu.org/licenses/>.
17+
#
18+
19+
from __future__ import absolute_import
20+
from __future__ import division
21+
from __future__ import print_function
22+
23+
import maya.cmds
24+
25+
import mmSolver.logger
26+
import mmSolver.utils.time as time_utils
27+
import mmSolver.utils.python_compat as pycompat
28+
import mmSolver.utils.node as node_utils
29+
import mmSolver.ui.channelboxutils as channelbox_utils
30+
import mmSolver.tools.attributecurvefilterpops.constant as const
31+
32+
33+
LOG = mmSolver.logger.get_logger()
34+
35+
36+
def get_frame_range(frame_range_mode, custom_start_frame, custom_end_frame):
37+
assert isinstance(frame_range_mode, pycompat.TEXT_TYPE)
38+
assert frame_range_mode in const.FRAME_RANGE_MODE_VALUES
39+
assert isinstance(custom_start_frame, pycompat.INT_TYPES)
40+
assert isinstance(custom_end_frame, pycompat.INT_TYPES)
41+
LOG.debug("frame_range_mode: %r", frame_range_mode)
42+
43+
frame_range = None
44+
if frame_range_mode == const.FRAME_RANGE_MODE_TIMELINE_INNER_VALUE:
45+
frame_range = time_utils.get_maya_timeline_range_inner()
46+
elif frame_range_mode == const.FRAME_RANGE_MODE_TIMELINE_OUTER_VALUE:
47+
frame_range = time_utils.get_maya_timeline_range_outer()
48+
elif frame_range_mode == const.FRAME_RANGE_MODE_CUSTOM_VALUE:
49+
frame_range = time_utils.FrameRange(custom_start_frame, custom_end_frame)
50+
else:
51+
LOG.error("Invalid frame range mode: %r", frame_range_mode)
52+
return frame_range
53+
54+
55+
def _get_node_attrs_for_selected_keyframes():
56+
"""Get animCurve nodes from the selected keyframes (in the graph
57+
editor).
58+
"""
59+
selected_anim_curve_nodes = (
60+
maya.cmds.keyframe(query=True, selected=True, name=True) or []
61+
)
62+
node_attrs = set()
63+
for anim_curve_node in selected_anim_curve_nodes:
64+
node_attr = anim_curve_node + '.output'
65+
conns = (
66+
maya.cmds.listConnections(
67+
node_attr, destination=False, source=False, plugs=True
68+
)
69+
or []
70+
)
71+
node_attrs |= set(conns)
72+
return list(sorted(node_attrs))
73+
74+
75+
def _get_selected_graph_editor_outliner_node_attributes():
76+
"""
77+
Get the attributes from the selected graph editor outliner.
78+
"""
79+
# NOTE: Do we need to support multiple graph editors?
80+
connection_object = 'graphEditor1FromOutliner'
81+
objects = (
82+
maya.cmds.selectionConnection(connection_object, query=True, object=True) or []
83+
)
84+
85+
node_attrs = set()
86+
for obj in objects:
87+
if not isinstance(obj, pycompat.TEXT_TYPE):
88+
continue
89+
if '.' in obj:
90+
node_attrs.add(obj)
91+
return list(sorted(node_attrs))
92+
93+
94+
def _get_selected_channelbox_attributes():
95+
"""Get the selected attributes from the channel box."""
96+
name = channelbox_utils.get_ui_name()
97+
attrs = maya.cmds.channelBox(name, query=True, selectedMainAttributes=True) or []
98+
return attrs
99+
100+
101+
def get_selected_node_attrs(nodes):
102+
"""
103+
Get the selected node attributes, from the Graph Editor or
104+
Channel Box.
105+
106+
A universal function to get whatever the user has "selected" for
107+
the animation curve. This includes looking at the channel box, or
108+
using the selected curves in the Graph Editor, etc.
109+
"""
110+
node_attrs = _get_node_attrs_for_selected_keyframes()
111+
if len(node_attrs) > 0:
112+
return node_attrs
113+
114+
node_attrs = _get_selected_graph_editor_outliner_node_attributes()
115+
if len(node_attrs) > 0:
116+
return node_attrs
117+
118+
channelbox_attrs = _get_selected_channelbox_attributes()
119+
if len(channelbox_attrs) == 0:
120+
LOG.warn("Please select at least 1 attribute in the Channel Box.")
121+
return
122+
123+
# Combine nodes with attributes
124+
node_attrs = []
125+
for node in nodes:
126+
for attr in channelbox_attrs:
127+
if node_utils.attribute_exists(attr, node):
128+
node_attr = '{}.{}'.format(node, attr)
129+
node_attrs.append(node_attr)
130+
131+
return node_attrs
132+
133+
134+
def get_attribute_anim_curves(node_attrs):
135+
"""
136+
Return anim curve nodes from attributes.
137+
138+
:param node_attrs: Node attribute names.
139+
:rtype: []
140+
"""
141+
assert len(node_attrs) > 0
142+
assert isinstance(node_attrs, (list, set))
143+
anim_curve_node = maya.cmds.listConnections(node_attrs, type='animCurve') or []
144+
145+
anim_curve_nodes = []
146+
if isinstance(anim_curve_node, pycompat.TEXT_TYPE):
147+
anim_curve_nodes = [anim_curve_node]
148+
elif isinstance(anim_curve_node, list):
149+
anim_curve_nodes = anim_curve_node
150+
return anim_curve_nodes
151+
152+
153+
def filter_curves_pops(anim_curve_nodes, start_frame, end_frame, threshold):
154+
"""
155+
Filter pops from curves on the attributes on nodes.
156+
157+
:param anim_curve_nodes: AnimCurve nodes to filter.
158+
:param start_frame: Start frame to filter.
159+
:param end_frame: End frame to filter.
160+
:param threshold: The threshold to use for pop detection.
161+
"""
162+
assert len(anim_curve_nodes) > 0
163+
assert isinstance(start_frame, pycompat.INT_TYPES)
164+
assert isinstance(end_frame, pycompat.INT_TYPES)
165+
assert isinstance(threshold, float)
166+
167+
maya.cmds.mmAnimCurveFilterPops(
168+
anim_curve_nodes,
169+
startFrame=start_frame,
170+
endFrame=end_frame,
171+
threshold=threshold,
172+
)
173+
return

0 commit comments

Comments
 (0)