Constrain an object to time

A quick useful constraint system. Constrain an object to another in time. Works like a Parent Constraint, with time as an added variable.

To use simply copy paste the code below into a shelf icon. Then use it as you would any other constraint. Select the driver, then the driven objects and press the button.

A new attribute will be created on the driven object for each axis constrained. A value of 0 means to follow perfectly. Just like a parent constraint.

A negative value means to move that many frames delayed in time. A positive value means to move that many frames into the future.

# Offset constraint for Maya
# Created By Jason Dixon.
# Wrap the outermost function calls in the Report class
# As a decorator or as a context manager on the outermost function calls
# For instance, decorate your Main() function,
# or any function that is called directly by a GUI
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.

import pymel.core as pmc
import pymel.core.uitypes as ui

AXIS = tuple(a + b for a in "trs" for b in "xyz")
TRANSLATE = slice(0, 3)
ROTATE = slice(3, 6)
SCALE = slice(6, 9)

class Offset_Constraint(object):
""" Time constraint. """
def __init__(s):
name = "offset_gui"

if pmc.window(name, ex=True):

with pmc.window(name, t="Offset Constraint"):
with ui.ColumnLayout(adj=True):
with pmc.frameLayout(l="Constraint Axis:"):
s.cache = ui.CheckBoxGrp(l="Use Cache: ")
s.offset = ui.CheckBoxGrp(l="Maintain Offset:").setValue1(True)
s.trans = ui.CheckBoxGrp(l="Translate:").setValue1(True).onCommand(lambda x:s.uncheck_boxes(s.trans_ax))
s.trans_ax = ui.CheckBoxGrp(l="", la3=("X","Y","Z"), ncb=3).onCommand(lambda x:s.uncheck_boxes(s.trans))
s.rot = ui.CheckBoxGrp(l="Rotate:").setValue1(True).onCommand(lambda x:s.uncheck_boxes(s.rot_ax))
s.rot_ax = ui.CheckBoxGrp(l="", la3=("X","Y","Z"), ncb=3).onCommand(lambda x:s.uncheck_boxes(s.rot)) = ui.CheckBoxGrp(l="Scale:").setValue1(True).onCommand(lambda x:s.uncheck_boxes(s.sca_ax))
s.sca_ax = ui.CheckBoxGrp(l="", la3=("X","Y","Z"), ncb=3).onCommand(lambda x:s.uncheck_boxes(

ui.Button(l="Apply", h=40).setCommand(lambda x: s.check_options())

def uncheck_boxes(s, boxes):
""" Uncheck boxes """

def check_options(s):
""" Check the options and validate selection """
sel =, type="transform")
if len(sel) != 2: return pmc.warning("Please Select Two Objects.")
trans = s.trans.getValue1()
trans_ax = [s.trans_ax.getValue1(), s.trans_ax.getValue2(), s.trans_ax.getValue3()]
rot = s.rot.getValue1()
rot_ax = [s.rot_ax.getValue1(), s.rot_ax.getValue2(), s.rot_ax.getValue3()]
sca =
sca_ax = [s.sca_ax.getValue1(), s.sca_ax.getValue2(), s.sca_ax.getValue3()]
if (not trans and True not in trans_ax) and (not rot and True not in rot_ax) and (not sca and True not in sca_ax):
return pmc.warning("Please Select at least One Axis.")

axis = set() # Pull out requested axis
if trans: axis |= set(AXIS[TRANSLATE])
if rot: axis |= set(AXIS[ROTATE])
if sca: axis |= set(AXIS[SCALE])
axis |= set(b for a, b in zip(trans_ax + rot_ax + sca_ax, AXIS) if a)

s.apply_constraint(sel[0], sel[1], axis)

def apply_constraint(s, driver, driven, attributes):
""" Attach constraint to object """
err = pmc.undoInfo(openChunk=True)
use_cache = s.cache.getValue1()
time = pmc.nt.Time("time1") # Time node
wrapper =, n="%s_time_constraint" % driven)
driven_base =, n="%s_offset_driven" % driven)
pmc.parent(driven_base, wrapper)
if not use_cache:
driver_base =, n="%s_offset_driver" % driven)
pmc.parent(driver_base, wrapper)
pmc.parentConstraint(driver, driver_base)
pmc.scaleConstraint(driver, driver_base)
expression = ["$frame = frame;"]

OK = not use_cache # Are we ok to continue?
for attr in attributes:
if use_cache:
for anim_curve in driver.attr(attr).connections(type="animCurve", d=False): # Get anim curve

offset_name = "offset_%s" % attr # Create attribute to offset
if not hasattr(driven, offset_name):
driven.addAttr(offset_name, k=True)
offset_at = driven.attr(offset_name)

add_node = pmc.nt.AddDoubleLinear(n="%s_offset_%s" % (driven, attr)) # Create our nodes
cache_node = pmc.nt.FrameCache(n="%s_cache_%s" % (driven, attr))

offset_at.connect(add_node.input1) # Connect our offset controller
time.outTime.connect(add_node.input2) # Connect the scene time

add_node.output.connect(cache_node.varyTime) # Connect to cache
anim_curve.output.connect( # Connect animation curve to cache


OK = True
driver_at = driver_base.attr(attr) # Get driver attribute
driven_at = driven_base.attr(attr)

offset_name = "offset_%s" % attr
if not hasattr(driven, offset_name): # Build our offset attribute
driven.addAttr(offset_name, k=True)
offset_at = driven.attr(offset_name)

expression.append("%s = `getAttr -t ($frame + %s) %s`;" % (driven_at, offset_at, driver_at))

if not use_cache: pmc.nt.Expression().setExpression("\n".join(expression))

maintain_offset = s.offset.getValue1()
# Create a Locator and size it
driven_matrix = driven.getMatrix(ws=True)
b_box = driven.getBoundingBox()
b_size = (b_box.width(), b_box.height(), b_box.depth())
scale = 1.5
loc = pmc.spaceLocator()
for a, b in zip("XYZ", b_size):
loc.attr("localScale%s" % a).set(b * 0.5 * scale)
if maintain_offset:
pmc.xform(loc, m=driven_matrix)
pmc.parent(loc, driven_base)
pmc.parent(loc, driven_base, r=True)

# Attach object to locator
skip = set(AXIS) - set(attributes)

skip_trans = [b for a, b in skip if a == "t"]
skip_rot = [b for a, b in skip if a == "r"]
if len(skip_trans) < 3 or len(skip_rot) < 3:

skip_scale = [b for a, b in skip if a == "s"]
if len(skip_scale) < 3:

except Exception as err:
if err: pmc.undo()