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. http://internetimagery.com
#
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# 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):
            pmc.deleteUI(name)

        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))
                    s.sca = 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(s.sca))

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

    def uncheck_boxes(s, boxes):
        """ Uncheck boxes """
        boxes.setValueArray3([False]*3)

    def check_options(s):
        """ Check the options and validate selection """
        sel = pmc.ls(sl=True, 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 = s.sca.getValue1()
        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)
        try:
            use_cache = s.cache.getValue1()
            time = pmc.nt.Time("time1") # Time node
            wrapper = pmc.group(em=True, n="%s_time_constraint" % driven)
            driven_base = pmc.group(em=True, n="%s_offset_driven" % driven)
            pmc.parent(driven_base, wrapper)
            if not use_cache:
                driver_base = pmc.group(em=True, 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(cache_node.stream) # Connect animation curve to cache

                        cache_node.varying.connect(driven_base.attr(attr))

                        OK = True
                else:
                    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)
            else:
                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:
                pmc.parentConstraint(
                    loc,
                    driven,
                    st=skip_trans,
                    sr=skip_rot,
                )

            skip_scale = [b for a, b in skip if a == "s"]
            if len(skip_scale) < 3:
                pmc.scaleConstraint(
                    loc,
                    driven,
                    sk=skip_scale,
                )

        except Exception as err:
            raise
        finally:
            pmc.undoInfo(closeChunk=True)
            if err: pmc.undo()

Offset_Constraint()
rope_end