RunCMake(context, force, extraArgs)
-EMBREE = Dependency("Embree", InstallEmbree, "include/embree3/rtcore.h")
+EMBREE = Dependency("Embree", InstallEmbree, "include/embree3/rtcore.h")
+
+############################################################
+# AnimX
+
+# This GitHub project has no releases, so we take the latest.
+# As of 2023, there have been no commits since 2018.
+ANIMX_URL = "https://github.com/Autodesk/animx/archive/refs/heads/master.zip"
+
+def InstallAnimX(context, force, buildArgs):
+ with CurrentWorkingDirectory(DownloadURL(ANIMX_URL, context, force)):
+ # AnimX strangely installs its output to the inst root, rather than the
+ # lib subdirectory. Fix.
+ PatchFile("src/CMakeLists.txt",
+ [("LIBRARY DESTINATION .", "LIBRARY DESTINATION lib")])
+
+ extraArgs = [
+ '-DANIMX_BUILD_MAYA_TESTSUITE=OFF',
+ '-DMAYA_64BIT_TIME_PRECISION=ON',
+ '-DANIMX_BUILD_SHARED=ON',
+ '-DANIMX_BUILD_STATIC=OFF'
+ ]
+ RunCMake(context, force, extraArgs)
+
+ANIMX = Dependency("AnimX", InstallAnimX, "include/animx.h")
+
############################################################
# USD
else:
extraArgs.append('-DPXR_ENABLE_MATERIALX_SUPPORT=OFF')
+ if context.buildMayapyTests:
+ extraArgs.append('-DPXR_BUILD_MAYAPY_TESTS=ON')
+ extraArgs.append('-DMAYAPY_LOCATION="{mayapyLocation}"'
+ .format(mayapyLocation=context.mayapyLocation))
+ else:
+ extraArgs.append('-DPXR_BUILD_MAYAPY_TESTS=OFF')
+
+ if context.buildAnimXTests:
+ extraArgs.append('-DPXR_BUILD_ANIMX_TESTS=ON')
+ else:
+ extraArgs.append('-DPXR_BUILD_ANIMX_TESTS=OFF')
+
if Windows():
# Increase the precompiled header buffer limit.
extraArgs.append('-DCMAKE_CXX_FLAGS="/Zm150"')
subgroup.add_argument("--no-materialx", dest="build_materialx", action="store_false",
help="Disable MaterialX support")
+group = parser.add_argument_group(title="Spline Test Options")
+subgroup = group.add_mutually_exclusive_group()
+subgroup.add_argument("--mayapy-tests",
+ dest="build_mayapy_tests", action="store_true",
+ default=False,
+ help="Build mayapy spline tests")
+subgroup.add_argument("--no-mayapy-tests",
+ dest="build_mayapy_tests", action="store_false",
+ help="Do not build mayapy spline tests (default)")
+group.add_argument("--mayapy-location", type=str,
+ help="Directory where mayapy is installed")
+subgroup = group.add_mutually_exclusive_group()
+subgroup.add_argument("--animx-tests",
+ dest="build_animx_tests", action="store_true",
+ default=False,
+ help="Build AnimX spline tests")
+subgroup.add_argument("--no-animx-tests",
+ dest="build_animx_tests", action="store_false",
+ help="Do not build AnimX spline tests (default)")
+
args = parser.parse_args()
class InstallContext:
# - MaterialX Plugin
self.buildMaterialX = args.build_materialx
+ # - Spline Tests
+ self.buildMayapyTests = args.build_mayapy_tests
+ self.mayapyLocation = args.mayapy_location
+ self.buildAnimXTests = args.build_animx_tests
+
def GetBuildArguments(self, dep):
return self.buildArgs.get(dep.name.lower(), [])
if context.buildUsdview:
requiredDependencies += [PYOPENGL, PYSIDE]
+if context.buildAnimXTests:
+ requiredDependencies += [ANIMX]
+
# Assume zlib already exists on Linux platforms and don't build
# our own. This avoids potential issues where a host application
# loads an older version of zlib than the one we'd build and link
.format(" or ".join(set(pyside2Uic+pyside6Uic))))
sys.exit(1)
+if context.buildMayapyTests:
+ if not context.buildPython:
+ PrintError("--mayapy-tests requires --python")
+ sys.exit(1)
+ if not context.buildTests:
+ PrintError("--mayapy-tests requires --tests")
+ sys.exit(1)
+ if not context.mayapyLocation:
+ PrintError("--mayapy-tests requires --mayapy-location")
+ sys.exit(1)
+
+if context.buildAnimXTests:
+ if not context.buildTests:
+ PrintError("--animx-tests requires --tests")
+ sys.exit(1)
+
# Summarize
summaryMsg = """
Building with settings:
Python docs: {buildPythonDocs}
Documentation {buildHtmlDocs}
Tests {buildTests}
+ Mayapy Tests: {buildMayapyTests}
+ AnimX Tests: {buildAnimXTests}
Examples {buildExamples}
Tutorials {buildTutorials}
Tools {buildTools}
buildAlembic=("On" if context.buildAlembic else "Off"),
buildDraco=("On" if context.buildDraco else "Off"),
buildMaterialX=("On" if context.buildMaterialX else "Off"),
+ buildMayapyTests=("On" if context.buildMayapyTests else "Off"),
+ buildAnimXTests=("On" if context.buildAnimXTests else "Off"),
enableHDF5=("On" if context.enableHDF5 else "Off"))
Print(summaryMsg)
option(PXR_ENABLE_OSL_SUPPORT "Enable OSL (OpenShadingLanguage) based components" OFF)
option(PXR_ENABLE_PTEX_SUPPORT "Enable Ptex support" OFF)
option(PXR_ENABLE_OPENVDB_SUPPORT "Enable OpenVDB support" OFF)
+option(PXR_BUILD_MAYAPY_TESTS "Build mayapy spline tests" OFF)
+option(PXR_BUILD_ANIMX_TESTS "Build AnimX spline tests" OFF)
option(PXR_ENABLE_NAMESPACES "Enable C++ namespaces." ON)
option(PXR_PREFER_SAFETY_OVER_SPEED
"Enable certain checks designed to avoid crashes or out-of-bounds memory reads with malformed input files. These checks may negatively impact performance."
"PXR_ENABLE_PYTHON_SUPPORT=OFF")
set(PXR_BUILD_PYTHON_DOCUMENTATION "OFF" CACHE BOOL "" FORCE)
endif()
-endif()
\ No newline at end of file
+endif()
add_definitions(-DPXR_OSL_SUPPORT_ENABLED)
endif()
+if (PXR_BUILD_ANIMX_TESTS)
+ find_package(AnimX REQUIRED)
+endif()
+
# ----------------------------------------------
# Try and find Imath or fallback to OpenEXR
--- /dev/null
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+# Finds AnimX library. Provides the results by defining the variables
+# ANIMX_LIBRARY and ANIMX_INCLUDES.
+#
+
+find_library(ANIMX_LIBRARY AnimX)
+find_path(ANIMX_INCLUDES animx.h)
+
+find_package_handle_standard_args(
+ AnimX
+ REQUIRED_VARS
+ ANIMX_LIBRARY
+ ANIMX_INCLUDES
+)
work
plug
vt
+ ts
# bin
)
--- /dev/null
+set(PXR_PREFIX pxr/base)
+set(PXR_PACKAGE ts)
+
+set(libs
+ arch
+ gf
+ plug
+ tf
+ trace
+ vt
+ ${Boost_PYTHON_LIBRARY}
+)
+
+set(include
+ ${Boost_INCLUDE_DIRS}
+)
+
+set(classes
+ data
+ diff
+ evalCache
+ keyFrame
+ keyFrameMap
+ keyFrameUtils
+ loopParams
+ mathUtils
+ simplify
+ spline
+ spline_KeyFrames
+ tsTest_Evaluator
+ tsTest_Museum
+ tsTest_SampleBezier
+ tsTest_SampleTimes
+ tsTest_SplineData
+ tsTest_TsEvaluator
+ tsTest_Types
+ types
+ typeRegistry
+)
+
+set(pyfiles
+ __init__.py
+ TsTest_Comparator.py
+ TsTest_CompareBaseline.py
+ TsTest_Grapher.py
+)
+
+set(pycpp
+ module.cpp
+ wrapKeyFrame.cpp
+ wrapLoopParams.cpp
+ wrapSimplify.cpp
+ wrapSpline.cpp
+ wrapTsTest_Evaluator.cpp
+ wrapTsTest_Museum.cpp
+ wrapTsTest_SampleBezier.cpp
+ wrapTsTest_SampleTimes.cpp
+ wrapTsTest_SplineData.cpp
+ wrapTsTest_TsEvaluator.cpp
+ wrapTsTest_Types.cpp
+ wrapTypes.cpp
+)
+
+if (${PXR_BUILD_MAYAPY_TESTS})
+ list(APPEND pyfiles
+ TsTest_MayapyDriver.py
+ TsTest_MayapyEvaluator.py
+ )
+endif()
+
+if (${PXR_BUILD_ANIMX_TESTS})
+ list(APPEND libs ${ANIMX_LIBRARY})
+ list(APPEND include ${ANIMX_INCLUDES})
+ list(APPEND classes tsTest_AnimXEvaluator)
+ list(APPEND pycpp wrapTsTest_AnimXEvaluator.cpp)
+endif()
+
+pxr_library(ts
+ LIBRARIES
+ ${libs}
+
+ INCLUDE_DIRS
+ ${include}
+
+ PUBLIC_CLASSES
+ ${classes}
+
+ PUBLIC_HEADERS
+ api.h
+ evaluator.h
+
+ PRIVATE_HEADERS
+ wrapUtils.h
+
+ PRIVATE_CLASSES
+ evalUtils
+
+ PYTHON_CPPFILES
+ moduleDeps.cpp
+
+ PYMODULE_CPPFILES
+ ${pycpp}
+
+ PYMODULE_FILES
+ ${pyfiles}
+
+ DOXYGEN_FILES
+ tsTest.dox
+)
+
+pxr_build_test(
+ testTs_HardToReach
+ CPPFILES
+ testenv/testTs_HardToReach.cpp
+ LIBRARIES
+ ts
+ gf
+ tf
+)
+
+pxr_build_test(
+ testTsThreadedCOW
+ CPPFILES
+ testenv/testTsThreadedCOW.cpp
+ LIBRARIES
+ ts
+ tf
+ vt
+)
+
+pxr_test_scripts(
+ testenv/testTsSpline.py
+ testenv/testTsSplineAPI.py
+ testenv/testTsKeyFrame.py
+ testenv/testTsSimplify.py
+ testenv/tsTest_TsFramework.py
+)
+
+pxr_install_test_dir(
+ SRC testenv/tsTest_TsFramework.testenv
+ DEST tsTest_TsFramework
+)
+
+pxr_register_test(
+ testTsSpline
+ PYTHON
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTsSpline"
+)
+
+pxr_register_test(
+ testTsSplineAPI
+ PYTHON
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTsSplineAPI"
+)
+
+pxr_register_test(
+ testTsKeyFrame
+ PYTHON
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTsKeyFrame"
+)
+
+pxr_register_test(
+ testTsSimplify
+ PYTHON
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTsSimplify"
+)
+
+pxr_register_test(
+ testTs_HardToReach
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTs_HardToReach"
+)
+
+pxr_register_test(
+ testTsThreadedCOW
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testTsThreadedCOW"
+)
+
+pxr_register_test(
+ tsTest_TsFramework
+ PYTHON
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/tsTest_TsFramework"
+)
+
+if (${PXR_BUILD_MAYAPY_TESTS})
+ pxr_test_scripts(
+ testenv/tsTest_MayapyFramework.py
+ testenv/tsTest_TsVsMayapy.py
+ )
+
+ pxr_install_test_dir(
+ SRC testenv/tsTest_MayapyFramework.testenv
+ DEST tsTest_MayapyFramework
+ )
+
+ set(mayapyBin "${MAYAPY_LOCATION}/mayapy")
+
+ set(cmd "${CMAKE_INSTALL_PREFIX}/tests/tsTest_MayapyFramework")
+ pxr_register_test(
+ tsTest_MayapyFramework
+ PYTHON
+ COMMAND "${cmd} ${mayapyBin}"
+ )
+
+ set(cmd "${CMAKE_INSTALL_PREFIX}/tests/tsTest_TsVsMayapy")
+ pxr_register_test(
+ tsTest_TsVsMayapy
+ PYTHON
+ COMMAND "${cmd} ${mayapyBin}"
+ )
+endif()
+
+if (${PXR_BUILD_ANIMX_TESTS})
+ pxr_test_scripts(testenv/tsTest_AnimXFramework.py)
+
+ pxr_install_test_dir(
+ SRC testenv/tsTest_AnimXFramework.testenv
+ DEST tsTest_AnimXFramework
+ )
+
+ pxr_register_test(
+ tsTest_AnimXFramework
+ PYTHON
+ COMMAND "${CMAKE_INSTALL_PREFIX}/tests/tsTest_AnimXFramework"
+ )
+endif()
+
+if (${PXR_BUILD_MAYAPY_TESTS} AND ${PXR_BUILD_ANIMX_TESTS})
+ pxr_test_scripts(testenv/tsTest_MayapyVsAnimX.py)
+
+ set(cmd "${CMAKE_INSTALL_PREFIX}/tests/tsTest_MayapyVsAnimX")
+ set(mayapyBin "${MAYAPY_LOCATION}/mayapy")
+ pxr_register_test(
+ tsTest_MayapyVsAnimX
+ PYTHON
+ COMMAND "${cmd} ${mayapyBin}"
+ )
+endif()
--- /dev/null
+
+# TS LIBRARY DEVELOPMENT STATUS
+
+The Ts library is under development. This is not a final version. The code
+here was extracted from Pixar's Presto software, and will be modified
+extensively before public release.
+
+This document lists all of the major changes that are expected. These changes
+will make Ts (and satellite code in other libraries) match the description in
+the [USD Anim Proposal](https://github.com/PixarAnimationStudios/OpenUSD-proposals/blob/main/proposals/spline-animation/README.md).
+
+
+# TESTS
+
+There are two families of tests: those that begin with `tsTest`, and everything
+else. The `tsTest` system is new, and most or all tests will eventually be
+migrated to it. `tsTest` provides a test framework that includes features like
+graphical output, and it allows multiple evaluation backends for comparison.
+See the file `tsTest.dox` for details.
+
+There will be extensive coverage of spline evaluation and the Ts API. Tests
+will be written as parts of the library reach their intended state.
+
+
+# API REORGANIZATION
+
+## Series Classes
+
+Ts stands for "time series". A series is a generalization of a spline. Splines
+are the most flexible type of series; there are other types of series that lack
+some of the features of splines. Series types will be chosen according to the
+value type that they hold. Value type categories, and their associated series
+types, are detailed in the USD Anim proposal. In addition to splines, there
+will be `TsLerpSeries`, `TsQuatSeries`, and `TsHeldSeries`. There will also be
+a `TsSeries` container that can hold any of these, and a common
+`TsSeriesInterface` implemented by all of them.
+
+Currently there is only `TsSpline`, with varying behavior depending on value
+type. `TsSpline` wil be split out into the series classes, and the type
+registry will change substantially or be eliminated.
+
+## Spline API
+
+Many methods of `TsSpline`, `TsKeyFrame`, etc will change. Some changes will be
+to accomodate new or changed features; some will be for clarity or convenience.
+
+## Nomenclature
+
+The object-model vocabulary will change in several ways. Some examples:
+"keyframes" will become "knots"; "left" and "right" will become "pre" and
+"post".
+
+## Dual Tangents
+
+Tangents are currently specified only in Presto form (slope and length). It
+will also become possible to specify tangents in Maya form (height and length).
+
+
+# EVALUATION BEHAVIOR
+
+## Maya Evaluator
+
+Ts currently uses its own Bezier evaluator. In the future, it will switch to a
+Maya-identical evaluator. The two give very similar results, but exact matches
+with Maya will be a plus.
+
+## Quaternion Easing
+
+Currently quaternions are only interpolated linearly. We will add an "eased"
+option that comes from Maya.
+
+## No Half-Beziers
+
+Currently, Ts only allows tangents on Bezier-typed knots. When there is a
+Bezier knot followed by a held knot, a Bezier knot followed by a linear knot, or
+a linear knot followed by a Bezier knot, we end up with a "half-Bezier" segment,
+one of whose tangents cannot be specified, and is given an implied automatic
+value.
+
+In the new Ts object model, "knot type" will give way to "next segment
+interpolation method". Any knot that follows a Bezier segment will have an
+in-tangent, regardless of the interpolation method of the following segment.
+Depending on the surrounding segment methods, knots may have no tangents, only
+in-tangents, only out-tangents, or both.
+
+## New Time-Regressive Behavior
+
+When very long tangents cause Bezier segments to form "recurves" or
+"crossovers", that results in a non-function from time to value (multiple values
+per time). Presto and Maya both force strict functional behavior in these
+situations, but they do it differently, and neither behavior is particularly
+easy to predict or control. For crossovers, we will likely create a cusp at the
+crossover point. Details are TBD for recurves. We don't expect these to be
+common cases, but we want our behavior to be unsurprising when they occur.
+
+
+# NEW FEATURES
+
+## Hermites
+
+Currently Ts only supports Bezier curves. It will be expanded to support
+Hermites as well, matching Maya behavior. Hermites have fewer degrees of
+freedom than Beziers - tangent lengths are fixed - and, in return, they are
+faster to evaluate, requiring only a cubic evaluation rather than an iterative
+parametric solve.
+
+## Extrapolating Loops
+
+Currently Ts supports only one form of looping: "inner" looping, where a
+specific portion of a series is repeated. Ts will also support Maya's
+extrapolating loops, where the portion of the series outside all knots is
+evaluated using repeats of the series shape. See the USD Anim proposal for
+details.
+
+## New Extrapolation Modes
+
+We will add the extrapolation modes "sloped", which is similar to "linear" but
+allows explicit specification of the slope; and "none", which causes a series
+not to have a value at all outside of its knot range.
+
+## Automatic Tangents
+
+Ts will support one of Maya's automatic tangent algorithms, allowing Bezier
+knots to assume "nice" tangents based only on their values. The specific
+algorithm has yet to be chosen.
+
+## Reduction
+
+Ts will not require clients to implement every feature. A simple client, for
+example, may permit series authoring, but support only Bezier curves. We want
+all clients to be able to read all series, so we will allow clients to transform
+Ts series into other Ts series that use only a specified set of features. Most
+such conversions will be exact. Examples include the emulation of Hermites,
+looping, and dual-valued knots.
+
+## Knot Metadata
+
+Ts will support arbitrary, structured metadata on any knot, similar to the
+metadata on USD properties.
+
+
+# USD INTEGRATION
+
+## Serialization
+
+Splines and series will serialize to and from `usda` and `usdc`.
+
+## Attribute Value Resolution
+
+This will be the main purpose of USD Anim: to have a USD attribute's value be
+determined by a Ts spline or series.
+
+
+# TUNING
+
+## Storage Reorganization
+
+The current in-memory representation of splines will need to change in order to
+accommodate new features, but we may also change it in order to ensure efficient
+memory access. One example might be side-allocation of less commonly used
+fields like metadata.
+
+## Performance Tests and Optimization
+
+We will write tests that profile the most common Ts operations, and optimize the
+implementation accordingly.
+
+
+# AUXILIARY WORK
+
+## usdview
+
+Basic visualization of splines will be added to `usdview`.
+
+## Scalar xformOps
+
+Currently, USD's `xformOp`s support scalar-valued rotations, but for translation
+and scaling, only vector values (three scalars, one for each spatial axis). We
+will expand the `xformOp` schema to allow scalar values for translations and
+scales also. This will make USD Anim more useful in its first release: all
+basic transforms will be animatable with splines. (Ts will not support splines
+of vectors, only linearly interpolating `TsLerpSeries` of them.)
+
+## Documentation
+
+Ts will be extensively documented.
+
+
+# INVISIBLE WORK
+
+At Pixar, there will be much work to keep Presto running on top of Ts, even as
+Ts changes from the form it had in Presto (it has already departed somewhat).
+This will result in development periods during which little obvious change is
+occurring in the open-source code. It may also result in minor Ts changes that
+don't serve any obvious external purpose.
--- /dev/null
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from .TsTest_Grapher import TsTest_Grapher
+
+class TsTest_Comparator(object):
+
+ @classmethod
+ def Init(cls):
+ return TsTest_Grapher.Init()
+
+ def __init__(self, title, widthPx = 1000, heightPx = 1500):
+ self._sampleSets = []
+ self._grapher = TsTest_Grapher(title, widthPx, heightPx)
+ self._haveCompared = False
+
+ def AddSpline(self, name, splineData, samples, baked = None):
+ self._sampleSets.append(samples)
+ self._grapher.AddSpline(name, splineData, samples, baked)
+
+ def Display(self):
+ self._Compare()
+ self._grapher.Display()
+
+ def Write(self, filePath):
+ self._Compare()
+ self._grapher.Write(filePath)
+
+ def GetMaxDiff(self):
+ self._Compare()
+ return self._maxDiff
+
+ def _Compare(self):
+
+ if self._haveCompared:
+ return
+
+ self._FindDiffs()
+ self._grapher.AddDiffData(self._diffs)
+ self._haveCompared = True
+
+ def _FindDiffs(self):
+
+ self._diffs = []
+ self._maxDiff = 0
+
+ if len(self._sampleSets) != 2:
+ raise Exception("Comparator: must call AddSpline exactly twice")
+
+ if len(self._sampleSets[0]) != len(self._sampleSets[1]):
+ raise Exception("Mismatched eval results")
+
+ for i in range(len(self._sampleSets[0])):
+
+ sample1 = self._sampleSets[0][i]
+ sample2 = self._sampleSets[1][i]
+
+ if sample2.time - sample1.time > 1e-4:
+ raise Exception("Mismatched eval times at index %d" % i)
+
+ diff = sample2.value - sample1.value
+ self._maxDiff = max(self._maxDiff, abs(diff))
+ self._diffs.append(TsTest_Grapher.Diff(sample1.time, diff))
--- /dev/null
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from .TsTest_Grapher import TsTest_Grapher
+from .TsTest_Comparator import TsTest_Comparator
+from pxr import Ts, Gf
+import os, re
+
+
+def TsTest_CompareBaseline(
+ testName, splineData, samples, precision = 7):
+ """
+ A test helper function for spline evaluation. Compares samples against the
+ contents of a baseline file, and returns whether they match within the
+ specified precision.
+
+ Precision is specified as a number of decimal digits to the right of the
+ decimal point.
+
+ One of the following will occur:
+
+ - If there is no baseline file yet, a candidate baseline file will be
+ written to the test run directory, along with a graph made from the spline
+ data and samples. If the graph looks right, both of these files should be
+ copied into the test source, such that they are installed into a "baseline"
+ subdirectory of the test run directory. (The graph isn't necessary for
+ operation of the test, but is a useful reference for maintainers.) The
+ function will return False in this case.
+
+ - If the spline data, sample times, or precision differ from those recorded
+ in the baseline file, that is an error in the test setup; it is a difference
+ in the test inputs rather than the outputs. Candidate baseline files will
+ be written as in the above case, and the function will return False. If the
+ inputs are being changed deliberately, the new baseline files should be
+ inspected and installed.
+
+ - If any sample values differ from the baseline values by more than the
+ specified precision, all differing samples will be listed on stdout,
+ candidate baseline files will be written, a graph of the differences will
+ also be written, and the function will return False. If the change in
+ output is expected, the diff graph should be inspected and the new baseline
+ files installed.
+
+ - Otherwise, no files will be written, and the function will return True.
+ """
+ baseliner = _Baseliner(testName, splineData, samples, precision)
+ return baseliner.Validate()
+
+
+class _Baseliner(object):
+
+ def __init__(self, testName, splineData, samples, precision):
+
+ self._testName = testName
+ self._fileName = "%s_TsTestBaseline.txt" % testName
+ self._splineData = splineData
+ self._samples = samples
+ self._precision = precision
+ self._epsilon = 10 ** -self._precision
+
+ # These are filled in by _ReadBaseline.
+ self._baselineSplineDesc = ""
+ self._baselineSampleLineOffset = 0
+ self._baselinePrecision = 0
+ self._baselineSamples = []
+
+ def Validate(self):
+
+ # If there's no baseline file, create candidate baseline and graph.
+ baselinePath = os.path.join("baseline", self._fileName)
+ if not os.path.exists(baselinePath):
+ print("%s: no baseline yet" % self._testName)
+ self._WriteCandidates()
+ return False
+
+ # Read baseline file. Verify it can be parsed correctly.
+ with open(baselinePath) as infile:
+ if not self._ReadBaseline(infile, baselinePath):
+ self._WriteCandidates()
+ return False
+
+ # Verify evaluation inputs match baseline inputs.
+ if not self._ValidateInputs():
+ self._WriteCandidates()
+ return False
+
+ # Verify sample values match baseline values.
+ if not self._ValidateValues():
+ self._WriteCandidates()
+ self._WriteDiffGraph()
+ return False
+
+ # Everything matches.
+ return True
+
+ def _ValidateInputs(self):
+
+ # Input spline data should always match.
+ splineDesc = self._splineData.GetDebugDescription()
+ if splineDesc != self._baselineSplineDesc:
+ print("Error: %s: spline data mismatch" % self._testName)
+ print("Baseline:\n%s" % self._baselineSplineDesc)
+ print("Actual:\n%s" % splineDesc)
+ return False
+
+ # Precision should always match.
+ if self._precision != self._baselinePrecision:
+ print("Error: %s: precision mismatch; "
+ "baseline %d, actual %d"
+ & (self._testName, self._baselinePrecision, self._precision))
+ return False
+
+ # Should always have same number of samples.
+ count = len(self._samples)
+ baselineCount = len(self._baselineSamples)
+ if count != baselineCount:
+ print("Error: %s: sample count mismatch; "
+ "baseline %d, actual %d"
+ % (self._testName, baselineCount, count))
+ return False
+
+ # Sample times should always match.
+ for i in range(baselineCount):
+
+ lineNum = self._baselineSampleLineOffset + i
+ base = self._baselineSamples[i]
+ sample = self._samples[i]
+
+ if not Gf.IsClose(sample.time, base.time, self._epsilon):
+ print("Error: %s: time mismatch on line %d; "
+ "baseline %g, actual %g, diff %g"
+ % (self._fileName, lineNum,
+ base.time, sample.time,
+ abs(sample.time - base.time)))
+ return False
+
+ # All inputs match.
+ return True
+
+ def _ValidateValues(self):
+
+ # Examine all samples for mismatches.
+ mismatch = False
+ for i in range(len(self._baselineSamples)):
+
+ lineNum = self._baselineSampleLineOffset + i
+ base = self._baselineSamples[i]
+ sample = self._samples[i]
+
+ if not Gf.IsClose(sample.value, base.value, self._epsilon):
+ print("%s: value mismatch on line %d; "
+ "baseline %g, actual %g, diff %g"
+ % (self._fileName, lineNum,
+ base.value, sample.value,
+ abs(sample.value - base.value)))
+ mismatch = True
+
+ return not mismatch
+
+ def _WriteCandidates(self):
+
+ self._WriteBaseline()
+ self._WriteSingleGraph()
+
+ def _WriteBaseline(self):
+
+ with open(self._fileName, "w") as outfile:
+
+ # Write prologue with input data description.
+ print(self._splineData.GetDebugDescription(),
+ file = outfile, end = "")
+ print("-----", file = outfile)
+
+ # Write samples, one per line. Each is a time and a value.
+ for s in self._samples:
+ print("%.*f %.*f"
+ % (self._precision, s.time, self._precision, s.value),
+ file = outfile)
+
+ print("Wrote baseline candidate %s" % self._fileName)
+
+ def _ReadBaseline(self, infile, path):
+
+ lineNum = 0
+ readingSamples = False
+
+ # Read lines.
+ for line in infile:
+
+ lineNum += 1
+
+ # Read prologue containing spline input data description.
+ if not readingSamples:
+ if line.strip() == "-----":
+ readingSamples = True
+ self._baselineSampleLineOffset = lineNum + 1
+ else:
+ self._baselineSplineDesc += line
+ continue
+
+ # Parse sample lines. Each is a time and a value.
+ match = re.search(r"^(-?\d+\.(\d+)) (-?\d+\.(\d+))$", line)
+ if not match:
+ print(
+ "Error: %s: unexpected format on line %d"
+ % (path, lineNum))
+ return False
+ timeStr, timeFracStr, valStr, valFracStr = match.groups()
+
+ # Verify consistent baseline precision.
+ timePrecision = len(timeFracStr)
+ valPrecision = len(valFracStr)
+ if not self._baselinePrecision:
+ self._baselinePrecision = valPrecision
+ if valPrecision != timePrecision \
+ or valPrecision != self._baselinePrecision:
+ print("Error: %s: inconsistent precision" % path)
+ return False
+
+ # Append a new in-memory sample for each line.
+ sample = Ts.TsTest_Sample()
+ sample.time = float(timeStr)
+ sample.value = float(valStr)
+ self._baselineSamples.append(sample)
+
+ return True
+
+ def _WriteSingleGraph(self):
+
+ if not TsTest_Grapher.Init():
+ return
+
+ grapher = TsTest_Grapher(
+ title = self._testName,
+ widthPx = 1000, heightPx = 750)
+ grapher.AddSpline(self._testName, self._splineData, self._samples)
+
+ graphFileName = "%s_TsTestGraph.png" % self._testName
+ grapher.Write(graphFileName)
+ print("Wrote candidate graph %s" % graphFileName)
+
+ def _WriteDiffGraph(self):
+
+ if not TsTest_Comparator.Init():
+ return
+
+ comparator = TsTest_Comparator(
+ title = self._testName,
+ widthPx = 1000, heightPx = 1500)
+ comparator.AddSpline(
+ "Baseline", self._splineData, self._baselineSamples)
+ comparator.AddSpline(
+ "Actual", self._splineData, self._samples)
+
+ graphFileName = "%s_TsTestDiff.png" % self._testName
+ comparator.Write(graphFileName)
+ print("Wrote diff graph %s" % graphFileName)
--- /dev/null
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from . import TsTest_SplineData as SData
+
+import sys
+
+
+class TsTest_Grapher(object):
+
+ class Spline(object):
+ def __init__(self, name, data, samples, baked):
+ self.data = data
+ self.name = name
+ self.baked = baked
+ self.samples = samples
+
+ class Diff(object):
+ def __init__(self, time, value):
+ self.time = time
+ self.value = value
+
+ class _StyleTable(object):
+
+ class _Region(object):
+ def __init__(self, start, openStart, isDim):
+ self.start = start
+ self.openStart = openStart
+ self.isDim = isDim
+
+ def __init__(self, data, forKnots):
+
+ self._regions = []
+
+ knots = list(data.GetKnots())
+ lp = data.GetInnerLoopParams()
+
+ # Before first knot: dim
+ self._regions.append(self._Region(
+ float('-inf'),
+ openStart = True, isDim = True))
+
+ # No knots: all dim
+ if not knots:
+ return
+
+ # After first knot: normal
+ self._regions.append(self._Region(
+ knots[0].time,
+ openStart = False, isDim = False))
+
+ # Looping regions
+ if lp.enabled:
+
+ # Prepeats: dim
+ if lp.preLoopStart < lp.protoStart:
+ self._regions.append(self._Region(
+ lp.preLoopStart,
+ openStart = False, isDim = True))
+
+ # Prototype: normal
+ self._regions.append(self._Region(
+ lp.protoStart,
+ openStart = False, isDim = False))
+
+ # Repeats: dim
+ if lp.postLoopEnd > lp.protoEnd:
+ self._regions.append(self._Region(
+ lp.protoEnd,
+ openStart = False, isDim = True))
+
+ # After: normal. A knot exactly on the boundary belongs to the
+ # prior region (openStart).
+ self._regions.append(self._Region(
+ lp.postLoopEnd,
+ openStart = forKnots, isDim = False))
+
+ # After last knot: dim. A knot exactly on the boundary belongs to
+ # the prior region (openStart).
+ self._regions.append(self._Region(
+ knots[-1].time, openStart = forKnots, isDim = True))
+
+ def IsDim(self, time):
+
+ numRegs = len(self._regions)
+ for i in range(numRegs):
+
+ reg = self._regions[i]
+ nextReg = self._regions[i + 1] if i < numRegs - 1 else None
+
+ # Search regions in order until we either run out, or find one
+ # whose start time exceeds the specified time. There can be
+ # consecutive regions with identical start times; in those
+ # cases, we will return the later region, which is what we want
+ # as far as drawing goes.
+ if ((not nextReg)
+ or nextReg.start > time
+ or (nextReg.openStart and nextReg.start == time)):
+ return reg.isDim
+
+ assert False, "Can't find region"
+
+ class _KeyframeData(object):
+
+ def __init__(self, splineData):
+
+ self.splineData = splineData
+
+ # Points: 1D arrays
+ self.knotTimes, self.knotValues = [], []
+ self.tanPtTimes, self.tanPtValues = [], []
+
+ # Lines: 2D arrays
+ self.tanLineTimes, self.tanLineValues = [[], []], [[], []]
+
+ def Draw(self, ax, color):
+
+ if self.knotTimes:
+ ax.scatter(
+ self.knotTimes, self.knotValues,
+ color = color, marker = "o")
+
+ if self.tanPtTimes:
+ if not self.splineData.GetIsHermite():
+ ax.scatter(
+ self.tanPtTimes, self.tanPtValues,
+ color = color, marker = "s")
+ ax.plot(
+ self.tanLineTimes, self.tanLineValues,
+ color = color, linestyle = "dashed")
+
+ @classmethod
+ def Init(cls):
+
+ if hasattr(cls, "_initialized"):
+ return cls._initialized
+
+ try:
+ import matplotlib
+ cls._initialized = True
+ return True
+ except ImportError:
+ cls._initialized = False
+ print("Could not import matplotlib. "
+ "Graphical output is disabled. "
+ "To enable it, install the Python matplotlib module.",
+ file = sys.stderr)
+ return False
+
+ def __init__(
+ self, title,
+ widthPx = 1000, heightPx = 750,
+ includeScales = True):
+
+ self._title = title
+ self._widthPx = widthPx
+ self._heightPx = heightPx
+ self._includeScales = includeScales
+
+ self._splines = []
+ self._diffs = None
+ self._figure = None
+
+ def AddSpline(self, name, splineData, samples, baked = None):
+
+ self._splines.append(
+ TsTest_Grapher.Spline(
+ name, splineData, samples,
+ baked or splineData))
+
+ # Reset the graph in case we're working incrementally.
+ self._ClearGraph()
+
+ def AddDiffData(self, diffs):
+ self._diffs = diffs
+
+ def Display(self):
+ self._MakeGraph()
+ from matplotlib import pyplot
+ pyplot.show()
+
+ def Write(self, filePath):
+ self._MakeGraph()
+ self._figure.savefig(filePath)
+
+ @staticmethod
+ def _DimColor(colorStr):
+ """Transform a matplotlib color string to reduced opacity."""
+ from matplotlib import colors
+ return colors.to_rgba(colorStr, 0.35)
+
+ def _ConfigureAxes(self, axes):
+ """
+ Set whole-axes properties.
+ """
+ from matplotlib import ticker
+
+ if not self._includeScales:
+ axes.get_xaxis().set_major_locator(ticker.NullLocator())
+ axes.get_xaxis().set_minor_locator(ticker.NullLocator())
+ axes.get_yaxis().set_major_locator(ticker.NullLocator())
+ axes.get_yaxis().set_major_locator(ticker.NullLocator())
+
+ def _MakeGraph(self):
+ """
+ Set up self._figure, a matplotlib Figure.
+ """
+ if self._figure:
+ return
+
+ if not TsTest_Grapher.Init():
+ raise Exception("matplotlib initialization failed")
+
+ from matplotlib import pyplot, lines
+
+ # Grab the color cycle. Default colors are fine. These are '#rrggbb'
+ # strings.
+ colorCycle = pyplot.rcParams['axes.prop_cycle'].by_key()['color']
+
+ # Figure, with one or two graphs
+ numGraphs = 2 if self._diffs else 1
+ self._figure, axSet = pyplot.subplots(
+ nrows = numGraphs, squeeze = False)
+ self._figure.set(
+ dpi = 100.0,
+ figwidth = self._widthPx / 100.0,
+ figheight = self._heightPx / 100.0)
+
+ # Main graph
+ axMain = axSet[0][0]
+ axMain.set_title(self._title)
+ self._ConfigureAxes(axMain)
+
+ legendNames = []
+ legendLines = []
+
+ # Individual splines
+ for splineIdx in range(len(self._splines)):
+
+ # Determine drawing color for this spline.
+ splineColor = colorCycle[splineIdx]
+
+ # Collect legend data. Fake up a Line2D artist.
+ legendNames.append(self._splines[splineIdx].name)
+ legendLines.append(lines.Line2D([0], [0], color = splineColor))
+
+ sampleTimes = []
+ sampleValues = []
+
+ # Build style region tables.
+ styleTable = self._StyleTable(
+ self._splines[splineIdx].data,
+ forKnots = False)
+ knotStyleTable = self._StyleTable(
+ self._splines[splineIdx].data,
+ forKnots = True)
+
+ # Find regions to draw. A region is a time extent in which the
+ # spline is drawn with the same styling. Things that require new
+ # regions include vertical discontinuities, extrapolation, and
+ # looping.
+ samples = self._splines[splineIdx].samples
+ for sampleIdx in range(len(samples)):
+
+ # Determine whether we have a vertical discontinuity. That is
+ # signaled by two consecutive samples with identical times.
+ # Allow some fuzz in the detection of "identical", since
+ # left-side evaluation in Maya is emulated with a small delta.
+ sample = samples[sampleIdx]
+ nextSample = samples[sampleIdx + 1] \
+ if sampleIdx < len(samples) - 1 else None
+ isCliff = (nextSample
+ and abs(nextSample.time - sample.time) < 1e-4)
+
+ # Determine whether we have crossed into a different style
+ # region.
+ isRegionEnd = (
+ nextSample
+ and styleTable.IsDim(nextSample.time) !=
+ styleTable.IsDim(sample.time))
+
+ # Append this sample for drawing.
+ sampleTimes.append(sample.time)
+ sampleValues.append(sample.value)
+
+ # If this is the end of a region, and sample times have been set
+ # up correctly, then the next sample falls exactly on a region
+ # boundary. Include that sample to end this region... unless
+ # this is also a cliff, in which case the vertical will span the
+ # gap instead.
+ if isRegionEnd and not isCliff:
+ sampleTimes.append(nextSample.time)
+ sampleValues.append(nextSample.value)
+
+ # At the end of each region, draw.
+ if (not nextSample) or isCliff or isRegionEnd:
+
+ # Use this spline's color, possibly dimmed.
+ color = splineColor
+ if styleTable.IsDim(sample.time):
+ color = self._DimColor(color)
+
+ # Draw.
+ axMain.plot(sampleTimes, sampleValues, color = color)
+
+ # Reset data for next region.
+ sampleTimes = []
+ sampleValues = []
+
+ # At discontinuities, draw a dashed vertical.
+ if isCliff:
+
+ # Verticals span no time, so the region rules are the same
+ # as for knots.
+ color = splineColor
+ if knotStyleTable.IsDim(sample.time):
+ color = self._DimColor(color)
+
+ axMain.plot(
+ [sample.time, nextSample.time],
+ [sample.value, nextSample.value],
+ color = color,
+ linestyle = "dashed")
+
+ # Legend, if multiple splines
+ if len(legendNames) > 1:
+ axMain.legend(legendLines, legendNames)
+
+ # Determine if all splines have the same keyframes and parameters.
+ sharedData = not any(
+ s for s in self._splines[1:] if s.baked != self._splines[0].baked)
+
+ # Keyframe points and tangents
+ knotSplines = [self._splines[0]] if sharedData else self._splines
+ for splineIdx in range(len(knotSplines)):
+ splineData = knotSplines[splineIdx].baked
+
+ # Build style region table.
+ styleTable = self._StyleTable(
+ knotSplines[splineIdx].data,
+ forKnots = True)
+
+ normalKnotData = self._KeyframeData(splineData)
+ dimKnotData = self._KeyframeData(splineData)
+
+ knots = list(splineData.GetKnots())
+ for knotIdx in range(len(knots)):
+ knot = knots[knotIdx]
+ prevKnot = knots[knotIdx - 1] if knotIdx > 0 else None
+ nextKnot = \
+ knots[knotIdx + 1] if knotIdx < len(knots) - 1 else None
+
+ # Decide whether to draw dim or not
+ if styleTable.IsDim(knot.time):
+ knotData = dimKnotData
+ else:
+ knotData = normalKnotData
+
+ # Pre-value
+ if knotIdx > 0 \
+ and prevKnot.nextSegInterpMethod == SData.InterpHeld:
+ knotData.knotTimes.append(knot.time)
+ knotData.knotValues.append(prevKnot.value)
+ elif knot.isDualValued:
+ knotData.knotTimes.append(knot.time)
+ knotData.knotValues.append(knot.preValue)
+
+ # Knot
+ knotData.knotTimes.append(knot.time)
+ knotData.knotValues.append(knot.value)
+
+ # In-tangent
+ if prevKnot \
+ and prevKnot.nextSegInterpMethod == SData.InterpCurve \
+ and not knot.preAuto:
+
+ if splineData.GetIsHermite():
+ preLen = (knot.time - prevKnot.time) / 3.0
+ else:
+ preLen = knot.preLen
+
+ if preLen > 0:
+ value = \
+ knot.preValue if knot.isDualValued else knot.value
+ knotData.tanPtTimes.append(knot.time - preLen)
+ knotData.tanPtValues.append(
+ value - knot.preSlope * preLen)
+ knotData.tanLineTimes[0].append(knotData.tanPtTimes[-1])
+ knotData.tanLineTimes[1].append(knot.time)
+ knotData.tanLineValues[0].append(
+ knotData.tanPtValues[-1])
+ knotData.tanLineValues[1].append(value)
+
+ # Out-tangent
+ if nextKnot \
+ and knot.nextSegInterpMethod == SData.InterpCurve \
+ and not knot.postAuto:
+
+ if splineData.GetIsHermite():
+ postLen = (nextKnot.time - knot.time) / 3.0
+ else:
+ postLen = knot.postLen
+
+ if postLen > 0:
+ knotData.tanPtTimes.append(knot.time + postLen)
+ knotData.tanPtValues.append(
+ knot.value + knot.postSlope * postLen)
+ knotData.tanLineTimes[0].append(knot.time)
+ knotData.tanLineTimes[1].append(
+ knotData.tanPtTimes[-1])
+ knotData.tanLineValues[0].append(knot.value)
+ knotData.tanLineValues[1].append(
+ knotData.tanPtValues[-1])
+
+ # Draw keyframes and tangents.
+ color = 'black' if sharedData else colorCycle[splineIdx]
+ normalKnotData.Draw(axMain, color)
+ dimKnotData.Draw(axMain, self._DimColor(color))
+
+ # Diff graph
+ if self._diffs:
+ axDiffs = axSet[1][0]
+ axDiffs.set_title("Diffs")
+ self._ConfigureAxes(axDiffs)
+ diffTimes = [d.time for d in self._diffs]
+ diffValues = [d.value for d in self._diffs]
+ axDiffs.plot(diffTimes, diffValues)
+
+ def _ClearGraph(self):
+
+ if self._figure:
+ self._figure = None
+ from matplotlib import pyplot
+ pyplot.clf()
--- /dev/null
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+#
+################################################################################
+# Runs inside mayapy.
+# Started by, and communicates via pipe with, TsTest_MayapyEvaluator.
+# Tested against Maya 2022 Update 4, Linux (Python 3.7.7).
+################################################################################
+#
+
+import maya.standalone
+from maya.api.OpenMayaAnim import MFnAnimCurve as Curve
+from maya.api.OpenMaya import MTime
+import sys
+
+
+gDebugFilePath = None
+
+
+class Ts(object):
+ """
+ POD classes coresponding to TsTest classes. Rather than try to get libTs to
+ import into mayapy, we just use these equivalents as containers, so that we
+ can use eval and repr to pass objects in and out of this script.
+ """
+ class TsTest_SplineData(object):
+
+ InterpHeld = 0
+ InterpLinear = 1
+ InterpCurve = 2
+
+ ExtrapHeld = 0
+ ExtrapLinear = 1
+ ExtrapSloped = 2
+ ExtrapLoop = 3
+
+ LoopNone = 0
+ LoopContinue = 1
+ LoopRepeat = 2
+ LoopReset = 3
+ LoopOscillate = 4
+
+ def __init__(self,
+ isHermite,
+ preExtrapolation, postExtrapolation,
+ knots = [],
+ innerLoopParams = None):
+ self.isHermite = isHermite
+ self.preExtrapolation = preExtrapolation
+ self.postExtrapolation = postExtrapolation
+ self.knots = knots
+ self.innerLoopParams = innerLoopParams
+
+ class Knot(object):
+ def __init__(self,
+ time, nextSegInterpMethod, value,
+ preSlope, postSlope, preLen, postLen, preAuto, postAuto,
+ leftValue = None):
+ self.time = time
+ self.nextSegInterpMethod = nextSegInterpMethod
+ self.value = value
+ self.preSlope = preSlope
+ self.postSlope = postSlope
+ self.preLen = preLen
+ self.postLen = postLen
+ self.preAuto = preAuto
+ self.postAuto = postAuto
+ self.leftValue = leftValue
+
+ class InnerLoopParams(object):
+ def __init__(self,
+ enabled,
+ protoStart, protoLen,
+ numPreLoops, numPostLoops):
+ self.enabled = enabled
+ self.protoStart = protoStart
+ self.protoLen = protoLen
+ self.numPreLoops = numPreLoops
+ self.numPostLoops = numPostLoops
+
+ class Extrapolation(object):
+ def __init__(self, method, slope = 0.0, loopMode = 0):
+ self.method = method
+ self.slope = slope
+ self.loopMode = loopMode
+
+ class TsTest_SampleTimes(object):
+
+ def __init__(self, times):
+ self.times = times
+
+ class SampleTime(object):
+ def __init__(self, time, left):
+ self.time = time
+ self.left = left
+
+ class TsTest_Sample(object):
+
+ def __init__(self, time, value):
+ self.time = float(time)
+ self.value = float(value)
+
+ def __repr__(self):
+ return "Ts.TsTest_Sample(" \
+ "float.fromhex('%s'), float.fromhex('%s'))" \
+ % (float.hex(self.time), float.hex(self.value))
+
+# Convenience abbreviation
+SData = Ts.TsTest_SplineData
+
+
+_InterpTypeMap = {
+ SData.InterpHeld: Curve.kTangentStep,
+ SData.InterpLinear: Curve.kTangentLinear,
+ SData.InterpCurve: Curve.kTangentGlobal
+}
+
+_AutoTypeMap = {
+ "auto": Curve.kTangentAuto,
+ "smooth": Curve.kTangentSmooth
+}
+
+_ExtrapTypeMap = {
+ SData.ExtrapHeld: Curve.kConstant,
+ SData.ExtrapLinear: Curve.kLinear,
+ SData.ExtrapSloped: Curve.kLinear,
+}
+
+_ExtrapLoopMap = {
+ SData.LoopRepeat: Curve.kCycleRelative,
+ SData.LoopReset: Curve.kCycle,
+ SData.LoopOscillate: Curve.kOscillate
+}
+
+def _GetTanType(tanType, isAuto, opts):
+
+ if tanType != Curve.kTangentGlobal \
+ or not isAuto:
+ return tanType
+
+ return _AutoTypeMap[opts["autoTanMethod"]]
+
+def SetUpCurve(curve, data, opts):
+
+ # types: Curve.kTangent{Global,Linear,Step,Smooth,Auto}
+ # XXX:
+ # figure out what global means
+ # verify fixed, flat, clamped, and plateau aren't needed (auth hints)
+ # autodesk said 3 smooth types - what's the third?
+
+ # Maya Bezier tangent specifications:
+ # X coordinates are time, Y coordinates are value.
+ # They specify tangent endpoints.
+ # They are relative to keyframe time and value.
+ # Both are negated for in-tangents (+X = longer, +Y = pointing down).
+ # X and Y values must be multiplied by 3.
+
+ tanType = Curve.kTangentGlobal
+
+ times = []
+ values = []
+ tanInTypes = []
+ tanInXs = []
+ tanInYs = []
+ tanOutTypes = []
+ tanOutXs = []
+ tanOutYs = []
+
+ # Build keyframe data.
+ for knot in data.knots:
+
+ if knot.leftValue is not None:
+ raise Exception("Maya splines can't use dual-valued knots")
+
+ # Time and value are straightforward.
+ times.append(knot.time)
+ values.append(knot.value)
+
+ if data.isHermite:
+ # Hermite spline. Tan lengths may be zero and are ignored. Any
+ # nonzero length will allow us to establish a slope in X and Y, so
+ # use length 1.
+ preLen = 1.0
+ postLen = 1.0
+ else:
+ # Non-Hermite spline. Use tan lengths as specified. Multiply by 3.
+ preLen = knot.preLen * 3.0
+ postLen = knot.postLen * 3.0
+
+ # Use previous segment type as in-tan type.
+ tanInTypes.append(_GetTanType(tanType, knot.preAuto, opts))
+ tanInXs.append(preLen)
+ tanInYs.append(knot.preSlope * preLen)
+
+ # Determine new out-tan type and record that for the next in-tan.
+ tanType = _InterpTypeMap[knot.nextSegInterpMethod]
+ tanOutTypes.append(_GetTanType(tanType, knot.postAuto, opts))
+ tanOutXs.append(postLen)
+ tanOutYs.append(knot.postSlope * postLen)
+
+ # Implement linear & sloped pre-extrap with explicit linear tangents.
+ if data.preExtrapolation.method == SData.ExtrapLinear:
+ tanInTypes[0] = Curve.kTangentLinear
+ if len(times) == 1 or tanOutTypes[0] == Curve.kTangentStep:
+ tanInXs[0] = 1.0
+ tanInYs[0] = 0.0
+ elif tanOutTypes[0] == Curve.kTangentLinear:
+ tanInXs[0] = times[1] - times[0]
+ tanInYs[0] = values[1] - values[0]
+ else:
+ tanInXs[0] = tanOutXs[0]
+ tanInYs[0] = tanOutYs[0]
+ elif data.preExtrapolation.method == SData.ExtrapSloped:
+ tanInTypes[0] = Curve.kTangentLinear
+ tanInXs[0] = 1.0
+ tanInYs[0] = data.preExtrapolation.slope
+
+ # Implement linear & sloped post-extrap with explicit linear tangents.
+ if data.postExtrapolation.method == SData.ExtrapLinear:
+ tanOutTypes[-1] = Curve.kTangentLinear
+ if len(times) == 1 or tanInTypes[-1] == Curve.kTangentStep:
+ tanOutXs[-1] = 1.0
+ tanOutYs[-1] = 0.0
+ elif tanInTypes[-1] == Curve.kTangentLinear:
+ tanOutXs[-1] = times[-1] - times[-2]
+ tanOutYs[-1] = values[-1] - values[-2]
+ else:
+ tanOutXs[-1] = tanInXs[-1]
+ tanOutYs[-1] = tanInYs[-1]
+ elif data.postExtrapolation.method == SData.ExtrapSloped:
+ tanOutTypes[-1] = Curve.kTangentLinear
+ tanOutXs[-1] = 1.0
+ tanOutYs[-1] = data.postExtrapolation.slope
+
+ # Debug dump.
+ _Debug("times: %s" % times)
+ _Debug("values: %s" % values)
+ _Debug("tanInTypes: %s" % tanInTypes)
+ _Debug("tanInXs: %s" % tanInXs)
+ _Debug("tanInYs: %s" % tanInYs)
+ _Debug("tanOutTypes: %s" % tanOutTypes)
+ _Debug("tanOutXs: %s" % tanOutXs)
+ _Debug("tanOutYs: %s" % tanOutYs)
+
+ # Set overall spline flags.
+ curve.setIsWeighted(not data.isHermite)
+
+ # Set pre-infinity type.
+ if data.preExtrapolation.method == SData.ExtrapLoop:
+ curve.setPreInfinityType(
+ _ExtrapLoopMap[data.preExtrapolation.loopMode])
+ else:
+ curve.setPreInfinityType(
+ _ExtrapTypeMap[data.preExtrapolation.method])
+
+ # Set post-infinity type.
+ if data.postExtrapolation.method == SData.ExtrapLoop:
+ curve.setPostInfinityType(
+ _ExtrapLoopMap[data.postExtrapolation.loopMode])
+ else:
+ curve.setPostInfinityType(
+ _ExtrapTypeMap[data.postExtrapolation.method])
+
+ # I haven't been able to find a way to add diverse keyframes without API
+ # problems. So far, the most effective workaround appears to be (1) call
+ # addKeysWithTangents; (2) call set{In,Out}TangentType for certain types.
+
+ # Create keyframes.
+ curve.addKeysWithTangents(
+ [MTime(t) for t in times], values,
+ tangentInTypeArray = tanInTypes,
+ tangentInXArray = tanInXs, tangentInYArray = tanInYs,
+ tangentOutTypeArray = tanOutTypes,
+ tangentOutXArray = tanOutXs, tangentOutYArray = tanOutYs)
+
+ # Re-establish certain tangent types that seem to get confused by
+ # addKeysWithTangents.
+ for i in range(len(data.knots)):
+ if data.knots[i].preAuto:
+ curve.setInTangentType(i, tanInTypes[i])
+ if data.knots[i].nextSegInterpMethod == SData.InterpHeld \
+ or data.knots[i].postAuto:
+ curve.setOutTangentType(i, tanOutTypes[i])
+
+def DoEval(data, times, opts):
+
+ if data.innerLoopParams:
+ raise Exception("Maya splines can't use inner loops")
+
+ if (data.preExtrapolation.loopMode == SData.LoopContinue
+ or data.postExtrapolation.loopMode == SData.LoopContinue):
+ raise Exception("Maya splines can't use LoopContinue")
+
+ result = []
+
+ curve = Curve()
+ curveObj = curve.create(Curve.kAnimCurveTU)
+
+ if data.knots:
+ SetUpCurve(curve, data, opts)
+
+ for sampleTime in times.times:
+
+ # Emulate left-side evaluation by subtracting an arbitrary tiny time
+ # delta (but large enough to avoid Maya snapping it to a knot time). We
+ # will return a sample with a differing time, which will allow the
+ # result to be understood as a small delta rather than an instantaneous
+ # change.
+ time = sampleTime.time
+ if sampleTime.left:
+ time -= 1e-5
+
+ value = curve.evaluate(MTime(time))
+ result.append(Ts.TsTest_Sample(time, value))
+
+ return result
+
+
+def _Debug(text, truncate = False):
+
+ if not gDebugFilePath:
+ return
+
+ # Open, write, and close every time.
+ # This is slow, but reliable.
+ mode = "w" if truncate else "a"
+ with open(gDebugFilePath, mode) as outfile:
+ print(text, file = outfile)
+
+if __name__ == "__main__":
+
+ # Accept optional debug file path command-line arg.
+ if len(sys.argv) > 1:
+ gDebugFilePath = sys.argv[1]
+
+ # Initialize, then signal readiness with an empty line on stdout.
+ # The initialize() call can take a long time, like 30 seconds.
+ _Debug("Initializing...", truncate = True)
+ maya.standalone.initialize("Python")
+ _Debug("Done initializing")
+ print()
+ sys.stdout.flush()
+
+ # Loop until we're killed.
+ while True:
+ # Read until newline, then eval to deserialize.
+ _Debug("Reading...")
+ data, times, opts = eval(sys.stdin.readline())
+ _Debug("Done reading")
+
+ # Perform spline evaluation.
+ result = DoEval(data, times, opts)
+
+ # Serialize output with repr. The print() function includes a
+ # newline terminator, which will signal end of output.
+ _Debug("Writing...")
+ print(repr(result))
+ sys.stdout.flush()
--- /dev/null
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr import Ts
+import os, subprocess, threading, queue, selectors
+
+
+class TsTest_MayapyEvaluator(object):
+ """
+ An implementation of TsTest_Evaluator, though not formally derived.
+ Evaluates spline data using mayapy. Creates a child process that runs in
+ mayapy, and communicates with it.
+ """
+
+ # XXX: would be nice to switch from text mode and readline to raw mode and
+ # os.read. This would allow us to just read whatever is available in a
+ # pipe, without blocking. This might improve reliability. So far, however,
+ # the readline solution is working; even if the child dies with an error,
+ # complete lines of output are sent.
+
+ # Override this method in a subclass in order to do something with log
+ # messages. This is only for debugging. The messages passed to this method
+ # are already newline-terminated.
+ #
+ # Note that this is one of two debug logs. It is used for debug messages
+ # from this process (the parent process). The other log is for debug
+ # messages from the child process, which runs in mayapy. That log file is
+ # specified in the subprocessDebugFilePath argument to the constructor.
+ def _DebugLog(self, msg):
+ pass
+
+ def __init__(self, mayapyPath, subprocessDebugFilePath = None):
+
+ self._stderrThread = None
+
+ # Verify we can find mayapy, whose location is specified by our caller.
+ assert os.path.isfile(mayapyPath)
+
+ # Find the mayapy script that sits next to us.
+ mayapyScriptPath = os.path.join(
+ os.path.dirname(__file__), "TsTest_MayapyDriver.py")
+ assert os.path.isfile(mayapyScriptPath)
+
+ # Modify environment for child process. Make I/O unbuffered so that we
+ # immediately get any stderr output generated by the Maya guts.
+ envDict = dict(os.environ)
+ envDict["PYTHONUNBUFFERED"] = "1"
+
+ # Create subprocess args: mayapy binary, script path, options.
+ args = [mayapyPath, mayapyScriptPath]
+ if subprocessDebugFilePath:
+ args.append(subprocessDebugFilePath)
+
+ # Start MayapyDriver in an asynchronous subprocess.
+ self._DebugLog("Starting MayapyDriver...\n")
+ self._DebugLog(str.join(" ", args) + "\n")
+ self._mayapyProcess = subprocess.Popen(
+ args,
+ env = envDict,
+ stdin = subprocess.PIPE,
+ stdout = subprocess.PIPE,
+ stderr = subprocess.PIPE,
+ text = True)
+ if not self._IsMayapyRunning():
+ raise Exception("Can't start MayapyDriver")
+
+ # Start stderr reader thread for nonblocking reads.
+ self._stderrQueue = queue.SimpleQueue()
+ self._stderrThread = threading.Thread(
+ target = self._StderrThreadMain)
+ self._stderrThreadExit = False
+ self._stderrThread.start()
+
+ # Set up stdout polling interface.
+ self._stdoutSelector = selectors.DefaultSelector()
+ self._stdoutSelector.register(
+ self._mayapyProcess.stdout, selectors.EVENT_READ)
+
+ # Wait for MayapyDriver to signal readiness for input.
+ # It sends an empty line on stdout after initialization.
+ self._ReadFromChild()
+ self._DebugLog("Done starting MayapyDriver\n")
+
+ def __del__(self):
+ self.Shutdown(wait = False)
+
+ def Shutdown(self, wait = True):
+
+ # Tell thread to exit.
+ self._stderrThreadExit = True
+
+ # Clean up child process. This will unblock the thread.
+ if self._IsMayapyRunning():
+ self._DebugLog("Terminating MayapyDriver...\n")
+ self._mayapyProcess.terminate()
+ if wait:
+ self._mayapyProcess.wait()
+ self._mayapyProcess.stdin.close()
+ self._mayapyProcess.stdout.close()
+ self._mayapyProcess.stderr.close()
+ self._DebugLog("Done terminating MayapyDriver\n")
+
+ # Wait for reader thread to exit.
+ if wait and self._stderrThread:
+ self._stderrThread.join()
+
+ def _IsMayapyRunning(self):
+ """
+ Return whether the MayapyDriver process is running and ready for input.
+ """
+ return self._mayapyProcess.poll() is None
+
+ def _StderrThreadMain(self):
+ """
+ Repeatedly read from the child process' stderr, and post the resulting
+ lines of text to the queue.
+ """
+ while True:
+ # XXX: it seems like this readline() could hang forever if the child
+ # dies, but in practice this hasn't happened.
+ line = self._mayapyProcess.stderr.readline()
+ if line:
+ self._stderrQueue.put(line)
+ if self._stderrThreadExit:
+ break
+
+ def _ReadFromChild(self):
+ """
+ Wait for child to either produce newline-terminated output on stdout, or
+ die. On success, return the output, including the newline. On failure,
+ raise an exception.
+ """
+ # Poll for output, which is newline-terminated. Also poll for child
+ # death.
+ self._DebugLog("Waiting for MayapyDriver output...\n")
+ stdoutStr = None
+ while True:
+ if not self._mayapyProcess.stdout.readable() \
+ or not self._IsMayapyRunning():
+ # Child has died.
+ break
+ if self._stdoutSelector.select(timeout = 1):
+ # Ready to read; assume we will be able read an entire line,
+ # possibly after blocking. The child could theoretically die in
+ # the middle of sending output, but that hasn't been observed.
+ stdoutStr = self._mayapyProcess.stdout.readline()
+ break
+ self._DebugLog("Done waiting for output\n")
+
+ # If there is anything on stderr, display it. This can happen whether
+ # or not the child process has died.
+ stderrLines = []
+ while True:
+ try:
+ line = self._stderrQueue.get_nowait()
+ if line:
+ stderrLines.append(line)
+ except queue.Empty:
+ break
+ if stderrLines:
+ self._DebugLog("MayapyDriver stderr:\n")
+ for line in stderrLines:
+ self._DebugLog(line)
+
+ # If the child has died, dump any accumulated stdout, then bail.
+ if not self._IsMayapyRunning():
+ if stdoutStr:
+ self._DebugLog("MayapyDriver stdout:\n")
+ self._DebugLog(stdoutStr + "\n")
+ raise Exception("MayapyDriver failure")
+
+ return stdoutStr
+
+ def Eval(self, data, times, opts = {}):
+ """
+ Send repr((data, times, opts)) to the MayapyDriver process.
+ Wait; read, eval, and return result from the MayapyDriver process.
+ These are the expected types, although they are opaque to this function:
+ data: Ts.TestUtilsSplineData
+ times: Ts.TestUtilsSampleTimes
+ opts: dict
+ autoTanMethod: 'auto' or 'smooth'
+ return value: list(Ts.TsTest_Sample)
+ """
+ if not self._IsMayapyRunning():
+ raise Exception("mayapy not running")
+
+ # Send input. Use repr for serialization. The print() function
+ # includes a newline terminator, which will signal end of input.
+ inputStr = repr((data, times, opts))
+ print(inputStr, file = self._mayapyProcess.stdin, flush = True)
+
+ # Wait for output. Deserialize with eval.
+ outputStr = self._ReadFromChild()
+ return eval(outputStr)
--- /dev/null
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr import Tf
+Tf.PreparePythonModule()
+del Tf
+
+from .TsTest_CompareBaseline import TsTest_CompareBaseline
+from .TsTest_Grapher import TsTest_Grapher
+from .TsTest_Comparator import TsTest_Comparator
+
+# MayapyEvaluator isn't always built.
+try:
+ from .TsTest_MayapyEvaluator import TsTest_MayapyEvaluator
+except ImportError:
+ pass
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_USD_TS_API_H
+#define PXR_USD_TS_API_H
+
+#include "pxr/base/arch/export.h"
+
+#if defined(PXR_STATIC)
+# define TS_API
+# define TS_API_TEMPLATE_CLASS(...)
+# define TS_API_TEMPLATE_STRUCT(...)
+# define TS_LOCAL
+#else
+# if defined(TS_EXPORTS)
+# define TS_API ARCH_EXPORT
+# define TS_API_TEMPLATE_CLASS(...) ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__)
+# define TS_API_TEMPLATE_STRUCT(...) ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__)
+# else
+# define TS_API ARCH_IMPORT
+# define TS_API_TEMPLATE_CLASS(...) ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__)
+# define TS_API_TEMPLATE_STRUCT(...) ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__)
+# endif
+# define TS_LOCAL ARCH_HIDDEN
+#endif
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/data.h"
+
+#include <cmath>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+static const double Ts_slopeDiffMax = 1.0e-4;
+
+template <>
+void
+Ts_TypedData<float>::ResetTangentSymmetryBroken()
+{
+ if (_knotType == TsKnotBezier) {
+ float slopeDiff = fabs(_GetLeftTangentSlope() -
+ _GetRightTangentSlope());
+ if (slopeDiff >= Ts_slopeDiffMax) {
+ SetTangentSymmetryBroken(true);
+ }
+ }
+}
+
+template <>
+void
+Ts_TypedData<double>::ResetTangentSymmetryBroken()
+{
+ if (_knotType == TsKnotBezier) {
+ double slopeDiff = fabs(_GetLeftTangentSlope() -
+ _GetRightTangentSlope());
+ if (slopeDiff >= Ts_slopeDiffMax) {
+ SetTangentSymmetryBroken(true);
+ }
+ }
+}
+
+template <>
+bool
+Ts_TypedData<float>::ValueCanBeInterpolated() const
+{
+ return std::isfinite(_GetRightValue()) &&
+ (!_isDual || std::isfinite(_GetLeftValue()));
+}
+
+template <>
+bool
+Ts_TypedData<double>::ValueCanBeInterpolated() const
+{
+ return std::isfinite(_GetRightValue()) &&
+ (!_isDual || std::isfinite(_GetLeftValue()));
+}
+
+template class Ts_TypedData<float>;
+template class Ts_TypedData<double>;
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_DATA_H
+#define PXR_BASE_TS_DATA_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/arch/demangle.h"
+#include "pxr/base/gf/math.h"
+#include "pxr/base/tf/diagnostic.h"
+#include "pxr/base/ts/evalCache.h"
+#include "pxr/base/ts/mathUtils.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/vt/value.h"
+
+#include <string>
+#include <math.h>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class Ts_PolymorphicDataHolder;
+
+/// \class Ts_Data
+/// \brief Holds the data for an TsKeyFrame.
+///
+/// \c Ts_Data is an interface for holding \c TsKeyFrame data.
+///
+class Ts_Data {
+public:
+ virtual ~Ts_Data() = default;
+ virtual void CloneInto(Ts_PolymorphicDataHolder *holder) const = 0;
+
+ // Create and return an EvalCache that represents the spline segment from
+ // this keyframe to kf2.
+ virtual std::shared_ptr<Ts_UntypedEvalCache> CreateEvalCache(
+ Ts_Data const* kf2) const = 0;
+
+ // Evaluate between this keyframe data and \p kf2 at \p time. This is
+ // useful for callers that do not otherwise want or need to create/retain an
+ // eval cache.
+ virtual VtValue
+ EvalUncached(Ts_Data const *kf2, TsTime time) const = 0;
+
+ // Evaluate the derivative between this keyframe data and \p kf2 at \p time.
+ // This is useful for callers that do not otherwise want or need to
+ // create/retain an eval cache.
+ virtual VtValue
+ EvalDerivativeUncached(Ts_Data const *kf2, TsTime time) const = 0;
+
+ virtual bool operator==(const Ts_Data &) const = 0;
+
+ // Time
+ inline TsTime GetTime() const {
+ return _time;
+ }
+ inline void SetTime(TsTime newTime) {
+ _time = newTime;
+ }
+
+ // Knot type
+ virtual TsKnotType GetKnotType() const = 0;
+ virtual void SetKnotType( TsKnotType knotType ) = 0;
+ virtual bool CanSetKnotType( TsKnotType knotType,
+ std::string *reason ) const = 0;
+
+ // Values
+ virtual VtValue GetValue() const = 0;
+ virtual void SetValue( VtValue val ) = 0;
+ virtual VtValue GetValueDerivative() const = 0;
+ virtual bool GetIsDualValued() const = 0;
+ virtual void SetIsDualValued( bool isDual ) = 0;
+ virtual VtValue GetLeftValue() const = 0;
+ virtual VtValue GetLeftValueDerivative() const = 0;
+ virtual void SetLeftValue( VtValue ) = 0;
+ virtual VtValue GetZero() const = 0;
+ virtual bool ValueCanBeInterpolated() const = 0;
+
+ // Extrapolation.
+ // Note these methods don't actually use any data from this object
+ // and only depend on the spline type and the given parameters.
+ //
+ virtual VtValue GetSlope( const Ts_Data& ) const = 0;
+ virtual VtValue Extrapolate( const VtValue& value, TsTime dt,
+ const VtValue& slope) const = 0;
+
+ // Tangents
+
+ /// True if the data type supports tangents, and the knot type is one that
+ /// shows tangents in the UI. True only for Bezier. Linear and held knots
+ /// return false, even though their tangents can be set.
+ virtual bool HasTangents() const = 0;
+
+ /// If true, implies the tangents can be written. For historical reasons,
+ /// linear and held knots support tangents. This means that these types
+ /// return true for ValueTypeSupportsTangents() but false for HasTangents().
+ // XXX: pixar-ism?
+ virtual bool ValueTypeSupportsTangents() const = 0;
+
+ virtual VtValue GetLeftTangentSlope() const = 0;
+ virtual VtValue GetRightTangentSlope() const = 0;
+ virtual TsTime GetLeftTangentLength() const = 0;
+ virtual TsTime GetRightTangentLength() const = 0;
+ virtual void SetLeftTangentSlope( VtValue ) = 0;
+ virtual void SetRightTangentSlope( VtValue ) = 0;
+ virtual void SetLeftTangentLength( TsTime ) = 0;
+ virtual void SetRightTangentLength( TsTime ) = 0;
+ virtual bool GetTangentSymmetryBroken() const = 0;
+ virtual void SetTangentSymmetryBroken( bool broken ) = 0;
+ virtual void ResetTangentSymmetryBroken() = 0;
+
+private:
+
+ TsTime _time = 0.0;
+};
+
+// Typed keyframe data class.
+template <typename T>
+class Ts_TypedData : public Ts_Data {
+public:
+ typedef T ValueType;
+ typedef Ts_TypedData<T> This;
+
+ Ts_TypedData(const T&);
+ Ts_TypedData(
+ const TsTime &t,
+ bool isDual,
+ const T& leftValue,
+ const T& rightValue,
+ const T& leftTangentSlope,
+ const T& rightTangentSlope);
+
+ ~Ts_TypedData() override = default;
+
+ void CloneInto(Ts_PolymorphicDataHolder *holder) const override;
+
+ // Create a untyped eval cache for the segment defined by ourself and the
+ // given keyframe.
+ std::shared_ptr<Ts_UntypedEvalCache> CreateEvalCache(
+ Ts_Data const* kf2) const override;
+
+ // Evaluate between this keyframe data and \p kf2 at \p time. This is
+ // useful for callers that do not otherwise want or need to create/retain an
+ // eval cache.
+ VtValue EvalUncached(
+ Ts_Data const *kf2, TsTime time) const override;
+
+ // Evaluate the derivative between this keyframe data and \p kf2 at \p time.
+ // This is useful for callers that do not otherwise want or need to
+ // create/retain an eval cache.
+ VtValue EvalDerivativeUncached(
+ Ts_Data const *kf2, TsTime time) const override;
+
+ // Create a typed eval cache for the segment defined by ourself and the
+ // given keyframe.
+ std::shared_ptr<Ts_EvalCache<T,
+ TsTraits<T>::interpolatable> > CreateTypedEvalCache(
+ Ts_Data const* kf2) const;
+
+ bool operator==(const Ts_Data &) const override;
+
+ // Knot type
+ TsKnotType GetKnotType() const override;
+ void SetKnotType( TsKnotType knotType ) override;
+ bool CanSetKnotType(
+ TsKnotType knotType, std::string *reason ) const override;
+
+ // Values
+ VtValue GetValue() const override;
+ void SetValue( VtValue ) override;
+ VtValue GetValueDerivative() const override;
+ bool GetIsDualValued() const override;
+ void SetIsDualValued( bool isDual ) override;
+ VtValue GetLeftValue() const override;
+ VtValue GetLeftValueDerivative() const override;
+ void SetLeftValue( VtValue ) override;
+ VtValue GetZero() const override;
+ bool ValueCanBeInterpolated() const override;
+
+ // Tangents
+ bool HasTangents() const override;
+ bool ValueTypeSupportsTangents() const override;
+ VtValue GetLeftTangentSlope() const override;
+ VtValue GetRightTangentSlope() const override;
+ TsTime GetLeftTangentLength() const override;
+ TsTime GetRightTangentLength() const override;
+ void SetLeftTangentSlope( VtValue ) override;
+ void SetRightTangentSlope( VtValue ) override;
+ void SetLeftTangentLength( TsTime ) override;
+ void SetRightTangentLength( TsTime ) override;
+ bool GetTangentSymmetryBroken() const override;
+ void SetTangentSymmetryBroken( bool broken ) override;
+ void ResetTangentSymmetryBroken() override;
+
+public:
+
+ // Extrapolation methods.
+
+ VtValue GetSlope(const Ts_Data &right) const override
+ {
+ if constexpr (TsTraits<T>::extrapolatable)
+ {
+ const TsTime dx = right.GetTime() - GetTime();
+ const TsTime dxInv = 1.0 / dx;
+
+ const T y1 = GetValue().template Get<T>();
+ const T y2 = right.GetLeftValue().template Get<T>();
+ const T dy = y2 - y1;
+
+ // This is effectively dy/dx, but some types lack operator/, so
+ // phrase in terms of operator*.
+ const T slope = dy * dxInv;
+ return VtValue(slope);
+ }
+ else
+ {
+ return VtValue(TsTraits<T>::zero);
+ }
+ }
+
+ VtValue Extrapolate(
+ const VtValue &value, TsTime dt, const VtValue &slope) const override
+ {
+ if constexpr (TsTraits<T>::extrapolatable)
+ {
+ const T v = value.template Get<T>();
+ const T s = slope.template Get<T>();
+ const T result = v + dt * s;
+ return VtValue(result);
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+private:
+
+ // Convenience accessors for the data stored inside the _Values struct.
+
+ T const& _GetRightValue() const {
+ return _values.Get()._rhv;
+ }
+ T const& _GetLeftValue() const {
+ return _values.Get()._lhv;
+ }
+ T const& _GetRightTangentSlope() const {
+ return _values.Get()._rightTangentSlope;
+ }
+ T const& _GetLeftTangentSlope() const {
+ return _values.Get()._leftTangentSlope;
+ }
+
+ void _SetRightValue(T const& rhv) {
+ _values.GetMutable()._rhv = rhv;
+ }
+ void _SetLeftValue(T const& lhv) {
+ _values.GetMutable()._lhv = lhv;
+ }
+ void _SetRightTangentSlope(T const& rightTangentSlope) {
+ _values.GetMutable()._rightTangentSlope = rightTangentSlope;
+ }
+ void _SetLeftTangentSlope(T const& leftTangentSlope) {
+ _values.GetMutable()._leftTangentSlope = leftTangentSlope;
+ }
+
+private:
+ friend class TsKeyFrame;
+ friend class Ts_UntypedEvalCache;
+ friend class Ts_EvalQuaternionCache<T>;
+ friend class Ts_EvalCache<T, TsTraits<T>::interpolatable>;
+
+ // A struct containing all the member variables that depend on type T.
+ template <class V>
+ struct _Values {
+
+ explicit _Values(
+ V const& lhv=TsTraits<T>::zero,
+ V const& rhv=TsTraits<T>::zero,
+ V const& leftTangentSlope=TsTraits<T>::zero,
+ V const& rightTangentSlope=TsTraits<T>::zero) :
+ _lhv(lhv),
+ _rhv(rhv),
+ _leftTangentSlope(leftTangentSlope),
+ _rightTangentSlope(rightTangentSlope)
+ {
+ }
+
+ // Left and right hand values.
+ // Single-value knots only use _rhv; dual-value knots use both.
+ V _lhv, _rhv;
+
+ // Tangent slope, or derivative, in units per frame.
+ V _leftTangentSlope, _rightTangentSlope;
+ };
+
+ // A wrapper for _Values with small-object optimization. The _ValuesHolder
+ // object is always the same size. If T is sizeof(double) or smaller, the
+ // _Values struct is held in member data. If T is larger than double, the
+ // struct is heap-allocated.
+ class _ValuesHolder
+ {
+ private:
+ static constexpr size_t _size = sizeof(_Values<double>);
+ static constexpr bool _isSmall = (sizeof(_Values<T>) <= _size);
+
+ // Storage implementation for small types.
+ struct _LocalStorage
+ {
+ _LocalStorage(_Values<T> &&values)
+ : _data(std::move(values)) {}
+
+ const _Values<T>& Get() const { return _data; }
+ _Values<T>& GetMutable() { return _data; }
+
+ _Values<T> _data;
+ };
+
+ // Storage implementation for large types.
+ struct _HeapStorage
+ {
+ _HeapStorage(_Values<T> &&values)
+ : _data(new _Values<T>(std::move(values))) {}
+
+ // Copy constructor: deep-copies data.
+ _HeapStorage(const _HeapStorage &other)
+ : _data(new _Values<T>(other.Get())) {}
+
+ const _Values<T>& Get() const { return *_data; }
+ _Values<T>& GetMutable() { return *_data; }
+
+ std::unique_ptr<_Values<T>> _data;
+ };
+
+ // Select storage implementation.
+ using _Storage =
+ typename std::conditional<
+ _isSmall, _LocalStorage, _HeapStorage>::type;
+
+ public:
+ // Construct from _Values rvalue.
+ explicit _ValuesHolder(_Values<T> &&values)
+ : _storage(std::move(values)) {}
+
+ // Copy constructor.
+ _ValuesHolder(const _ValuesHolder &other)
+ : _storage(other._storage) {}
+
+ // Destructor: explicitly call _Storage destructor.
+ ~_ValuesHolder() { _storage.~_Storage(); }
+
+ // Accessors.
+ const _Values<T>& Get() const { return _storage.Get(); }
+ _Values<T>& GetMutable() { return _storage.GetMutable(); }
+
+ private:
+ union
+ {
+ _Storage _storage;
+ char _padding[_size];
+ };
+ };
+
+ // Sanity check: every instantiation of _ValuesHolder is the same size.
+ static_assert(
+ sizeof(_ValuesHolder) == sizeof(_Values<double>),
+ "_ValuesHolder does not have expected type-independent size");
+
+private:
+ _ValuesHolder _values;
+
+ // Tangent length, in frames.
+ TsTime _leftTangentLength, _rightTangentLength;
+
+ TsKnotType _knotType;
+ bool _isDual;
+ bool _tangentSymmetryBroken;
+};
+
+// A wrapper for Ts_TypedData<T> for arbitrary T, exposed as a pointer to the
+// non-templated base class Ts_Data, but allocated in member data rather than
+// on the heap.
+class Ts_PolymorphicDataHolder
+{
+public:
+ // Wrapper for held-knot-at-time-zero constructor.
+ template <typename T>
+ void New(const T &val)
+ {
+ new (&_storage) Ts_TypedData<T>(val);
+ }
+
+ // Wrapper for general constructor.
+ template <typename T>
+ void New(
+ const TsTime &t,
+ bool isDual,
+ const T &leftValue,
+ const T &rightValue,
+ const T &leftTangentSlope,
+ const T &rightTangentSlope)
+ {
+ new (&_storage) Ts_TypedData<T>(
+ t, isDual, leftValue, rightValue,
+ leftTangentSlope, rightTangentSlope);
+ }
+
+ // Copy constructor.
+ template <typename T>
+ void New(const Ts_TypedData<T> &other)
+ {
+ new (&_storage) Ts_TypedData<T>(other);
+ }
+
+ // Explicit destructor. Clients call this method from their destructors,
+ // and prior to calling New to replace an existing knot.
+ void Destroy()
+ {
+ reinterpret_cast<Ts_Data*>(&_storage)->~Ts_Data();
+ }
+
+ // Const accessor.
+ const Ts_Data* Get() const
+ {
+ return reinterpret_cast<const Ts_Data*>(&_storage);
+ }
+
+ // Non-const accessor.
+ Ts_Data* GetMutable()
+ {
+ return reinterpret_cast<Ts_Data*>(&_storage);
+ }
+
+private:
+ // Our buffer is sized for Ts_TypedData<T>. This is always the same size
+ // regardless of T; see Ts_TypedData::_ValuesHolder.
+ using _Storage =
+ typename std::aligned_storage<
+ sizeof(Ts_TypedData<double>), sizeof(void*)>::type;
+
+private:
+ _Storage _storage;
+};
+
+////////////////////////////////////////////////////////////////////////
+// Ts_TypedData
+
+template <typename T>
+Ts_TypedData<T>::Ts_TypedData(const T& value) :
+ _values(_Values<T>(value,value)),
+ _leftTangentLength(0.0),
+ _rightTangentLength(0.0),
+ _knotType(TsKnotHeld),
+ _isDual(false),
+ _tangentSymmetryBroken(false)
+{
+}
+
+template <typename T>
+Ts_TypedData<T>::Ts_TypedData(
+ const TsTime &t,
+ bool isDual,
+ const T& leftValue,
+ const T& rightValue,
+ const T& leftTangentSlope,
+ const T& rightTangentSlope) :
+ _values(_Values<T>(leftValue,rightValue,
+ leftTangentSlope,rightTangentSlope)),
+ _leftTangentLength(0.0),
+ _rightTangentLength(0.0),
+ _knotType(TsKnotHeld),
+ _isDual(isDual),
+ _tangentSymmetryBroken(false)
+{
+ SetTime(t);
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::CloneInto(Ts_PolymorphicDataHolder *holder) const
+{
+ holder->New(*this);
+}
+
+template <typename T>
+std::shared_ptr<Ts_UntypedEvalCache>
+Ts_TypedData<T>::CreateEvalCache(Ts_Data const* kf2) const
+{
+ // Cast kf2 to the correct typed data. This is a private class, and we
+ // assume kf2 is from the same spline, so it will have the same value type.
+ Ts_TypedData<T> const* typedKf2 =
+ static_cast<Ts_TypedData<T> const*>(kf2);
+
+ // Construct and return a new EvalCache of the appropriate type.
+ return std::make_shared<
+ Ts_EvalCache<T, TsTraits<T>::interpolatable>>(this, typedKf2);
+}
+
+template <typename T>
+std::shared_ptr<Ts_EvalCache<T, TsTraits<T>::interpolatable> >
+Ts_TypedData<T>::CreateTypedEvalCache(Ts_Data const* kf2) const
+{
+ Ts_TypedData<T> const* typedKf2 =
+ static_cast<Ts_TypedData<T> const*>(kf2);
+
+ return std::shared_ptr<Ts_EvalCache<T, TsTraits<T>::interpolatable> >(
+ new Ts_EvalCache<T, TsTraits<T>::interpolatable>(this, typedKf2));
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>
+::EvalUncached(Ts_Data const *kf2, TsTime time) const
+{
+ // Cast kf2 to the correct typed data. This is a private class, and we
+ // assume kf2 is from the same spline, so it will have the same value type.
+ Ts_TypedData<T> const* typedKf2 =
+ static_cast<Ts_TypedData<T> const*>(kf2);
+
+ return Ts_EvalCache<T, TsTraits<T>::interpolatable>(this, typedKf2)
+ .Eval(time);
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>
+::EvalDerivativeUncached(Ts_Data const *kf2, TsTime time) const
+{
+ // Cast kf2 to the correct typed data. This is a private class, and we
+ // assume kf2 is from the same spline, so it will have the same value type.
+ Ts_TypedData<T> const* typedKf2 =
+ static_cast<Ts_TypedData<T> const*>(kf2);
+
+ return Ts_EvalCache<T, TsTraits<T>::interpolatable>(this, typedKf2)
+ .EvalDerivative(time);
+}
+
+template <typename T>
+bool
+Ts_TypedData<T>::operator==(const Ts_Data &rhs) const
+{
+ if (!TsTraits<T>::supportsTangents) {
+ return
+ GetKnotType() == rhs.GetKnotType() &&
+ GetTime() == rhs.GetTime() &&
+ GetValue() == rhs.GetValue() &&
+ GetIsDualValued() == rhs.GetIsDualValued() &&
+ (!GetIsDualValued() || (GetLeftValue() == rhs.GetLeftValue()));
+ }
+
+ return
+ GetTime() == rhs.GetTime() &&
+ GetValue() == rhs.GetValue() &&
+ GetKnotType() == rhs.GetKnotType() &&
+ GetIsDualValued() == rhs.GetIsDualValued() &&
+ (!GetIsDualValued() || (GetLeftValue() == rhs.GetLeftValue())) &&
+ GetLeftTangentLength() == rhs.GetLeftTangentLength() &&
+ GetRightTangentLength() == rhs.GetRightTangentLength() &&
+ GetLeftTangentSlope() == rhs.GetLeftTangentSlope() &&
+ GetRightTangentSlope() == rhs.GetRightTangentSlope() &&
+ GetTangentSymmetryBroken() == rhs.GetTangentSymmetryBroken();
+}
+
+template <typename T>
+TsKnotType
+Ts_TypedData<T>::GetKnotType() const
+{
+ return _knotType;
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetKnotType( TsKnotType knotType )
+{
+ std::string reason;
+
+ if (!CanSetKnotType(knotType, &reason)) {
+ TF_CODING_ERROR(reason);
+ return;
+ }
+
+ _knotType = knotType;
+}
+
+template <typename T>
+bool
+Ts_TypedData<T>::CanSetKnotType( TsKnotType knotType,
+ std::string *reason ) const
+{
+ // Non-interpolatable values can only have held key frames.
+ if (!ValueCanBeInterpolated() && knotType != TsKnotHeld) {
+ if (reason) {
+ *reason = "Value cannot be interpolated; only 'held' " \
+ "key frames are allowed.";
+ }
+ return false;
+ }
+
+ // Only value types that support tangents can have bezier key frames.
+ if (!TsTraits<T>::supportsTangents && knotType == TsKnotBezier) {
+ if (reason) {
+ *reason = TfStringPrintf(
+ "Cannot set keyframe type %s; values of type '%s' "
+ "do not support tangents.",
+ TfEnum::GetDisplayName(knotType).c_str(),
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ }
+ return false;
+ }
+
+ return true;
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>::GetValue() const
+{
+ return VtValue(_GetRightValue());
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>::GetValueDerivative() const
+{
+ if (TsTraits<T>::supportsTangents) {
+ return GetRightTangentSlope();
+ } else {
+ return VtValue(TsTraits<T>::zero);
+ }
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetValue( VtValue val )
+{
+ VtValue v = val.Cast<T>();
+ if (!v.IsEmpty()) {
+ _SetRightValue(v.Get<T>());
+ if (!ValueCanBeInterpolated())
+ SetKnotType(TsKnotHeld);
+ } else {
+ TF_CODING_ERROR("cannot convert type '%s' to '%s' to assign "
+ "to keyframe", val.GetTypeName().c_str(),
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ }
+}
+
+template <typename T>
+bool
+Ts_TypedData<T>::GetIsDualValued() const
+{
+ return _isDual;
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetIsDualValued( bool isDual )
+{
+ if (isDual && !TsTraits<T>::interpolatable) {
+ TF_CODING_ERROR("keyframes of type '%s' cannot be dual-valued",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return;
+ }
+
+ _isDual = isDual;
+
+ if (_isDual) {
+ // The data stored for the left value was meaningless.
+ // Mirror the right-side value to the left.
+ SetLeftValue(GetValue());
+ }
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>::GetLeftValue() const
+{
+ return VtValue(_isDual ? _GetLeftValue() : _GetRightValue());
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>::GetLeftValueDerivative() const
+{
+ if (TsTraits<T>::supportsTangents) {
+ return GetLeftTangentSlope();
+ } else {
+ return VtValue(TsTraits<T>::zero);
+ }
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetLeftValue( VtValue val )
+{
+ if (!TsTraits<T>::interpolatable) {
+ TF_CODING_ERROR("keyframes of type '%s' cannot be dual-valued",
+ ArchGetDemangled(typeid(ValueType)).c_str() );
+ return;
+ }
+ if (!GetIsDualValued()) {
+ TF_CODING_ERROR("keyframe is not dual-valued; cannot set left value");
+ return;
+ }
+
+ VtValue v = val.Cast<T>();
+ if (!v.IsEmpty()) {
+ _SetLeftValue(v.Get<T>());
+ if (!ValueCanBeInterpolated())
+ SetKnotType(TsKnotHeld);
+ } else {
+ TF_CODING_ERROR("cannot convert type '%s' to '%s' to assign to "
+ "keyframe", val.GetTypeName().c_str(),
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ }
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>::GetZero() const
+{
+ return VtValue(TsTraits<T>::zero);
+}
+
+template <typename T>
+bool
+Ts_TypedData<T>::ValueCanBeInterpolated() const
+{
+ return TsTraits<T>::interpolatable;
+}
+
+template <typename T>
+bool
+Ts_TypedData<T>::HasTangents() const
+{
+ return TsTraits<T>::supportsTangents && _knotType == TsKnotBezier;
+}
+
+template <typename T>
+bool
+Ts_TypedData<T>::ValueTypeSupportsTangents() const
+{
+ // Oddly, linear and held knots have settable tangents. Animators use
+ // this when switching Beziers to Held and then back again.
+ return TsTraits<T>::supportsTangents;
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>::GetLeftTangentSlope() const
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return VtValue();
+ }
+
+ return VtValue(_GetLeftTangentSlope());
+}
+
+template <typename T>
+VtValue
+Ts_TypedData<T>::GetRightTangentSlope() const
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str() );
+ return VtValue();
+ }
+
+ return VtValue(_GetRightTangentSlope());
+}
+
+template <typename T>
+TsTime
+Ts_TypedData<T>::GetLeftTangentLength() const
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return 0;
+ }
+
+ return _leftTangentLength;
+}
+
+template <typename T>
+TsTime
+Ts_TypedData<T>::GetRightTangentLength() const
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return 0;
+ }
+
+ return _rightTangentLength;
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetLeftTangentSlope( VtValue val )
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return;
+ }
+
+ VtValue v = val.Cast<T>();
+ if (!v.IsEmpty()) {
+ _SetLeftTangentSlope(val.Get<T>());
+ } else {
+ TF_CODING_ERROR("cannot convert type '%s' to '%s' to assign to "
+ "keyframe", val.GetTypeName().c_str(),
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ }
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetRightTangentSlope( VtValue val )
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return;
+ }
+
+ VtValue v = val.Cast<T>();
+ if (!v.IsEmpty()) {
+ _SetRightTangentSlope(val.Get<T>());
+ } else {
+ TF_CODING_ERROR("cannot convert type '%s' to '%s' to assign to keyframe"
+ , val.GetTypeName().c_str(),
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ }
+}
+
+#define TS_LENGTH_EPSILON 1e-6
+
+template <typename T>
+void
+Ts_TypedData<T>::SetLeftTangentLength( TsTime newLen )
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR( "keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return;
+ }
+ if (std::isnan(newLen)) {
+ TF_CODING_ERROR("Cannot set tangent length to NaN; ignoring");
+ return;
+ }
+ if (std::isinf(newLen)) {
+ TF_CODING_ERROR("Cannot set tangent length to inf; ignoring");
+ return;
+ }
+ if (newLen < 0.0) {
+ if (-newLen < TS_LENGTH_EPSILON) {
+ newLen = 0.0;
+ } else {
+ TF_CODING_ERROR(
+ "Cannot set tangent length to negative value; ignoring");
+ return;
+ }
+ }
+
+ _leftTangentLength = newLen;
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetRightTangentLength( TsTime newLen )
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return;
+ }
+ if (std::isnan(newLen)) {
+ TF_CODING_ERROR("Cannot set tangent length to NaN; ignoring");
+ return;
+ }
+ if (std::isinf(newLen)) {
+ TF_CODING_ERROR("Cannot set tangent length to inf; ignoring");
+ return;
+ }
+ if (newLen < 0.0) {
+ if (-newLen < TS_LENGTH_EPSILON) {
+ newLen = 0.0;
+ } else {
+ TF_CODING_ERROR(
+ "Cannot set tangent length to negative value; ignoring");
+ return;
+ }
+ }
+
+ _rightTangentLength = newLen;
+}
+
+template <typename T>
+bool
+Ts_TypedData<T>::GetTangentSymmetryBroken() const
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return false;
+ }
+
+ return _tangentSymmetryBroken;
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::SetTangentSymmetryBroken( bool broken )
+{
+ if (!TsTraits<T>::supportsTangents) {
+ TF_CODING_ERROR("keyframes of type '%s' do not have tangents",
+ ArchGetDemangled(typeid(ValueType)).c_str());
+ return;
+ }
+
+ if (_tangentSymmetryBroken != broken) {
+ _tangentSymmetryBroken = broken;
+ if (!_tangentSymmetryBroken) {
+ _SetLeftTangentSlope(_GetRightTangentSlope());
+ }
+ }
+}
+
+template <typename T>
+void
+Ts_TypedData<T>::ResetTangentSymmetryBroken()
+{
+ // do nothing -- no tangents
+}
+
+// Declare specializations for float and double.
+// Definitions are in Data.cpp.
+template <>
+TS_API void
+Ts_TypedData<float>::ResetTangentSymmetryBroken();
+
+template <>
+TS_API void
+Ts_TypedData<double>::ResetTangentSymmetryBroken();
+
+template <>
+TS_API bool
+Ts_TypedData<float>::ValueCanBeInterpolated() const;
+
+template <>
+TS_API bool
+Ts_TypedData<double>::ValueCanBeInterpolated() const;
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/diff.h"
+
+#include "pxr/base/ts/keyFrameUtils.h"
+#include "pxr/base/ts/spline.h"
+#include "pxr/base/ts/types.h"
+
+#include "evalUtils.h"
+
+#include "pxr/base/gf/interval.h"
+#include "pxr/base/gf/math.h"
+#include "pxr/base/trace/trace.h"
+#include "pxr/base/vt/value.h"
+
+#include <limits>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+//
+// FindChangedInterval
+//
+
+namespace {
+
+// Helper class for taking two splines and computing the GfInterval in which
+// they differ from each other evaluatively.
+class Ts_SplineChangedIntervalHelper
+{
+ typedef TsSpline::const_iterator KeyFrameIterator;
+ typedef TsSpline::const_reverse_iterator KeyFrameReverseIterator;
+
+public:
+ Ts_SplineChangedIntervalHelper(
+ const TsSpline *s1, const TsSpline *s2);
+ GfInterval ComputeChangedInterval();
+
+private:
+ // Helper functions for tightening the changed interval from the left
+ static KeyFrameReverseIterator _GetFirstKeyFrame(
+ const TsSpline& spline, KeyFrameReverseIterator kf);
+ KeyFrameIterator _GetNextNonFlatKnot(
+ const TsSpline &spline, const KeyFrameIterator &startKeyFrame);
+ bool _TightenToNextKeyFrame(bool extrapolateHeldLeft = false);
+ void _TightenFromLeft();
+
+ // Helper functions for tightening the changed interval from the right
+ static KeyFrameIterator _GetLastKeyFrame(
+ const TsSpline& spline, KeyFrameIterator kf);
+ KeyFrameReverseIterator _GetPreviousNonFlatKnot(
+ const TsSpline &spline, const KeyFrameReverseIterator &startKeyFrame);
+ bool _TightenToPreviousKeyFrame(bool extrapolateHeldRight = false);
+ void _TightenFromRight();
+
+ const TsSpline *_s1;
+ const TsSpline *_s2;
+ KeyFrameIterator _s1Iter;
+ KeyFrameIterator _s2Iter;
+ KeyFrameReverseIterator _s1ReverseIter;
+ KeyFrameReverseIterator _s2ReverseIter;
+
+ GfInterval _changedInterval;
+};
+
+}
+
+
+Ts_SplineChangedIntervalHelper::Ts_SplineChangedIntervalHelper(
+ const TsSpline *s1, const TsSpline *s2):
+ _s1(s1),
+ _s2(s2)
+{
+}
+
+GfInterval
+Ts_SplineChangedIntervalHelper::ComputeChangedInterval()
+{
+ TRACE_FUNCTION();
+
+ // First assume everything changed.
+ _changedInterval = GfInterval::GetFullInterval();
+
+ // If both splines are empty then splines aren't different so just return
+ // the empty interval.
+ if (_s1->empty() && _s2->empty()) {
+ _changedInterval = GfInterval();
+ return _changedInterval;
+ }
+ // If either spline is empty then just return the entire interval.
+ if (_s1->empty() || _s2->empty()) {
+ return _changedInterval;
+ }
+
+ // Try to tighten interval from right side
+ _TightenFromRight();
+
+ if (!_changedInterval.IsEmpty()) {
+ // Try to tighten interval from left side
+ _TightenFromLeft();
+ }
+
+ if (_changedInterval.IsEmpty()) {
+ _changedInterval = GfInterval();
+ }
+
+ return _changedInterval;
+}
+
+// Return the iterator representing the last keyframe: if extrapolating, return
+// end(); otherwise, return the last keyframe
+Ts_SplineChangedIntervalHelper::KeyFrameIterator
+Ts_SplineChangedIntervalHelper::_GetLastKeyFrame(
+ const TsSpline &spline,
+ KeyFrameIterator kf)
+{
+ TF_VERIFY(kf+1 == spline.end());
+
+ if (Ts_GetEffectiveExtrapolationType(*kf, spline, TsRight)
+ == TsExtrapolationHeld) {
+ return spline.end();
+ } else {
+ return kf;
+ }
+}
+
+// This function finds the first key frame after or including startKeyFrame
+// that is not part of a constant flat spline segment starting at
+// startKeyFrame's right side value.
+Ts_SplineChangedIntervalHelper::KeyFrameIterator
+Ts_SplineChangedIntervalHelper::_GetNextNonFlatKnot(
+ const TsSpline &spline,
+ const KeyFrameIterator &startKeyFrame)
+{
+ TRACE_FUNCTION();
+
+ // Start off assuming the next non flat key frame is the one we passed in.
+ KeyFrameIterator kf = startKeyFrame;
+ VtValue prevHeldValue;
+
+ // For array-valued spline, assume the next knot is non-flat.
+ // This is primarily an optimization to address expensive comparisons
+ // required by the loop below for large arrays with large identical prefixes
+ // between adjacent knots (which is often the case for animation data).
+ // The outer loop calling this function will still iterate over each knot
+ // to tighten the invalidation interval.
+ if (kf == spline.end()) {
+ return kf;
+ } else if (kf->GetValue().IsArrayValued()) {
+ ++kf;
+
+ // If startKeyFrame is the last key frame, check the extrapolation to
+ // the right
+ if (kf == spline.end()) {
+ return _GetLastKeyFrame(spline, startKeyFrame);
+ } else {
+ return kf;
+ }
+ }
+
+ while (kf != spline.end()) {
+ // With the exception of the key frame we're starting with, check for
+ // for value consistency from the left side to the right side
+ if (kf != startKeyFrame) {
+ // A dual valued not with different values means this key frame
+ // is the next non flat knot
+ if (kf->GetIsDualValued() &&
+ kf->GetLeftValue() != kf->GetValue()) {
+ return kf;
+ }
+ // If the previous knot was held and this knot's right value is
+ // different than the held value, then this key frame is the next
+ // non-flat one.
+ if (!prevHeldValue.IsEmpty() &&
+ kf->GetValue() != prevHeldValue) {
+ return kf;
+ }
+ }
+ // If this key frame is held, then we're automatically flat until the
+ // next key frame so skip to it. We specifically check the held case
+ // instead of using Ts_IsSegmentFlat as Ts_IsSegmentFlat requires
+ // that the next knot's left value be the same for flatness while this
+ // function does not.
+ if (kf->GetKnotType() == TsKnotHeld) {
+ // Store the held value so we can compare it.
+ prevHeldValue = kf->GetValue();
+ ++kf;
+ continue;
+ } else {
+ // Clear the previous held value if we're not held.
+ prevHeldValue = VtValue();
+ }
+
+ // Get the next key frame
+ KeyFrameIterator nextKeyFrameIt = kf;
+ ++nextKeyFrameIt;
+
+ // If we're looking at the last key frame, then check the extrapolation
+ // to the right
+ if (nextKeyFrameIt == spline.end()) {
+ return _GetLastKeyFrame(spline, kf);
+ }
+
+ // If the segment from this key frame to the next one is not flat,
+ // then this key frame is the next non flat one.
+ if (!Ts_IsSegmentFlat(*kf, *nextKeyFrameIt)) {
+ return kf;
+ }
+
+ // We passed all the flatness conditions so move to the next key frame.
+ ++kf;
+ }
+
+ return kf;
+}
+
+// Tightens the left side of the changed interval up to the next key frame if
+// possible and returns whether the interval can potentially be tightened
+// any more.
+bool
+Ts_SplineChangedIntervalHelper::_TightenToNextKeyFrame(
+ bool extrapolateHeldLeft)
+{
+ TRACE_FUNCTION();
+ const TsTime infinity = std::numeric_limits<TsTime>::infinity();
+
+ // By default assume we can't tighten the interval beyond the next
+ // key frame.
+ bool canTightenMore = false;
+
+ // If we're holding extrapolation for the left, then we can only tighten
+ // if the left side values of the current key frames are equal.
+ if (extrapolateHeldLeft) {
+ if (_s1Iter->GetLeftValue() != _s2Iter->GetLeftValue()) {
+ return false;
+ }
+ }
+
+ // First find the next non flat knots from the current key frames.
+ KeyFrameIterator s1NextNonFlat = _s1Iter;
+ KeyFrameIterator s2NextNonFlat = _s2Iter;
+
+ // If we're held extrapolating from the left (meaning this is the first
+ // left side tightening) but the key frame is dual valued and sides don't
+ // match in value, then we know that the next flat knot is the first knot
+ // in this case. In this instance we don't want to get the next non flat
+ // knot as that function ignores anything on the left side of the initial
+ // knot. For all other cases, we just get the next non flat knot as normal
+ // since we will have already covered the left side of the current knot.
+ if (!extrapolateHeldLeft ||
+ !_s1Iter->GetIsDualValued() ||
+ _s1Iter->GetValue() == _s1Iter->GetLeftValue()) {
+ s1NextNonFlat = _GetNextNonFlatKnot(*_s1, _s1Iter);
+ }
+ if (!extrapolateHeldLeft ||
+ !_s2Iter->GetIsDualValued() ||
+ _s2Iter->GetValue() == _s2Iter->GetLeftValue()) {
+ s2NextNonFlat = _GetNextNonFlatKnot(*_s2, _s2Iter);
+ }
+
+ // If we're extrapolating held from the left or we found flat segments of
+ // the same value on both splines, then we can do the flat segment
+ // interval tightening.
+ if (extrapolateHeldLeft ||
+ (s1NextNonFlat != _s1Iter && s2NextNonFlat != _s2Iter &&
+ _s1Iter->GetValue() == _s2Iter->GetValue())) {
+
+ // Get the times of the end of the flat segment (could be infinity if
+ // the spline is flat all the way past the last key frame).
+ TsTime s1NextKfTime =
+ (s1NextNonFlat == _s1->end()) ? infinity : s1NextNonFlat->GetTime();
+ TsTime s2NextKfTime =
+ (s2NextNonFlat == _s2->end()) ? infinity : s2NextNonFlat->GetTime();
+
+ // At this point we know we're tightening the interval from the left,
+ // we still need to determine if beginning of the interval should be
+ // closed or open and whether we can potentially continue tightening
+ // from the left.
+ bool closed = false;
+ if (s1NextKfTime < s2NextKfTime)
+ {
+ // If s1's flat segment ends before s2's then the interval is
+ // closed if either side of s1's key frame differs from the held
+ // segment's value.
+ closed = s1NextNonFlat->GetValue() != _s2Iter->GetValue() ||
+ (s1NextNonFlat->GetIsDualValued() &&
+ s1NextNonFlat->GetValue() != s1NextNonFlat->GetLeftValue());
+ }
+ else if (s2NextKfTime < s1NextKfTime)
+ {
+ // If s2's flat segment ends before s1's then the interval is
+ // closed if either side of s2's key frame differs from the held
+ // segment's value.
+ closed = s2NextNonFlat->GetValue() != _s1Iter->GetValue() ||
+ (s2NextNonFlat->GetIsDualValued() &&
+ s2NextNonFlat->GetValue() != s2NextNonFlat->GetLeftValue());
+ }
+ else // (s2NextKfTime == s1NextKfTime)
+ {
+ // Otherwise both spline's flat segments end at the same time.
+
+ // If the splines are flat to the end, then there is no
+ // evaluative difference between the two. The changed interval
+ // is empty.
+ if (s1NextKfTime == infinity) {
+ _changedInterval = GfInterval();
+ return false;
+ }
+ // The interval is closed if either the splines don't match values
+ // on either side of the keyframe.
+ closed = s1NextNonFlat->GetValue() != s2NextNonFlat->GetValue() ||
+ s1NextNonFlat->GetLeftValue() != s2NextNonFlat->GetLeftValue();
+ // We can only potentially tighten more if the key frames have
+ // equivalent values on both sides.
+ canTightenMore = !closed;
+ }
+
+ // Update the changed interval with the new min value.
+ _changedInterval.SetMin(GfMin(s1NextKfTime, s2NextKfTime), closed);
+
+ // Update the forward iterators to the end of the flat segments we
+ // just checked.
+ _s1Iter = s1NextNonFlat;
+ _s2Iter = s2NextNonFlat;
+ }
+
+ // Otherwise we're not looking at a flat segment so just do a standard
+ // segment equivalence check.
+ else {
+ // First make sure the right sides of the current key frames are
+ // equivalent.
+ if (_s1Iter->IsEquivalentAtSide(*_s2Iter, TsRight)) {
+ // Move to the next key frames and check if they're left equivalent.
+ ++_s1Iter;
+ ++_s2Iter;
+ if (_s1Iter != _s1->end() && _s2Iter != _s2->end() &&
+ _s1Iter->IsEquivalentAtSide(*_s2Iter, TsLeft)) {
+
+ // Compare the right side values to determine if the interval
+ // should be closed
+ bool closed = (_s1Iter->GetValue() != _s2Iter->GetValue());
+ _changedInterval.SetMin(_s1Iter->GetTime(), closed);
+ // We can continue tightening if the knots are right equivalent.
+ canTightenMore = !closed;
+ }
+ }
+ }
+
+ return canTightenMore;
+}
+
+void
+Ts_SplineChangedIntervalHelper::_TightenFromLeft()
+{
+ TRACE_FUNCTION();
+
+ // Initialize the iterators to the first key frame in each spline
+ _s1Iter = _s1->begin();
+ _s2Iter = _s2->begin();
+
+ // Get the effective extrapolations of each spline on the left side
+ const TsExtrapolationType aExtrapLeft =
+ Ts_GetEffectiveExtrapolationType(*_s1Iter, *_s1, TsLeft);
+ const TsExtrapolationType bExtrapLeft =
+ Ts_GetEffectiveExtrapolationType(*_s2Iter, *_s2, TsLeft);
+
+ // We can't tighten if the extrapolations or the extrapolated values are
+ // different.
+ if (aExtrapLeft != bExtrapLeft ||
+ _s1Iter->GetLeftValue() != _s2Iter->GetLeftValue()) {
+ return;
+ }
+
+ // If the extrapolation is held then tighten to the next key frame
+ // with left held extrapolation.
+ if (aExtrapLeft == TsExtrapolationHeld) {
+ if (!_TightenToNextKeyFrame(true /*extrapolateHeldLeft*/)) {
+ // If we can't continue tightening then return.
+ return;
+ }
+ }
+ // Otherwise the extrapolation is linear so only if the time and
+ // slopes match, do we not have a change before the first keyframes
+ // XXX: We could potentially improve upon how much we invalidate in
+ // the linear extrapolation case but it may not be worth it at this
+ // time.
+ else if (_s1Iter->GetTime() == _s2Iter->GetTime() &&
+ _s1Iter->GetLeftTangentSlope() ==
+ _s2Iter->GetLeftTangentSlope()) {
+ bool closed = _s1Iter->GetValue() != _s2Iter->GetValue();
+ _changedInterval.SetMin(_s1Iter->GetTime(), closed );
+ // If the interval is closed, then we can't tighten any more so
+ // just return.
+ if (closed) {
+ return;
+ }
+ } else {
+ // Otherwise we our extrapolations are not tightenable so just
+ // return.
+ return;
+ }
+
+ // Now just continue tightening the interval to the next key frame
+ // until we can no longer do so.
+ while(_TightenToNextKeyFrame());
+}
+
+// Return the iterator representing the last keyframe: if extrapolating, return
+// end(); otherwise, return the last keyframe
+Ts_SplineChangedIntervalHelper::KeyFrameReverseIterator
+Ts_SplineChangedIntervalHelper::_GetFirstKeyFrame(
+ const TsSpline &spline,
+ KeyFrameReverseIterator kf)
+{
+ TF_VERIFY(kf+1 == spline.rend());
+
+ if (Ts_GetEffectiveExtrapolationType(*kf, spline, TsLeft)
+ == TsExtrapolationHeld) {
+ return spline.rend();
+ } else {
+ return kf;
+ }
+}
+
+// This function finds the left most key frame before startKeyFrame that begins
+// a constant flat spline segment that continues up to but does not include
+// startKeyFrame.
+Ts_SplineChangedIntervalHelper::KeyFrameReverseIterator
+Ts_SplineChangedIntervalHelper::_GetPreviousNonFlatKnot(
+ const TsSpline &spline,
+ const KeyFrameReverseIterator &startKeyFrame)
+{
+ TRACE_FUNCTION();
+
+ // Start off assuming the previous non flat key frame is the one we
+ // passed in.
+ KeyFrameReverseIterator kf = startKeyFrame;
+
+ // For array-valued spline, assume the previous knot is non-flat.
+ // This is primarily an optimization to address expensive comparisons
+ // required by the loop below for large arrays with large identical prefixes
+ // between adjacent knots (which is often the case for animation data).
+ // The outer loop calling this function will still iterate over each knot
+ // to tighten the invalidation interval.
+ if (kf == spline.rend()) {
+ return kf;
+ } else if (startKeyFrame->GetValue().IsArrayValued()) {
+ ++kf;
+
+ // If startKeyFrame is the first key frame, check the extrapolation to
+ // the left
+ if (kf == spline.rend()) {
+ return _GetFirstKeyFrame(spline, startKeyFrame);
+ } else {
+ return kf;
+ }
+ }
+
+ while (kf != spline.rend()) {
+ // With the exception of the key frame we're starting with, check for
+ // for value consistency from the left side to the right side
+ if (kf != startKeyFrame) {
+ // A dual valued not with different values means this key frame
+ // is the next non flat knot
+ if (kf->GetIsDualValued() && kf->GetLeftValue() != kf->GetValue()) {
+ return kf;
+ }
+ }
+
+ // Get the previous key frame
+ KeyFrameReverseIterator prevKeyFrameIt = kf;
+ ++prevKeyFrameIt;
+
+ // If we're looking at the first key frame, then check the extrapolation
+ // to the left
+ if (prevKeyFrameIt == spline.rend()) {
+ return _GetFirstKeyFrame(spline, kf);
+ }
+
+ // If the previous key frame is held, then we're automatically flat
+ // up to the current key frame as long as the current key frame's left
+ // value matches the previous key frame's held value or is the current
+ // key frame is the starting key frame.
+ if (prevKeyFrameIt->GetKnotType() == TsKnotHeld) {
+ if (kf == startKeyFrame ||
+ kf->GetLeftValue() == prevKeyFrameIt->GetValue()) {
+ ++kf;
+ continue;
+ }
+ }
+
+ // If the segment from the previous key frame to the current one is not
+ // flat, then this key frame is the next non flat one.
+ if (!Ts_IsSegmentFlat(*prevKeyFrameIt, *kf)) {
+ return kf;
+ }
+
+ // We passed all the flatness conditions so move to the next key frame.
+ ++kf;
+ }
+ return kf;
+}
+
+// Tightens the right side of the changed interval up to the previous key frame
+// if possible and returns whether the interval can potentially be tightened
+// any more.
+bool
+Ts_SplineChangedIntervalHelper::_TightenToPreviousKeyFrame(
+ bool extrapolateHeldRight)
+{
+ TRACE_FUNCTION();
+ const TsTime infinity = std::numeric_limits<TsTime>::infinity();
+
+ // By default assume we won't be able to tighten any more beyond the
+ // previous key frame.
+ bool canTightenMore = false;
+
+ // If we're holding extrapolation for the right, then we can only tighten
+ // if the right side values of the current key frames are equal.
+ if (extrapolateHeldRight) {
+ if (_s1ReverseIter->GetValue() !=
+ _s2ReverseIter->GetValue()) {
+ return false;
+ }
+ }
+
+ // First find the previous non flat knots from the current key frames.
+ KeyFrameReverseIterator s1PrevNonFlat =
+ _GetPreviousNonFlatKnot(*_s1, _s1ReverseIter);
+ KeyFrameReverseIterator s2PrevNonFlat =
+ _GetPreviousNonFlatKnot(*_s2, _s2ReverseIter);
+
+ // Store the values of the previous key frames (if the previous key frame
+ // is past the left end of the spline, then we use the left value of the
+ // spline's first key frame).
+ const VtValue s1PrevValue = s1PrevNonFlat == _s1->rend() ?
+ _s1->begin()->GetLeftValue() : s1PrevNonFlat->GetValue();
+ const VtValue s2PrevValue = s2PrevNonFlat == _s2->rend() ?
+ _s2->begin()->GetLeftValue() : s2PrevNonFlat->GetValue();
+
+ // We have to do some extra checks if we're extrapolating held to the
+ // right of our current key frames as _GetPreviousNonFlatKnot doesn't
+ // look at the current key frame at all.
+ if (extrapolateHeldRight) {
+ // If the previous non flat knot is different then the current knot,
+ // then we verify that the held value of the previous knot is the same
+ // as the value of both sides of the current knot to ensure that
+ // segment is completely flat from the previous knot to infinity
+ // extrapolated beyond the current knot. If this check fails, then
+ // we have to roll the previous knot back to being the current knot.
+ if (s1PrevNonFlat != _s1ReverseIter) {
+ if (s1PrevValue != _s1ReverseIter->GetValue() ||
+ (_s1ReverseIter->GetIsDualValued() &&
+ _s1ReverseIter->GetValue() !=
+ _s1ReverseIter->GetLeftValue())) {
+ s1PrevNonFlat = _s1ReverseIter;
+ }
+ }
+ if (s2PrevNonFlat != _s2ReverseIter) {
+ if (s2PrevValue != _s2ReverseIter->GetValue() ||
+ (_s2ReverseIter->GetIsDualValued() &&
+ _s2ReverseIter->GetValue() !=
+ _s2ReverseIter->GetLeftValue())) {
+ s2PrevNonFlat = _s2ReverseIter;
+ }
+ }
+ }
+
+ // If we're extrapolating held from the right or we found flat segments of
+ // the same value on both splines, then we can do the flat segment
+ // interval tightening.
+ if (extrapolateHeldRight ||
+ (s1PrevNonFlat != _s1ReverseIter && s2PrevNonFlat != _s2ReverseIter &&
+ s1PrevValue == s2PrevValue)) {
+
+ // If the splines are flat to the end, then there is no
+ // evaluative difference between the two. Return an empty
+ // interval.
+ const TsTime s1PrevKfTime =
+ (s1PrevNonFlat == _s1->rend()) ? -infinity : s1PrevNonFlat->GetTime();
+ const TsTime s2PrevKfTime =
+ (s2PrevNonFlat == _s2->rend()) ? -infinity : s2PrevNonFlat->GetTime();
+
+ // At this point we know we're tightening the interval from the right,
+ // we still need to determine if end of the interval should be
+ // closed or open and whether we can potentially continue tightening
+ // from the right.
+ bool closed = false;
+ if (s1PrevKfTime > s2PrevKfTime)
+ {
+ // If s1's flat segment begins after s2's then the interval is
+ // closed only if s1's key frame has differing left and right side
+ // values.
+ closed = (s1PrevNonFlat->GetIsDualValued() &&
+ s1PrevNonFlat->GetValue() != s1PrevNonFlat->GetLeftValue());
+ }
+ else if (s2PrevKfTime > s1PrevKfTime)
+ {
+ // If s2's flat segment begins after s1's then the interval is
+ // closed only if s2's key frame has differing left and right side
+ // values.
+ closed = (s2PrevNonFlat->GetIsDualValued() &&
+ s2PrevNonFlat->GetValue() != s2PrevNonFlat->GetLeftValue());
+ }
+ else // (s2PrevKfTime == s1PrevKfTime)
+ {
+ // Otherwise both spline's flat segments begin at the same time.
+
+ // If the splines are flat to the end, then there is no
+ // evaluative difference between the two. Return an empty
+ // interval.
+ if (s1PrevKfTime == -infinity) {
+ _changedInterval = GfInterval();
+ return false;
+ }
+ // The interval is closed if the left values of the previous key
+ // frames don't match (we've already guaranteed that the right
+ // values match above).
+ //
+ // Note that the value *at* this time will not change, but since
+ // we produce intervals that contain changed knots, we want an
+ // interval that is closed on the right if the left values are
+ // different.
+ closed =
+ s1PrevNonFlat->GetLeftValue() != s2PrevNonFlat->GetLeftValue();
+
+ // We can only potentially tighten more if the key frames have
+ // equivalent values on both sides.
+ canTightenMore = !closed;
+ }
+
+ // Update the changed interval with the new max value.
+ _changedInterval.SetMax(GfMax(s1PrevKfTime, s2PrevKfTime), closed);
+
+ // Update the reverse iterators to the beginning of the flat segments we
+ // just checked.
+ _s1ReverseIter = s1PrevNonFlat;
+ _s2ReverseIter = s2PrevNonFlat;
+ }
+
+ // Otherwise we're not looking at a flat segment so just do a standard
+ // segment equivalence check.
+ else {
+ // First make sure the left sides of the current key frames are
+ // equivalent.
+ if (_s1ReverseIter->IsEquivalentAtSide(*_s2ReverseIter, TsLeft)) {
+ // Move to the previous key frames and check if they're right
+ // equivalent.
+ ++_s1ReverseIter;
+ ++_s2ReverseIter;
+ if (_s1ReverseIter != _s1->rend() &&
+ _s2ReverseIter != _s2->rend() &&
+ _s1ReverseIter->IsEquivalentAtSide(*_s2ReverseIter, TsRight)) {
+ // Compare the left side values to determine if the interval
+ // should be closed.
+ //
+ // Note that the value *at* this time will not change since
+ // the right values are the same, but since we produce
+ // intervals that contain changed knots, we want an interval
+ // that is closed on the right if the left values are
+ // different.
+ const bool closed = (_s1ReverseIter->GetLeftValue() !=
+ _s2ReverseIter->GetLeftValue());
+ _changedInterval.SetMax(_s1ReverseIter->GetTime(), closed);
+
+ // We can continue tightening if the knots are left equivalent.
+ canTightenMore = !closed;
+ }
+ }
+ }
+
+ return canTightenMore;
+}
+
+void
+Ts_SplineChangedIntervalHelper::_TightenFromRight()
+{
+ TRACE_FUNCTION();
+
+ // Initialize the reverse iterators to the last key frame in each spline
+ _s1ReverseIter = _s1->rbegin();
+ _s2ReverseIter = _s2->rbegin();
+
+ // Get the effective extrapolations of each spline on the right side
+ const TsExtrapolationType aExtrapRight =
+ Ts_GetEffectiveExtrapolationType(*_s1ReverseIter, *_s1, TsRight);
+ const TsExtrapolationType bExtrapRight =
+ Ts_GetEffectiveExtrapolationType(*_s2ReverseIter, *_s2, TsRight);
+
+ // We can't tighten if the extrapolations or the extrapolated values are
+ // different.
+ if (aExtrapRight != bExtrapRight ||
+ _s1ReverseIter->GetValue() != _s2ReverseIter->GetValue()) {
+ return;
+ }
+
+ // If the extrapolation is held then tighten to the previous key frame
+ // with right held extrapolation.
+ if (aExtrapRight == TsExtrapolationHeld) {
+ if (!_TightenToPreviousKeyFrame(true /*extrapolateHeldRight*/)) {
+ // If we can't continue tightening then return.
+ return;
+ }
+ }
+ // Otherwise the extrapolation is linear so only if the time and
+ // slopes match, do we not have a change after the last keyframes
+ else if (_s1ReverseIter->GetTime() == _s2ReverseIter->GetTime() &&
+ _s1ReverseIter->GetRightTangentSlope() ==
+ _s2ReverseIter->GetRightTangentSlope()) {
+ // Note that the value *at* this time will not change since the
+ // right values are the same, but since we produce intervals
+ // that contain changed knots, we want an interval that is
+ // closed on the right if the left values are different.
+ const bool closed = (_s1ReverseIter->GetLeftValue() !=
+ _s2ReverseIter->GetLeftValue());
+ _changedInterval.SetMax(_s1ReverseIter->GetTime(), closed);
+ // If the interval is closed, then we can't tighten any more so
+ // just return.
+ if (closed) {
+ return;
+ }
+ } else {
+ // Otherwise we our extrapolations are not tightenable so just
+ // return.
+ return;
+ }
+
+ // Now just continue tightening the interval to the previous key frame
+ // until we can no longer do so.
+ while(_TightenToPreviousKeyFrame());
+}
+
+GfInterval
+TsFindChangedInterval(const TsSpline &s1, const TsSpline &s2)
+{
+ TRACE_FUNCTION();
+ return Ts_SplineChangedIntervalHelper(&s1, &s2).ComputeChangedInterval();
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_DIFF_H
+#define PXR_BASE_TS_DIFF_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/types.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TsSpline;
+
+/// Returns the interval in which the splines \p s1 and \p s2 will
+/// evaluate to different values or in which knots in the splines have
+/// different values.
+///
+/// In particular, if the rightmost changed knot is a dual-valued knot
+/// where the left value has changed and the right value is unchanged,
+/// the returned interval will be closed on the right, even though the
+/// value of the spline *at* the rightmost time does not change.
+TS_API
+GfInterval
+TsFindChangedInterval(
+ const TsSpline &s1,
+ const TsSpline &s2);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/evalCache.h"
+
+#include "pxr/base/ts/data.h"
+#include "pxr/base/ts/keyFrameUtils.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+// static
+Ts_UntypedEvalCache::SharedPtr
+Ts_UntypedEvalCache::New(const TsKeyFrame &kf1, const TsKeyFrame &kf2)
+{
+ return Ts_GetKeyFrameData(kf1)->CreateEvalCache(Ts_GetKeyFrameData(kf2));
+}
+
+VtValue
+Ts_UntypedEvalCache::EvalUncached(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2,
+ TsTime time)
+{
+ return Ts_GetKeyFrameData(kf1)->
+ EvalUncached(Ts_GetKeyFrameData(kf2), time);
+}
+
+VtValue
+Ts_UntypedEvalCache::EvalDerivativeUncached(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2,
+ TsTime time)
+{
+ return Ts_GetKeyFrameData(kf1)->
+ EvalDerivativeUncached(Ts_GetKeyFrameData(kf2), time);
+}
+
+
+////////////////////////////////////////////////////////////////////////
+// ::New implementations for Ts_EvalCache GfQuat specializations
+
+std::shared_ptr<Ts_EvalCache<GfQuatd, true> >
+Ts_EvalCache<GfQuatd, true>::New(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2)
+{
+ return static_cast<const Ts_TypedData<GfQuatd>*>(
+ Ts_GetKeyFrameData(kf1))->
+ CreateTypedEvalCache(Ts_GetKeyFrameData(kf2));
+}
+
+std::shared_ptr<Ts_EvalCache<GfQuatf, true> >
+Ts_EvalCache<GfQuatf, true>::New(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2)
+{
+ return static_cast<const Ts_TypedData<GfQuatf>*>(
+ Ts_GetKeyFrameData(kf1))->
+ CreateTypedEvalCache(Ts_GetKeyFrameData(kf2));
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_EVAL_CACHE_H
+#define PXR_BASE_TS_EVAL_CACHE_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/gf/math.h"
+#include "pxr/base/ts/keyFrameUtils.h"
+#include "pxr/base/ts/mathUtils.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/vt/value.h"
+
+#include "pxr/base/tf/tf.h"
+
+#include <type_traits>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TsKeyFrame;
+template <typename T> class Ts_TypedData;
+
+// Bezier data. This holds two beziers (time and value) as both points
+// and the coefficients of a cubic polynomial.
+template <typename T>
+class Ts_Bezier {
+public:
+ Ts_Bezier() { }
+ Ts_Bezier(const TsTime timePoints[4], const T valuePoints[4]);
+ void DerivePolynomial();
+
+public:
+ TsTime timePoints[4];
+ TsTime timeCoeff[4];
+ T valuePoints[4];
+ T valueCoeff[4];
+};
+
+template <typename T>
+Ts_Bezier<T>::Ts_Bezier(const TsTime time[4], const T value[4])
+{
+ timePoints[0] = time[0];
+ timePoints[1] = time[1];
+ timePoints[2] = time[2];
+ timePoints[3] = time[3];
+ valuePoints[0] = value[0];
+ valuePoints[1] = value[1];
+ valuePoints[2] = value[2];
+ valuePoints[3] = value[3];
+ DerivePolynomial();
+}
+
+template <typename T>
+void
+Ts_Bezier<T>::DerivePolynomial()
+{
+ timeCoeff[0] = timePoints[0];
+ timeCoeff[1] = -3.0 * timePoints[0] +
+ 3.0 * timePoints[1];
+ timeCoeff[2] = 3.0 * timePoints[0] +
+ -6.0 * timePoints[1] +
+ 3.0 * timePoints[2];
+ timeCoeff[3] = -1.0 * timePoints[0] +
+ 3.0 * timePoints[1] +
+ -3.0 * timePoints[2] +
+ timePoints[3];
+ valueCoeff[0] = valuePoints[0];
+ valueCoeff[1] = -3.0 * valuePoints[0] +
+ 3.0 * valuePoints[1];
+ valueCoeff[2] = 3.0 * valuePoints[0] +
+ -6.0 * valuePoints[1] +
+ 3.0 * valuePoints[2];
+ valueCoeff[3] = -1.0 * valuePoints[0] +
+ 3.0 * valuePoints[1] +
+ -3.0 * valuePoints[2] +
+ valuePoints[3];
+}
+
+class Ts_UntypedEvalCache {
+public:
+ typedef std::shared_ptr<Ts_UntypedEvalCache> SharedPtr;
+
+ /// Construct and return a new eval cache for the given keyframes.
+ static SharedPtr New(const TsKeyFrame &kf1, const TsKeyFrame &kf2);
+
+ virtual VtValue Eval(TsTime) const = 0;
+ virtual VtValue EvalDerivative(TsTime) const = 0;
+
+ // Equivalent to invoking New() and Eval(time) on the newly created cache,
+ // but without the heap allocation.
+ static VtValue EvalUncached(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2,
+ TsTime time);
+
+ // Equivalent to invoking New() and EvalDerivative(time) on the newly
+ // created cache, but without the heap allocation.
+ static VtValue EvalDerivativeUncached(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2,
+ TsTime time);
+
+protected:
+ ~Ts_UntypedEvalCache() = default;
+
+ // Compute the Bezier control points.
+ template <typename T>
+ static void _SetupBezierGeometry(TsTime* timePoints, T* valuePoints,
+ const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2);
+
+ // Compute the time coordinate of the 2nd Bezier control point. This
+ // synthesizes tangents for held and linear knots.
+ template <typename T>
+ static TsTime _GetBezierPoint2Time(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2);
+
+ // Compute the time coordinate of the 3rd Bezier control point. This
+ // synthesizes tangents for held and linear knots.
+ template <typename T>
+ static TsTime _GetBezierPoint3Time(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2);
+
+ // Compute the value coordinate of the 2nd Bezier control point. This
+ // synthesizes tangents for held and linear knots.
+ template <typename T>
+ static T _GetBezierPoint2Value(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2);
+
+ // Compute the value coordinate of the 3rd Bezier control point. This
+ // synthesizes tangents for held and linear knots.
+ template <typename T>
+ static T _GetBezierPoint3Value(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2);
+
+ // Compute the value coordinate of the 4th Bezier control point. This
+ // synthesizes tangents for held and linear knots.
+ template <typename T>
+ static T _GetBezierPoint4Value(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2);
+};
+
+template <typename T, bool INTERPOLATABLE = TsTraits<T>::interpolatable >
+class Ts_EvalCache;
+
+template <typename T>
+class Ts_EvalQuaternionCache : public Ts_UntypedEvalCache {
+protected:
+ static_assert(std::is_same<T, GfQuatf>::value
+ || std::is_same<T, GfQuatd>::value
+ , "T must be Quatd or Quatf");
+ Ts_EvalQuaternionCache(const Ts_EvalQuaternionCache<T> * rhs);
+ Ts_EvalQuaternionCache(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2);
+ Ts_EvalQuaternionCache(const TsKeyFrame & kf1,
+ const TsKeyFrame & kf2);
+
+public:
+ T TypedEval(TsTime) const;
+ T TypedEvalDerivative(TsTime) const;
+
+ VtValue Eval(TsTime t) const override;
+ VtValue EvalDerivative(TsTime t) const override;
+private:
+ void _Init(const Ts_TypedData<T>* kf1, const Ts_TypedData<T>* kf2);
+ double _kf1_time, _kf2_time;
+ T _kf1_value, _kf2_value;
+ TsKnotType _kf1_knot_type;
+};
+
+template<>
+class Ts_EvalCache<GfQuatf, true> final
+ : public Ts_EvalQuaternionCache<GfQuatf> {
+public:
+ Ts_EvalCache(const Ts_EvalCache<GfQuatf, true> *rhs) :
+ Ts_EvalQuaternionCache<GfQuatf>(rhs) {}
+ Ts_EvalCache(const Ts_TypedData<GfQuatf>* kf1,
+ const Ts_TypedData<GfQuatf>* kf2) :
+ Ts_EvalQuaternionCache<GfQuatf>(kf1, kf2) {}
+ Ts_EvalCache(const TsKeyFrame & kf1, const TsKeyFrame & kf2) :
+ Ts_EvalQuaternionCache<GfQuatf>(kf1, kf2) {}
+
+ typedef std::shared_ptr<Ts_EvalCache<GfQuatf, true> > TypedSharedPtr;
+
+ /// Construct and return a new eval cache for the given keyframes.
+ static TypedSharedPtr New(const TsKeyFrame &kf1, const TsKeyFrame &kf2);
+};
+
+template<>
+class Ts_EvalCache<GfQuatd, true> final
+ : public Ts_EvalQuaternionCache<GfQuatd> {
+public:
+ Ts_EvalCache(const Ts_EvalCache<GfQuatd, true> *rhs) :
+ Ts_EvalQuaternionCache<GfQuatd>(rhs) {}
+ Ts_EvalCache(const Ts_TypedData<GfQuatd>* kf1,
+ const Ts_TypedData<GfQuatd>* kf2) :
+ Ts_EvalQuaternionCache<GfQuatd>(kf1, kf2) {}
+ Ts_EvalCache(const TsKeyFrame & kf1, const TsKeyFrame & kf2) :
+ Ts_EvalQuaternionCache<GfQuatd>(kf1, kf2) {}
+
+ typedef std::shared_ptr<Ts_EvalCache<GfQuatd, true> > TypedSharedPtr;
+
+ /// Construct and return a new eval cache for the given keyframes.
+ static TypedSharedPtr New(const TsKeyFrame &kf1, const TsKeyFrame &kf2);
+
+};
+
+// Partial specialization for types that cannot be interpolated.
+template <typename T>
+class Ts_EvalCache<T, false> final : public Ts_UntypedEvalCache {
+public:
+ Ts_EvalCache(const Ts_EvalCache<T, false> * rhs);
+ Ts_EvalCache(const Ts_TypedData<T>* kf1, const Ts_TypedData<T>* kf2);
+ Ts_EvalCache(const TsKeyFrame & kf1, const TsKeyFrame & kf2);
+ T TypedEval(TsTime) const;
+ T TypedEvalDerivative(TsTime) const;
+
+ VtValue Eval(TsTime t) const override;
+ VtValue EvalDerivative(TsTime t) const override;
+
+ typedef std::shared_ptr<Ts_EvalCache<T, false> > TypedSharedPtr;
+
+ /// Construct and return a new eval cache for the given keyframes.
+ static TypedSharedPtr New(const TsKeyFrame &kf1, const TsKeyFrame &kf2);
+
+private:
+ T _value;
+};
+
+// Partial specialization for types that can be interpolated.
+template <typename T>
+class Ts_EvalCache<T, true> final : public Ts_UntypedEvalCache {
+public:
+ Ts_EvalCache(const Ts_EvalCache<T, true> * rhs);
+ Ts_EvalCache(const Ts_TypedData<T>* kf1, const Ts_TypedData<T>* kf2);
+ Ts_EvalCache(const TsKeyFrame & kf1, const TsKeyFrame & kf2);
+ T TypedEval(TsTime) const;
+ T TypedEvalDerivative(TsTime) const;
+
+ VtValue Eval(TsTime t) const override;
+ VtValue EvalDerivative(TsTime t) const override;
+
+ const Ts_Bezier<T>* GetBezier() const;
+
+ typedef std::shared_ptr<Ts_EvalCache<T, true> > TypedSharedPtr;
+
+ /// Construct and return a new eval cache for the given keyframes.
+ static TypedSharedPtr New(const TsKeyFrame &kf1, const TsKeyFrame &kf2);
+
+private:
+ void _Init(const Ts_TypedData<T>* kf1, const Ts_TypedData<T>* kf2);
+
+private:
+ bool _interpolate;
+
+ // Value to use when _interpolate is false.
+ T _value;
+
+ Ts_Bezier<T> _cache;
+};
+
+////////////////////////////////////////////////////////////////////////
+// Ts_UntypedEvalCache
+
+template <typename T>
+TsTime
+Ts_UntypedEvalCache::_GetBezierPoint2Time(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ switch (kf1->_knotType) {
+ default:
+ case TsKnotHeld:
+ case TsKnotLinear:
+ return (2.0 * kf1->GetTime() + kf2->GetTime()) / 3.0;
+
+ case TsKnotBezier:
+ return kf1->GetTime() + kf1->_rightTangentLength;
+ }
+}
+
+template <typename T>
+TsTime
+Ts_UntypedEvalCache::_GetBezierPoint3Time(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ // If the the first keyframe is held then the we treat the third bezier
+ // point as held too.
+ TsKnotType knotType = (kf1->_knotType == TsKnotHeld) ?
+ TsKnotHeld : kf2->_knotType;
+
+ switch (knotType) {
+ default:
+ case TsKnotHeld:
+ case TsKnotLinear:
+ return (kf1->GetTime() + 2.0 * kf2->GetTime()) / 3.0;
+
+ case TsKnotBezier:
+ return kf2->GetTime() - kf2->_leftTangentLength;
+ }
+}
+
+template <typename T>
+T
+Ts_UntypedEvalCache::_GetBezierPoint2Value(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ switch (kf1->_knotType) {
+ default:
+ case TsKnotHeld:
+ return kf1->_GetRightValue();
+
+ case TsKnotLinear:
+ return (1.0 / 3.0) *
+ (2.0 * kf1->_GetRightValue() +
+ (kf2->_isDual ? kf2->_GetLeftValue() : kf2->_GetRightValue()));
+
+ case TsKnotBezier:
+ return kf1->_GetRightValue() +
+ kf1->_rightTangentLength * kf1->_GetRightTangentSlope();
+ }
+}
+
+template <typename T>
+T
+Ts_UntypedEvalCache::_GetBezierPoint3Value(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ // If the first key frame is held then the we just use the first key frame's
+ // value
+ if (kf1->_knotType == TsKnotHeld) {
+ return kf1->_GetRightValue();
+ }
+
+ switch (kf2->_knotType) {
+ default:
+ case TsKnotHeld:
+ if (kf1->_knotType != TsKnotLinear) {
+ return kf2->_isDual ? kf2->_GetLeftValue() : kf2->_GetRightValue();
+ }
+ // Fall through to linear case if the first knot is linear
+ [[fallthrough]];
+
+ case TsKnotLinear:
+ return (1.0 / 3.0) *
+ (kf1->_GetRightValue() + 2.0 *
+ (kf2->_isDual ? kf2->_GetLeftValue() : kf2->_GetRightValue()));
+
+ case TsKnotBezier:
+ return (kf2->_isDual ? kf2->_GetLeftValue() : kf2->_GetRightValue()) -
+ kf2->_leftTangentLength * kf2->_GetLeftTangentSlope();
+ }
+}
+
+template <typename T>
+T
+Ts_UntypedEvalCache::_GetBezierPoint4Value(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ // If the first knot is held then the last value is still the value of
+ // the first knot, otherwise it's the left side of the second knot
+ if (kf1->_knotType == TsKnotHeld) {
+ return kf1->_GetRightValue();
+ } else {
+ return (kf2->_isDual ? kf2->_GetLeftValue() : kf2->_GetRightValue());
+ }
+}
+
+template <typename T>
+void
+Ts_UntypedEvalCache::_SetupBezierGeometry(
+ TsTime* timePoints, T* valuePoints,
+ const Ts_TypedData<T>* kf1, const Ts_TypedData<T>* kf2)
+{
+ timePoints[0] = kf1->GetTime();
+ timePoints[1] = _GetBezierPoint2Time(kf1, kf2);
+ timePoints[2] = _GetBezierPoint3Time(kf1, kf2);
+ timePoints[3] = kf2->GetTime();
+ valuePoints[0] = kf1->_GetRightValue();
+ valuePoints[1] = _GetBezierPoint2Value(kf1, kf2);
+ valuePoints[2] = _GetBezierPoint3Value(kf1, kf2);
+ valuePoints[3] = _GetBezierPoint4Value(kf1, kf2);
+}
+
+////////////////////////////////////////////////////////////////////////
+// Ts_EvalCache non-interpolatable
+
+template <typename T>
+Ts_EvalCache<T, false>::Ts_EvalCache(const Ts_EvalCache<T, false> * rhs)
+{
+ _value = rhs->_value;
+}
+
+template <typename T>
+Ts_EvalCache<T, false>::Ts_EvalCache(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ if (!kf1 || !kf2) {
+ TF_CODING_ERROR("Constructing an Ts_EvalCache from invalid keyframes");
+ return;
+ }
+
+ _value = kf1->_GetRightValue();
+}
+
+template <typename T>
+Ts_EvalCache<T, false>::Ts_EvalCache(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2)
+{
+ // Cast to the correct typed data. This is a private class, and we assume
+ // callers are passing only keyframes from the same spline, and correctly
+ // arranging our T to match.
+ Ts_TypedData<T> *data =
+ static_cast<Ts_TypedData<T> const*>(Ts_GetKeyFrameData(kf1));
+
+ _value = data->_GetRightValue();
+}
+
+template <typename T>
+VtValue
+Ts_EvalCache<T, false>::Eval(TsTime t) const {
+ return VtValue(TypedEval(t));
+}
+
+template <typename T>
+VtValue
+Ts_EvalCache<T, false>::EvalDerivative(TsTime t) const {
+ return VtValue(TypedEvalDerivative(t));
+}
+
+template <typename T>
+T
+Ts_EvalCache<T, false>::TypedEval(TsTime) const
+{
+ return _value;
+}
+
+template <typename T>
+T
+Ts_EvalCache<T, false>::TypedEvalDerivative(TsTime) const
+{
+ return TsTraits<T>::zero;
+}
+
+////////////////////////////////////////////////////////////////////////
+// Ts_EvalCache interpolatable
+
+template <typename T>
+Ts_EvalCache<T, true>::Ts_EvalCache(const Ts_EvalCache<T, true> * rhs)
+{
+ _interpolate = rhs->_interpolate;
+ _value = rhs->_value;
+ _cache = rhs->_cache;
+}
+
+template <typename T>
+Ts_EvalCache<T, true>::Ts_EvalCache(const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ _Init(kf1,kf2);
+}
+
+template <typename T>
+Ts_EvalCache<T, true>::Ts_EvalCache(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2)
+{
+ // Cast to the correct typed data. This is a private class, and we assume
+ // callers are passing only keyframes from the same spline, and correctly
+ // arranging our T to match.
+ _Init(static_cast<Ts_TypedData<T> const*>(Ts_GetKeyFrameData(kf1)),
+ static_cast<Ts_TypedData<T> const*>(Ts_GetKeyFrameData(kf2)));
+}
+
+template <typename T>
+void
+Ts_EvalCache<T, true>::_Init(
+ const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ if (!kf1 || !kf2) {
+ TF_CODING_ERROR("Constructing an Ts_EvalCache from invalid keyframes");
+ return;
+ }
+
+ // Curve for same knot types or left half of blend for different knot types
+ _SetupBezierGeometry(_cache.timePoints, _cache.valuePoints, kf1, kf2);
+ _cache.DerivePolynomial();
+
+ if (kf1->ValueCanBeInterpolated() && kf2->ValueCanBeInterpolated()) {
+ _interpolate = true;
+ } else {
+ _interpolate = false;
+ _value = kf1->_GetRightValue();
+ }
+}
+
+template <typename T>
+VtValue
+Ts_EvalCache<T, true>::Eval(TsTime t) const {
+ return VtValue(TypedEval(t));
+}
+
+template <typename T>
+VtValue
+Ts_EvalCache<T, true>::EvalDerivative(TsTime t) const {
+ return VtValue(TypedEvalDerivative(t));
+}
+
+template <typename T>
+T
+Ts_EvalCache<T, true>::TypedEval(TsTime time) const
+{
+ if (!_interpolate)
+ return _value;
+
+ double u = GfClamp(Ts_SolveCubic(_cache.timeCoeff, time), 0.0, 1.0);
+ return Ts_EvalCubic(_cache.valueCoeff, u);
+}
+
+template <typename T>
+T
+Ts_EvalCache<T, true>::TypedEvalDerivative(TsTime time) const
+{
+ if (!TsTraits<T>::supportsTangents || !_interpolate) {
+ return TsTraits<T>::zero;
+ }
+
+ // calculate the derivative as
+ // u = t^-1(time)
+ // dx(u)
+ // ----
+ // du dx(u)
+ // -------- = -----
+ // dt(u) dt(u)
+ // ----
+ // du
+ double u;
+ u = GfClamp(Ts_SolveCubic(_cache.timeCoeff, time), 0.0, 1.0);
+ T x = Ts_EvalCubicDerivative(_cache.valueCoeff, u);
+ TsTime t = Ts_EvalCubicDerivative(_cache.timeCoeff, u);
+ T derivative = x * (1.0 / t);
+ return derivative;
+}
+
+template <typename T>
+const Ts_Bezier<T>*
+Ts_EvalCache<T, true>::GetBezier() const
+{
+ return &_cache;
+}
+
+template <typename T>
+std::shared_ptr<Ts_EvalCache<T, true> >
+Ts_EvalCache<T, true>::New(const TsKeyFrame &kf1, const TsKeyFrame &kf2)
+{
+ // Cast to the correct typed data. This is a private class, and we assume
+ // callers are passing only keyframes from the same spline, and correctly
+ // arranging our T to match.
+ return static_cast<const Ts_TypedData<T>*>(
+ Ts_GetKeyFrameData(kf1))->
+ CreateTypedEvalCache(Ts_GetKeyFrameData(kf2));
+}
+
+template <typename T>
+std::shared_ptr<Ts_EvalCache<T, false> >
+Ts_EvalCache<T, false>::New(const TsKeyFrame &kf1, const TsKeyFrame &kf2)
+{
+ // Cast to the correct typed data. This is a private class, and we assume
+ // callers are passing only keyframes from the same spline, and correctly
+ // arranging our T to match.
+ return static_cast<const Ts_TypedData<T>*>(
+ Ts_GetKeyFrameData(kf1))->
+ CreateTypedEvalCache(Ts_GetKeyFrameData(kf2));
+}
+
+////////////////////////////////////////////////////////////////////////
+// Ts_EvalQuaternionCache
+
+template <typename T>
+Ts_EvalQuaternionCache<T>::Ts_EvalQuaternionCache(
+ const Ts_EvalQuaternionCache<T> * rhs)
+{
+ _kf1_knot_type = rhs->_kf1_knot_type;
+
+ _kf1_time = rhs->_kf1_time;
+ _kf2_time = rhs->_kf2_time;
+
+ _kf1_value = rhs->_kf1_value;
+ _kf2_value = rhs->_kf2_value;
+}
+
+template <typename T>
+Ts_EvalQuaternionCache<T>::Ts_EvalQuaternionCache(
+ const Ts_TypedData<T>* kf1, const Ts_TypedData<T>* kf2)
+{
+ _Init(kf1,kf2);
+}
+
+template <typename T>
+Ts_EvalQuaternionCache<T>::Ts_EvalQuaternionCache(const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2)
+{
+ // Cast to the correct typed data. This is a private class, and we assume
+ // callers are passing only keyframes from the same spline, and correctly
+ // arranging our T to match.
+ _Init(static_cast<Ts_TypedData<T> const*>(Ts_GetKeyFrameData(kf1)),
+ static_cast<Ts_TypedData<T> const*>(Ts_GetKeyFrameData(kf2)));
+}
+
+template <typename T>
+void
+Ts_EvalQuaternionCache<T>::_Init(
+ const Ts_TypedData<T>* kf1,
+ const Ts_TypedData<T>* kf2)
+{
+ if (!kf1 || !kf2) {
+ TF_CODING_ERROR("Constructing an Ts_EvalQuaternionCache"
+ " from invalid keyframes");
+ return;
+ }
+
+ _kf1_knot_type = kf1->_knotType;
+
+ _kf1_time = kf1->GetTime();
+ _kf2_time = kf2->GetTime();
+
+ _kf1_value = kf1->_GetRightValue();
+ _kf2_value = kf2->_isDual ? kf2->_GetLeftValue() : kf2->_GetRightValue();
+}
+
+template <typename T>
+VtValue
+Ts_EvalQuaternionCache<T>::Eval(TsTime t) const {
+ return VtValue(TypedEval(t));
+}
+
+template <typename T>
+T Ts_EvalQuaternionCache<T>::TypedEval(TsTime time) const
+{
+ if (_kf1_knot_type == TsKnotHeld) {
+ return _kf1_value;
+ }
+
+ // XXX: do we want any snapping here? divide-by-zero avoidance?
+ // The following code was in Presto; not sure it belongs in Ts.
+ //
+ //if (fabs(_kf2_time - _kf1_time) < ARCH_MIN_FLOAT_EPS_SQR) {
+ // return _kf1_value;
+ //}
+
+ double u = (time - _kf1_time) / (_kf2_time - _kf1_time);
+ return GfSlerp(_kf1_value, _kf2_value, u);
+}
+
+template<typename T>
+VtValue Ts_EvalQuaternionCache<T>::EvalDerivative(TsTime t) const {
+ return VtValue(TypedEvalDerivative(t));
+}
+
+template<typename T>
+T Ts_EvalQuaternionCache<T>::TypedEvalDerivative(TsTime) const {
+ return TsTraits<T>::zero;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "evalUtils.h"
+
+#include "pxr/base/ts/keyFrameUtils.h"
+
+#include "pxr/base/gf/math.h"
+#include "pxr/base/gf/interval.h"
+
+#include <limits>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TsExtrapolationType
+Ts_GetEffectiveExtrapolationType(
+ const TsKeyFrame& kf,
+ const TsExtrapolationPair &extrapolation,
+ bool kfIsOnlyKeyFrame,
+ TsSide side)
+{
+ // Check for held extrapolation
+ if ((side == TsLeft && extrapolation.first == TsExtrapolationHeld) ||
+ (side == TsRight && extrapolation.second == TsExtrapolationHeld)) {
+ return TsExtrapolationHeld;
+ }
+
+ // Extrapolation is held if key frame is Held
+ if (kf.GetKnotType() == TsKnotHeld) {
+ return TsExtrapolationHeld;
+ }
+
+ // Extrapolation is held if key frame is dual valued and doesn't have
+ // tangents (because there's no slope to extrapolate due to the dual
+ // value discontinuity).
+ if ((!kf.HasTangents() ) &&
+ kf.GetIsDualValued()) {
+ return TsExtrapolationHeld;
+ }
+
+ // Extrapolation is held if there's only one key frame and it
+ // doesn't have tangents.
+ if ((!kf.HasTangents()) && kfIsOnlyKeyFrame) {
+ return TsExtrapolationHeld;
+ }
+
+ // Use extrapolation on spline
+ return (side == TsLeft) ? extrapolation.first : extrapolation.second;
+}
+
+TsExtrapolationType
+Ts_GetEffectiveExtrapolationType(
+ const TsKeyFrame& kf,
+ const TsSpline &spline,
+ TsSide side)
+{
+ return Ts_GetEffectiveExtrapolationType(kf,spline.GetExtrapolation(),
+ spline.size() == 1, side);
+}
+
+////////////////////////////////////////////////////////////////////////
+
+static VtValue
+_GetSlope(
+ TsTime time,
+ TsSpline::const_iterator i,
+ const TsSpline & val,
+ TsSide side)
+{
+ const TsKeyFrame &kf = *i;
+ VtValue slope;
+ switch (Ts_GetEffectiveExtrapolationType(kf, val, side)) {
+ default:
+ case TsExtrapolationHeld:
+ slope = kf.GetZero();
+ break;
+
+ case TsExtrapolationLinear:
+ if (kf.HasTangents() ) {
+ slope = (side == TsLeft) ? kf.GetLeftTangentSlope() :
+ kf.GetRightTangentSlope();
+ }
+ else {
+ // Set i and j to the left and right key frames of the segment
+ // with the slope we want to extrapolate.
+ TsSpline::const_iterator j = i;
+ if (side == TsLeft) {
+ // i is on the left so move j to the right
+ ++j;
+ }
+ else {
+ // i is on the right so move it to the left
+ --i;
+ }
+ slope = Ts_GetKeyFrameData(*i)->GetSlope(*Ts_GetKeyFrameData(*j));
+ }
+ break;
+ }
+
+ return slope;
+}
+
+static VtValue
+_Extrapolate(
+ TsTime time,
+ TsSpline::const_iterator i,
+ const TsSpline &val,
+ TsSide side)
+{
+ VtValue slope = _GetSlope(time, i, val, side);
+ const TsKeyFrame& kf = *i;
+ VtValue value = (side == TsLeft) ? kf.GetLeftValue() : kf.GetValue();
+ TsTime dt = time - kf.GetTime();
+
+ return Ts_GetKeyFrameData(kf)->Extrapolate(value,dt,slope);
+}
+
+static VtValue
+_ExtrapolateDerivative(
+ TsTime time,
+ TsSpline::const_iterator i,
+ const TsSpline &val,
+ TsSide side)
+{
+ return _GetSlope(time, i, val, side);
+}
+
+VtValue
+Ts_Eval(
+ const TsSpline &val,
+ TsTime time, TsSide side,
+ Ts_EvalType evalType)
+{
+ if (val.empty()) {
+ return VtValue();
+ }
+
+ // XXX: do we want any snapping here? divide-by-zero avoidance?
+ // The following code was in Presto; not sure it belongs in Ts. In
+ // particular, we shouldn't assume integer knot times.
+ //
+ // If very close to a knot, eval at the knot. Because of dual
+ // valued knots, all sorts of trouble can result if you mean to
+ // sample at a knot time and are actually epsilon off.
+ //TsTime rounded = round(time);
+ //if (fabs(rounded-time) < ARCH_MIN_FLOAT_EPS_SQR)
+ // time = rounded;
+
+ // Get the keyframe after time
+ TsSpline::const_iterator iAfterTime = val.upper_bound(time);
+ TsSpline::const_iterator i = iAfterTime;
+
+ // Check boundary cases
+ if (i == val.begin()) {
+ // Before first keyframe. Extrapolate to the left.
+ return (evalType == Ts_EvalValue) ?
+ _Extrapolate(time, i, val, TsLeft) :
+ _ExtrapolateDerivative(time, i, val, TsLeft);
+ }
+ // Note if at or after last keyframe.
+ bool last = (i == val.end());
+
+ // Get the keyframe at or before time
+ --i;
+
+ if (i->GetTime() == time && side == TsLeft) {
+ // Evaluate at a keyframe on the left. If the previous
+ // keyframe is held then use the right side of the previous
+ // keyframe.
+ if (i != val.begin()) {
+ TsSpline::const_iterator j = i;
+ --j;
+ if (j->GetKnotType() == TsKnotHeld) {
+ return (evalType == Ts_EvalValue) ?
+ j->GetValue() :
+ j->GetValueDerivative();
+ }
+ }
+ // handle derivatives of linear knots at keyframes differently
+ if (i->GetKnotType() == TsKnotLinear &&
+ evalType == Ts_EvalDerivative) {
+ // if we are next to last, eval from the right,
+ // otherwise use the specified direction
+ return _GetSlope(time, i, val, last && (side == TsLeft) ?
+ TsRight :
+ side);
+ }
+ return (evalType == Ts_EvalValue) ?
+ i->GetLeftValue() :
+ i->GetLeftValueDerivative();
+ }
+ else if (last) {
+ // After last key frame. Extrapolate to the right.
+ return (evalType == Ts_EvalValue) ?
+ _Extrapolate(time, i, val, TsRight) :
+ _ExtrapolateDerivative(time, i, val, TsRight);
+ }
+ else if (i->GetTime() == time) {
+ // Evaluate at a keyframe on the right
+ // handle derivatives of linear knots at keyframes differently
+ if (i->GetKnotType() == TsKnotLinear
+ && evalType == Ts_EvalDerivative)
+ {
+ return _GetSlope(time, i, val,
+ (i == val.begin() && side == TsRight) ?
+ TsLeft : side);
+ }
+ return (evalType == Ts_EvalValue) ?
+ i->GetValue() :
+ i->GetValueDerivative();
+ }
+ else {
+ // Evaluate at a keyframe on the right or between keyframes
+ return (evalType == Ts_EvalValue) ?
+ Ts_UntypedEvalCache::EvalUncached(*i, *iAfterTime, time) :
+ Ts_UntypedEvalCache::EvalDerivativeUncached(
+ *i, *iAfterTime, time);
+ }
+}
+
+// For the routine below, define loose comparisons to account for precision
+// errors. This epsilon value is always used on the parameter space [0, 1],
+// meaning it has the same effect no matter what the domain and range of the
+// segment are.
+#define EPS 1e-6
+#define LT(a,b) ((b)-(a) > EPS)
+
+bool
+Ts_IsSegmentValueMonotonic( const TsKeyFrame &kf1, const TsKeyFrame &kf2 )
+{
+ bool monotonic = false;
+ VtValue kf2LeftVtVal = kf2.GetLeftValue();
+ VtValue kf1VtVal = kf1.GetValue();
+ VtValue kf2LeftTangentSlopeVtVal = kf2.GetLeftTangentSlope();
+ VtValue kf1RightTangentSlopeVtVal= kf1.GetRightTangentSlope();
+
+ if (kf1.GetTime() >= kf2.GetTime()) {
+ TF_CODING_ERROR("The first key frame must come before the second.");
+ return false;
+ }
+
+ if (kf1.GetKnotType() == TsKnotBezier &&
+ kf2.GetKnotType() == TsKnotBezier &&
+ kf1VtVal.IsHolding<double>() &&
+ kf2LeftVtVal.IsHolding<double>() &&
+ kf1RightTangentSlopeVtVal.IsHolding<double>() &&
+ kf2LeftTangentSlopeVtVal.IsHolding<double>())
+ {
+ monotonic = true;
+ //get Bezier control points
+ double x0 = kf1VtVal.Get<double>();
+ double x1 = kf1VtVal.Get<double>() +
+ ( kf1.GetRightTangentSlope().Get<double>() *
+ kf1.GetRightTangentLength());
+ double x2 = kf2LeftVtVal.Get<double>() -
+ ( kf2.GetLeftTangentSlope().Get<double>() *
+ kf2.GetLeftTangentLength());
+ double x3 = kf2LeftVtVal.Get<double>();
+ // By taking the derivative of Bezier curve equation we obtain:
+ //
+ // f'(x0 + (-3x0+3x1)*t + (-x0+3x1-3x2+x3)*t^2+(-3x0+9x1-9x2+3x3)*t^3)=
+ // (-3x0 + 9x1-9x2+3x3)*t^2 + (6x0-12x1-6x2)*t+(-3x0+3x1)
+ //
+ // (-3x0 + 9x1-9x2+3x3)*t^2 + (6x0-12x1-6x2)*t+(-3x0+3x1) =0
+ //
+ // divide by 3:
+ // (-x0 + 3x1-3x2+x3)*t^2 + (2x0-4x1-2x2)*t+(-x0+x1)=0
+ double a = -x0+(3*x1)-(3*x2)+x3;
+ double b = (2*x0)-(4*x1)+(2*x2);
+ double c = -x0+x1;
+ double polyDeriv[3] = {c, b, a};
+ double root0 = 0.0, root1 = 0.0;
+
+ // compute the roots of the equation
+ // using _SolveQuadratic() to solve for the roots
+ if (Ts_SolveQuadratic(polyDeriv, &root0, &root1)) {
+
+ //IF we have a parabola there will be only one maximum/minimum
+ // if a == 0, than the cubic term of the bezier equation is
+ // zero as well, giving us the quadratic bezier curve.
+ if ((GfIsClose(a, 0, EPS)) && (LT(0, root0) && LT(root0, 1))) {
+ monotonic = false;
+ }
+ //IF we have a hyperbola there can be two maxima/minima
+ //IF two roots are equal: we have a point where the slope
+ // becomes horizontal but than it continues in the way it was
+ // before the point (monotonic = true)
+ //IF two roots are different: and either of them falls in the
+ // range between 0 and 1 we have a maximum/minimum
+ // (monotonic = false)
+ else if ((!GfIsClose(root0, root1, EPS)) &&
+ ((LT(0, root0) && LT(root0, 1)) ||
+ (LT(0, root1) && LT(root1, 1))) )
+ {
+ monotonic = false;
+ }
+ }
+ }
+ return monotonic;
+}
+
+////////////////////////////////////////////////////////////////////////
+// Functions shared by piecewise linear sampling and range functions
+
+static
+std::pair<TsSpline::const_iterator, TsSpline::const_iterator>
+_GetBounds( const TsSpline & val, TsTime startTime, TsTime endTime )
+{
+ if (startTime > endTime) {
+ TF_CODING_ERROR("invalid interval (start > end)");
+ return std::make_pair(val.end(), val.end());
+ }
+
+ // Find the bounding keyframes. We first find the keyframe at or before
+ // the startTime and the keyframe after endTime to determine the segments.
+ // If there is no keyframe at or before startTime we use the first keyframe
+ // and if there is no keyframe after endTime we use the last keyframe.
+ // This function assumes there's at least one keyframe.
+ TsSpline::const_iterator i = val.upper_bound(startTime);
+ if (i != val.begin()) {
+ --i;
+ }
+ TsSpline::const_iterator j = val.upper_bound(endTime);
+ if (j == val.end()) {
+ --j;
+ }
+
+ return std::make_pair(i, j);
+}
+
+////////////////////////////////////////////////////////////////////////
+// Range functions
+
+// Note that if there is a knot at endTime that is discontinuous, its right
+// side will be ignored
+template<typename T>
+ static std::pair<T, T>
+_GetBezierRange( const Ts_Bezier<T>* bezier,
+ double startTime, double endTime )
+{
+ T min = std::numeric_limits<T>::infinity();
+ T max = -std::numeric_limits<T>::infinity();
+
+ // Find the limits of the spline parameter within [startTime,endTime).
+ double uMin = 0.0, uMax = 1.0;
+ if (startTime > bezier->timePoints[0] || endTime < bezier->timePoints[3]) {
+ if (startTime > bezier->timePoints[0]) {
+ uMin = GfClamp(Ts_SolveCubic(bezier->timeCoeff,
+ startTime), 0.0, 1.0);
+ }
+ if (endTime < bezier->timePoints[3]) {
+ uMax = GfClamp(Ts_SolveCubic(bezier->timeCoeff,
+ endTime), 0.0, 1.0);
+ }
+ if (uMin > uMax) {
+ uMin = uMax;
+ }
+ }
+
+ // Get initial bounds from the endpoints.
+ if (uMin == 0.0) {
+ min = GfMin(min, bezier->valuePoints[0]);
+ max = GfMax(max, bezier->valuePoints[0]);
+ }
+ else {
+ T y = Ts_EvalCubic(bezier->valueCoeff, uMin);
+ min = GfMin(min, y);
+ max = GfMax(max, y);
+ }
+ if (uMax == 1.0) {
+ min = GfMin(min, bezier->valuePoints[3]);
+ max = GfMax(max, bezier->valuePoints[3]);
+ }
+ else {
+ T y = Ts_EvalCubic(bezier->valueCoeff, uMax);
+ min = GfMin(min, y);
+ max = GfMax(max, y);
+ }
+
+ // Find the roots of the derivative of the value Bezier. The values
+ // at these points plus the end points are the candidates for the
+ // min and max.
+ double valueDeriv[3], root0, root1;
+ Ts_CubicDerivative(bezier->valueCoeff, valueDeriv);
+ if (Ts_SolveQuadratic(valueDeriv, &root0, &root1)) {
+ if (root0 > uMin && root0 < uMax) {
+ T y = Ts_EvalCubic(bezier->valueCoeff, root0);
+ min = GfMin(min, y);
+ max = GfMax(max, y);
+ }
+ if (root1 > uMin && root1 < uMax) {
+ T y = Ts_EvalCubic(bezier->valueCoeff, root1);
+ min = GfMin(min, y);
+ max = GfMax(max, y);
+ }
+ }
+
+ return std::make_pair(min, max);
+}
+
+// Note that if there is a knot at endTime that is discontinuous, its right
+// side will be ignored
+template<typename T>
+static std::pair<T, T>
+_GetSegmentRange(const Ts_EvalCache<T> * cache,
+ double startTime, double endTime )
+{
+ return _GetBezierRange<T>(cache->GetBezier(), startTime, endTime);
+}
+
+template<typename T>
+static std::pair<VtValue, VtValue>
+_GetCurveRange( const TsSpline & val, double startTime, double endTime )
+{
+ T min = std::numeric_limits<T>::infinity();
+ T max = -std::numeric_limits<T>::infinity();
+
+ // Find the latest key that's <= startTime; if all are later, use the
+ // first key.
+ //
+ // This returns the first key that's > startTime.
+ TsSpline::const_iterator i = val.upper_bound(startTime);
+ if (i == val.begin()) {
+ // All are > startTime; include left side of first keyframe
+ T v = i->GetLeftValue().template Get<T>();
+ min = GfMin(min, v);
+ max = GfMax(max, v);
+ }
+ else {
+ // The one before must be <= startTime
+ --i;
+ }
+
+ // Normally, we don't have to do anything to include the value of right
+ // side of the last knot that's wihin the range, but there are a couple of
+ // cases where we do have to force this, below.
+ bool forceRightSideOfLowerBound = false;
+
+ // Find the earliest key that's >= endTime; if all are earlier, use the
+ // latest.
+ //
+ // This returns the earliest key that's >= endTime.
+ TsSpline::const_iterator j = val.lower_bound(endTime);
+ if (j == val.end()) {
+ // all are < endTime; for j, use the latest and make sure we include
+ // its right side below
+ --j;
+ forceRightSideOfLowerBound = true;
+ }
+
+ // The other case where we need to force inclusion of the right side of
+ // the last knot is when it's at endTime, and it's discontinuous.
+ // (_GetSegmentRange below deals in bezier's which can't be
+ // discontinuous, and so it does not consider the right side of
+ // discontiguous knots at the right boundary.)
+ if (!forceRightSideOfLowerBound && j->GetTime() == endTime) {
+ if (j->GetIsDualValued())
+ forceRightSideOfLowerBound = true;
+ // Is prev knot held?
+ else if (j != val.begin()) {
+ TsSpline::const_iterator k = j;
+ k--;
+ if (k->GetKnotType() == TsKnotHeld)
+ forceRightSideOfLowerBound = true;
+ }
+ }
+
+ // If right side forced, include it now
+ if (forceRightSideOfLowerBound) {
+ T v = j->GetValue().template Get<T>();
+ min = GfMin(min, v);
+ max = GfMax(max, v);
+ }
+
+ // Handle the keyframe segments in the interval, excluding the region
+ // (if any) past the end of the last keyframe, as this region is always
+ // held, and its range would not contribute to the total range.
+ TsSpline::const_iterator i2 = i; i2++;
+ while (i != j) {
+ if (i2 != val.end()) {
+ Ts_EvalCache<T> cache(*i, *i2);
+
+ std::pair<T, T> range =
+ _GetSegmentRange<T>(&cache, startTime, endTime);
+ min = GfMin(min, range.first);
+ max = GfMax(max, range.second);
+ }
+
+ i = i2;
+ i2++;
+ }
+
+ return std::make_pair(VtValue(min), VtValue(max));
+}
+
+std::pair<VtValue, VtValue>
+Ts_GetRange( const TsSpline & val, TsTime startTime, TsTime endTime )
+{
+ if (startTime > endTime) {
+ TF_CODING_ERROR("invalid interval (start > end)");
+ return std::make_pair(VtValue(), VtValue());
+ }
+
+ if (val.IsEmpty()) {
+ return std::make_pair(VtValue(), VtValue());
+ }
+
+ // Range at a point is just the value at that point. We want to
+ // ignore extrapolation so ensure we're within the interval covered
+ // by key frames.
+ if (startTime == endTime) {
+ if (startTime < val.begin()->GetTime()) {
+ VtValue y = val.Eval(val.begin()->GetTime(), TsLeft);
+ return std::make_pair(y, y);
+ }
+ else if (startTime >= val.rbegin()->GetTime()) {
+ VtValue y = val.Eval(val.rbegin()->GetTime(), TsRight);
+ return std::make_pair(y, y);
+ }
+ else {
+ VtValue y = val.Eval(startTime, TsRight);
+ return std::make_pair(y, y);
+ }
+ }
+
+ // Get the range over the segments
+ const std::type_info & t = val.GetTypeid();
+ if (TfSafeTypeCompare(t, typeid(double))) {
+ return _GetCurveRange<double>(val, startTime, endTime);
+ }
+ else if (TfSafeTypeCompare(t, typeid(float))) {
+ return _GetCurveRange<float>(val, startTime, endTime);
+ }
+ else {
+ // Cannot interpolate
+ return std::make_pair(VtValue(), VtValue());
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+// Piecewise linear sampling functions
+
+// Determine how far the inner Bezier polygon points are from the line
+// connecting the outer points. Return the maximum distance.
+template <typename T>
+static double
+_BezierHeight( const TsTime timeBezier[4], const T valueBezier[4],
+ double timeScale, double valueScale )
+{
+ T dv = (valueBezier[3] - valueBezier[0]) * valueScale;
+ TsTime dt = ( timeBezier[3] - timeBezier[0]) * timeScale;
+ T dv1 = (valueBezier[1] - valueBezier[0]) * valueScale;
+ TsTime dt1 = ( timeBezier[1] - timeBezier[0]) * timeScale;
+ T dv2 = (valueBezier[2] - valueBezier[0]) * valueScale;
+ TsTime dt2 = ( timeBezier[2] - timeBezier[0]) * timeScale;
+
+ double len = dv * dv + dt * dt;
+
+ double t1 = (dv1 * dv + dt1 * dt) / len;
+ double t2 = (dv2 * dv + dt2 * dt) / len;
+
+ double d1 = hypot(dv1 - t1 * dv, dt1 - t1 * dt);
+ double d2 = hypot(dv2 - t2 * dv, dt2 - t2 * dt);
+
+ return GfMax(d1, d2);
+}
+
+template <typename T>
+static void
+_SubdivideBezier( const T inBezier[4], T outBezier[4], double u, bool leftSide)
+{
+ if (leftSide) {
+ // Left Bezier
+ T mid = GfLerp(u, inBezier[1], inBezier[2]);
+ T tmp1 = GfLerp(u, inBezier[2], inBezier[3]);
+ T tmp0 = GfLerp(u, mid, tmp1);
+ outBezier[0] = inBezier[0];
+ outBezier[1] = GfLerp(u, inBezier[0], inBezier[1]);
+ outBezier[2] = GfLerp(u, outBezier[1], mid);
+ outBezier[3] = GfLerp(u, outBezier[2], tmp0);
+ }
+ else {
+ // Right Bezier
+ T mid = GfLerp(u, inBezier[1], inBezier[2]);
+ T tmp1 = GfLerp(u, inBezier[0], inBezier[1]);
+ T tmp0 = GfLerp(u, tmp1, mid);
+ outBezier[3] = inBezier[3];
+ outBezier[2] = GfLerp(u, inBezier[2], inBezier[3]);
+ outBezier[1] = GfLerp(u, mid, outBezier[2]);
+ outBezier[0] = GfLerp(u, tmp0, outBezier[1]);
+ }
+}
+
+// Sample a pair of Beziers (value and time) with results in samples.
+template <typename T>
+static void
+_SampleBezier( const TsTime timeBezier[4], const T valueBezier[4],
+ double startTime, double endTime,
+ double timeScale, double valueScale, double tolerance,
+ TsSamples & samples )
+{
+ // Beziers have the convex hull property and are easily subdivided.
+ // We use the convex hull to determine if a linear interpolation is
+ // sufficently accurate and, if not, we subdivide and recurse. If
+ // timeBezier is outside the time domain then we simply discard it.
+
+ // Discard if left >= right. If this happens it should only be by
+ // a tiny amount due to round off error.
+ if (timeBezier[0] >= timeBezier[3]) {
+ return;
+ }
+
+ // Discard if outside the domain
+ if (timeBezier[0] >= endTime ||
+ timeBezier[3] <= startTime) {
+ return;
+ }
+
+ // Find the distance from the inner points of the Bezier polygon to
+ // the line connecting the outer points. If the larger of these
+ // distances is smaller than tolerance times some factor then we
+ // decide that the Bezier is flat and we sample it with a line,
+ // otherwise we subdivide.
+ //
+ // Since the Bezier cannot reach its inner convex hull vertices, the
+ // distances to those vertices is an overestimate of the error. So
+ // we increase the tolerance by some factor determined by what works.
+ static const double toleranceFactor = 1.0;
+ double e = _BezierHeight(timeBezier, valueBezier, timeScale, valueScale);
+ if (e <= toleranceFactor * tolerance) {
+ // Linear approximation
+ samples.push_back(TsValueSample(timeBezier[0],
+ VtValue(valueBezier[0]),
+ timeBezier[3],
+ VtValue(valueBezier[3])));
+ }
+
+ // Blur sample if we're below the tolerance in time
+ else if (timeScale * (timeBezier[3] - timeBezier[0]) <= tolerance) {
+ Ts_Bezier<T> tmpBezier(timeBezier, valueBezier);
+ std::pair<T, T> range =
+ _GetBezierRange<T>(&tmpBezier, startTime, endTime);
+ samples.push_back(
+ TsValueSample(GfMax(timeBezier[0], startTime),
+ VtValue(range.first),
+ GfMin(timeBezier[3], endTime),
+ VtValue(range.second),
+ true));
+ }
+
+ // Subdivide
+ else {
+ T leftValue[4], rightValue[4];
+ TsTime leftTime[4], rightTime[4];
+ _SubdivideBezier(valueBezier, leftValue, 0.5, true);
+ _SubdivideBezier(timeBezier, leftTime, 0.5, true);
+ _SubdivideBezier(valueBezier, rightValue, 0.5, false);
+ _SubdivideBezier(timeBezier, rightTime, 0.5, false);
+
+ // Recurse
+ _SampleBezier(leftTime, leftValue, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+ _SampleBezier(rightTime, rightValue, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+ }
+}
+
+template <typename T>
+static void
+_SampleBezierClip( const TsTime timePoly[4],
+ const TsTime timeBezier[4], const T valueBezier[4],
+ double startTime, double endTime,
+ double timeScale, double valueScale, double tolerance,
+ TsSamples & samples )
+{
+ static const double rootTolerance = 1.0e-10;
+
+ // Check to see if the first derivative ever goes to 0 in the interval
+ // [0,1]. If it does then the cubic is not monotonically increasing
+ // in that interval.
+ double root0 = 0, root1 = 1;
+ double timeDeriv[3];
+ Ts_CubicDerivative( timePoly, timeDeriv );
+ if (Ts_SolveQuadratic( timeDeriv, &root0, &root1 )) {
+ if (root0 >= 0.0 - rootTolerance &&
+ root1 <= 1.0 + rootTolerance) {
+ // Bezier doubles back on itself in the interval. We
+ // subdivide the Bezier into a segment somewhere before
+ // the double back and a segment after it such that the
+ // first ends (in time) exactly where the second begins.
+
+ // First compute at what time we should subdivide. We take
+ // the average of the times where the derivative is zero
+ // clamped to the values at the end points.
+ TsTime t0 = Ts_EvalCubic(timePoly, root0);
+ TsTime t1 = Ts_EvalCubic(timePoly, root1);
+ TsTime t = 0.5 * (GfClamp(t0, timeBezier[0], timeBezier[3]) +
+ GfClamp(t1, timeBezier[0], timeBezier[3]));
+
+ // If t0 < t1 then it's the interval [root0,root1] where the
+ // curve is monotonically increasing and not the intervals
+ // [0,root0] and [root1,1]. This can happen if the Bezier
+ // has zero length tangents and in that case [root0,root1]
+ // should be [0,1]. (It will also happen if the tangents
+ // are pointing in the wrong direction but that violates
+ // our assumptions so we don't handle it.) Since [0,1] is
+ // the whole segment we'll just evaluate normally in that
+ // case.
+ if (t0 >= t1) {
+ // Find the solutions for t in the intervals [0,root0]
+ // and [root1,1]. These are the parameters where we
+ // subdivide the Bezier.
+ root0 = Ts_SolveCubicInInterval(timePoly, timeDeriv, t,
+ GfInterval(0, root0));
+ root1 = Ts_SolveCubicInInterval(timePoly, timeDeriv, t,
+ GfInterval(root1, 1));
+
+ // Now compute the Bezier from 0 to root0 and the Bezier
+ // from root1 to 1. The former ends on t and the latter
+ // begins on t and both are monotonically increasing.
+ //
+ T leftValue[4], rightValue[4];
+ TsTime leftTime[4], rightTime[4];
+ _SubdivideBezier(valueBezier, leftValue, root0, true);
+ _SubdivideBezier(timeBezier, leftTime, root0, true);
+ _SubdivideBezier(valueBezier, rightValue, root1, false);
+ _SubdivideBezier(timeBezier, rightTime, root1, false);
+
+ // Left curve ends and right curve begins at t.
+ leftTime[3] = t;
+ rightTime[0] = t;
+
+ // Now evaluate the Beziers. Since the left Bezier will end
+ // at exactly the time the right Bezier starts but they end
+ // and start at different values there'll be a gap in the
+ // samples. Technically that gap is real but we don't want
+ // it anyway so we'll slightly shorten the last sample and
+ // add a new one to bridge the gap. We also need to handle
+ // the situation where either the left or right interval
+ // generates no samples. We still bridge the gap but we
+ // compute the extra sample differently in each case.
+ _SampleBezier(leftTime, leftValue, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+ size_t numSamples1 = samples.size();
+ if (numSamples1 > 0) {
+ // We may need to add sample across the gap between the
+ // left and right sides so add it here now so it stays
+ // in time order. We will remove it or adjust its values
+ // after we sample the right side.
+ samples.push_back(
+ TsValueSample(0, VtValue(), 0, VtValue()));
+ numSamples1++;
+ }
+
+ _SampleBezier(rightTime, rightValue, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+ size_t numSamples2 = samples.size();
+
+ // If there are no left samples (we check against 2 because we
+ // also added a gap sample if there were left samples)
+ if (numSamples1 < 2) {
+ return;
+ }
+
+ if (numSamples1 != numSamples2) {
+ // Samples in right interval and there are samples
+ // before the right interval.
+ TsValueSample& s = samples[numSamples1 - 2];
+ TsValueSample& gap = samples[numSamples1 - 1];
+ TsTime d = GfMin(0.001,
+ 0.001 * (s.rightTime - s.leftTime));
+ s.rightTime -= d;
+
+ // Update the gap closing sample with the correct values
+ gap.leftTime = s.rightTime;
+ gap.leftValue = VtValue(s.rightValue);
+ gap.rightTime = rightTime[0];
+ gap.rightValue = VtValue(rightValue[0]);
+ }
+ else {
+ // No samples in right interval but there are samples.
+ // Add a sample across the gap only if the gap is in
+ // the sampled domain. If not then the left Bezier
+ // wasn't sampled up to where the gap is.
+ TsValueSample& s = samples[numSamples1 - 2];
+ if (s.rightTime < endTime) {
+ TsValueSample& gap = samples[numSamples1 - 1];
+ TsTime d = GfMin(0.001,
+ 0.001 * (s.rightTime - s.leftTime));
+ s.rightTime -= d;
+ // Update the gap closing sample with the correct values
+ gap.leftTime = s.rightTime;
+ gap.leftValue = VtValue(s.rightValue);
+ gap.rightTime = rightTime[3];
+ gap.rightValue = VtValue(rightValue[3]);
+ } else {
+ // Delete the gap closing sample as it is unneeded.
+ samples.pop_back();
+ }
+ }
+
+ return;
+ }
+ }
+ }
+
+ // Bezier does not double back on itself
+ _SampleBezier(timeBezier, valueBezier, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+}
+
+// Sample segment with results in samples.
+template <typename T>
+static void
+_SampleSegment(const Ts_EvalCache<T, TsTraits<T>::interpolatable> * cache,
+ double startTime, double endTime,
+ double timeScale, double valueScale, double tolerance,
+ TsSamples & samples )
+{
+ const Ts_Bezier<T>* bezier = cache->GetBezier();
+ // Sample the Bezier
+ _SampleBezierClip(bezier->timeCoeff,
+ bezier->timePoints, bezier->valuePoints,
+ startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+}
+
+static void
+_AddExtrapolateSample( const TsSpline & val, double t, double dtExtrapolate,
+ TsSamples& samples )
+{
+ // Get segment endpoints
+ VtValue yLeft, yRight;
+ if (dtExtrapolate < 0.0) {
+ yLeft = val.Eval(t + dtExtrapolate, TsRight);
+ yRight = val.Eval(t, TsLeft);
+ samples.push_back(TsValueSample(t + dtExtrapolate, yLeft,
+ t, yRight));
+ }
+ else {
+ yLeft = val.Eval(t, TsRight);
+ yRight = val.Eval(t + dtExtrapolate, TsLeft);
+ samples.push_back(TsValueSample(t, yLeft,
+ t + dtExtrapolate, yRight));
+ }
+}
+
+// XXX: Is this adequate? What if the time scale is huge? Does it need to be
+// scaled based on the times in use?
+static const double extrapolateDistance = 100.0;
+
+static void
+_EvalLinear( const TsSpline & val,
+ TsTime startTime, TsTime endTime,
+ TsSamples& samples )
+{
+ const TsKeyFrame & first = *val.begin();
+ const TsKeyFrame & last = *val.rbegin();
+
+ // Sample to left of first keyframe if necessary. We'll take a sample
+ // way to its left.
+ if (startTime < first.GetTime()) {
+ // Extrapolate from first keyframe
+ _AddExtrapolateSample(val, first.GetTime(),
+ (startTime - first.GetTime() - extrapolateDistance), samples);
+
+ // If endTime is at or before the first keyframe then we're done
+ if (endTime <= first.GetTime()) {
+ return;
+ }
+
+ // New start time is the time of the first keyframe
+ startTime = first.GetTime();
+ }
+
+ // Find the bounding keyframes. (We've already handled extrapolation to
+ // the left above and we'll handle extrapolation to the right at the end.)
+ std::pair<TsSpline::const_iterator, TsSpline::const_iterator> bounds =
+ _GetBounds(val, startTime, endTime);
+ TsSpline::const_iterator i = bounds.first;
+ TsSpline::const_iterator j = bounds.second;
+
+ // On a linear or held segment we just take a sample at the endpoints.
+ while (i != j) {
+ const TsKeyFrame& curKf = *i;
+ const TsKeyFrame& nextKf = *(++i);
+
+ // Sample
+ TsTime t0 = curKf.GetTime();
+ TsTime t1 = nextKf.GetTime();
+ samples.push_back(
+ TsValueSample(t0, val.Eval(t0, TsRight),
+ t1, val.Eval(t1, TsLeft)));
+ }
+
+ // Sample to the right of the last keyframe if necessary. We'll take
+ // a sample 100 frames beyond the end time.
+ if (endTime > last.GetTime()) {
+ // Extrapolate from last keyframe
+ _AddExtrapolateSample(val, last.GetTime(),
+ (endTime - last.GetTime() + extrapolateDistance), samples);
+ }
+}
+
+template<typename T>
+static void
+_EvalCurve( const TsSpline & val,
+ TsTime startTime, TsTime endTime,
+ double timeScale, double valueScale, double tolerance,
+ TsSamples& samples )
+{
+ const TsKeyFrame & first = *val.begin();
+ const TsKeyFrame & last = *val.rbegin();
+
+ // Sample to left of first keyframe if necessary. We'll take a sample
+ // 100 frames before the start time.
+ if (startTime < first.GetTime()) {
+ // Extrapolate from first keyframe
+ _AddExtrapolateSample(val, first.GetTime(),
+ (startTime - first.GetTime() - extrapolateDistance), samples);
+
+ // If endTime is at or before the first keyframe then we're done
+ if (endTime <= first.GetTime()) {
+ return;
+ }
+
+ // New start time is the time of the first keyframe
+ startTime = first.GetTime();
+ }
+
+ // Find the bounding keyframes. (We've already handled extrapolation to
+ // the left above and we'll handle extrapolation to the right at the end.)
+ std::pair<TsSpline::const_iterator, TsSpline::const_iterator> bounds =
+ _GetBounds(val, startTime, endTime);
+ TsSpline::const_iterator i = bounds.first;
+ TsSpline::const_iterator j = bounds.second;
+
+
+ // Handle the keyframe segments in the interval, excluding the region
+ // (if any) after the last keyframe, as this region is handled seperately
+ // afterward.
+ TsSpline::const_iterator i2 = i; i2++;
+ while (i != j) {
+ if (i2 != val.end()) {
+ Ts_EvalCache<T> cache(*i, *i2);
+
+ _SampleSegment<T>(&cache, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+ }
+
+ i = i2;
+ i2++;
+ }
+
+ // Sample to the right of the last keyframe if necessary. We'll take
+ // a sample 100 frames after the end time.
+ if (endTime > last.GetTime()) {
+ // Extrapolate from last keyframe
+ _AddExtrapolateSample(val, last.GetTime(),
+ (endTime - last.GetTime() + extrapolateDistance), samples);
+ }
+}
+
+TsSamples
+Ts_Sample( const TsSpline & val, TsTime startTime, TsTime endTime,
+ double timeScale, double valueScale, double tolerance )
+{
+ TsSamples samples;
+
+ if (startTime > endTime) {
+ TF_CODING_ERROR("invalid interval (start > end)");
+ return samples;
+ }
+ else if (val.IsEmpty() || startTime == endTime) {
+ return samples;
+ }
+
+ // Sample the segments between keyframes
+ const std::type_info & t = val.GetTypeid();
+ if (TfSafeTypeCompare(t, typeid(double))) {
+ _EvalCurve<double>(val, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+ }
+ else if (TfSafeTypeCompare(t, typeid(float))) {
+ _EvalCurve<float>(val, startTime, endTime,
+ timeScale, valueScale, tolerance, samples);
+ }
+ else {
+ _EvalLinear(val, startTime, endTime, samples);
+ }
+
+ return samples;
+}
+
+////////////////////////////////////////////////////////////////////////
+// Breakdown
+
+template<typename T>
+static void
+_Breakdown(TsKeyFrameMap * k,
+ const TsKeyFrameMap::iterator & k1,
+ const TsKeyFrameMap::iterator & k2,
+ const TsKeyFrameMap::iterator & k3)
+{
+ // Wrap the keyframes in a spline in order to get an eval cache for the
+ // segment.
+ TsSpline spline(*k);
+
+ // Setup Bezier cache for key frames k1 and k3
+ Ts_EvalCache<T> cache(*spline.begin(), *spline.rbegin());
+
+ // Get the Bezier from the cache
+ const Ts_Bezier<T>* bezier = cache.GetBezier();
+
+ // Compute the spline parameter for the time of k2 in the Bezier
+ // defined by k1 and k3.
+ double u = Ts_SolveCubic(bezier->timeCoeff, k2->GetTime());
+
+ // Subdivide the Bezier at u
+ T leftValue[4], rightValue[4];
+ TsTime leftTime[4], rightTime[4];
+ _SubdivideBezier(bezier->valuePoints, leftValue, u, true);
+ _SubdivideBezier(bezier->timePoints, leftTime, u, true);
+ _SubdivideBezier(bezier->valuePoints, rightValue, u, false);
+ _SubdivideBezier(bezier->timePoints, rightTime, u, false);
+
+ // Update the middle key frame's slope.
+ if (k2->SupportsTangents()) {
+ k2->SetLeftTangentSlope (VtValue(( leftValue[3] - leftValue[2]) /
+ ( leftTime[3] - leftTime[2])));
+ k2->SetRightTangentSlope (VtValue((rightValue[1] - rightValue[0]) /
+ ( rightTime[1] - rightTime[0])));
+ }
+
+ // Update the tangent lengths. We change the inner lengths of k1 and k3,
+ // and both of k2.
+ if (k1->SupportsTangents())
+ k1->SetRightTangentLength(leftTime[1] - leftTime[0]);
+ if (k2->SupportsTangents())
+ k2->SetLeftTangentLength (leftTime[3] - leftTime[2]);
+ if (k2->SupportsTangents())
+ k2->SetRightTangentLength(rightTime[1] - rightTime[0]);
+ if (k3->SupportsTangents())
+ k3->SetLeftTangentLength (rightTime[3] - rightTime[2]);
+}
+
+void
+Ts_Breakdown( TsKeyFrameMap * k )
+{
+ // Sanity checks
+ if (k->size() != 3) {
+ TF_CODING_ERROR("Wrong number of key frames in breakdown");
+ return;
+ }
+ TsKeyFrameMap::iterator k1 = k->begin();
+ TsKeyFrameMap::iterator k2 = k1; k2++;
+ TsKeyFrameMap::iterator k3 = k2; k3++;
+
+ if (k1->GetTime() >= k2->GetTime() ||
+ k2->GetTime() >= k3->GetTime()) {
+ TF_CODING_ERROR("Bad key frame ordering in breakdown");
+ return;
+ }
+
+ // Breakdown
+ VtValue v = k1->GetZero();
+ if (TfSafeTypeCompare(v.GetTypeid(), typeid(double))) {
+ _Breakdown<double>(k, k1, k2, k3);
+ }
+ else if (TfSafeTypeCompare(v.GetTypeid(), typeid(float))) {
+ _Breakdown<float>(k, k1, k2, k3);
+ }
+ else {
+ // No tangents for this value type so nothing to do
+ }
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_EVAL_UTILS_H
+#define PXR_BASE_TS_EVAL_UTILS_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/keyFrame.h"
+#include "pxr/base/ts/keyFrameMap.h"
+#include "pxr/base/ts/spline.h"
+#include "pxr/base/ts/types.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+enum Ts_EvalType {
+ Ts_EvalValue,
+ Ts_EvalDerivative
+};
+
+// Evaluate either the value or derivative at a given time on a given side.
+VtValue Ts_Eval(
+ const TsSpline &val,
+ TsTime time, TsSide side,
+ Ts_EvalType evalType);
+
+// Return piecewise linear samples for a value between two times to within
+// a given tolerance.
+TsSamples Ts_Sample( const TsSpline & val,
+ TsTime startTime, TsTime endTime,
+ double timeScale, double valueScale,
+ double tolerance );
+
+// Return the minimum and maximum values of a value over an interval.
+std::pair<VtValue, VtValue> Ts_GetRange( const TsSpline & val,
+ TsTime startTime,
+ TsTime endTime );
+
+// k has exactly three key frames. The first and last define a segment
+// of a spline and the middle is where we want a breakdown. This modifies
+// tangents on the three key frames to keep the shape of the spline the
+// same (as best it can). We assume that the middle key frame's value has
+// already been set correctly.
+void Ts_Breakdown( TsKeyFrameMap* k );
+
+TsExtrapolationType Ts_GetEffectiveExtrapolationType(
+ const TsKeyFrame& kf,
+ const TsExtrapolationPair &extrapolation,
+ bool kfIsOnlyKeyFrame,
+ TsSide side);
+
+TsExtrapolationType Ts_GetEffectiveExtrapolationType(
+ const TsKeyFrame& kf,
+ const TsSpline &spline,
+ TsSide side);
+
+// Returns true if the segment between the given (adjacent) key
+// frames is monotonic (i.e. no extremes).
+bool Ts_IsSegmentValueMonotonic( const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2 );
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_EVALUATOR_H
+#define PXR_BASE_TS_EVALUATOR_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/evalCache.h"
+#include "pxr/base/ts/spline.h"
+#include "pxr/base/ts/types.h"
+
+#include "pxr/base/trace/trace.h"
+
+#include <vector>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+/// \class TsEvaluator
+/// \brief Opaque interface to a spline for evaluations using cached segments.
+///
+/// Use this evaluator when performing many evaluations on an unchanging
+/// TsSpline whose knots support tangents (e.g., Bezier splines). Evals on
+/// this class are required to be thread-safe.
+///
+template <typename T>
+class TsEvaluator {
+
+public:
+
+ /// Default constructor; falls back to empty spline.
+ TsEvaluator();
+
+ /// Constructs the evaluator and its caches for the given spline.
+ TsEvaluator(TsSpline spline);
+
+ /// Evaluates the spline at the given time. Note that left side evals do not
+ /// benefit from the cached segments.
+ T Eval(const TsTime &time, TsSide side=TsRight) const;
+
+private:
+
+ // Vector of typed Ts_EvalCaches, one for each Bezier segment in the
+ // spline.
+ std::vector<std::shared_ptr<Ts_EvalCache<T> > > _segments;
+
+ // The spline being evaluated.
+ TsSpline _spline;
+};
+
+template <typename T>
+TsEvaluator<T>::TsEvaluator()
+{
+}
+
+template <typename T>
+TsEvaluator<T>::TsEvaluator(TsSpline spline) :
+_spline(spline)
+{
+ TRACE_FUNCTION();
+
+ if (spline.size() > 1) {
+
+ // Only set up eval caches when there are Bezier segments.
+ bool bezier = false;
+ for (const TsKeyFrame &kf : spline) {
+ if (kf.GetKnotType() == TsKnotBezier) {
+ bezier = true;
+ break;
+ }
+ }
+ if (!bezier) {
+ return;
+ }
+
+ _segments.reserve(spline.size() - 1);
+
+ TF_FOR_ALL(splItr, spline) {
+
+ // Create and store an eval cache for each segment (defined by a
+ // pair of adjacent keyframes) of the spline.
+
+ TsSpline::const_iterator iAfterTime = splItr;
+ iAfterTime++;
+
+ if (iAfterTime == spline.end()) {
+ break;
+ }
+
+ std::shared_ptr<Ts_EvalCache<T> > segmentCache =
+ Ts_EvalCache<T>::New(*splItr, *iAfterTime);
+
+ if (TF_VERIFY(segmentCache)) {
+ _segments.push_back(segmentCache);
+ }
+ }
+
+ }
+}
+
+template <typename T>
+T
+TsEvaluator<T>::Eval(const TsTime &time,
+ TsSide side) const
+{
+
+ // Only right-side evals can benefit from cached segments.
+ if (!_segments.empty() && side == TsRight) {
+
+ // Only use eval caches for times that are between the authored knots on
+ // the spline. Boundary extrapolation cases are evaluated directly.
+ if (time >= _spline.begin()->GetTime() &&
+ time <= _spline.rbegin()->GetTime()) {
+
+ // Get the closest keyframe <= the requested time.
+ TsSpline::const_iterator sample = _spline.lower_bound(time);
+ if (TF_VERIFY(sample != _spline.end())) {
+
+ // We will index into the _segments vector using the iterator
+ // offset of the given sample. We need another decrement if our
+ // sample is > than the requested time (we want the requested
+ // time to be in between the two keyframes contained in the eval
+ // cache entry.
+ size_t idx = sample - _spline.begin();
+ if (sample->GetTime() > time && TF_VERIFY(idx > 0)) {
+ idx--;
+ }
+
+ if (TF_VERIFY(idx < _segments.size())
+ && TF_VERIFY(_segments[idx])) {
+ return _segments[idx]->TypedEval(time);
+ }
+ }
+ }
+ }
+
+ // If we did not get a cache hit, evaluate directly on the spline.
+ if (!_spline.empty()) {
+ return _spline.Eval(time).template Get<T>();
+ }
+
+ // If we're evaluating an empty spline, fall back to zero.
+ return TsTraits<T>::zero;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/keyFrame.h"
+#include "pxr/base/ts/data.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/ts/typeRegistry.h"
+#include "pxr/base/tf/mallocTag.h"
+#include "pxr/base/tf/safeTypeCompare.h"
+#include "pxr/base/tf/stringUtils.h"
+#include "pxr/base/gf/range2d.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using std::string;
+
+// Tolerance for deciding whether tangent slopes are parallel.
+static const double SLOPE_DIFF_THRESHOLD = 1e-4f;
+
+TF_REGISTRY_FUNCTION(TfType)
+{
+ TfType::Define<TsKeyFrame>();
+}
+
+TsKeyFrame::TsKeyFrame( const TsTime & time,
+ const VtValue & val,
+ TsKnotType knotType,
+ const VtValue & leftTangentSlope,
+ const VtValue & rightTangentSlope,
+ TsTime leftTangentLength,
+ TsTime rightTangentLength)
+{
+ TsTypeRegistry::GetInstance().InitializeDataHolder(&_holder,val);
+
+ _Initialize(time, knotType, leftTangentSlope, rightTangentSlope,
+ leftTangentLength, rightTangentLength);
+}
+
+TsKeyFrame::TsKeyFrame( const TsTime & time,
+ const VtValue & lhv,
+ const VtValue & rhv,
+ TsKnotType knotType,
+ const VtValue & leftTangentSlope,
+ const VtValue & rightTangentSlope,
+ TsTime leftTangentLength,
+ TsTime rightTangentLength)
+{
+ TsTypeRegistry::GetInstance().InitializeDataHolder(&_holder,rhv);
+
+ SetIsDualValued( true );
+ SetLeftValue( lhv );
+
+ _Initialize(time, knotType, leftTangentSlope, rightTangentSlope,
+ leftTangentLength, rightTangentLength);
+}
+
+void
+TsKeyFrame::_Initialize(
+ const TsTime & time,
+ TsKnotType knotType,
+ const VtValue & leftTangentSlope,
+ const VtValue & rightTangentSlope,
+ TsTime leftTangentLength,
+ TsTime rightTangentLength)
+{
+ SetTime( time );
+
+ _InitializeKnotType(knotType);
+
+ if (SupportsTangents()) {
+ if (!leftTangentSlope.IsEmpty())
+ SetLeftTangentSlope( leftTangentSlope );
+ if (!rightTangentSlope.IsEmpty())
+ SetRightTangentSlope( rightTangentSlope );
+ }
+
+ _InitializeTangentLength(leftTangentLength,rightTangentLength);
+}
+
+void
+TsKeyFrame::_InitializeKnotType(TsKnotType knotType)
+{
+ if (!IsInterpolatable() && knotType != TsKnotHeld) {
+ knotType = TsKnotHeld;
+ }
+ else if (IsInterpolatable() && !SupportsTangents() &&
+ knotType == TsKnotBezier) {
+ knotType = TsKnotLinear;
+ }
+
+ SetKnotType( knotType );
+}
+
+void
+TsKeyFrame::_InitializeTangentLength(TsTime left, TsTime right)
+{
+ if (SupportsTangents()) {
+ SetLeftTangentLength(left);
+ SetRightTangentLength(right);
+ ResetTangentSymmetryBroken();
+ }
+}
+
+TsKeyFrame::TsKeyFrame()
+{
+ _holder.New(TsTraits<double>::zero);
+
+ SetKnotType( TsKnotLinear );
+}
+
+TsKeyFrame::TsKeyFrame( const TsKeyFrame & kf )
+{
+ kf._holder.Get()->CloneInto(&_holder);
+}
+
+TsKeyFrame::~TsKeyFrame()
+{
+ _holder.Destroy();
+}
+
+TsKeyFrame &
+TsKeyFrame::operator=(const TsKeyFrame &rhs)
+{
+ if (this != &rhs) {
+ _holder.Destroy();
+ rhs._holder.Get()->CloneInto(&_holder);
+ }
+ return *this;
+}
+
+bool
+TsKeyFrame::operator==(const TsKeyFrame &rhs) const
+{
+ return this == &rhs || (*_holder.Get() == *rhs._holder.Get());
+}
+
+bool
+TsKeyFrame::operator!=(const TsKeyFrame &rhs) const
+{
+ return !(*this == rhs);
+}
+
+bool
+TsKeyFrame::IsEquivalentAtSide(const TsKeyFrame &keyFrame, TsSide side) const
+{
+ if (GetKnotType() != keyFrame.GetKnotType() ||
+ GetTime() != keyFrame.GetTime() ||
+ HasTangents() != keyFrame.HasTangents()) {
+ return false;
+ }
+
+ if (side == TsLeft) {
+ if (HasTangents()) {
+ if (GetLeftTangentLength() != keyFrame.GetLeftTangentLength() ||
+ GetLeftTangentSlope() != keyFrame.GetLeftTangentSlope()) {
+ return false;
+ }
+ }
+ return GetLeftValue() == keyFrame.GetLeftValue();
+ } else {
+ if (HasTangents()) {
+ if (GetRightTangentLength() != keyFrame.GetRightTangentLength() ||
+ GetRightTangentSlope() != keyFrame.GetRightTangentSlope()) {
+ return false;
+ }
+ }
+ return GetValue() == keyFrame.GetValue();
+ }
+}
+
+TsKnotType
+TsKeyFrame::GetKnotType() const
+{
+ return _holder.Get()->GetKnotType();
+}
+
+void
+TsKeyFrame::SetKnotType( TsKnotType newType )
+{
+ _holder.GetMutable()->SetKnotType( newType );
+}
+
+bool
+TsKeyFrame::CanSetKnotType( TsKnotType newType,
+ std::string *reason ) const
+{
+ return _holder.Get()->CanSetKnotType( newType, reason );
+}
+
+VtValue
+TsKeyFrame::GetValue() const
+{
+ return _holder.Get()->GetValue();
+}
+
+VtValue
+TsKeyFrame::GetLeftValue() const
+{
+ return _holder.Get()->GetLeftValue();
+}
+
+void
+TsKeyFrame::SetValue( VtValue val )
+{
+ _holder.GetMutable()->SetValue( val );
+}
+
+VtValue
+TsKeyFrame::GetValue( TsSide side ) const
+{
+ return (side == TsLeft) ? GetLeftValue() : GetValue();
+}
+
+void
+TsKeyFrame::SetValue( VtValue val, TsSide side )
+{
+ if (side == TsLeft) {
+ SetLeftValue(val);
+ } else {
+ SetValue(val);
+ }
+}
+
+VtValue
+TsKeyFrame::GetValueDerivative() const
+{
+ return _holder.Get()->GetValueDerivative();
+}
+
+VtValue
+TsKeyFrame::GetZero() const
+{
+ return _holder.Get()->GetZero();
+}
+
+void
+TsKeyFrame::SetLeftValue( VtValue val )
+{
+ _holder.GetMutable()->SetLeftValue( val );
+}
+
+VtValue
+TsKeyFrame::GetLeftValueDerivative() const
+{
+ return _holder.Get()->GetLeftValueDerivative();
+}
+
+bool
+TsKeyFrame::GetIsDualValued() const
+{
+ return _holder.Get()->GetIsDualValued();
+}
+
+void
+TsKeyFrame::SetIsDualValued( bool isDual )
+{
+ _holder.GetMutable()->SetIsDualValued(isDual);
+}
+
+bool
+TsKeyFrame::IsInterpolatable() const
+{
+ return _holder.Get()->ValueCanBeInterpolated();
+}
+
+bool
+TsKeyFrame::SupportsTangents() const
+{
+ return _holder.Get()->ValueTypeSupportsTangents();
+}
+
+bool
+TsKeyFrame::HasTangents() const
+{
+ return _holder.Get()->HasTangents();
+}
+
+TsTime
+TsKeyFrame::GetLeftTangentLength() const
+{
+ return _holder.Get()->GetLeftTangentLength();
+}
+
+VtValue
+TsKeyFrame::GetLeftTangentSlope() const
+{
+ return _holder.Get()->GetLeftTangentSlope();
+}
+
+TsTime
+TsKeyFrame::GetRightTangentLength() const
+{
+ return _holder.Get()->GetRightTangentLength();
+}
+
+VtValue
+TsKeyFrame::GetRightTangentSlope() const
+{
+ return _holder.Get()->GetRightTangentSlope();
+}
+
+bool
+TsKeyFrame::_ValidateTangentSetting() const
+{
+ if (!SupportsTangents()) {
+ TF_CODING_ERROR("value type %s does not support tangents",
+ GetValue().GetTypeName().c_str());
+ return false;
+ }
+
+ return true;
+}
+
+void
+TsKeyFrame::SetLeftTangentLength( TsTime newLen )
+{
+ if (!_ValidateTangentSetting())
+ return;
+
+ _holder.GetMutable()->SetLeftTangentLength( newLen );
+}
+
+void
+TsKeyFrame::SetLeftTangentSlope( VtValue newSlope )
+{
+ if (!_ValidateTangentSetting())
+ return;
+
+ _holder.GetMutable()->SetLeftTangentSlope( newSlope );
+}
+
+void
+TsKeyFrame::SetRightTangentLength( TsTime newLen )
+{
+ if (!_ValidateTangentSetting())
+ return;
+
+ _holder.GetMutable()->SetRightTangentLength( newLen );
+}
+
+void
+TsKeyFrame::SetRightTangentSlope( VtValue newSlope)
+{
+ if (!_ValidateTangentSetting())
+ return;
+
+ _holder.GetMutable()->SetRightTangentSlope( newSlope );
+}
+
+bool
+TsKeyFrame::GetTangentSymmetryBroken() const
+{
+ return _holder.Get()->GetTangentSymmetryBroken();
+}
+
+void
+TsKeyFrame::SetTangentSymmetryBroken( bool broken )
+{
+ if (!_ValidateTangentSetting())
+ return;
+
+ _holder.GetMutable()->SetTangentSymmetryBroken( broken );
+}
+
+void
+TsKeyFrame::ResetTangentSymmetryBroken()
+{
+ if (!_ValidateTangentSetting())
+ return;
+
+ _holder.GetMutable()->ResetTangentSymmetryBroken();
+}
+
+static std::string
+_GetValue(const TsKeyFrame & val)
+{
+ if (val.GetIsDualValued()) {
+ return TfStringify(val.GetLeftValue()) + " - " +
+ TfStringify(val.GetValue());
+ }
+
+ return TfStringify(val.GetValue());
+}
+
+std::ostream& operator<<(std::ostream& out, const TsKeyFrame & val)
+{
+ if (val.SupportsTangents()) {
+ return out << "Ts.KeyFrame("
+ << val.GetTime() << ", "
+ << _GetValue(val) << ", "
+ << val.GetKnotType() << ", "
+ << val.GetLeftTangentSlope() << ", "
+ << val.GetRightTangentSlope() << ", "
+ << val.GetLeftTangentLength() << ", "
+ << val.GetRightTangentLength() << ")";
+ } else {
+ return out << "Ts.KeyFrame("
+ << val.GetTime() << ", "
+ << _GetValue(val) << ", "
+ << val.GetKnotType() << ")";
+ }
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_KEY_FRAME_H
+#define PXR_BASE_TS_KEY_FRAME_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/arch/demangle.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/ts/data.h"
+#include "pxr/base/vt/value.h"
+#include "pxr/base/vt/traits.h"
+#include "pxr/base/tf/diagnostic.h"
+#include "pxr/base/tf/stringUtils.h"
+
+#include <iostream>
+#include <typeinfo>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TsSpline;
+
+/// \class TsKeyFrame
+/// \brief Specifies the value of an TsSpline object at a particular
+/// point in time.
+///
+/// Keyframes also specify the shape of a spline as it passes through each
+/// keyframe: the knot type specifies what interpolation technique to use
+/// (TsKnotHeld, TsKnotLinear, or TsKnotBezier), and tangent handles
+/// specify the shape of the spline as it passes through the keyframe.
+///
+/// It is also possible for keyframes to be "dual-valued." This means
+/// that a separate keyframe value -- the left-side value -- is used
+/// when approaching the keyframe from lower time values. The regular
+/// value is then used starting at the keyframe's time and when
+/// approaching that time from higher times to the right. Dual-value
+/// knots are necessary to compensate for instantaneous shifts
+/// in coordinate frames, such as the shift that occurs when there is a
+/// constraint switch. The spline can snap to the new value required to
+/// maintain the same position in worldspace.
+///
+/// <b>Note:</b> TsKeyFrame is a value, not a formal object.
+///
+class TsKeyFrame final
+{
+public: // methods
+
+ /// Constructs a default double keyframe.
+ TS_API
+ TsKeyFrame();
+
+ /// \name Constructors
+ /// @{
+ ///
+ /// There are four variations on the constructor, to support
+ /// single-valued or dual-valued keyframes, with values supplied as
+ /// either VtValues or a template parameter.
+
+ /// Constructs a single-valued keyframe.
+ template <typename T>
+ TsKeyFrame( const TsTime & time,
+ const T & val,
+ TsKnotType knotType = TsKnotLinear,
+ const T & leftTangentSlope = TsTraits<T>::zero,
+ const T & rightTangentSlope = TsTraits<T>::zero,
+ TsTime leftTangentLength = 0,
+ TsTime rightTangentLength = 0);
+
+ /// Constructs a single-valued keyframe with VtValues.
+ TS_API
+ TsKeyFrame( const TsTime & time,
+ const VtValue & val,
+ TsKnotType knotType = TsKnotLinear,
+ const VtValue & leftTangentSlope = VtValue(),
+ const VtValue & rightTangentSlope = VtValue(),
+ TsTime leftTangentLength = 0,
+ TsTime rightTangentLength = 0);
+
+ /// Constructs a dual-valued keyframe.
+ template <typename T>
+ TsKeyFrame( const TsTime & time,
+ const T & lhv,
+ const T & rhv,
+ TsKnotType knotType = TsKnotLinear,
+ const T & leftTangentSlope = TsTraits<T>::zero,
+ const T & rightTangentSlope = TsTraits<T>::zero,
+ TsTime leftTangentLength = 0,
+ TsTime rightTangentLength = 0);
+
+ /// Constructs a dual-valued keyframe with VtValues.
+ TS_API
+ TsKeyFrame( const TsTime & time,
+ const VtValue & lhv,
+ const VtValue & rhv,
+ TsKnotType knotType = TsKnotLinear,
+ const VtValue & leftTangentSlope = VtValue(),
+ const VtValue & rightTangentSlope = VtValue(),
+ TsTime leftTangentLength = 0,
+ TsTime rightTangentLength = 0);
+
+ /// Constructs a keyframe by duplicating an existing TsKeyFrame.
+ TS_API
+ TsKeyFrame( const TsKeyFrame & kf );
+
+ /// @}
+
+ /// Non-virtual destructor; this class should not be subclassed.
+ TS_API
+ ~TsKeyFrame();
+
+ /// \name Primary API
+ /// @{
+
+ /// Assignment operator.
+ TS_API
+ TsKeyFrame & operator=(const TsKeyFrame &rhs);
+
+ /// Compare this keyframe with another.
+ TS_API
+ bool operator==(const TsKeyFrame &) const;
+
+ TS_API
+ bool operator!=(const TsKeyFrame &) const;
+
+ /// Gets whether this key frame is at the same time and is equivalent to
+ /// \p keyFrame on the given \p side. In other words, replacing this
+ /// key frame with \p keyFrame in a spline will have no effect on how the
+ /// spline evaluates for any time on the given \p side of this key frame.
+ TS_API
+ bool IsEquivalentAtSide(const TsKeyFrame &keyFrame, TsSide side) const;
+
+ /// Gets the time of this keyframe.
+ TS_API
+ TsTime GetTime() const {
+ return _holder.Get()->GetTime();
+ }
+
+ /// Sets the time of this keyframe.
+ TS_API
+ void SetTime( const TsTime & newTime ) {
+ _holder.GetMutable()->SetTime(newTime);
+ }
+
+ /// Gets the value at this keyframe.
+ TS_API
+ VtValue GetValue() const;
+
+ /// Sets the value at this keyframe.
+ TS_API
+ void SetValue( VtValue val );
+
+ /// Gets the value at this keyframe on the given side.
+ TS_API
+ VtValue GetValue( TsSide side ) const;
+
+ /// Sets the value at this keyframe on the given side.
+ TS_API
+ void SetValue( VtValue val, TsSide side );
+
+ /// Gets the value of the derivative at this keyframe.
+ TS_API
+ VtValue GetValueDerivative() const;
+
+ /// Gets a zero for this keyframe's value type.
+ TS_API
+ VtValue GetZero() const;
+
+ /// Gets the knot type
+ TS_API
+ TsKnotType GetKnotType() const;
+
+ /// Sets the knot type
+ TS_API
+ void SetKnotType( TsKnotType knotType );
+
+ /// Checks whether the key frame's value type supports the given knot
+ /// type.
+ TS_API
+ bool CanSetKnotType( TsKnotType, std::string *reason=NULL ) const;
+
+ /// @}
+
+ /// \name Dual-value API
+ ///
+ /// Keyframes have a "left side" and a "right side". The right side is
+ /// conceptually later than the left, even though they occur at the same
+ /// time. The two sides most often have the same value, but it is also
+ /// possible for the two sides to have different values. The purpose of
+ /// having different values on the two sides is to allow instantaneous value
+ /// discontinuities. This is useful, for example, when a constraint
+ /// changes, and the meaning of another property (like an IkTx)
+ /// instantaneously changes because of the constraint switch.
+ ///
+ /// Most spline evaluation takes place on the right side. Calling GetValue
+ /// returns the sole value for a single-valued keyframe, and the right value
+ /// for a double-valued keyframe.
+ ///
+ /// Note the difference between, on the one hand, asking a keyframe for its
+ /// left value; and on the other hand, evaluating a spline at the left side
+ /// of that keyframe's time. Usually these two methods agree. But when a
+ /// keyframe is preceded by a held segment, spline evaluation at the
+ /// keyframe's left side will yield the held value from the prior segment,
+ /// but the keyframe itself knows nothing about the prior segment, so
+ /// GetLeftValue returns the left value stored in the keyframe (which is the
+ /// right value for a single-valued knot). Another way to look at this
+ /// situation is that, when a keyframe B is preceded by a held keyframe A,
+ /// the left value of B is never consulted in spline evaluation. This
+ /// arrangement ensures that the instantaneous value change at the end of a
+ /// held segment occurs exactly at the time of the keyframe that ends the
+ /// segment.
+ ///
+ /// Note also the difference between GetIsDualValued and
+ /// TsSpline::DoSidesDiffer. Usually these two methods agree. But in the
+ /// after-held-knot case described above, they do not. They also do not
+ /// agree when SetIsDualValued(true) has been called, but the keyframe has
+ /// the same value on both sides.
+ ///
+ /// @{
+
+ /// Gets whether this knot is dual-valued. See the note above about
+ /// TsSpline::DoSidesDiffer.
+ TS_API
+ bool GetIsDualValued() const;
+
+ /// Sets whether this knot is dual-valued. When a knot is first made
+ /// dual-valued, the left value is copied from the right value.
+ TS_API
+ void SetIsDualValued( bool isDual );
+
+ /// Gets the left value of this dual-valued knot. Returns the right value
+ /// if this is not a dual-valued knot.
+ TS_API
+ VtValue GetLeftValue() const;
+
+ /// Sets the left value of this dual-valued knot. It is an error to call
+ /// this method on single-valued knots.
+ TS_API
+ void SetLeftValue( VtValue val );
+
+ /// Gets the value of the derivative on the left side. This is a synonym
+ /// for GetLeftTangentSlope for knot types that support tangents; for other
+ /// types, this method returns zero.
+ TS_API
+ VtValue GetLeftValueDerivative() const;
+
+ /// @}
+
+ /// \name Tangents
+ /// @{
+
+ /// Gets whether the value type of this keyframe is interpolatable.
+ TS_API
+ bool IsInterpolatable() const;
+
+ /// Gets whether the value type of this keyframe supports tangents. This
+ /// will return true not only for Bezier, but also for Linear and Held,
+ /// because when authors switch from Bezier to Linear/Held and back to
+ /// Bezier, we want to preserve the original tangents, and thus we track
+ /// tangent data for Linear and Held knots. If you really want to write
+ /// just to Beziers, call HasTangents().
+ TS_API
+ bool SupportsTangents() const;
+
+ /// Gets whether the knot of this keyframe has tangents. This is true when
+ /// the value type supports tangents, and the knot is a Bezier.
+ TS_API
+ bool HasTangents() const;
+
+ /// Gets the length of the projection of the knot's left tangent onto the
+ /// time axis.
+ TS_API
+ TsTime GetLeftTangentLength() const;
+
+ /// Gets the left-side tangent slope (in units per frame) of this knot.
+ TS_API
+ VtValue GetLeftTangentSlope() const;
+
+ /// Gets the length of the projection of the knot's right tangent onto the
+ /// time axis.
+ TS_API
+ TsTime GetRightTangentLength() const;
+
+ /// Gets the right-side tangent slope (in units per frame) of this knot.
+ TS_API
+ VtValue GetRightTangentSlope() const;
+
+ /// Sets the left-side tangent length (in time) of this knot. Issues a
+ /// coding error if this knot does not support tangents
+ TS_API
+ void SetLeftTangentLength( TsTime );
+
+ /// Sets the left-side tangent slope (in units per frame) of this knot.
+ /// Issues a coding error if this knot does not support tangents
+ TS_API
+ void SetLeftTangentSlope( VtValue );
+
+ /// Sets the right-side tangent length (in time) of this knot.
+ /// Issues a coding error if this knot does not support tangents
+ TS_API
+ void SetRightTangentLength( TsTime );
+
+ /// Sets the right-side tangent slope (in units per frame) of this knot.
+ /// Issues a coding error if this knot does not support tangents
+ TS_API
+ void SetRightTangentSlope( VtValue newSlope);
+
+ /// Gets whether tangent symmetry has been broken. In this context,
+ /// "symmetric" refers to the tangents having equal slope but not
+ /// necessarily equal length.
+ ///
+ /// If tangent symmetry is broken, tangent handles will not
+ /// automatically stay symmetric as they are changed.
+ TS_API
+ bool GetTangentSymmetryBroken() const;
+
+ /// Sets whether tangent symmetry is broken. Setting this to false
+ /// will make the tangents symmetric if they are not already by
+ /// reflecting the right tangent to the left side. Issues a
+ /// coding error if this knot does not support tangents
+ TS_API
+ void SetTangentSymmetryBroken( bool broken );
+
+ /// Sets the flag that enforces tangent symmetry based on whether
+ /// the tangets are already symmetric. If they are symmetric, the
+ /// 'broken' flag will be cleared so that future edits maintain
+ /// symmetry. If they are not symmetric, they will be marked as
+ /// 'broken'.
+ ///
+ /// The intent is to help provide policy for newly received
+ /// tangent data: if the tangents happen to be symmetric, keep them
+ /// so; but if they are asymmetric, don't bother. Issues a
+ /// coding error if this knot does not support tangents.
+ TS_API
+ void ResetTangentSymmetryBroken();
+
+private:
+
+ // Give the rest of the library access to the Ts_Data object held
+ // in this keyframe through the Ts_GetKeyFrameData function.
+ friend Ts_Data* Ts_GetKeyFrameData(TsKeyFrame &kf);
+ friend Ts_Data const* Ts_GetKeyFrameData(TsKeyFrame const& kf);
+
+ // Shared initialization
+ void _Initialize(
+ const TsTime & time,
+ TsKnotType knotType,
+ const VtValue & leftTangentSlope,
+ const VtValue & rightTangentSlope,
+ TsTime leftTangentLength,
+ TsTime rightTangentLength);
+
+ // XXX: exported because called from inlined templated constructors
+ TS_API
+ void _InitializeKnotType(TsKnotType knotType);
+ TS_API
+ void _InitializeTangentLength(TsTime leftTangentLength,
+ TsTime rightTangentLength);
+
+ // Helper function which tests the setability of tangents for this knot,
+ // and reports an error if tangents not supported
+ bool _ValidateTangentSetting() const;
+
+private:
+
+ Ts_PolymorphicDataHolder _holder;
+};
+
+////////////////////////////////////////////////////////////////////////
+
+TS_API
+std::ostream& operator<<(std::ostream &out, const TsKeyFrame &val);
+
+template <typename T>
+TsKeyFrame::TsKeyFrame( const TsTime & time,
+ const T & val,
+ TsKnotType knotType,
+ const T & leftTangentSlope,
+ const T & rightTangentSlope,
+ TsTime leftTangentLength,
+ TsTime rightTangentLength)
+{
+ static_assert( TsTraits<T>::isSupportedSplineValueType );
+
+ _holder.New(time, false /*isDual*/,
+ val, val, leftTangentSlope, rightTangentSlope);
+
+ _InitializeKnotType(knotType);
+ _InitializeTangentLength(leftTangentLength,rightTangentLength);
+}
+
+template <typename T>
+TsKeyFrame::TsKeyFrame( const TsTime & time,
+ const T & lhv,
+ const T & rhv,
+ TsKnotType knotType,
+ const T & leftTangentSlope,
+ const T & rightTangentSlope,
+ TsTime leftTangentLength,
+ TsTime rightTangentLength)
+{
+ static_assert( TsTraits<T>::isSupportedSplineValueType );
+
+ _holder.New(time, true /*isDual*/, lhv, rhv,
+ leftTangentSlope, rightTangentSlope);
+
+ _InitializeKnotType(knotType);
+ _InitializeTangentLength(leftTangentLength,rightTangentLength);
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/keyFrameMap.h"
+
+#include "pxr/base/ts/data.h"
+#include "pxr/base/ts/keyFrame.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+namespace {
+
+/// A predicate for TfFindBoundary which finds the lower
+/// bound at a given time. It returns true for all keyframes
+/// which have a time less than \p t.
+class _LowerBoundPredicate {
+public:
+ explicit _LowerBoundPredicate(TsTime t) : _time(t) {}
+
+ bool operator()(const TsKeyFrame &kf) const {
+ return kf.GetTime() < _time;
+ }
+private:
+ TsTime _time;
+};
+
+/// A predicate for TfFindBoundary which finds the upper
+/// bound at a given time. It returns true for all keyframes
+/// which have a time less than or equal to \p t.
+class _UpperBoundPredicate {
+public:
+ explicit _UpperBoundPredicate(TsTime t) : _time(t) {}
+
+ bool operator()(const TsKeyFrame &kf) const {
+ return kf.GetTime() <= _time;
+ }
+private:
+ TsTime _time;
+};
+
+} // anon
+
+template <class Iterator, class Predicate>
+static Iterator
+Ts_FindBoundaryImpl(Iterator begin, Iterator end,
+ TsTime t, Predicate const &pred)
+{
+ static const int MaxSteps = 3;
+
+ // Empty range.
+ if (begin == end) {
+ return end;
+ }
+ Iterator last = std::prev(end);
+ // If predicate is true for last element, return end.
+ if (pred(*last)) {
+ return end;
+ }
+ // If predicate is false for first element, return begin.
+ if (!pred(*begin)) {
+ return begin;
+ }
+
+ // Splines often have keys that are fairly evenly spaced, so we can guess an
+ // index to look near by using where `t` falls within the range of times.
+ // We take the fraction (t - firstKey.GetTime()) / (lastKey.GetTime() -
+ // firstKey.GetTime()), and multiply that by the number of keyframes to get
+ // an index. We then start searching from there. If we don't find the
+ // position of interest within a couple of steps, we resort to binary search
+ // on the remaining range.
+
+ TsTime firstTime = begin->GetTime();
+ TsTime lastTime = last->GetTime();
+
+ size_t len = std::distance(begin, end);
+ double frac = (t - firstTime) / (lastTime - firstTime);
+ size_t guessIdx(len * frac);
+
+ // We should really only ever take this branch, since times outside the
+ // range should've been handled when we checked the endpoints above. This
+ // guard is here just in case some floating point error in the fraction
+ // calculation pushes us slightly off the ends.
+ if (ARCH_LIKELY(guessIdx >= 0 && guessIdx < len)) {
+ Iterator guessIter = std::next(begin, guessIdx);
+ if (pred(*guessIter)) {
+ // Walk forward a few steps to try to find the boundary.
+ ++guessIter;
+ for (int i = 0;
+ i != MaxSteps && guessIter != end; ++i, ++guessIter) {
+ if (!pred(*guessIter)) {
+ return guessIter;
+ }
+ }
+ // Did not find the boundary -- fall back to binary search.
+ return guessIter == end ? guessIter :
+ TfFindBoundary(guessIter, end, pred);
+ }
+ else {
+ if (guessIter == begin) {
+ return guessIter;
+ }
+ // Walk backward a few steps to try to find the boundary.
+ for (int i = 0;
+ i != MaxSteps && guessIter != begin; ++i, --guessIter) {
+ if (pred(*std::prev(guessIter))) {
+ return guessIter;
+ }
+ }
+ // Did not find the boundary -- fall back to binary search.
+ return guessIter == begin ? guessIter :
+ TfFindBoundary(begin, guessIter, pred);
+ }
+ }
+ else {
+ // Our guess is not within the range -- just binary search.
+ return TfFindBoundary(begin, end, pred);
+ }
+}
+
+TsKeyFrameMap::iterator
+TsKeyFrameMap::lower_bound(TsTime t)
+{
+ return Ts_FindBoundaryImpl(
+ _data.begin(), _data.end(), t, _LowerBoundPredicate(t));
+}
+
+TsKeyFrameMap::const_iterator
+TsKeyFrameMap::lower_bound(TsTime t) const
+{
+ return Ts_FindBoundaryImpl(
+ _data.begin(), _data.end(), t, _LowerBoundPredicate(t));
+}
+
+TsKeyFrameMap::iterator
+TsKeyFrameMap::upper_bound(TsTime t)
+{
+ return Ts_FindBoundaryImpl(
+ _data.begin(), _data.end(), t, _UpperBoundPredicate(t));
+}
+
+TsKeyFrameMap::const_iterator
+TsKeyFrameMap::upper_bound(TsTime t) const
+{
+ return Ts_FindBoundaryImpl(
+ _data.begin(), _data.end(), t, _UpperBoundPredicate(t));
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_KEY_FRAME_MAP_H
+#define PXR_BASE_TS_KEY_FRAME_MAP_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/keyFrame.h"
+#include "pxr/base/tf/stl.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+/// \class TsKeyFrameMap
+///
+/// \brief An ordered sequence of keyframes with STL-compliant API
+/// for finding, inserting, and erasing keyframes while maintaining order.
+///
+/// We use this instead of a map or set of keyframes because it allows
+/// the keyframes to be stored with fewer heap allocation and better
+/// data locality.
+///
+/// For the sake of efficiency, this class makes two assumptions:
+/// The keyframes are always ordered
+/// There is never more than one key frame at a given time.
+///
+/// The client (TsSpline) is responsible for maintaining these
+/// preconditions.
+class TsKeyFrameMap {
+
+public:
+ typedef std::vector<TsKeyFrame>::iterator iterator;
+ typedef std::vector<TsKeyFrame>::const_iterator const_iterator;
+ typedef std::vector<TsKeyFrame>::reverse_iterator reverse_iterator;
+ typedef std::vector<TsKeyFrame>::const_reverse_iterator const_reverse_iterator;
+
+ TS_API
+ TsKeyFrameMap() {
+ }
+
+ TS_API
+ TsKeyFrameMap(TsKeyFrameMap const& other) :
+ _data(other._data) {
+ }
+
+ TS_API
+ TsKeyFrameMap& operator=(TsKeyFrameMap const& other) {
+ _data = other._data;
+ return *this;
+ }
+
+ TS_API
+ iterator begin() {
+ return _data.begin();
+ }
+
+ TS_API
+ const_iterator begin() const {
+ return _data.begin();
+ }
+
+ TS_API
+ iterator end() {
+ return _data.end();
+ }
+
+ TS_API
+ const_iterator end() const {
+ return _data.end();
+ }
+
+ TS_API
+ reverse_iterator rbegin() {
+ return _data.rbegin();
+ }
+
+ TS_API
+ const_reverse_iterator rbegin() const {
+ return _data.rbegin();
+ }
+
+ TS_API
+ reverse_iterator rend() {
+ return _data.rend();
+ }
+
+ TS_API
+ const_reverse_iterator rend() const {
+ return _data.rend();
+ }
+
+ TS_API
+ size_t size() const {
+ return _data.size();
+ }
+
+ TS_API
+ size_t max_size() const {
+ return _data.max_size();
+ }
+
+ TS_API
+ bool empty() const {
+ return _data.empty();
+ }
+
+ TS_API
+ void reserve(size_t size) {
+ _data.reserve(size);
+ }
+
+ TS_API
+ void clear() {
+ _data.clear();
+ }
+
+ TS_API
+ void swap(TsKeyFrameMap& other) {
+ other._data.swap(_data);
+ }
+
+ TS_API
+ void swap(std::vector<TsKeyFrame>& other) {
+ other.swap(_data);
+ }
+
+ TS_API
+ void erase(iterator first, iterator last) {
+ _data.erase(first,last);
+ }
+
+ TS_API
+ void erase(iterator i) {
+ _data.erase(i);
+ }
+
+ TS_API
+ bool operator==(const TsKeyFrameMap& other) const {
+ return (_data == other._data);
+ }
+
+ TS_API
+ bool operator!=(const TsKeyFrameMap& other) const {
+ return (_data != other._data);
+ }
+
+ TS_API
+ iterator lower_bound(TsTime t);
+
+ TS_API
+ const_iterator lower_bound(TsTime t) const;
+
+ TS_API
+ iterator upper_bound(TsTime t);
+
+ TS_API
+ const_iterator upper_bound(TsTime t) const;
+
+ TS_API
+ iterator find(const TsTime &t) {
+ iterator i = lower_bound(t);
+ if (i != _data.end() && i->GetTime() == t) {
+ return i;
+ }
+ return _data.end();
+ }
+
+ TS_API
+ const_iterator find(const TsTime &t) const {
+ const_iterator i = lower_bound(t);
+ if (i != _data.end() && i->GetTime() == t) {
+ return i;
+ }
+ return _data.end();
+ }
+
+ TS_API
+ iterator insert(TsKeyFrame const& value) {
+ // If the inserted value comes at the end, then avoid doing the
+ // lower_bound and just insert there.
+ iterator i = end();
+ if (!empty() && value.GetTime() <= _data.back().GetTime()) {
+ i = lower_bound(value.GetTime());
+ }
+
+ return _data.insert(i,value);
+ }
+
+ template <class Iter>
+ void insert(Iter const& first, Iter const& last) {
+ for(Iter i = first; i != last; i++) {
+ insert(*i);
+ }
+ }
+
+ TS_API
+ void erase(TsTime const& t) {
+ iterator i = find(t);
+ if (i != _data.end()) {
+ erase(i);
+ }
+ }
+
+ TS_API
+ TsKeyFrame& operator[](const TsTime& t) {
+ iterator i = lower_bound(t);
+ if (i != _data.end() && i->GetTime() == t) {
+ return *i;
+ }
+ TsKeyFrame &k = *(_data.insert(i,TsKeyFrame()));
+ k.SetTime(t);
+ return k;
+ }
+
+private:
+ std::vector<TsKeyFrame> _data;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/keyFrameUtils.h"
+
+#include "pxr/base/ts/data.h"
+#include "pxr/base/ts/keyFrame.h"
+#include "pxr/base/ts/keyFrameMap.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+const TsKeyFrame*
+Ts_GetClosestKeyFrame(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime )
+{
+ if (keyframes.empty())
+ return 0;
+
+ const TsKeyFrame* closest;
+
+ // Try the first frame with time >= targetTime
+ TsKeyFrameMap::const_iterator it =
+ keyframes.lower_bound( targetTime );
+
+ // Nothing >=, so return the last element
+ if (it == keyframes.end())
+ return &(*keyframes.rbegin());
+
+ closest = &(*it);
+
+ // Now try the preceding frame
+ if (it != keyframes.begin()) {
+ --it;
+ if ( (targetTime - it->GetTime()) < (closest->GetTime() - targetTime) )
+ closest = &(*it);
+ }
+
+ return closest;
+}
+
+const TsKeyFrame*
+Ts_GetClosestKeyFrameBefore(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime )
+{
+
+ if (keyframes.empty())
+ return 0;
+
+ TsKeyFrameMap::const_iterator it =
+ keyframes.lower_bound( targetTime );
+
+ if (it == keyframes.end())
+ return &(*keyframes.rbegin());
+
+ if (it != keyframes.begin()) {
+ --it;
+ return &(*it);
+ }
+
+ return 0;
+}
+
+const TsKeyFrame*
+Ts_GetClosestKeyFrameAfter(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime )
+{
+ if (keyframes.empty())
+ return 0;
+
+ TsKeyFrameMap::const_iterator it =
+ keyframes.lower_bound( targetTime );
+
+ // Skip over keyframes that have the same time; we want the first
+ // keyframe after them
+ if (it != keyframes.end() && it->GetTime() == targetTime)
+ ++it;
+ if (it != keyframes.end())
+ return &(*it);
+
+ return 0;
+}
+
+std::pair<const TsKeyFrame *, const TsKeyFrame *>
+Ts_GetClosestKeyFramesSurrounding(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime )
+{
+ std::pair<const TsKeyFrame *, const TsKeyFrame *> result;
+ result.first = result.second = NULL;
+ if (keyframes.empty())
+ return result;
+
+ // First the earliest at or after the targetTime
+ TsKeyFrameMap::const_iterator itAtOrAfter =
+ keyframes.lower_bound( targetTime );
+
+ // Set result.first
+ if (itAtOrAfter == keyframes.end())
+ result.first = &(*keyframes.rbegin());
+ else {
+ if (itAtOrAfter != keyframes.begin())
+ result.first = &(*(itAtOrAfter - 1));
+ // Else, result.first remains at NULL
+ }
+
+ // Set result.second
+ // Skip over keyframes that have the same time; we want the first
+ // keyframe after them
+ if (itAtOrAfter != keyframes.end() && itAtOrAfter->GetTime() == targetTime)
+ ++itAtOrAfter;
+ if (itAtOrAfter != keyframes.end())
+ result.second = &(*itAtOrAfter);
+ // Else, result.second remains at NULL
+
+ return result;
+}
+
+// Note: In the future this could be extended to evaluate the spline, and
+// by doing so we could support removing key frames that are redundant
+// but are not on flat sections of the spline. Also, doing so would
+// avoid problems where such frames invalidate the frame cache. If
+// all splines are cubic polynomials, then evaluating the spline at
+// four points, two before the key frame and two after, would be
+// sufficient to tell if a particular key frame was redundant.
+bool
+Ts_IsKeyFrameRedundant(
+ const TsKeyFrameMap &keyframes,
+ const TsKeyFrame &keyFrame,
+ const TsLoopParams &loopParams,
+ const VtValue& defaultValue)
+{
+ // If a knot is dual-valued, it can't possibly be redundant unless both of
+ // its values are equal.
+ if (keyFrame.GetIsDualValued()
+ && !Ts_IsClose(keyFrame.GetLeftValue(), keyFrame.GetValue())){
+ return false;
+ }
+
+ TsTime t = keyFrame.GetTime();
+ const TsKeyFrame *prev = Ts_GetClosestKeyFrameBefore(keyframes,t);
+ const TsKeyFrame *next = Ts_GetClosestKeyFrameAfter(keyframes,t);
+
+ // For looping splines, the first and last knot in the master interval are
+ // special as they interpolate, potentially, to multiple knots. It's not
+ // clear if the looping spline workflow calls for keeping these knots,
+ // even if redundant, so we err on the side of conservatism and leave them
+ // in.
+ if (loopParams.IsValid()) {
+ GfInterval master = loopParams.GetMasterInterval();
+ if (master.Contains(t)) {
+ // First in master interval? Yes if there's no prev, or there is
+ // a prev but it's not in the master interval
+ if (!prev || !master.Contains(prev->GetTime()))
+ return false;
+ // Similar for last in master interval
+ if (!next || !master.Contains(next->GetTime()))
+ return false;
+ }
+ }
+
+ if (prev && next) {
+ if (keyFrame.GetKnotType() == TsKnotHeld &&
+ prev->GetKnotType() == TsKnotHeld &&
+ prev->GetValue() == keyFrame.GetValue()) {
+ // If the both the previous key frame and the key frame we're
+ // checking are held with the same value, then the key frame is
+ // redundant.
+ return true;
+ } else {
+ // The key frame has two neighbors. If the spline is flat across all
+ // three key frames, then the middle one is redundant.
+ return Ts_IsSegmentFlat(*prev, keyFrame) &&
+ Ts_IsSegmentFlat(keyFrame, *next);
+ }
+ } else if (!prev && next) {
+ // This is the first key frame. If the spline is flat to the next
+ // key frame, the first one is redundant.
+ return Ts_IsSegmentFlat(keyFrame, *next);
+ } else if (prev && !next) {
+ // This is the last key frame. If the spline is flat to the previous
+ // key frame, the last one is redundant.
+ return Ts_IsSegmentFlat(*prev, keyFrame);
+ } else if (!defaultValue.IsEmpty()) {
+ // This is the only key frame. If its value is the same as the default
+ // value, it's redundant.
+ return (Ts_IsClose(keyFrame.GetValue(), defaultValue));
+ }
+
+ return false;
+}
+
+// Note that this function is checking for flatness from the right side value
+// of kf1 up to and including the left side value of kf2.
+bool
+Ts_IsSegmentFlat( const TsKeyFrame &kf1, const TsKeyFrame &kf2 )
+{
+ if (kf1.GetTime() >= kf2.GetTime()) {
+ TF_CODING_ERROR("The first key frame must come before the second.");
+ return false;
+ }
+
+ // If the second knot in the comparison is dual-valued, we should consider
+ // its left value.
+ const VtValue &v1 = kf1.GetValue();
+ const VtValue &v2 =
+ (kf2.GetIsDualValued()) ? kf2.GetLeftValue() : kf2.GetValue();
+
+ // If the values differ, the segment cannot be flat.
+ if (!Ts_IsClose(v1, v2)) {
+ return false;
+ }
+
+ // Special case for held knots.
+ if (kf1.GetKnotType() == TsKnotHeld) {
+ // Otherwise all segments starting with a held knot are flat until the
+ // the next key frame
+ return true;
+ }
+
+ // Make sure the tangents are flat.
+ //
+ // XXX: TsKeyFrame::GetValueDerivative() returns the
+ // slope of the tangents, regardless of the knot type.
+ //
+ if (kf1.HasTangents()
+ && !Ts_IsClose(kf1.GetValueDerivative(), kf1.GetZero())) {
+ return false;
+ }
+
+ if (kf2.HasTangents()
+ && !Ts_IsClose(kf2.GetLeftValueDerivative(), kf2.GetZero())) {
+ return false;
+ }
+
+ return true;
+}
+
+Ts_Data* Ts_GetKeyFrameData(TsKeyFrame &kf)
+{
+ return kf._holder.GetMutable();
+}
+
+Ts_Data const* Ts_GetKeyFrameData(TsKeyFrame const& kf)
+{
+ return kf._holder.Get();
+}
+
+bool Ts_IsClose(const VtValue &v0, const VtValue &v1)
+{
+ static const double EPS = 1e-6;
+ double v0dbl = 0.0; // Make compiler happy
+ double v1dbl = 0.0;
+
+ // Note that we don't use CanCast and Cast here because that would be
+ // slower, and also, we don't want to cast int and bool.
+
+ // Get out the v0 val if a float or double
+ if ( v0.IsHolding<double>() )
+ v0dbl = v0.UncheckedGet<double>();
+ else if ( v0.IsHolding<float>() )
+ v0dbl = v0.UncheckedGet<float>();
+ else
+ // Not either, so use ==
+ return v0 == v1;
+
+ // Get out the v1 val if a float or double
+ if ( v1.IsHolding<double>() )
+ v1dbl = v1.UncheckedGet<double>();
+ else if ( v1.IsHolding<float>() )
+ v1dbl = v1.UncheckedGet<float>();
+ else
+ // Not either, so use ==
+ return v0 == v1;
+
+ return TfAbs(v0dbl - v1dbl) < EPS;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_KEY_FRAME_UTILS_H
+#define PXR_BASE_TS_KEY_FRAME_UTILS_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/ts/loopParams.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class Ts_Data;
+class TsKeyFrame;
+class TsKeyFrameMap;
+
+/// \brief Finds the keyframe in \p keyframes closest to the given
+/// time. Returns NULL if there are no keyframes.
+const TsKeyFrame* Ts_GetClosestKeyFrame(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime );
+
+/// \brief Finds the closest keyframe in \p keyframes before the given
+/// time. Returns NULL if no such keyframe exists.
+const TsKeyFrame* Ts_GetClosestKeyFrameBefore(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime );
+
+/// \brief Finds the closest keyframe in \p keyframes after the given
+/// time. Returns NULL if no such keyframe exists.
+const TsKeyFrame* Ts_GetClosestKeyFrameAfter(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime );
+
+/// \brief Equivalant to calling Ts_GetClosestKeyFrameBefore and
+/// Ts_GetClosestKeyFrameAfter, but twice the speed; for performance
+/// critical applications.
+std::pair<const TsKeyFrame *, const TsKeyFrame *>
+Ts_GetClosestKeyFramesSurrounding(
+ const TsKeyFrameMap &keyframes,
+ const TsTime targetTime );
+
+/// \brief Returns true if the segment between the given (adjacent) key
+/// frames is flat.
+bool Ts_IsSegmentFlat(const TsKeyFrame &kf1, const TsKeyFrame &kf2 );
+
+/// \brief Returns true if the given key frame is redundant.
+bool Ts_IsKeyFrameRedundant(
+ const TsKeyFrameMap &keyframes,
+ const TsKeyFrame &keyFrame,
+ const TsLoopParams &loopParams=TsLoopParams(),
+ const VtValue& defaultValue=VtValue());
+
+// Return a pointer to the Ts_Data object held by the keyframe.
+// XXX: exported because used by templated functions starting from TsEvaluator
+TS_API
+Ts_Data* Ts_GetKeyFrameData(TsKeyFrame &kf);
+
+// Return a const pointer to the Ts_Data object held by the keyframe.
+// XXX: exported because used by templated functions starting from TsEvaluator
+TS_API
+Ts_Data const* Ts_GetKeyFrameData(TsKeyFrame const& kf);
+
+// Uses a fixed epsilon to compare the values, iff both are float or double,
+// else falls back to VtValue ==.
+bool Ts_IsClose(const VtValue &v0, const VtValue &v1);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/loopParams.h"
+
+#include <iostream>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TsLoopParams::TsLoopParams(
+ bool looping,
+ TsTime start,
+ TsTime period,
+ TsTime preRepeatFrames,
+ TsTime repeatFrames,
+ double valueOffset) :
+ _looping(looping),
+ _valueOffset(valueOffset)
+{
+ if (period <= 0 || preRepeatFrames < 0 || repeatFrames < 0)
+ return; // We will be invalid
+
+ _loopedInterval = GfInterval(
+ start - preRepeatFrames,
+ start + period + repeatFrames,
+ true /* min closed */,
+ false /* max closed */);
+
+ _masterInterval = GfInterval(
+ start, start + period,
+ true /* min closed */,
+ false /* max closed */);
+}
+
+TsLoopParams::TsLoopParams() :
+ _looping(false),
+ _valueOffset(0.0)
+{
+}
+
+void
+TsLoopParams::SetLooping(bool looping)
+{
+ _looping = looping;
+}
+
+bool
+TsLoopParams::GetLooping() const
+{
+ return _looping;
+}
+
+TsTime
+TsLoopParams::GetStart() const
+{
+ return _masterInterval.GetMin();
+}
+
+TsTime
+TsLoopParams::GetPeriod() const
+{
+ return _masterInterval.GetMax() - _masterInterval.GetMin();
+}
+
+TsTime
+TsLoopParams::GetPreRepeatFrames() const
+{
+ return _masterInterval.GetMin() - _loopedInterval.GetMin();
+}
+
+TsTime
+TsLoopParams::GetRepeatFrames() const
+{
+ return _loopedInterval.GetMax() - _masterInterval.GetMax();
+}
+
+const GfInterval &
+TsLoopParams::GetMasterInterval() const
+{
+ return _masterInterval;
+}
+
+const GfInterval &
+TsLoopParams::GetLoopedInterval() const
+{
+ return _loopedInterval;
+}
+
+bool
+TsLoopParams::IsValid() const
+{
+ return !_loopedInterval.IsEmpty() && !_masterInterval.IsEmpty();
+}
+
+void
+TsLoopParams::SetValueOffset(double valueOffset)
+{
+ _valueOffset = valueOffset;
+}
+
+double
+TsLoopParams::GetValueOffset() const
+{
+ return _valueOffset;
+}
+
+std::ostream&
+operator<<(std::ostream& out, const TsLoopParams& lp)
+{
+ out << "("
+ << lp.GetLooping() << ", "
+ << lp.GetStart() << ", "
+ << lp.GetPeriod() << ", "
+ << lp.GetPreRepeatFrames() << ", "
+ << lp.GetRepeatFrames() << ", "
+ << lp.GetValueOffset()
+ << ")";
+ return out;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_LOOP_PARAMS_H
+#define PXR_BASE_TS_LOOP_PARAMS_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/types.h"
+
+#include <iosfwd>
+#include <string>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TsLoopParams
+{
+public:
+ TS_API
+ TsLoopParams(
+ bool looping,
+ TsTime start,
+ TsTime period,
+ TsTime preRepeatFrames,
+ TsTime repeatFrames,
+ double valueOffset);
+
+ TS_API
+ TsLoopParams();
+
+ TS_API
+ void SetLooping(bool looping);
+
+ TS_API
+ bool GetLooping() const;
+
+ TS_API
+ double GetStart() const;
+
+ TS_API
+ double GetPeriod() const;
+
+ TS_API
+ double GetPreRepeatFrames() const;
+
+ TS_API
+ double GetRepeatFrames() const;
+
+ TS_API
+ const GfInterval &GetMasterInterval() const;
+
+ TS_API
+ const GfInterval &GetLoopedInterval() const;
+
+ TS_API
+ bool IsValid() const;
+
+ TS_API
+ void SetValueOffset(double valueOffset);
+
+ TS_API
+ double GetValueOffset() const;
+
+ TS_API
+ bool operator==(const TsLoopParams &rhs) const {
+ return _looping == rhs._looping &&
+ _loopedInterval == rhs._loopedInterval &&
+ _masterInterval == rhs._masterInterval &&
+ _valueOffset == rhs._valueOffset;
+ }
+
+ TS_API
+ bool operator!=(const TsLoopParams &rhs) const {
+ return !(*this == rhs);
+ }
+
+private:
+ bool _looping;
+ GfInterval _loopedInterval;
+ GfInterval _masterInterval;
+ double _valueOffset;
+};
+
+TS_API
+std::ostream& operator<<(std::ostream& out, const TsLoopParams& lp);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/mathUtils.h"
+
+#include "pxr/base/gf/vec3d.h"
+#include "pxr/base/gf/rotation.h"
+#include "pxr/base/gf/quatd.h"
+#include "pxr/base/gf/matrix3d.h"
+#include "pxr/base/gf/matrix4d.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+////////////////////////////////////////////////////////////////////////
+// Polynomial evaluation & root-solving
+
+
+// Solve for the roots of a quadratic equation.
+//
+// From numerical recipes ch. 5.6f, stable quadratic roots.
+// Return true if any real roots, false if not.
+// Roots are sorted root0 <= root1.
+//
+bool
+Ts_SolveQuadratic( const double poly[3], double *root0, double *root1 )
+{
+ double a,b,c,disc,q,sq;
+
+ a=poly[2]; b=poly[1]; c=poly[0];
+
+ disc = b*b - 4*a*c;
+ sq = sqrt(fabs(disc));
+
+ /* If this is zero, sqrt(disc) is too small for a float... this avoids
+ * having an epsilon for size of discriminant... compute in
+ * double and then cast to float. If it becomes 0, then we're ok. */
+
+ // Linear case
+ if (a == 0.0) {
+ if (b == 0.0) {
+ // Constant y=0; infinite roots.
+ *root1 = *root0 = 0.0;
+ return false;
+ }
+ *root1 = *root0 = -c / b;
+ return true;
+ }
+
+ /* if the disciminant is positive or it's very close to zero,
+ we'll go on */
+ if ((disc >= 0.0) || (((float) sq) == 0.0f)) {
+
+ if (b >= 0.0)
+ q = -0.5 * (b + sq);
+ else
+ q = -0.5 * (b - sq);
+
+ *root0 = q / a;
+
+ /* if q is zero, then we avoid the divide by zero.
+ * q is zero means that b is zero and c is zero.
+ * that implies that the root is zero.
+ */
+ if (q != 0.0)
+ *root1 = c / q;
+ else {
+ /* b and c are zero, so this has gotta be zero */
+ // I have not been able to construct test data to exercise
+ // this case.
+ *root1 = 0.0;
+ }
+
+ /* order root0 < root1 */
+ if (*root0 > *root1) {
+ std::swap(*root0, *root1);
+ }
+ return true;
+ }
+
+ // Zero real roots.
+ *root1 = *root0 = 0.0;
+ return false;
+}
+
+// Solve f(x) = y for x where f is a cubic polynomial.
+static double
+_SolveCubic_RegulaFalsi(
+ const TsTime poly[4], TsTime y, const GfInterval& bounds )
+{
+ static const int NUM_ITERS = 20;
+ static const double EPSILON_1 = 1e-4;
+ static const double EPSILON_2 = 1e-6;
+
+ double x0 = bounds.GetMin();
+ double x1 = bounds.GetMax();
+ double y0 = Ts_EvalCubic( poly, x0 ) - y;
+ double y1 = Ts_EvalCubic( poly, x1 ) - y;
+ double x, yEst;
+
+ if (fabs(y0) < EPSILON_1) {
+ return x0;
+ }
+ if (fabs(y1) < EPSILON_1) {
+ // I have not been able to construct test data to exercise
+ // this case.
+ return x1;
+ }
+ if (y0 * y1 > 0) {
+ // Either no root or 2 roots, so punt
+ // I have not been able to construct test data to exercise
+ // this case.
+ return -1;
+ }
+
+ // Regula Falsi iteration
+ for (int i = 0; i < NUM_ITERS; ++i ) {
+ x = x0 - y0 * (x1 - x0) / (y1 - y0);
+ yEst = Ts_EvalCubic(poly, x) - y;
+ if (fabs(yEst) < EPSILON_2)
+ break;
+ if (y0 * yEst <= 0) {
+ y1 = yEst;
+ x1 = x;
+ } else {
+ y0 = yEst;
+ x0 = x;
+ }
+ }
+ return x;
+}
+
+// Solve f(x) = y for x in the given bounds where f is a cubic polynomial.
+double
+Ts_SolveCubicInInterval(
+ const TsTime poly[4], const TsTime polyDeriv[3],
+ TsTime y, const GfInterval& bounds)
+{
+ static const int NUM_ITERS = 20;
+ static const double EPSILON = 1e-5;
+
+ double x = (bounds.GetMin() + bounds.GetMax()) * 0.5;
+ for (int i = 0; i < NUM_ITERS; ++i ) {
+ double dx = (Ts_EvalCubic(poly, x) - y) /
+ Ts_EvalQuadratic(polyDeriv, x);
+ x -= dx;
+ if (!bounds.Contains(x))
+ return _SolveCubic_RegulaFalsi( poly, y, bounds );
+ if (fabs(dx) < EPSILON)
+ break;
+ }
+ return x;
+}
+
+// Solve f(x) = y for x where f is a cubic polynomial.
+ double
+Ts_SolveCubic(const TsTime poly[4], TsTime y)
+{
+ double root0 = 0, root1 = 1;
+ GfInterval bounds(0, 1);
+
+ // Check to see if the first derivative ever goes to 0 in the interval
+ // [0,1]. If it does then the cubic is not monotonically increasing
+ // in that interval.
+ double polyDeriv[3];
+ Ts_CubicDerivative( poly, polyDeriv );
+ if (Ts_SolveQuadratic( polyDeriv, &root0, &root1 )) {
+ if (root0 >= 0.0 && root1 <= 1.0) {
+ // The curve's inverse doubles back on itself in the interval
+ // (root0,root1). In that interval there are 3 solutions
+ // for any y. To disambiguate the solutions we'll use the
+ // solution in [0,root0] for y < tmid and the solution in
+ // [root1,1] for y >= tmid, where tmid is some value for
+ // which there are 3 solutions. We choose tmid as the
+ // average of the values at root0 and root1.
+ //
+ // If the value at root0 is less than the value at root1 then
+ // only the segment of the curve between root0 and root1 is
+ // valid (monotonically increasing). That shouldn't normally
+ // happen but it's possible and will happen if Bezier tangent
+ // lengths are zero. In this case we'll use the [root0,root1]
+ // interval.
+ double t0 = Ts_EvalCubic( poly, 0 );
+ double t1 = Ts_EvalCubic( poly, 1 );
+ double tlo = Ts_EvalCubic( poly, root0 );
+ double thi = Ts_EvalCubic( poly, root1 );
+ double tmid = (GfClamp(tlo, t0, t1) + GfClamp(thi, t0, t1)) * 0.5;
+ if (tlo < thi) {
+ bounds = GfInterval( root0, root1 );
+ }
+ else if (tmid > y) {
+ bounds = GfInterval( 0, root0 );
+ } else {
+ bounds = GfInterval( root1, 1 );
+ }
+ }
+ }
+
+ return Ts_SolveCubicInInterval(poly, polyDeriv, y, bounds);
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_MATH_UTILS_H
+#define PXR_BASE_TS_MATH_UTILS_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/types.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class GfMatrix4d;
+
+// Solve the cubic polynomial time=f(u) where
+// f(u) = c[0] + u * c[1] + u^2 * c[2] + u^3 * c[3].
+// XXX: exported because used by templated functions starting from TsEvaluator
+TS_API
+double Ts_SolveCubic(const TsTime c[4], TsTime time);
+
+// Take the first derivative of a cubic polynomial:
+// 3*c[3]*u^2 + 2*c[2] + c[1]
+template <typename T>
+void
+Ts_CubicDerivative( const T poly[4], double deriv[3] )
+{
+ deriv[2] = 3. * poly[3];
+ deriv[1] = 2. * poly[2];
+ deriv[0] = poly[1];
+}
+
+// Solve f(x) = y for x in the given bounds where f is a cubic polynomial.
+double
+Ts_SolveCubicInInterval(
+ const TsTime poly[4], const TsTime polyDeriv[3],
+ TsTime y, const GfInterval& bounds);
+
+// Solve for the roots of a quadratic equation.
+bool
+Ts_SolveQuadratic( const double poly[3], double *root0, double *root1 );
+
+// Evaluate the quadratic polynomial in c[] at u.
+template <typename T>
+T
+Ts_EvalQuadratic(const T c[3], double u)
+{
+ return u * (u * c[2] + c[1]) + c[0];
+}
+
+// Evaluate the cubic polynomial in c[] at u.
+template <typename T>
+T
+Ts_EvalCubic(const T c[4], double u)
+{
+ return u * (u * (u * c[3] + c[2]) + c[1]) + c[0];
+}
+
+template <typename T>
+T
+Ts_EvalCubicDerivative(const T c[4], double u)
+{
+ return u * (u * 3.0 * c[3] + 2.0 * c[2]) + c[1];
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/tf/pyModule.h"
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+TF_WRAP_MODULE
+{
+ TF_WRAP(KeyFrame);
+ TF_WRAP(LoopParams);
+ TF_WRAP(Simplify);
+ TF_WRAP(Spline);
+ TF_WRAP(TsTest_Evaluator);
+ TF_WRAP(TsTest_Museum);
+ TF_WRAP(TsTest_SampleBezier);
+ TF_WRAP(TsTest_SampleTimes);
+ TF_WRAP(TsTest_SplineData);
+ TF_WRAP(TsTest_TsEvaluator);
+ TF_WRAP(TsTest_Types);
+ TF_WRAP(Types);
+
+#ifdef PXR_BUILD_ANIMX_TESTS
+ TF_WRAP(TsTest_AnimXEvaluator);
+#endif
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/tf/registryManager.h"
+#include "pxr/base/tf/scriptModuleLoader.h"
+#include "pxr/base/tf/token.h"
+
+#include <vector>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TF_REGISTRY_FUNCTION(TfScriptModuleLoader) {
+ // List of direct dependencies for this library.
+ const std::vector<TfToken> reqs = {
+ TfToken("arch"),
+ TfToken("gf"),
+ TfToken("plug"),
+ TfToken("tf"),
+ TfToken("trace"),
+ TfToken("vt")
+ };
+ TfScriptModuleLoader::GetInstance().
+ RegisterLibrary(TfToken("ts"), TfToken("pxr.Ts"), reqs);
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+// WARNING: THIS FILE IS GENERATED. DO NOT EDIT.
+//
+
+#define TF_MAX_ARITY 7
+#include "pxr/pxr.h"
+#include "pxr/base/arch/defines.h"
+#if defined(ARCH_OS_DARWIN)
+#include <mach/mach_time.h>
+#endif
+#if defined(ARCH_OS_LINUX)
+#include <unistd.h>
+#include <x86intrin.h>
+#endif
+#if defined(ARCH_OS_WINDOWS)
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+
+
+#endif
+#include <algorithm>
+#include <any>
+#include <array>
+#include <atomic>
+#include <cfloat>
+#include <cinttypes>
+#include <cmath>
+#include <cstdarg>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <deque>
+#include <float.h>
+#include <functional>
+#include <intrin.h>
+#include <iosfwd>
+#include <iostream>
+#include <iterator>
+#include <limits>
+#include <list>
+#include <locale>
+#include <map>
+#include <math.h>
+#include <memory>
+#include <mutex>
+#include <new>
+#include <numeric>
+#include <set>
+#include <sstream>
+#include <stdarg.h>
+#include <stddef.h>
+#include <string>
+#include <sys/types.h>
+#include <thread>
+#include <tuple>
+#include <type_traits>
+#include <typeindex>
+#include <typeinfo>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+#include <boost/function.hpp>
+#include <boost/functional/hash.hpp>
+#include <boost/intrusive_ptr.hpp>
+#include <boost/preprocessor/seq/enum.hpp>
+#include <boost/preprocessor/seq/filter.hpp>
+#include <boost/preprocessor/seq/for_each.hpp>
+#ifdef PXR_PYTHON_SUPPORT_ENABLED
+#include <boost/python.hpp>
+#include <boost/python/class.hpp>
+#include <boost/python/converter/from_python.hpp>
+#include <boost/python/converter/registered.hpp>
+#include <boost/python/converter/registrations.hpp>
+#include <boost/python/converter/registry.hpp>
+#include <boost/python/converter/rvalue_from_python_data.hpp>
+#include <boost/python/converter/to_python_function_type.hpp>
+#include <boost/python/def.hpp>
+#include <boost/python/def_visitor.hpp>
+#include <boost/python/default_call_policies.hpp>
+#include <boost/python/dict.hpp>
+#include <boost/python/errors.hpp>
+#include <boost/python/extract.hpp>
+#include <boost/python/handle.hpp>
+#include <boost/python/implicit.hpp>
+#include <boost/python/list.hpp>
+#include <boost/python/module.hpp>
+#include <boost/python/object.hpp>
+#include <boost/python/object/iterator.hpp>
+#include <boost/python/object_fwd.hpp>
+#include <boost/python/object_operators.hpp>
+#include <boost/python/operators.hpp>
+#include <boost/python/raw_function.hpp>
+#include <boost/python/refcount.hpp>
+#include <boost/python/return_by_value.hpp>
+#include <boost/python/scope.hpp>
+#include <boost/python/to_python_converter.hpp>
+#include <boost/python/tuple.hpp>
+#include <boost/python/type_id.hpp>
+#if defined(__APPLE__) // Fix breakage caused by Python's pyport.h.
+#undef tolower
+#undef toupper
+#endif
+#endif // PXR_PYTHON_SUPPORT_ENABLED
+#include <tbb/blocked_range.h>
+#include <tbb/cache_aligned_allocator.h>
+#include <tbb/enumerable_thread_specific.h>
+#include <tbb/parallel_for.h>
+#include <tbb/parallel_for_each.h>
+#include <tbb/spin_mutex.h>
+#include <tbb/spin_rw_mutex.h>
+#include <tbb/task_group.h>
+#ifdef PXR_PYTHON_SUPPORT_ENABLED
+#include "pxr/base/tf/pySafePython.h"
+#endif // PXR_PYTHON_SUPPORT_ENABLED
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/arch/defines.h"
+#include "pxr/base/work/loops.h"
+
+#include "pxr/base/ts/simplify.h"
+
+#include "pxr/base/ts/evalCache.h"
+#include "pxr/base/ts/evaluator.h"
+#include "pxr/base/ts/keyFrameUtils.h"
+
+#include "pxr/base/trace/trace.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+#define SIMPLIFY_DEBUG 0
+
+static const double _toleranceEspilon = 1e-6;
+static const double _minTanLength = 0.1;
+
+// ***************** SIMPLIFY ***********************************
+
+// Overview: This is a "greedy" algorithm which iteratively removes keys and
+// adjusts the neighbor tangents' lengths to compensate. If runs over all the
+// keys and measures the error resulting from removing each one and making the
+// best compensation possible. Then, in a loop it removes the one with the
+// least error (compensating the neighbor tangents) and then re-evaluates the
+// neighbors for the error-if-removed metric. It stops when the smallest such
+// error is too big.
+
+// We use RMS for compensating the tangents, since the derivative must be
+// smooth. The user-facing tolerance is based on Max.
+enum _SimplifyErrorType {
+ _SimplifyErrorTypeRMS,
+ _SimplifyErrorTypeMAX,
+};
+
+// Utility routine for setting the left tangent length.
+static void
+_SetLeftTangentLength(
+ TsKeyFrame *key,
+ double length)
+{
+ if (!key)
+ return;
+ if (!key->SupportsTangents())
+ return;
+
+ key->SetLeftTangentLength(length);
+}
+
+// Similar to the above, for the right side
+static void
+_SetRightTangentLength(
+ TsKeyFrame *key,
+ double length)
+{
+ if (!key)
+ return;
+ if (!key->SupportsTangents())
+ return;
+
+ key->SetRightTangentLength(length);
+}
+
+// Compute the error within spanInterval at each frame. vals are the reference
+// values in the original spline in the interval valsInterval. If useMax,
+// then return the max error, else rms
+static double
+_ComputeError(const TsSpline &spline,
+ const GfInterval &spanInterval,
+ const std::vector<double> &vals,
+ const GfInterval &valsInterval,
+ bool useMax)
+{
+ if (!TF_VERIFY(spanInterval.GetMin() >= valsInterval.GetMin()))
+ return DBL_MAX;
+ if (!TF_VERIFY(vals.size() == valsInterval.GetSize() + 1))
+ return DBL_MAX;
+ // where to start looking in vals
+ size_t valsBase = size_t(spanInterval.GetMin() - valsInterval.GetMin());
+ if (!TF_VERIFY(valsBase < vals.size()))
+ return DBL_MAX;
+
+ size_t numSamples = size_t(spanInterval.GetSize() + 1);
+ if (!TF_VERIFY(valsBase + numSamples <= vals.size()))
+ return DBL_MAX;
+
+ double err=0;
+ for (size_t i = 0; i < numSamples; i++) {
+ double t = spanInterval.GetMin() + i;
+ double thisVal = spline.Eval(t).Get<double>();
+ double thisErr = thisVal - vals[valsBase + i];
+ if (useMax) {
+ // max
+ double absErr = TfAbs(thisErr);
+ if (absErr > err)
+ err = absErr;
+ } else {
+ // rms
+ err += thisErr * thisErr;
+ }
+ }
+ return useMax ? err : sqrt(err/numSamples);
+}
+
+// Compute the error from setting the left or right tangent of k to l within
+// spanInterval at each frame. vals are the reference values in the original
+// spline in the interval valsInterval
+static double
+_ComputeErrorForLength(bool affectRight, double l,
+ const TsKeyFrame &k,
+ TsSpline* spline,
+ const GfInterval &spanInterval,
+ const std::vector<double> &vals,
+ const GfInterval &valsInterval)
+{
+ TsKeyFrame nk = k;
+ TsTime spanSize = spanInterval.GetSize();
+
+ // Set the length.
+ if (affectRight)
+ _SetRightTangentLength(&nk, l * spanSize);
+ else
+ _SetLeftTangentLength(&nk, l * spanSize);
+
+ spline->SetKeyFrame(nk);
+ return _ComputeError(*spline, spanInterval, vals, valsInterval,
+ /* useMax = */ false );
+}
+
+// Assumed knots at the ends of the spanInterval and none inside; will
+// world
+// stretch the inner tangents for best result. vals are the reference values
+// in the original spline in valsInterval, at each frame, that we will compute
+// the error in refernce to.
+static void _SimplifySpan(TsSpline* spline,
+ const GfInterval &spanInterval,
+ const std::vector<double> &vals,
+ const GfInterval &valsInterval)
+{
+ TRACE_FUNCTION();
+
+ std::vector<TsKeyFrame> keyFrames
+ = spline->GetKeyFramesInMultiInterval(GfMultiInterval(spanInterval));
+
+ // Not illegal, but we can't simplify the span without 2 knots.
+ if (keyFrames.size() != 2) {
+ return;
+ }
+
+ // If there is no error, even before messing with the tangents, then
+ // we're already done. This could typically happen if we're in a flat
+ // stretch
+ double initialErr =
+ _ComputeError(*spline, spanInterval, vals, valsInterval,
+ /* useMax = */ false );
+ if (initialErr < 1e-10)
+ return;
+
+ TsKeyFrame k0 = keyFrames.front();
+ TsKeyFrame k1 = keyFrames.back();
+ double v0 = k0.GetValue().Get<double>();
+ double v1 = k1.GetValue().Get<double>();
+
+ const double minVal = TfMin(v0, v1);
+ const double maxVal = TfMax(v0, v1);
+ const double tolerance = (maxVal - minVal) / 20000;
+
+ TsTime spanSize = spanInterval.GetSize();
+ if (spanSize == 0)
+ return;
+
+ // Initial guess at tangent lengths
+ _SetRightTangentLength(&k0, .33 * spanSize);
+ _SetLeftTangentLength(&k1, .33 * spanSize);
+
+ spline->SetKeyFrame(k0);
+ spline->SetKeyFrame(k1);
+
+ // These are lengths for the tangent, as fractions of the span size
+ double lo, hi;
+
+ int iter = 0;
+ // Delta length (normalized) to use for slop calc; we'll sample this delta
+ // +/- the current guess to approximate the slope
+ const double ldel = .00001;
+ // The error due to the last 2 iterations
+ double lastErr = 1e10;
+ double thisErr = 1e10;
+ // Each iteration adjusts one tangent length for best results,
+ // alternating, using binary search. Stop when the error is tiny, or
+ // stops changing very much (or is not converging)
+ for (; iter < 100; iter++) {
+#if SIMPLIFY_DEBUG
+ printf("ITER %d **************\n", iter);
+#endif
+ // The range for our guesses spans from lo to hi. If we look from 0
+ // to 1, then our tangent handles could overlap, so instead we only
+ // look from 0 to 0.5, ensuring that they will never cross (this is
+ // what the animators seems to like). Also we use _minTanLength to
+ // avoid going all the way to the low limit so we don't allow tangents
+ // to become unusably short. _minTanLength is expressed in absolute
+ // length, where lo is normalized to [0,1] on the spanInterval. So
+ // convert to normalized.
+ lo = _minTanLength / spanSize;
+ hi = 0.5 - 2 * ldel;
+ while (1) {
+ // New guess
+ double g = (lo + hi) * .5;
+ bool isK0 = (iter & 1) == 0;
+ double err0 = _ComputeErrorForLength(isK0,
+ (g - ldel), isK0 ? k0:k1, spline,
+ spanInterval, vals, valsInterval);
+ double err1 = _ComputeErrorForLength(isK0,
+ (g + ldel), isK0 ? k0:k1, spline,
+ spanInterval, vals, valsInterval);
+
+
+ double slope = (err1 - err0) / (2 * ldel);
+#if SIMPLIFY_DEBUG
+ printf("guess g is %g err0 %g err1 %g, slope %g\n", g, err0, err1,
+ slope);
+#endif
+ if (slope > 0) {
+ hi = g;
+
+#if SIMPLIFY_DEBUG
+ printf("New hi %g\n", hi);
+#endif
+ }
+ else {
+ lo = g;
+
+#if SIMPLIFY_DEBUG
+ printf("New lo %g\n",lo);
+#endif
+ }
+
+ // If we've converged, time to break
+ if (hi - lo < .00005) {
+ // Recompute the error for the actual guess (also leaving the
+ // spline simplified for that guess)
+ thisErr = _ComputeErrorForLength(isK0,
+ g, isK0 ? k0:k1, spline,
+ spanInterval, vals, valsInterval);
+ break;
+ }
+ }
+
+
+ // If the error changed very little, done
+ if (TfAbs(lastErr - thisErr) < tolerance)
+ break;
+ lastErr = thisErr;
+ }
+#if SIMPLIFY_DEBUG
+ printf("span [%g %g] Num iters %d thisErr is %g\n",
+ spanInterval.GetMin(), spanInterval.GetMax(), iter, thisErr);
+#endif
+}
+
+// Return true if there is a kink in the spline in the interval. We look
+// just at the X cubic, i.e. time(u). Since elsewhere in Ts we fix this
+// function to be monotinically increasing, we'll call foul if we ever get a
+// slope close to 0 inside the interval. So, we just have to find the extereme
+// of time'(u); if it's between 0 and 1, and time'(u) opens upward, that's the
+// min; if it's close to 0 we have a kink.
+static bool
+_IsSplineKinkyInInterval(const TsSpline &spline,
+ const GfInterval &spanInterval)
+{
+ TsSpline::const_iterator ki0 = spline.find(spanInterval.GetMin());
+ TsSpline::const_iterator ki1 = spline.find(spanInterval.GetMax());
+ // This is now legal (but still false)
+ if (!(ki0 != spline.end() && ki1 != spline.end()))
+ return false;
+
+ // This can supply us the coefficients
+ Ts_EvalCache<double, true>::TypedSharedPtr cache;
+ cache = Ts_EvalCache<double, true>::New(*ki0, *ki1);
+ const Ts_Bezier<double> *bezier = cache->GetBezier();
+ // True if the parabola opens upward
+ bool vertexMin = bezier->timeCoeff[3] > 0;
+
+ double min = DBL_MAX;
+ // If not flat...
+ if (bezier->timeCoeff[3] != 0) {
+ // The u coord at the vertex, gotten by solving time''(u) = 0
+ TsTime uv = -bezier->timeCoeff[2]/(3*bezier->timeCoeff[3]);
+ // If the deriv is very flat near the edges of the interval, don't
+ // flag this as a kink
+ if (vertexMin && uv > .05 && uv < .95) {
+ // Get the value of time'(uv)
+ min = Ts_EvalCubicDerivative(bezier->timeCoeff, uv);
+ if (min < .001) {
+ // Kinky!
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+// If the key at the given time were removed, compute the resulting error.
+// Two intervals supplied; spanInterval is the interval to simplify over.
+// vals are the reference values in the original spline in valsInterval, at
+// each frame, that we will compute the error in refernce to. Note that the
+// spline will be unchanged upon return. This assumes routine that there are
+// knots at t and on the ends of spanInterval. If the simplify results in a
+// kink, we'll pretend the error was huge.
+double
+_ComputeErrorIfKeyRemoved(TsSpline* spline, TsTime t,
+ const GfInterval &spanInterval,
+ const std::vector<double> &vals,
+ const GfInterval &valsInterval)
+{
+ if (!TF_VERIFY(vals.size() == valsInterval.GetSize() + 1))
+ return DBL_MAX;
+
+ // Get the keys that will be changed by _SimplifySpan
+ TsSpline::const_iterator k0 = spline->find(spanInterval.GetMin());
+ TsSpline::const_iterator k = spline->find(t);
+ TsSpline::const_iterator k1 = spline->find(spanInterval.GetMax());
+
+ if (!TF_VERIFY(k != spline->end()))
+ return DBL_MAX;
+
+ TsKeyFrame kCopy = *k;
+ TsKeyFrame k0Copy;
+ TsKeyFrame k1Copy;
+ if (k0 != spline->end()) {
+ k0Copy = *k0;
+ }
+ if (k1 != spline->end()) {
+ k1Copy = *k1;
+ }
+
+ spline->RemoveKeyFrame(kCopy.GetTime());
+
+ // Find the best tangents for the neighbors
+ _SimplifySpan(spline, spanInterval, vals, valsInterval);
+
+ double err = DBL_MAX;
+ // If the spline has a kink in the interval, let the large error stand
+ if (!_IsSplineKinkyInInterval(*spline, spanInterval)) {
+ // Compute the error over the larger interval
+ err = _ComputeError(*spline, valsInterval, vals, valsInterval,
+ /* useMax = */ true);
+ }
+
+ // Put back the keys
+ spline->SetKeyFrame( kCopy);
+ // We may have set these in _SimplifySpan, so we want to set them back to
+ // what they were before.
+ if (k0 != spline->end()) {
+ spline->SetKeyFrame(k0Copy);
+ }
+ if (k1 != spline->end()) {
+ spline->SetKeyFrame(k1Copy);
+ }
+
+ return err;
+}
+
+// Info per knots in range
+namespace {
+struct _EditSimplifyKnotInfo {
+ TsTime t;
+ TsKnotType knotType;
+ bool removable;
+ // The error that would result in the spline were this removed
+ double errIfRemoved;
+};
+}
+
+// Set the error-if-removed for the ith element of the vector of
+// _EditSimplifyKnotInfo's
+static void _SetKnotInfoErrorIfKeyRemoved(
+ std::vector<_EditSimplifyKnotInfo> &ki,
+ size_t i, TsSpline* spline,
+ const std::vector<double> &vals,
+ const GfInterval &valsInterval)
+{
+ if (!TF_VERIFY(i >= 0 && i < ki.size()))
+ return;
+
+ if (ki[i].removable) {
+ // Must be inside to be removable
+ if (!TF_VERIFY(i > 0 && i < ki.size()-1))
+ return;
+
+ // We know it's not on the end
+ ki[i].errIfRemoved = _ComputeErrorIfKeyRemoved(spline, ki[i].t,
+ GfInterval(ki[i-1].t, ki[i+1].t),
+ vals, valsInterval);
+ } else {
+ // Shouldn't ever be accessing this if !k.removable, but just in
+ // case
+ ki[i].errIfRemoved = DBL_MAX;
+ }
+}
+
+// True if the knot has a flat segment on either side
+static
+bool _IsKnotOnPlateau(const TsSpline& spline, const TsKeyFrame& key)
+{
+ const TsKeyFrameMap& keyMap = spline.GetKeyFrames();
+
+ TsKeyFrameMap::const_iterator kIter =
+ keyMap.lower_bound(key.GetTime());
+
+ if (!TF_VERIFY(kIter != keyMap.end()))
+ return false;
+
+ if (kIter != keyMap.begin()) {
+ if (spline.IsSegmentFlat(*(kIter-1), key))
+ return true;
+ }
+
+ if (kIter+1 != keyMap.end()) {
+ if (spline.IsSegmentFlat(key, *(kIter+1)))
+ return true;
+ }
+
+ return false;
+}
+
+// True if the knot is an extreme. It must be > one neighbor and <= the
+// other, or < one and >= the other. The max value difference between it and
+// its neighbors must also be > tolerance.
+static
+bool _IsKnotAnExtreme(const TsSpline &spline, const TsKeyFrame &k,
+ double tolerance)
+{
+
+ std::pair<TsExtrapolationType, TsExtrapolationType> extrap =
+ spline.GetExtrapolation();
+
+ // Does it have left/right neighbor?
+ bool hasLeft = false;
+ bool hasRight = false;
+
+ const TsKeyFrameMap& keyMap = spline.GetKeyFrames();
+ // This points at k
+ TsKeyFrameMap::const_iterator kIter =
+ keyMap.lower_bound(k.GetTime());
+ if (!TF_VERIFY(kIter != keyMap.end()))
+ return false;
+
+ if (kIter != keyMap.begin()) {
+ hasLeft = true;
+ } else {
+ // Cases below get tricky to evaluate if we're at an end, and extrap
+ // is not held; very rare, so just return true
+ if (extrap.first != TsExtrapolationHeld)
+ return true;
+ hasLeft = false;
+ }
+
+ if (kIter+1 != keyMap.end()) {
+ hasRight = true;
+ } else {
+ // Cases below get tricky to evaluate if we're at an end, and extrap
+ // is not held; very rare, so just return true
+ if (extrap.second != TsExtrapolationHeld)
+ return true;
+ hasRight = false;
+ }
+
+ if (!hasLeft && !hasRight)
+ return false;
+
+ // Nomenclature:
+ // Knot values left to right: v0, v1, v, v2, v3 where v is k's value;
+ // v0 and v3 only used (below) if hasLeft and hasRight
+ double v1, v, v2;
+ v = k.GetValue().Get<double>();
+ // Set v1 and v2 to v in case the knots don't exist
+ v1 = v2 = v;
+
+ if (hasLeft) {
+ v1 = (kIter-1)->GetValue().Get<double>();
+ }
+
+ if (hasRight) {
+ v2 = (kIter+1)->GetValue().Get<double>();
+ }
+
+ // The values we will test v against
+ double vl = v1;
+ double vr = v2;
+
+ // For something to be an extreme, it should be monotonically bigger than
+ // its two neighbors in each direction (if they exist), and by at least
+ // 'tolerance'
+ if (hasLeft && hasRight) {
+ if (kIter-1 != keyMap.begin() && kIter+2 != keyMap.end()) {
+ double v0 = (kIter-2)->GetValue().Get<double>();
+ double v3 = (kIter+2)->GetValue().Get<double>();
+ if (v > v1 && v1 > v0 && v > v2 && v2 > v3) {
+ vl = v0;
+ vr = v3;
+ }
+ if (v < v1 && v1 < v0 && v < v2 && v2 < v3) {
+ vl = v0;
+ vr = v3;
+ }
+ }
+ }
+
+ double delta = 0;
+ if ((v > vl && v >= vr) || (v >= vl && v > vr)) {
+ delta = TfMax(v - vl, v - vr);
+ }
+ if ((v < vl && v <= vr) || (v <= vl && v < vr)) {
+ delta = TfMax(vl - v, vr - v);
+ }
+ return delta > tolerance;
+}
+
+// The actual tolerance is maxErrFract times the value range of the spline
+// within the bounds of the intervals.
+void TsSimplifySpline(TsSpline* spline,
+ const GfMultiInterval &inputIntervals,
+ double maxErrFract,
+ double extremeMaxErrFract)
+{
+ TRACE_FUNCTION();
+
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline maxErrFract: %g\n", maxErrFract);
+#endif
+
+ if (!spline) {
+ TF_CODING_ERROR("Invalid spline.");
+ return;
+ }
+
+ // If the max desired error is effectively zero, there's nothing to do.
+ if (maxErrFract < _toleranceEspilon) {
+ return;
+ }
+
+ // Reduce the intervals to a valid range.
+ GfMultiInterval intervals = inputIntervals;
+ intervals.Intersect(spline->GetFrameRange());
+
+ TsSpline splineCopy = *spline;
+
+ // Want to get the keframes in the bounds of the selection, plus an extra
+ // one on either end (if any).
+ GfInterval valsInterval = intervals.GetBounds();
+ if (valsInterval.IsEmpty())
+ return;
+
+ // Clear Redundant keys as a pre-pass to handle easy-to-remove keys in a
+ // linear fashion, rather than relying on the N^2 algorithm below.
+ // We'll play it safe and leave the last knot in each interval.
+ // See PRES-74561
+ bool anyRemoved = splineCopy.ClearRedundantKeyFrames(VtValue(), intervals);
+
+ GfInterval fullRange = splineCopy.GetFrameRange();
+
+ // Extra one before
+ if (!valsInterval.IsMinClosed() &&
+ splineCopy.count(valsInterval.GetMin())) {
+ // If the interval's min is open, check if there's a keyframe exactly
+ // at the min. If so add it by closing the min of the interval
+ valsInterval.SetMin(valsInterval.GetMin(), true);
+ } else {
+ std::optional<TsKeyFrame> before =
+ splineCopy.GetClosestKeyFrameBefore(intervals.GetBounds().GetMin());
+
+ // Expand the valsInterval if extra ones existed
+ if (before)
+ valsInterval.SetMin(before->GetTime());
+ }
+
+ // Extra one after
+ if (!valsInterval.IsMaxClosed() &&
+ splineCopy.count(valsInterval.GetMax())) {
+ // If the interval's max is open, check if there's a keyframe exactly
+ // at the max. If so add it by closing the min of the interval
+ valsInterval.SetMax(valsInterval.GetMax(), true);
+ } else {
+ std::optional<TsKeyFrame> after =
+ splineCopy.GetClosestKeyFrameAfter(intervals.GetBounds().GetMax());
+
+ // Expand the valsInterval if extra ones existed
+ if (after)
+ valsInterval.SetMax(after->GetTime());
+ }
+
+ // Get all the keys
+ std::vector<TsKeyFrame> keyFrames
+ = splineCopy.GetKeyFramesInMultiInterval(GfMultiInterval(valsInterval));
+
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline # of keyFrames: %ld\n", keyFrames.size());
+#endif
+
+ // Early out if not enough knots
+ if (keyFrames.size() < 3) {
+ if (anyRemoved) {
+ *spline = splineCopy;
+ }
+ return;
+ }
+
+ // Verify that the spline holds doubles
+ if (!keyFrames.front().GetValue().IsHolding<double>()) {
+ return;
+ }
+
+ // Compute the spline at every frame in 'valsInterval' for error
+ // calculation; remember the range
+ std::vector<double> vals;
+ double minVal = DBL_MAX;
+ double maxVal = -DBL_MAX;
+ for (double t = valsInterval.GetMin(); t <= valsInterval.GetMax();
+ t += 1.0) {
+ double v = splineCopy.Eval(t).Get<double>();
+ if (v > maxVal)
+ maxVal = v;
+ if (v < minVal)
+ minVal = v;
+ vals.push_back(v);
+ }
+
+ double tolerance = (maxVal - minVal) * maxErrFract;
+ // For fully flat (or almost fully flat) curves, set the tolerance
+ // a little above zero, else nothing will happen
+ if (TfAbs(maxVal - minVal) < _toleranceEspilon) {
+ tolerance = _toleranceEspilon;
+ }
+
+ // See _IsKnotAnExtreme
+ // Legacy code set this to a fixed fraction of the overall function range.
+ double extremeTolerance = (maxVal - minVal) * extremeMaxErrFract;
+
+ // Similar correction to the above, else for for fully flat (or almost
+ // fully flat) curves, everything will be considered and extreme and
+ // nothing will be removed.
+ if (TfAbs(maxVal - minVal) < _toleranceEspilon) {
+ extremeTolerance = _toleranceEspilon;
+ }
+
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline valsInterval min: %g max: %g\n", valsInterval.GetMin(), valsInterval.GetMax());
+ printf("TsSimplifySpline minVal: %g maxVal: %g\n", minVal, maxVal);
+ printf("TsSimplifySpline tolerance: %g\n", tolerance);
+#endif
+
+ // Set the tangents: If it's 1 frame away from its neighbors (or it's on
+ // the end, and its outgoing extrapolation is flat) then we are free to set
+ // its slope. Set it to flat for extremes, else catrom-like. For
+ // lengths, if the tangent's neighbor is in the interval and 1 frame away,
+ // we can set the length; set it to be 1/3 the way to its neighbor.
+
+ std::pair<TsExtrapolationType, TsExtrapolationType> extrap =
+ splineCopy.GetExtrapolation();
+
+ for (size_t i = 0; i < keyFrames.size(); ++i) {
+ // If not Bezier, nothing to do
+ TsKeyFrame k = keyFrames[i];
+ if (k.GetKnotType() != TsKnotBezier)
+ continue;
+
+ TsTime t = k.GetTime();
+ // Is there a knot 1 frame adjacent to the right?
+ bool rightAdjacent = i < keyFrames.size() - 1 &&
+ (keyFrames[i+1].GetTime() - t) == 1;
+ // Is there a knot 1 frame adjacent to the left?
+ bool leftAdjacent = i > 0 &&
+ (t - keyFrames[i-1].GetTime()) == 1;
+
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline keyFrame: %ld at time %g rightAdjacent: %d leftAdjacent: %d\n", i, t, rightAdjacent, leftAdjacent);
+#endif
+
+ if (!leftAdjacent && !rightAdjacent)
+ continue;
+
+ // Right val at this frame
+ double vr = keyFrames[i].GetValue().Get<double>();
+ // Left val at this frame
+ double vl = keyFrames[i].GetLeftValue().Get<double>();
+ // Prev, next if adjacent; init for compiler
+ double vp=0, vn=0;
+
+ if (leftAdjacent)
+ vp = keyFrames[i-1].GetValue().Get<double>();
+
+ if (rightAdjacent)
+ // Use left value if dual valued
+ vn = keyFrames[i+1].GetLeftValue().Get<double>();
+
+ std::optional<double> slope;
+
+ if (leftAdjacent && rightAdjacent) {
+ // If it's an extreme or on a plateau, flatten its slope
+ if (_IsKnotOnPlateau(splineCopy, keyFrames[i])
+ || _IsKnotAnExtreme(splineCopy, keyFrames[i],
+ extremeTolerance)) {
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline keyFrame: %ld at time %g"
+ " _IsKnotOnPlateau: YES\n", i, t);
+#endif
+ slope = 0.0;
+ } else {
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline keyFrame: %ld at time %g"
+ " _IsKnotOnPlateau: NO\n", i, t);
+#endif
+ // Parallel to neighbors
+ slope = (vn - vp) / 2.0;
+ }
+ } else if (t == fullRange.GetMin() && rightAdjacent &&
+ extrap.first == TsExtrapolationHeld) {
+ // Left edge, just point at right neighbor
+ slope = vn - vr;
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline keyFrame: %ld left edge\n", i);
+#endif
+ } else if (t == fullRange.GetMax() && leftAdjacent &&
+ extrap.second == TsExtrapolationHeld) {
+ // Right edge, just point at left neighbor
+ slope = vl - vp;
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline keyFrame: %ld at time %g right edge\n",
+ i, t);
+#endif
+ }
+
+ if (leftAdjacent)
+ _SetLeftTangentLength(&k, .3333 * 1.0);
+ if (rightAdjacent)
+ _SetRightTangentLength(&k, .3333 * 1.0);
+
+ if (slope && k.SupportsTangents()) {
+ k.SetLeftTangentSlope(VtValue(*slope));
+ k.SetRightTangentSlope(VtValue(*slope));
+ }
+ keyFrames[i] = k;
+ splineCopy.SetKeyFrame(k);
+
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline keyFrame: %ld at time %g result slope"
+ " %g/%g length %g/%g\n", i, t,
+ k.GetLeftTangentSlope().Get<double>(),
+ k.GetRightTangentSlope().Get<double>(),
+ k.GetLeftTangentLength(), k.GetRightTangentLength());
+#endif
+ }
+
+ // This holds the data about what's removable and the error ifremoved, per
+ // knot.
+ std::vector<_EditSimplifyKnotInfo> ki;
+ // We'll have the number of key frames, plus one on either side.
+ ki.reserve(keyFrames.size() + 2);
+
+ size_t numRemovable = 0;
+
+ // This is in order, so we prepend here, and push back after the
+ // loop
+ double extraPoint = keyFrames[0].GetTime() - 1.0;
+ _EditSimplifyKnotInfo k;
+ k.t = extraPoint;
+ k.knotType = keyFrames[0].GetKnotType();
+ k.removable = false;
+ ki.push_back(k);
+
+ // First figure out which are removable
+ for (size_t i = 0; i < keyFrames.size(); ++i) {
+ _EditSimplifyKnotInfo k;
+ k.t = keyFrames[i].GetTime();
+ k.knotType = keyFrames[i].GetKnotType();
+
+ // Removable if it's selected, not an extreme, and not on the ends of
+ // the valsInterval. (We only compute error within the valsInterval,
+ // so the effect of removing an end would not be known.)
+ k.removable = intervals.Contains(k.t) &&
+ // This is a little hacky, but the first frame is still not
+ // removable
+ i != 0
+ && !_IsKnotAnExtreme(splineCopy, keyFrames[i],
+ extremeTolerance);
+
+#if SIMPLIFY_DEBUG
+ printf("TsSimplifySpline keyFrame: %ld at time %g %s\n",
+ i, k.t, intervals.Contains(k.t)?"CONTAINED":"NOT CONTAINED");
+ printf("TsSimplifySpline keyFrame: %ld at time %g %s\n", i, k.t,
+ _IsKnotAnExtreme(splineCopy, keyFrames[i], extremeTolerance)
+ ? "EXTREME" : "normal");
+ printf("TsSimplifySpline keyFrame: %ld at time %g %s\n", i, k.t,
+ k.removable ? "REMOVABLE" : "KEEP");
+#endif
+
+ if (k.removable)
+ numRemovable++;
+
+ ki.push_back(k);
+ }
+ // Add the last one past the end of our knots
+ extraPoint = keyFrames[keyFrames.size()-1].GetTime() + 1.0;
+ k.t = extraPoint;
+ k.knotType = keyFrames[keyFrames.size()-1].GetKnotType();
+ k.removable = false;
+ ki.push_back(k);
+
+ if (numRemovable == 0) {
+ if (anyRemoved) {
+ *spline = splineCopy;
+ }
+ return;
+ }
+
+ // Set the error-if-removed for each one
+ for (size_t i = 0; i < ki.size(); ++i) {
+ _SetKnotInfoErrorIfKeyRemoved(
+ ki, i, &splineCopy, vals, valsInterval);
+ }
+
+ // At this point, keyFrames will not be reflective of what's in
+ // splineCopy; clear to make this evident
+ keyFrames.clear();
+
+ // Main loop
+ while (1) {
+ // Find the minimum error for those knots that are removable
+ size_t bestIndex = 0;
+ bool first = true;
+ for (size_t i = 0; i < ki.size(); ++i) {
+ if (ki[i].removable) {
+ if (first) {
+ bestIndex = i;
+ first = false;
+ } else {
+ if (ki[i].errIfRemoved < ki[bestIndex].errIfRemoved) {
+ bestIndex = i;
+ }
+ }
+ }
+ }
+
+#if SIMPLIFY_DEBUG
+ printf("Best to remove at time %g (errIfRemoved was %g, tol %g)\n",
+ ki[bestIndex].t, ki[bestIndex].errIfRemoved, tolerance);
+#endif
+
+ // If the best one is less than our tolerance, remove it
+ if (ki[bestIndex].errIfRemoved <= tolerance) {
+#if SIMPLIFY_DEBUG
+ printf(" Removing it\n");
+#endif
+ splineCopy.RemoveKeyFrame(ki[bestIndex].t);
+ // bestIndex should always be inside
+ if (!TF_VERIFY(bestIndex > 0 && bestIndex < ki.size()-1))
+ return;
+ // Fix the adjacent handles
+ _SimplifySpan(&splineCopy,
+ GfInterval(ki[bestIndex-1].t, ki[bestIndex+1].t),
+ vals, valsInterval);
+
+ // Now remove the entry from ki
+ ki.erase(ki.begin() + bestIndex);
+
+ // Now we have to fix the errIfRemoved data held in the adjcant
+ // knots. Deleting a Bezier only has affect on the new conjoined
+ // span.
+ _SetKnotInfoErrorIfKeyRemoved(
+ ki, bestIndex - 1, &splineCopy, vals, valsInterval);
+ _SetKnotInfoErrorIfKeyRemoved(
+ ki, bestIndex, &splineCopy, vals, valsInterval);
+
+ anyRemoved = true;
+ } else {
+ // Can't remove anything; done
+ break;
+ }
+ }
+
+ // If we removed any knots, then save the result.
+ // XXX: If we didn't remove anything, but maybe just adjusted handles,
+ // shouldn't we save that too?
+ if (anyRemoved) {
+ *spline = splineCopy;
+ }
+}
+
+void TsSimplifySplinesInParallel(
+ const std::vector<TsSpline *> &splines,
+ const std::vector<GfMultiInterval> &intervals,
+ double maxErrorFraction,
+ double extremeMaxErrFract)
+{
+ TRACE_FUNCTION();
+
+ // Per the API, an empty intervals means use the full interval of each
+ // spline
+ bool useFullRangeForEach = intervals.empty();
+
+ if (useFullRangeForEach) {
+ WorkParallelForEach(splines.begin(), splines.end(),
+ [&](TsSpline *spline)
+ {
+ TsSimplifySpline(spline,
+ GfMultiInterval(spline->GetFrameRange()),
+ maxErrorFraction, extremeMaxErrFract);
+ });
+ return;
+ }
+
+ const size_t numSplines = splines.size();
+
+ // If we're here, intervals was not empty, and hence must be the same size
+ // as splines
+ if (splines.size() != intervals.size()) {
+ TF_CODING_ERROR("splines size %zd != intervals size %zd",
+ splines.size(), intervals.size());
+ return;
+ }
+ // If just one, don't bother to construct the arg for WorkParallelForEach,
+ // just call TsSimplifySpline()
+ if (numSplines == 1) {
+ TsSimplifySpline(splines[0], intervals[0], maxErrorFraction,
+ extremeMaxErrFract);
+ return;
+ }
+
+ // Make an argument for WorkParallelForEach that we can pass iterators to
+ typedef std::pair<TsSpline *, GfMultiInterval> SplineAndIntervals;
+ std::vector<SplineAndIntervals> args;
+ args.reserve(numSplines);
+ for (size_t i = 0; i < numSplines; i++)
+ args.emplace_back(splines[i], intervals[i]);
+
+ WorkParallelForEach(args.begin(), args.end(),
+ [&](SplineAndIntervals &splineAndIntervals)
+ {
+ TsSimplifySpline(splineAndIntervals.first,
+ splineAndIntervals.second,
+ maxErrorFraction, extremeMaxErrFract);
+ });
+}
+
+void TsResampleSpline(TsSpline* spline,
+ const GfMultiInterval &inputIntervals,
+ double maxErrorFraction)
+{
+ if (!spline) {
+ TF_CODING_ERROR("Invalid spline.");
+ return;
+ }
+
+ // Reduce the intervals to a valid range.
+ GfMultiInterval intervals = inputIntervals;
+ intervals.Intersect(spline->GetFrameRange());
+
+ TsSpline splineCopy = *spline;
+
+ // Sample in all intervals by adding keyframes on every frame.
+ TF_FOR_ALL(it, intervals)
+ {
+ for (double t = it->GetMin(); t <= it->GetMax(); ++t)
+ splineCopy.Breakdown(t, TsKnotBezier, false, 0.33);
+ }
+
+ *spline = splineCopy;
+
+ // Now simplify to get rid of unneeded keyframes.
+ TsSimplifySpline(spline, intervals, maxErrorFraction);
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_SIMPLIFY_H
+#define PXR_BASE_TS_SIMPLIFY_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/spline.h"
+#include "pxr/base/gf/multiInterval.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+/// Remove as many knots as possible from spline without introducing
+/// error greater than maxErrorFraction, where maxErrorFraction is a
+/// percentage of the spline's total range (if the spline's value varies
+/// over a range of x, the largest error allowed will be x*maxErrorFraction).
+/// Only remove knots in intervals.
+TS_API
+void TsSimplifySpline(
+ TsSpline* spline,
+ const GfMultiInterval &intervals,
+ double maxErrorFraction,
+ double extremeMaxErrFract = .001);
+
+/// Run TsSimplifySpline() on a vector of splines in parallel. The splines
+/// in 'splines' are mutated in place. The first two args must have the same
+/// length, unless the intervals arg is empty, in which case the full frame
+/// range of each spline is used. The remaining args are as in
+/// TsSimplifySpline.
+TS_API
+void TsSimplifySplinesInParallel(
+ const std::vector<TsSpline *> &splines,
+ const std::vector<GfMultiInterval>& intervals,
+ double maxErrorFraction,
+ double extremeMaxErrFract = .001);
+
+/// First densely samples the spline within the given intervals
+/// by adding one knot per frame. Then executes the simplify algorithm
+/// to remove as many knots as possibe while keeping the error below
+/// the given maximum.
+TS_API
+void TsResampleSpline(
+ TsSpline* spline,
+ const GfMultiInterval &intervals,
+ double maxErrorFraction);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif // PXR_BASE_TS_SIMPLIFY_H
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/spline.h"
+
+#include "pxr/base/ts/spline_KeyFrames.h"
+#include "pxr/base/ts/loopParams.h"
+#include "pxr/base/ts/data.h"
+#include "pxr/base/ts/keyFrameUtils.h"
+#include "pxr/base/ts/evalUtils.h"
+#include "pxr/base/tf/enum.h"
+#include "pxr/base/tf/iterator.h"
+#include "pxr/base/tf/type.h"
+#include "pxr/base/tf/stl.h"
+#include "pxr/base/trace/trace.h"
+#include "pxr/base/arch/demangle.h"
+#include "pxr/base/gf/math.h"
+#include <limits>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using std::string;
+
+TF_REGISTRY_FUNCTION(TfType)
+{
+ TfType::Define<TsSpline>();
+}
+
+TsSpline::TsSpline() :
+ _data(new TsSpline_KeyFrames())
+{
+}
+
+TsSpline::TsSpline(const TsSpline &other) :
+ _data(other._data)
+{
+}
+
+TsSpline::TsSpline( const TsKeyFrameMap & kf,
+ TsExtrapolationType left,
+ TsExtrapolationType right,
+ const TsLoopParams &loopParams) :
+ _data(new TsSpline_KeyFrames())
+{
+ _data->SetExtrapolation(TsExtrapolationPair(left, right));
+ _data->SetLoopParams(loopParams);
+ _data->SetKeyFrames(kf);
+}
+
+TsSpline::TsSpline( const std::vector<TsKeyFrame> & kf,
+ TsExtrapolationType left,
+ TsExtrapolationType right,
+ const TsLoopParams &loopParams) :
+ _data(new TsSpline_KeyFrames())
+{
+ _data->SetExtrapolation(TsExtrapolationPair(left, right));
+ _data->SetLoopParams(loopParams);
+
+ // We can't construct a std::map from a std::vector directly.
+ // This also performs type checks for every keyframe coming in.
+ TF_FOR_ALL(it, kf)
+ SetKeyFrame(*it);
+}
+
+bool TsSpline::operator==(const TsSpline &rhs) const
+{
+ // The splines are equal if the data are the same object
+ // or if they contain the same information.
+ return (_data == rhs._data || *_data == *rhs._data);
+}
+
+bool TsSpline::operator!=(const TsSpline &rhs) const
+{
+ return !(*this == rhs);
+}
+
+bool TsSpline::IsEmpty() const
+{
+ return GetKeyFrames().empty();
+}
+
+void
+TsSpline::_Detach()
+{
+ TfAutoMallocTag2 tag( "Ts", "TsSpline::_Detach" );
+
+ if (!_data.unique()) {
+ std::shared_ptr<TsSpline_KeyFrames> newData(
+ new TsSpline_KeyFrames(*_data));
+ _data.swap(newData);
+ }
+}
+
+bool
+TsSpline
+::ClearRedundantKeyFrames( const VtValue &defaultValue /* = VtValue() */,
+ const GfMultiInterval &intervals /* = infinity */)
+{
+ // Retrieve a copy of the key frames;
+ // we'll be deleting from the spline as we iterate.
+ TsKeyFrameMap keyFrames = GetKeyFrames();
+
+ bool changed = false;
+
+ // For performance, skip testing inclusion if we have the infinite range.
+ bool needToTestIntervals =
+ intervals != GfMultiInterval(GfInterval(
+ -std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity()));
+ // Iterate in reverse. In many cases, this doesn't matter -- but for a
+ // sequence of contiguous redundant knots, this means that the first
+ // one will remain instead of the last one.
+ TF_REVERSE_FOR_ALL(i, keyFrames) {
+ const TsKeyFrame &kf = *i;
+ // Passing an invalid VtValue here has the effect that the final
+ // knot will not ever be deleted.
+ if (IsKeyFrameRedundant(kf, defaultValue)) {
+ // Punt if this knot is not in 'intervals'
+ if (needToTestIntervals && !intervals.Contains(kf.GetTime()))
+ continue;
+
+ // Immediately delete redundant key frames; this may affect
+ // whether subsequent key frames are redundant or not.
+ RemoveKeyFrame(i->GetTime());
+ changed = true;
+ }
+ }
+
+ return changed;
+}
+
+const TsKeyFrameMap &
+TsSpline::GetKeyFrames() const
+{
+ return _data->GetKeyFrames();
+}
+
+const TsKeyFrameMap &
+TsSpline::GetRawKeyFrames() const
+{
+ return _data->GetNormalKeyFrames();
+}
+
+std::vector<TsKeyFrame>
+TsSpline::GetKeyFramesInMultiInterval(
+ const GfMultiInterval &intervals) const
+{
+ TRACE_FUNCTION();
+ std::vector<TsKeyFrame> result;
+ TF_FOR_ALL(kf, GetKeyFrames()) {
+ if (intervals.Contains(kf->GetTime()))
+ result.push_back(*kf);
+ }
+ return result;
+}
+
+GfInterval
+TsSpline::GetFrameRange() const
+{
+ if (IsEmpty())
+ return GfInterval();
+
+ TsKeyFrameMap const & keyframes = GetKeyFrames();
+
+ return GfInterval( keyframes.begin()->GetTime(),
+ keyframes.rbegin()->GetTime() );
+}
+
+void
+TsSpline::SwapKeyFrames(std::vector<TsKeyFrame>* swapInto)
+{
+ _Detach();
+ _data->SwapKeyFrames(swapInto);
+}
+
+bool
+TsSpline::CanSetKeyFrame( const TsKeyFrame & kf, std::string *reason ) const
+{
+ if (IsEmpty())
+ return true;
+
+ const VtValue kfValue = kf.GetValue();
+
+ if (ARCH_UNLIKELY(!TfSafeTypeCompare(GetTypeid(), kfValue.GetTypeid())))
+ {
+ // Values cannot have keyframes of different types
+ if (reason) {
+ *reason = TfStringPrintf(
+ "cannot mix keyframes of different value types; "
+ "(adding %s to existing keyframes of type %s)",
+ ArchGetDemangled(kfValue.GetTypeid()).c_str(),
+ ArchGetDemangled(GetTypeid()).c_str() );
+ }
+ return false;
+ }
+
+ return true;
+}
+
+bool
+TsSpline::KeyFrameIsInLoopedRange(const TsKeyFrame & kf)
+{
+ TsLoopParams loopParams = GetLoopParams();
+ if (loopParams.GetLooping()) {
+ GfInterval loopedInterval =
+ loopParams.GetLoopedInterval();
+ GfInterval masterInterval =
+ loopParams.GetMasterInterval();
+
+ bool inMaster = masterInterval.Contains(kf.GetTime());
+ if (loopedInterval.Contains(kf.GetTime()) && !inMaster) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void
+TsSpline::SetKeyFrame( TsKeyFrame kf, GfInterval *intervalAffected )
+{
+ // Assume none affected
+ if (intervalAffected)
+ *intervalAffected = GfInterval();
+
+ std::string reason;
+ if (!CanSetKeyFrame(kf, &reason)) {
+ TF_CODING_ERROR(reason);
+ return;
+ }
+
+ _Detach();
+ _data->SetKeyFrame(kf, intervalAffected);
+}
+
+void
+TsSpline::RemoveKeyFrame( TsTime time, GfInterval *intervalAffected )
+{
+ _Detach();
+ _data->RemoveKeyFrame(time, intervalAffected);
+}
+
+std::optional<TsKeyFrame>
+TsSpline::Breakdown(
+ double x, TsKnotType type,
+ bool flatTangents, double tangentLength,
+ const VtValue &value,
+ GfInterval *intervalAffected )
+{
+ // It's not an error to try to beakdown in the unrolled region of a looped
+ // spline, but at present it's not supported either.
+ if (IsTimeLooped(x))
+ return std::optional<TsKeyFrame>();
+
+ TsKeyFrameMap newKeyframes;
+ _GetBreakdown( &newKeyframes, x, type, flatTangents, tangentLength, value );
+
+ if (!newKeyframes.empty()) {
+ std::string reason;
+ TF_FOR_ALL(i, newKeyframes) {
+ if (!CanSetKeyFrame(*i, &reason)) {
+ TF_CODING_ERROR(reason);
+ return std::optional<TsKeyFrame>();
+ }
+ }
+
+ // Assume none affected
+ if (intervalAffected)
+ *intervalAffected = GfInterval();
+
+ // Set the keyframes; this will stomp existing
+ TF_FOR_ALL(i, newKeyframes) {
+ // union together the intervals from each call to SetKeyFrame,
+ // which inits is interval arg
+ GfInterval interval;
+ SetKeyFrame(*i, intervalAffected ? &interval : NULL);
+ if (intervalAffected)
+ *intervalAffected |= interval;
+ }
+ }
+
+ // Return the newly created keyframe, or the existing one.
+ const_iterator i = find(x);
+ if (i != end()) {
+ return *i;
+ } else {
+ TF_RUNTIME_ERROR("Failed to find keyframe: %f", x);
+ return std::optional<TsKeyFrame>();
+ }
+}
+
+void
+TsSpline::_GetBreakdown(
+ TsKeyFrameMap* newKeyframes,
+ double x, TsKnotType type,
+ bool flatTangents, double tangentLength,
+ const VtValue &value) const
+{
+ newKeyframes->clear();
+
+ // Check for existing key frame
+ TsKeyFrameMap const & keyframes = GetKeyFrames();
+ TsKeyFrameMap::const_iterator i = keyframes.find(x);
+ if (i != keyframes.end()) {
+ return;
+ }
+
+
+ // If there are no key frames then we're just inserting.
+ if (keyframes.empty()) {
+ // XXX: we don't know the value type so we assume double
+ VtValue kfValue = value.IsEmpty() ? VtValue( 0.0 ) : value;
+
+ (*newKeyframes)[x] = TsKeyFrame( x, kfValue, type,
+ VtValue(), VtValue(),
+ tangentLength, tangentLength);
+ return;
+ }
+
+ // If we have a valid value, use it for the new keyframe; otherwise,
+ // evaluate the spline at the given time.
+ VtValue kfValue = value.IsEmpty() ? Eval( x ) : value;
+
+ // Non-Bezier types have no tangents so we have enough to return a key
+ // frame. We can also return if the value type doesn't support tangents.
+ if (type != TsKnotBezier ||
+ !keyframes.begin()->SupportsTangents()) {
+ (*newKeyframes)[x] = TsKeyFrame( x, kfValue, type );
+ return;
+ }
+
+ // Fallback to flat tangents
+ VtValue slope = keyframes.begin()->GetZero();
+
+ // Whether we are breaking down on a frame that is before or after
+ // all knots.
+ const bool leftOfAllKnots = (x < keyframes. begin()->GetTime());
+ const bool rightOfAllKnots = (x > keyframes.rbegin()->GetTime());
+
+ if (!flatTangents) {
+ // If we are breaking down before all knots and we have linear
+ // interpolation, use slope of the extrapolation.
+ if (leftOfAllKnots) {
+ if (GetExtrapolation().first == TsExtrapolationLinear) {
+ slope = EvalDerivative(x);
+ }
+ }
+ // Analogously if after all knots.
+ if (rightOfAllKnots) {
+ if (GetExtrapolation().second == TsExtrapolationLinear) {
+ slope = EvalDerivative(x);
+ }
+ }
+ }
+
+ // Insert the knot, possibly subsequently amending tangents to preserve
+ // shape
+ (*newKeyframes)[x] = TsKeyFrame( x, kfValue, type,
+ slope, slope,
+ tangentLength, tangentLength );
+
+ // If we want flat tangents then we're done. Similarly, if we are
+ // breaking down before or after all knots.
+ if (flatTangents || leftOfAllKnots || rightOfAllKnots) {
+ return;
+ }
+
+ // Copy the neighboring key frames into newKeyframes
+ i = keyframes.upper_bound(x);
+ newKeyframes->insert(*i);
+ --i;
+ newKeyframes->insert(*i);
+
+ // We now have the three key frames of interest in newKeyframes
+ // They're correct except we may need to change the length of
+ // the right side tangent of the first, the length of the left
+ // side tangent of the third and the slope and length on both
+ // sides of the middle. Ts_Breakdown() does that.
+ Ts_Breakdown(newKeyframes);
+}
+
+void
+TsSpline::Breakdown(
+ const std::set<double> ×,
+ TsKnotType type,
+ bool flatTangents,
+ double tangentLength,
+ const VtValue &value,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes)
+{
+ std::vector<double> timesVec(times.begin(), times.end());
+ std::vector<VtValue> values(times.size(), value);
+
+ _BreakdownMultipleValues(timesVec, type, flatTangents,
+ tangentLength, values, intervalAffected, keyFramesAtTimes);
+}
+
+void
+TsSpline::Breakdown(
+ const std::vector<double> ×,
+ TsKnotType type,
+ bool flatTangents,
+ double tangentLength,
+ const std::vector<VtValue> &values,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes)
+{
+ _BreakdownMultipleValues(times, type, flatTangents,
+ tangentLength, values, intervalAffected, keyFramesAtTimes);
+}
+
+void
+TsSpline::Breakdown(
+ const std::vector<double> ×,
+ const std::vector<TsKnotType> &types,
+ bool flatTangents,
+ double tangentLength,
+ const std::vector<VtValue> &values,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes)
+{
+ _BreakdownMultipleKnotTypes(times, types, flatTangents,
+ tangentLength, values, intervalAffected, keyFramesAtTimes);
+}
+
+void
+TsSpline::_BreakdownMultipleValues(
+ const std::vector<double> ×,
+ TsKnotType type,
+ bool flatTangents,
+ double tangentLength,
+ const std::vector<VtValue> &values,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes)
+{
+ if (times.size() != values.size()) {
+ TF_CODING_ERROR("Number of times and values do not match");
+ return;
+ }
+
+ std::vector<TsKnotType> types(times.size(), type);
+
+ _BreakdownMultipleKnotTypes(times, types, flatTangents,
+ tangentLength, values, intervalAffected, keyFramesAtTimes);
+}
+
+void
+TsSpline::_BreakdownMultipleKnotTypes(
+ const std::vector<double> ×,
+ const std::vector<TsKnotType> &types,
+ bool flatTangents,
+ double tangentLength,
+ const std::vector<VtValue> &values,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes)
+{
+ if (!(times.size() == types.size() && times.size() == values.size())) {
+ TF_CODING_ERROR("Numbers of times, values and knot types do not match");
+ return;
+ }
+
+ // We need to separate the key frames we're going to breakdown by knot type
+ // so that we can breakdown the beziers first. Beziers need to be broken
+ // down first because, when flatTangents aren't enabled, a bezier tries to
+ // preserve the shape of the spline as much as possible (see Ts_Breakdown in
+ // Ts/EvalUtils). So, we need to "lock down" the shape of the spline with
+ // beziers first then breakdown the other types. Additionally, only bezier
+ // knots have authored info that can change the shape of the spline at
+ // breakdown, i.e. flatTangents and tangentLength. For the other knot types
+ // those values are either computed or irrelevant. Thus, we must treat
+ // beziers separately and can treat the other types the same.
+
+ // Sample the spline or look up the given value at the given times before
+ // making any modifications, and store the samples per knot type.
+ std::map<TsKnotType, _Samples> samplesPerKnotType;
+
+ const TsKeyFrameMap &keyFrames = GetKeyFrames();
+
+ for (size_t index = 0; index < times.size(); ++index) {
+ const double x = times[index];
+ const VtValue &value = values[index];
+ const TsKnotType type = types[index];
+
+ TsKeyFrameMap::const_iterator i = keyFrames.find(x);
+ if (i != keyFrames.end()) {
+ // Save the existing keyframe in the result.
+ if (keyFramesAtTimes) {
+ (*keyFramesAtTimes)[x] = *i;
+ }
+ } else {
+ // Only need to sample where keyframes don't already exist.
+ if (value.IsEmpty()) {
+ samplesPerKnotType[type].push_back(std::make_pair(x, Eval(x)));
+ } else {
+ samplesPerKnotType[type].push_back(std::make_pair(x, value));
+ }
+ }
+ }
+
+ // Reset the affected interval, if applicable.
+ if (intervalAffected) {
+ *intervalAffected = GfInterval();
+ }
+
+ // Breakdown any bezier knots first.
+ if (samplesPerKnotType.count(TsKnotBezier) > 0) {
+ _BreakdownSamples(samplesPerKnotType[TsKnotBezier], TsKnotBezier,
+ flatTangents, tangentLength, intervalAffected, keyFramesAtTimes);
+ }
+
+ // Breakdown the remaining knot types.
+ for (const auto &mapIt : samplesPerKnotType) {
+ TsKnotType type = mapIt.first;
+ if (type != TsKnotBezier) {
+ _BreakdownSamples(mapIt.second, type, flatTangents, tangentLength,
+ intervalAffected, keyFramesAtTimes);
+ }
+ }
+}
+
+void
+TsSpline::_BreakdownSamples(
+ const _Samples &samples,
+ TsKnotType type,
+ bool flatTangents,
+ double tangentLength,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes)
+{
+ // Perform the Breakdown on the given samples.
+ for (const auto &sample : samples) {
+ GfInterval interval;
+ std::optional<TsKeyFrame> kf = Breakdown(
+ sample.first, type, flatTangents, tangentLength, sample.second,
+ intervalAffected ? &interval : NULL);
+
+ if (keyFramesAtTimes && kf) {
+ // Save the new keyframe in the result.
+ (*keyFramesAtTimes)[sample.first] = *kf;
+ }
+
+ if (intervalAffected) {
+ *intervalAffected |= interval;
+ }
+ }
+}
+
+void
+TsSpline::Clear()
+{
+ TsKeyFrameMap empty;
+ // We implement a specialized version of _Detach here. We're setting
+ // the keyframes here which is the heaviest part of the spline. A normal
+ // detach copies the whole spline including the keyframes. We'd like to
+ // avoid that copy since we're going to overwite the keyframes anyway.
+ if (!_data.unique()) {
+ // Invoke TsSpline_KeyFrames's generalized copy ctor that lets us
+ // specify keyframes to copy.
+ std::shared_ptr<TsSpline_KeyFrames> newData(
+ new TsSpline_KeyFrames(*_data, &empty));
+ _data.swap(newData);
+ } else {
+ // Our data is already unique, just set keyframes.
+ _data->SetKeyFrames(empty);
+ }
+}
+
+std::optional<TsKeyFrame>
+TsSpline::GetClosestKeyFrame( TsTime targetTime ) const
+{
+ const TsKeyFrame *k = Ts_GetClosestKeyFrame(GetKeyFrames(), targetTime);
+ if (k) {
+ return *k;
+ }
+ return std::optional<TsKeyFrame>();
+}
+
+std::optional<TsKeyFrame>
+TsSpline::GetClosestKeyFrameBefore( TsTime targetTime ) const
+{
+ const TsKeyFrame *k =
+ Ts_GetClosestKeyFrameBefore(GetKeyFrames(), targetTime);
+ if (k) {
+ return *k;
+ }
+ return std::optional<TsKeyFrame>();
+}
+
+std::optional<TsKeyFrame>
+TsSpline::GetClosestKeyFrameAfter( TsTime targetTime ) const
+{
+ const TsKeyFrame *k =
+ Ts_GetClosestKeyFrameAfter(GetKeyFrames(), targetTime);
+ if (k) {
+ return *k;
+ }
+ return std::optional<TsKeyFrame>();
+}
+
+// Note: In the future this could be extended to evaluate the spline, and
+// by doing so we could support removing key frames that are redundant
+// but are not on flat sections of the spline. Also, doing so would
+// avoid problems where such frames invalidate the frame cache. If
+// all splines are cubic polynomials, then evaluating the spline at
+// four points, two before the key frame and two after, would be
+// sufficient to tell if a particular key frame was redundant.
+bool
+TsSpline::IsKeyFrameRedundant(
+ const TsKeyFrame &keyFrame,
+ const VtValue& defaultValue /* = VtValue() */) const
+{
+ return Ts_IsKeyFrameRedundant(GetKeyFrames(), keyFrame,
+ GetLoopParams(), defaultValue);
+}
+
+bool
+TsSpline::IsKeyFrameRedundant(
+ TsTime keyFrameTime,
+ const VtValue &defaultValue /* = VtValue() */ ) const
+{
+ TsKeyFrameMap const & keyFrames = GetKeyFrames();
+
+ TsKeyFrameMap::const_iterator it = keyFrames.find( keyFrameTime );
+ if (it == keyFrames.end()) {
+ TF_CODING_ERROR("Time %0.02f doesn't correspond to a key frame!",
+ static_cast<double>(keyFrameTime));
+ return false;
+ }
+
+ return IsKeyFrameRedundant(*it, defaultValue );
+}
+
+bool
+TsSpline::HasRedundantKeyFrames(
+ const VtValue &defaultValue /* = VtValue() */ ) const
+{
+ const TsKeyFrameMap& allKeyFrames = GetKeyFrames();
+
+ TF_FOR_ALL(i, allKeyFrames) {
+ if (IsKeyFrameRedundant(*i, defaultValue)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool
+TsSpline::IsSegmentFlat(
+ const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2 ) const
+{
+ return Ts_IsSegmentFlat(kf1, kf2);
+}
+
+bool
+TsSpline::IsSegmentFlat( TsTime startTime, TsTime endTime ) const
+{
+ TsKeyFrameMap const & keyFrames = GetKeyFrames();
+
+ TsKeyFrameMap::const_iterator startFrame = keyFrames.find( startTime );
+ if (startFrame == keyFrames.end()) {
+ TF_CODING_ERROR("Start time %0.02f doesn't correspond to a key frame!",
+ static_cast<double>(startTime));
+ return false;
+ }
+
+ TsKeyFrameMap::const_iterator endFrame = keyFrames.find( endTime );
+ if (endFrame == keyFrames.end()) {
+ TF_CODING_ERROR("End time %0.02f doesn't correspond to a key frame!",
+ static_cast<double>(endTime));
+ return false;
+ }
+
+ return IsSegmentFlat( *startFrame, *endFrame);
+}
+
+bool
+TsSpline::IsSegmentValueMonotonic(
+ const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2 ) const
+{
+ return Ts_IsSegmentValueMonotonic(kf1, kf2);
+}
+
+bool
+TsSpline::IsSegmentValueMonotonic(
+ TsTime startTime,
+ TsTime endTime ) const
+{
+ TsKeyFrameMap const& keyFrames = GetKeyFrames();
+
+ TsKeyFrameMap::const_iterator startFrame = keyFrames.find( startTime );
+ if (startFrame == keyFrames.end()) {
+ TF_CODING_ERROR("Start time %0.02f doesn't correspond to a key frame!",
+ static_cast<double>(startTime));
+ return false;
+ }
+
+ TsKeyFrameMap::const_iterator endFrame = keyFrames.find( endTime );
+ if (endFrame == keyFrames.end()) {
+ TF_CODING_ERROR("End time %0.02f doesn't correspond to a key frame!",
+ static_cast<double>(endTime));
+ return false;
+ }
+
+ return Ts_IsSegmentValueMonotonic(*startFrame, *endFrame);
+}
+
+bool
+TsSpline::IsVarying() const
+{
+ return _IsVarying(/* tolerance */ 0.0);
+}
+
+bool
+TsSpline::IsVaryingSignificantly() const
+{
+ return _IsVarying(/* tolerance */ 1e-6);
+}
+
+bool
+TsSpline::_IsVarying(double tolerance) const
+{
+ const TsKeyFrameMap& keyFrames = GetKeyFrames();
+
+ // Empty splines do not vary
+ if (keyFrames.empty()) {
+ return false;
+ }
+
+ bool isDouble = keyFrames.begin()->GetValue().IsHolding<double>();
+
+ TRACE_FUNCTION();
+
+ // Get the extrapolation settings for the whole spline
+ std::pair<TsExtrapolationType, TsExtrapolationType> extrap =
+ GetExtrapolation();
+
+ // Iterate through key frames comparing values.
+ // To account for dual-valued knots we need to compare values
+ // on each side of a key frame in addition to values across
+ // keyframes.
+ //
+ TsKeyFrameMap::const_iterator prev = keyFrames.end();
+ TsKeyFrameMap::const_iterator cur;
+
+ // If this is a double spline, the min/max we've seen so far
+ double minDouble = std::numeric_limits<double>::infinity();
+ double maxDouble = -std::numeric_limits<double>::infinity();
+
+ // If this is not a double spline, the first value
+ VtValue firstValueIfNotDouble;
+ if (!isDouble)
+ firstValueIfNotDouble = keyFrames.begin()->GetLeftValue();
+
+ for (cur = keyFrames.begin(); cur != keyFrames.end(); ++cur) {
+ const TsKeyFrame& curKeyFrame = *cur;
+
+ if (isDouble) {
+ // Double case: update min/max and then see if they're too far
+ // apart
+ double v = curKeyFrame.GetValue().Get<double>();
+ minDouble = TfMin(v, minDouble);
+ maxDouble = TfMax(v, maxDouble);
+
+ // Check other side if dual valued
+ if (curKeyFrame.GetIsDualValued()) {
+ double v = curKeyFrame.GetLeftValue().Get<double>();
+ minDouble = TfMin(v, minDouble);
+ maxDouble = TfMax(v, maxDouble);
+ }
+
+ if (maxDouble - minDouble > tolerance)
+ return true;
+
+ } else {
+ // Non-double, just compare with the first
+ if (curKeyFrame.GetValue() != firstValueIfNotDouble)
+ return true;
+
+ if (curKeyFrame.GetIsDualValued()) {
+ if (curKeyFrame.GetLeftValue() != firstValueIfNotDouble)
+ return true;
+ }
+ }
+
+ // Check tangents
+ if (curKeyFrame.HasTangents()) {
+ bool isFirst = (cur == keyFrames.begin());
+ bool isLast = (cur == keyFrames.end()-1);
+
+ // Check the left tangent if:
+ // - This is the first knot and we have non-held left extrapolation
+ // - This is a subsequent knot and the previous knot is non-held
+ bool checkLeft =
+ isFirst ?
+ (extrap.first != TsExtrapolationHeld) :
+ (prev->GetKnotType() != TsKnotHeld);
+
+ // Check the right tangent if:
+ // - This is the last knot and we have non-held right extrapolation
+ // - This is an earlier knot and is non-held
+ bool checkRight =
+ isLast ?
+ (extrap.second != TsExtrapolationHeld) :
+ (curKeyFrame.GetKnotType() != TsKnotHeld);
+
+ // If a tangent that we check has a non-zero slope,
+ // the spline varies
+ VtValue zero = curKeyFrame.GetZero();
+ if ((checkLeft && curKeyFrame.GetLeftTangentLength() != 0 &&
+ curKeyFrame.GetLeftTangentSlope() != zero) ||
+ (checkRight && curKeyFrame.GetRightTangentLength() != 0 &&
+ curKeyFrame.GetRightTangentSlope() != zero)) {
+ return true;
+ }
+ }
+
+ prev = cur;
+ }
+
+ return false;
+}
+
+void
+TsSpline::SetExtrapolation(
+ TsExtrapolationType left,
+ TsExtrapolationType right)
+{
+ _Detach();
+ _data->SetExtrapolation(TsExtrapolationPair(left, right));
+}
+
+std::pair<TsExtrapolationType, TsExtrapolationType>
+TsSpline::GetExtrapolation() const
+{
+ return _data->GetExtrapolation();
+}
+
+const std::type_info &
+TsSpline::GetTypeid() const
+{
+ TsKeyFrameMap const & keyframes = GetKeyFrames();
+ if (keyframes.empty())
+ return typeid(void);
+ return keyframes.begin()->GetValue().GetTypeid();
+}
+
+TfType
+TsSpline::GetType() const
+{
+ static TfStaticData<TfType> unknown;
+ TsKeyFrameMap const & keyframes = GetKeyFrames();
+ if (keyframes.empty())
+ return *unknown;
+ return keyframes.begin()->GetValue().GetType();
+}
+
+std::string
+TsSpline::GetTypeName() const
+{
+ return ArchGetDemangled( GetTypeid() );
+}
+
+VtValue
+TsSpline::Eval( TsTime time, TsSide side ) const
+{
+ return Ts_Eval(*this, time, side, Ts_EvalValue);
+}
+
+// Finds the first keyframe on or before the given time and side, or the first
+// key after, if there are no keys on or before the time.
+static std::optional<TsKeyFrame>
+_FindHoldKey(const TsSpline &spline, TsTime time, TsSide side)
+{
+ if (spline.IsEmpty()) {
+ return {};
+ }
+
+ const TsKeyFrameMap &keyframes = spline.GetKeyFrames();
+
+ if (time <= keyframes.begin()->GetTime()) {
+ // This is equivalent to held extrapolation for the first keyframe.
+ return *keyframes.begin();
+ } else {
+ TsKeyFrameMap::const_iterator i = keyframes.find(time);
+
+ if (side == TsRight && i != keyframes.end()) {
+ // We have found an exact match.
+ return *i;
+ } else {
+ // Otherwise, we are either evaluating the left side of the given
+ // time, or there is no exact match. Find the closest prior
+ // keyframe.
+ return spline.GetClosestKeyFrameBefore(time);
+ }
+ }
+ return {};
+}
+
+VtValue
+TsSpline::EvalHeld(TsTime time, TsSide side) const
+{
+ if (IsEmpty()) {
+ return VtValue();
+ }
+
+ const std::optional<TsKeyFrame> kf = _FindHoldKey(*this, time, side);
+ if (!TF_VERIFY(kf)) {
+ return VtValue();
+ }
+ return kf->GetValue();
+}
+
+VtValue
+TsSpline::EvalDerivative( TsTime time, TsSide side ) const
+{
+ return Ts_Eval(*this, time, side, Ts_EvalDerivative);
+}
+
+bool
+TsSpline::DoSidesDiffer(const TsTime time) const
+{
+ const TsKeyFrameMap &keyFrames = GetKeyFrames();
+
+ // Find keyframe at specified time. If none, the answer is false.
+ const auto kfIt = keyFrames.find(time);
+ if (kfIt == keyFrames.end())
+ return false;
+
+ // Check whether dual-valued with differing values.
+ if (kfIt->GetIsDualValued()
+ && kfIt->GetLeftValue() != kfIt->GetValue())
+ {
+ return true;
+ }
+
+ // Check whether preceding segment is held with differing value.
+ if (kfIt != keyFrames.begin())
+ {
+ const auto prevKfIt = kfIt - 1;
+ if (prevKfIt->GetKnotType() == TsKnotHeld
+ && prevKfIt->GetValue() != kfIt->GetValue())
+ {
+ return true;
+ }
+ }
+
+ // If we didn't hit any of the above cases, then this is a keyframe time
+ // where our value is identical on left and right sides.
+ return false;
+}
+
+TsSamples
+TsSpline::Sample( TsTime startTime, TsTime endTime,
+ double timeScale, double valueScale,
+ double tolerance ) const
+{
+ return Ts_Sample( *this, startTime, endTime,
+ timeScale, valueScale, tolerance );
+}
+
+std::pair<VtValue, VtValue>
+TsSpline::GetRange( TsTime startTime, TsTime endTime ) const
+{
+ return Ts_GetRange( *this, startTime, endTime );
+}
+
+bool
+TsSpline::IsLinear() const
+{
+ // If this spline is linear then it will only have two keyframes
+ if ( empty() || size() != 2 )
+ return false;
+
+ // The two keyframes must also be single valued linear knots for the
+ // spline to be linear. The output value must also be a double
+ TF_FOR_ALL(it, *this) {
+ if (it->GetKnotType() != TsKnotLinear ||
+ !it->GetValue().IsHolding<double>() ||
+ it->GetIsDualValued() ) {
+ return false;
+ }
+ }
+
+ // Extrapolation on both ends need to be linear
+ if (GetExtrapolation().first != TsExtrapolationLinear ||
+ GetExtrapolation().second != TsExtrapolationLinear) {
+ return false;
+ }
+
+ // we passed all the linear tests!
+ return true;
+}
+
+void
+TsSpline::BakeSplineLoops()
+{
+ _Detach();
+ _data->BakeSplineLoops();
+}
+
+void
+TsSpline::SetLoopParams(const TsLoopParams& params)
+{
+ _Detach();
+ _data->SetLoopParams(params);
+}
+
+TsLoopParams
+TsSpline::GetLoopParams() const
+{
+ return _data->GetLoopParams();
+}
+
+bool
+TsSpline::IsTimeLooped(TsTime time) const
+{
+ const TsLoopParams ¶ms = GetLoopParams();
+
+ return params.GetLooping()
+ && params.GetLoopedInterval().Contains(time)
+ && !params.GetMasterInterval().Contains(time);
+}
+
+TsSpline::const_iterator
+TsSpline::find(const TsTime &time) const
+{
+ return const_iterator(GetKeyFrames().find(time));
+}
+
+TsSpline::const_iterator
+TsSpline::lower_bound(const TsTime &time) const
+{
+ return const_iterator(GetKeyFrames().lower_bound(time));
+}
+
+TsSpline::const_iterator
+TsSpline::upper_bound(const TsTime &time) const
+{
+ return const_iterator(GetKeyFrames().upper_bound(time));
+}
+
+std::ostream& operator<<(std::ostream& out, const TsSpline & val)
+{
+ out << "Ts.Spline(";
+ size_t counter = val.size();
+ if (counter > 0) {
+ out << "[";
+ TF_FOR_ALL(it, val) {
+ out << *it;
+ counter--;
+ out << (counter > 0 ? ", " : "]");
+ }
+ }
+ out << ")";
+ return out;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_SPLINE_H
+#define PXR_BASE_TS_SPLINE_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/keyFrame.h"
+#include "pxr/base/ts/keyFrameMap.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/ts/loopParams.h"
+#include "pxr/base/vt/value.h"
+
+#include <vector>
+#include <limits>
+#include <map>
+#include <typeinfo>
+#include <iostream>
+#include <optional>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TsSpline_KeyFrames;
+
+/// \class TsSpline
+///
+/// Represents a spline value object.
+///
+/// The TsSpline class defines spline representations. Use this class to
+/// define and manipulate avars over time. An TsSpline object is an
+/// anonymous value that can be freely passed around. It has no owning object.
+///
+/// Internally TsSpline is copy-on-write. This means that making a copy
+/// of an TsSpline is nearly free in both time and memory, but making an
+/// edit to a copy of an TsSpline may incur the cost of copying all the
+/// data.
+///
+/// TsSpline provides the basic thread safety guarantee: Multiple threads
+/// may read and copy an TsSpline object concurrently, but it's not safe
+/// to read from an TsSpline which another thread is concurrently writing
+/// to. Internally that means that any data which may be mutated by a const
+/// accessor must be protected by a mutex. Currently TsSpline has an immutable
+/// lock-free implementation.
+///
+class TsSpline final
+{
+public:
+ /// Constructs a spline with no key frames and held extrapolation.
+ TS_API
+ TsSpline();
+
+ /// Copy construct.
+ TS_API
+ TsSpline(const TsSpline &other);
+
+ /// Constructs a spline with the key frames \e keyFrames,
+ /// and optionally given extrapolation and looping parameters.
+ TS_API
+ explicit TsSpline( const TsKeyFrameMap & keyFrames,
+ TsExtrapolationType leftExtrapolation = TsExtrapolationHeld,
+ TsExtrapolationType rightExtrapolation = TsExtrapolationHeld,
+ const TsLoopParams &loopParams = TsLoopParams());
+
+ /// Constructs a spline with the key frames \e keyFrames,
+ /// and optionally given extrapolation and looping parameters.
+ TS_API
+ explicit TsSpline( const std::vector<TsKeyFrame> & keyFrames,
+ TsExtrapolationType leftExtrapolation = TsExtrapolationHeld,
+ TsExtrapolationType rightExtrapolation = TsExtrapolationHeld,
+ const TsLoopParams &loopParams = TsLoopParams());
+
+ /// Equality operator.
+ TS_API
+ bool operator==(const TsSpline &rhs) const;
+
+ /// Inequality operator.
+ TS_API
+ bool operator!=(const TsSpline &rhs) const;
+
+ /// Returns whether there are any keyframes.
+ TS_API
+ bool IsEmpty() const;
+
+ /// Replaces the KeyFrames in this TsSpline with those in
+ /// swapInto, and puts the KeyFrames in this TsSpline into
+ /// swapInto. Requires that the vectors in swapInto are sorted
+ /// in ascending order according to their time.
+ TS_API
+ void SwapKeyFrames(std::vector<TsKeyFrame>* swapInto);
+
+ /// Removes redundant keyframes from the spline in the specified
+ /// multi-interval.
+ /// \return True if the spline was changed, false if not.
+ /// \param defaultValue Used only to decide whether to remove the final
+ /// keyframe. The final keyframe is removed if defaultValue is specified
+ /// and the final keyframe has this value.
+ /// \param intervals Only keyframes in the given multiInterval will be
+ /// removed, although all keyframes will be considered in computing what
+ /// is redundant.
+ TS_API
+ bool ClearRedundantKeyFrames( const VtValue &defaultValue = VtValue(),
+ const GfMultiInterval &intervals =
+ GfMultiInterval(GfInterval(
+ -std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity())));
+
+
+ /// Returns the keyframes in this spline.
+ ///
+ /// Note that any non-const method invalidates the reference to
+ /// the KeyFrameMap. Do not hold on to the KeyFrameMap reference,
+ /// make a modification to the spline, and then use the original
+ /// KeyFrameMap reference.
+ TS_API
+ const TsKeyFrameMap& GetKeyFrames() const;
+
+ /// Returns the "raw" keyframes in this spline whether or not this
+ /// spline is looping. Note that this method is the sole exception to the
+ /// rule that the API presents the looped view of the spline when it is
+ /// a looping spline.
+ ///
+ /// Note that any non-const method invalidates the reference to
+ /// the KeyFrameMap. Do not hold on to the KeyFrameMap reference,
+ /// make a modification to the spline, and then use the original
+ /// KeyFrameMap reference.
+ TS_API
+ const TsKeyFrameMap& GetRawKeyFrames() const;
+
+ /// Returns the keyframes contained in the given GfMultiInterval.
+ TS_API
+ std::vector<TsKeyFrame>
+ GetKeyFramesInMultiInterval(const GfMultiInterval &) const;
+
+ /// Returns the minimum and maximum keyframe frames in the spline. If there
+ /// are no keyframes, the returned range will be empty.
+ TS_API
+ GfInterval GetFrameRange() const;
+
+ /// Sets a keyframe, optionally returning the time range affected.
+ /// If a keyframe already exists at the specified time, it will be
+ /// replaced. If the keyframe is not a valid type to set, an error
+ /// will be emitted; to avoid this, call CanSetKeyFrame() first.
+ TS_API
+ void SetKeyFrame(
+ TsKeyFrame kf, GfInterval *intervalAffected=nullptr );
+
+ /// Checks if the given keyframe is a valid candidate to set,
+ /// optionally returning the reason if it cannot.
+ TS_API
+ bool CanSetKeyFrame(
+ const TsKeyFrame & kf, std::string *reason=nullptr ) const;
+
+ /// \brief Breakdown at time \e x.
+ ///
+ /// If a key frame exists at \e x then this does nothing, otherwise it
+ /// inserts a key frame of type \e type at \e x. If the provided \e value is
+ /// empty (the default), the new key frame's value is chosen such that the
+ /// value at \e x doesn't change. If \e value is not empty, the new keyframe
+ /// is always given that value.
+ ///
+ /// If \e flatTangents is \c false and \e x is between the first and last
+ /// key frames then it will also try to preserve the shape of the spline as
+ /// much as possible. Otherwise, if the key frame type and value type
+ /// support tangents, the key frame will have tangents with zero slope and
+ /// length \e tangentLength.
+ ///
+ /// The return value is either the newly broken down keyframe, or the
+ /// existing keyframe at the given time. If an error has occurred, an
+ /// empty value may be returned.
+ TS_API
+ std::optional<TsKeyFrame>
+ Breakdown( double x, TsKnotType type,
+ bool flatTangents, double tangentLength,
+ const VtValue &value = VtValue(),
+ GfInterval *intervalAffected=nullptr );
+
+ /// Breaks down simultaneously at several times.
+ ///
+ /// When creating knots with flat tangents, the shape of the spline may
+ /// change between the new knot and its adjacent knots. Simply breaking
+ /// down a spline several times in a loop may result in key frame values
+ /// that drift away from their original values. This function samples the
+ /// spline first, ensuring that each new key frame will preserve the value
+ /// at that time.
+ ///
+ /// If \e value is not empty, \e value is used instead of sampling the
+ /// spline. For each time, if there is already a key frame at that time, the
+ /// value and type of that keyframe will not be changed.
+ ///
+ /// The arguments are the same as Breakdown(). If \p keyFramesAtTimes is
+ /// given, it will be populated with the newly broken down or previously
+ /// existing key frames at the given times.
+ TS_API
+ void
+ Breakdown( const std::set<double> & times, TsKnotType type,
+ bool flatTangents, double tangentLength,
+ const VtValue &value = VtValue(),
+ GfInterval *intervalAffected=nullptr,
+ TsKeyFrameMap *keyFramesAtTimes=nullptr);
+
+ /// Breaks down simultaneously at several times.
+ ///
+ /// Caller can provide a value for each time. If a value is not provided
+ /// at a given time (it is empty), this function will sample the spline.
+ /// If a knot already exists at a given time, its value is not modified.
+ ///
+ /// The arguments are the same as Breakdown(). If \p keyFramesAtTimes is
+ /// given, it will be populated with the newly broken down or previously
+ /// existing key frames at the given times.
+ TS_API
+ void
+ Breakdown( const std::vector<double> & times, TsKnotType type,
+ bool flatTangents, double tangentLength,
+ const std::vector<VtValue> & values,
+ GfInterval *intervalAffected=nullptr,
+ TsKeyFrameMap *keyFramesAtTimes=nullptr);
+
+ /// Breaks down simultaneously at several times with knot types specified
+ /// for each time.
+ ///
+ /// A knot type for each time must be provided, else it is a coding error.
+ ///
+ /// Caller can provide a value for each time. If a value is not provided
+ /// at a given time (it is empty), this function will sample the spline.
+ /// If a knot already exists at a given time, its value is not modified.
+ ///
+ /// The arguments are the same as Breakdown(). If \p keyFramesAtTimes is
+ /// given, it will be populated with the newly broken down or previously
+ /// existing key frames at the given times.
+ TS_API
+ void
+ Breakdown( const std::vector<double> & times,
+ const std::vector<TsKnotType> & types,
+ bool flatTangents, double tangentLength,
+ const std::vector<VtValue> & values,
+ GfInterval *intervalAffected=nullptr,
+ TsKeyFrameMap *keyFramesAtTimes=nullptr);
+
+ /// Removes the keyframe at the given time, optionally returning the
+ /// time range affected.
+ TS_API
+ void RemoveKeyFrame(
+ TsTime time, GfInterval *intervalAffected=nullptr);
+
+ /// Removes all keyframes. This does not affect extrapolation. If this
+ /// spline is looping, knots hidden under the loop echos will not be
+ /// removed.
+ TS_API
+ void Clear();
+
+ /// \brief Finds the keyframe closest to the given time.
+ /// Returns an empty value if there are no keyframes.
+ TS_API
+ std::optional<TsKeyFrame>
+ GetClosestKeyFrame( TsTime targetTime ) const;
+
+ /// \brief Finds the closest keyframe before the given time.
+ /// Returns an empty value if no such keyframe exists.
+ TS_API
+ std::optional<TsKeyFrame>
+ GetClosestKeyFrameBefore( TsTime targetTime )const;
+
+ /// \brief Finds the closest keyframe after the given time.
+ /// Returns an empty value if no such keyframe exists.
+ TS_API
+ std::optional<TsKeyFrame>
+ GetClosestKeyFrameAfter( TsTime targetTime ) const;
+
+ /// \brief Returns true if the given key frame is redundant.
+ ///
+ /// A key frame is redundant if it can be removed without affecting the
+ /// value of the spline at any time. If a spline has only one key frame
+ /// and that key frame has the same value as this spline's default
+ /// value, then that key frame is considered redundant. If a
+ /// \c defaultValue parameter is not supplied, the last knot on a spline
+ /// is never considered redundant.
+ TS_API
+ bool IsKeyFrameRedundant( const TsKeyFrame &keyFrame,
+ const VtValue &defaultValue = VtValue() ) const;
+
+ /// \brief Returns true if the key frame at the given time is redundant.
+ ///
+ /// This is a convenience function for the version that takes a
+ /// TsKeyFrame. If there is no key frame at the indicated time a
+ /// TF_CODING_ERROR will occur and false is returned.
+ TS_API
+ bool IsKeyFrameRedundant( TsTime keyFrameTime,
+ const VtValue &defaultValue = VtValue() ) const;
+
+ /// \brief Returns true if any of this spline's key frames are redundant.
+ TS_API
+ bool HasRedundantKeyFrames( const VtValue &defaultValue = VtValue() ) const;
+
+ /// \brief Returns true if the segment between the given (adjacent) key
+ /// frames is flat.
+ TS_API
+ bool IsSegmentFlat( const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2 ) const;
+
+ /// \brief Returns true if the segment between the given (adjacent) key
+ /// frames is flat.
+ ///
+ /// This function will log a TF_CODING_ERROR if there is no key frame at
+ /// either of the indicated times.
+ TS_API
+ bool IsSegmentFlat( TsTime startTime, TsTime endTime ) const;
+
+ /// \brief Returns true if the segment between the given (adjacent) key
+ /// frames is monotonic (i.e. no extremes).
+ ///
+ /// This function will log a TF_CODING_ERROR if kf1 >= kf2
+ /// TODO describe the preconditions
+ ///
+ TS_API
+ bool
+ IsSegmentValueMonotonic( const TsKeyFrame &kf1,
+ const TsKeyFrame &kf2 ) const;
+
+ /// \brief Returns true if the segment between the given (adjacent) key
+ /// frames is monotonic (i.e. no extremes).
+ ///
+ /// Given times must correspond to key frames.
+ /// see also IsSegmentValueMonotonic(kf1, kf2)
+ TS_API
+ bool
+ IsSegmentValueMonotonic( TsTime startTime, TsTime endTime ) const;
+
+ /// \brief Returns true if the value of the spline changes over time,
+ /// whether due to differing values among keyframes or knot sides,
+ /// or value changes via non-flat tangents. If allowEpsilonDifferences is
+ /// true, then if the spline is of type double, then knot value
+ /// differences that are tiny will count as 0.
+ TS_API
+ bool
+ IsVarying() const;
+
+ /// \brief Like IsVarying(), but for splines of type double, allows tiny
+ /// value differences.
+ TS_API
+ bool
+ IsVaryingSignificantly() const;
+
+ /// Sets the spline's extrapolation type on each side.
+ TS_API
+ void SetExtrapolation(
+ TsExtrapolationType left, TsExtrapolationType right);
+
+ /// Returns the spline's extrapolation type on each side (\c first
+ /// is the left side).
+ TS_API
+ std::pair<TsExtrapolationType, TsExtrapolationType>
+ GetExtrapolation() const;
+
+ /// Returns the typeid of the value type for keyframes in this spline.
+ /// If no keyframes have been set, this will return typeid(void).
+ TS_API
+ const std::type_info &
+ GetTypeid() const;
+
+ /// Returns the TfType of the value type for keyframes in this spline.
+ /// If no keyframes have been set, this will return unknown type
+ TS_API
+ TfType
+ GetType() const;
+
+ /// Returns the typename of the value type for keyframes in this spline,
+ /// If no keyframes have been set, this will return "void".
+ TS_API
+ std::string GetTypeName() const;
+
+ /// Evaluates the value of the spline at the given time, interpolating the
+ /// keyframes. If there are no keyframes, an empty VtValue is returned.
+ TS_API
+ VtValue Eval(
+ TsTime time, TsSide side=TsRight ) const;
+
+ /// Evaluates the value of the spline at the given time without any
+ /// interpolation, as if all keyframes and extrapolation modes were of
+ /// type "held".
+ ///
+ /// If there are no keyframes, an empty VtValue is returned.
+ TS_API
+ VtValue EvalHeld( TsTime time, TsSide side=TsRight ) const;
+
+ /// Evaluates the derivative of the spline at the given time, interpolating
+ /// the keyframes. If there are no keyframes, an empty VtValue is returned.
+ TS_API
+ VtValue EvalDerivative(
+ TsTime time, TsSide side=TsRight ) const;
+
+ /// Returns whether the left-side value and the right-side value at the
+ /// specified time are different. This is always false for a time where
+ /// there is no keyframe. For a keyframe time, the sides differ if (1)
+ /// there is a dual-valued keyframe with different values on the left and
+ /// right side; or (2) the keyframe follows a held segment whose value does
+ /// not match the keyframe's right-side value. Contrast this method with
+ /// TsKeyFrame::GetIsDualValued, which only reports whether a keyframe is
+ /// configured to have dual values.
+ TS_API
+ bool DoSidesDiffer(TsTime time) const;
+
+ /// \brief Evaluates the value of the spline over the given time interval.
+ /// When the returned samples are scaled by \e timeScale and
+ /// \e valueScale and linearly interpolated, the reconstructed curve
+ /// will nowhere have an error greater than \e tolerance.
+ ///
+ /// Samples may be point samples or "blur" samples. A blur sample
+ /// covers a finite time domain and a value range. It indicates that
+ /// the value varies very quickly in the domain and that, to the given
+ /// tolerance, only the minimum and maximum values are of interest.
+ /// Blur domains are always half-open on the right.
+ ///
+ /// Samples are returned in non-decreasing time order. Two samples
+ /// may have equal time in two cases. First, if both are point samples
+ /// then the first is the left side evaluation of the value at time
+ /// and the second is the right side evaluation. Second, if the first
+ /// sample is a point sample and second is a blur sample then the point
+ /// sample is the left side evaluation of time. Blur domains will not
+ /// overlap and point samples, with the above exception, will not be
+ /// inside any blur domain.
+ ///
+ /// Samples may be returned outside the given time interval.
+ TS_API
+ TsSamples Sample(
+ TsTime startTime, TsTime endTime,
+ double timeScale, double valueScale,
+ double tolerance ) const;
+
+ TS_API
+ std::pair<VtValue, VtValue>
+ GetRange( TsTime startTime, TsTime endTime ) const;
+
+ /// Returns whether spline represents a simple linear relationship.
+ TS_API
+ bool IsLinear() const;
+
+ /// Returns whether the given key frame is in the looped interval, but
+ /// not in the master interval.
+ TS_API
+ bool KeyFrameIsInLoopedRange(const TsKeyFrame & kf);
+
+ /// \brief Return an object describing all the looping parameters for
+ /// this spline.
+ TS_API
+ TsLoopParams GetLoopParams() const;
+
+ /// \brief Set the looping parameters for this spline.
+ TS_API
+ void SetLoopParams(const TsLoopParams&);
+
+ // If this spline is a looping spline, bakes the looped key frames
+ // out and turns looping off. Hidden keyframes will be lost.
+ TS_API
+ void BakeSplineLoops();
+
+ /// \brief Is the given time in the "unrolled" region of a spline that is
+ /// looping; i.e. not in the master region
+ TS_API
+ bool IsTimeLooped(TsTime time) const;
+
+ /// Our iterators are simply iterators into the contained TsKeyFrameMap
+ /// We only expose const iterators because when a KeyFrame changes we
+ /// need to update other internal state.
+ typedef TsKeyFrameMap::const_iterator const_iterator;
+ typedef TsKeyFrameMap::const_reverse_iterator const_reverse_iterator;
+
+ /// Some utilities (such as TfIterator) expect a class named 'iterator'.
+ /// We provide that as a typdef to const_iterator in order to avoid exposing
+ /// real non-const iterators.
+ typedef const_iterator iterator;
+ typedef const_reverse_iterator reverse_iterator;
+
+ /// \group Container API
+ ///
+ /// Provide STL container compliant API.
+ ///
+ /// Some of these methods are inlined because they are often called
+ /// as part of looping constructions where the cost of function calls
+ /// could be high.
+ /// @{
+
+ /// Returns the number of KeyFrames in this spline.
+ TS_API
+ size_t size() const {
+ return GetKeyFrames().size();
+ }
+
+ /// Return true if this spline has no KeyFrames.
+ TS_API
+ bool empty() const {
+ return GetKeyFrames().empty();
+ }
+
+ /// Return a const_iterator pointing to the beginning of the spline.
+ TS_API
+ const_iterator begin() const {
+ return const_iterator(GetKeyFrames().begin());
+ }
+
+ /// Returns a const_iterator pointing to the end of the spline. (one past
+ /// the last KeyFrame)
+ TS_API
+ const_iterator end() const {
+ return const_iterator(GetKeyFrames().end());
+ }
+
+ /// Return a const_reverse_iterator pointing to the end of the spline.
+ TS_API
+ const_reverse_iterator rbegin() const {
+ return const_reverse_iterator(GetKeyFrames().rbegin());
+ }
+
+ /// Returns a const_reverse_iterator pointing to the beginning of the
+ /// spline. (one before the first KeyFrame)
+ TS_API
+ const_reverse_iterator rend() const {
+ return const_reverse_iterator(GetKeyFrames().rend());
+ }
+
+
+ /// Returns a const_iterator to the KeyFrame at time \p t. This
+ /// will return end() if no KeyFrame exists at that time.
+ TS_API
+ const_iterator find(const TsTime &t) const;
+
+ /// Returns a const_iterator to the first KeyFrame with a time
+ /// that is not less than \p t.
+ TS_API
+ const_iterator lower_bound(const TsTime &t) const;
+
+ /// Returns a const_iterator to the first KeyFrame with a time
+ /// that is greater than \p t.
+ TS_API
+ const_iterator upper_bound(const TsTime &t) const;
+
+ /// Returns the number (either 0 or 1) of KeyFrames with time
+ /// \p t.
+ TS_API
+ size_t count(const TsTime &t) const {
+ return GetKeyFrames().find(t) != GetKeyFrames().end();
+ }
+
+ /// @}
+
+private:
+
+ void _BreakdownMultipleValues( const std::vector<double> ×,
+ TsKnotType type, bool flatTangents, double tangentLength,
+ const std::vector<VtValue> &values,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes);
+
+ void _BreakdownMultipleKnotTypes( const std::vector<double> ×,
+ const std::vector<TsKnotType> &types,
+ bool flatTangents, double tangentLength,
+ const std::vector<VtValue> &values,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes);
+
+ // Fills \p keyframes with the new keyframes to effect a breakdown
+ // at \p x. Subclasses can use this to implement \c Breakdown();
+ // they'll call this then set each key frame in \p keyframes.
+ void
+ _GetBreakdown( TsKeyFrameMap* newKeyframes, double x, TsKnotType type,
+ bool flatTangents, double tangentLength,
+ const VtValue &value ) const;
+
+ typedef std::vector<std::pair<TsTime, VtValue>> _Samples;
+
+ // Helper for _BreakdownMultipleKnotTypes. Performs the Breakdown on the
+ // given list of samples, i.e. time/value pairs.
+ void _BreakdownSamples(
+ const _Samples &samples,
+ TsKnotType type,
+ bool flatTangents,
+ double tangentLength,
+ GfInterval *intervalAffected,
+ TsKeyFrameMap *keyFramesAtTimes);
+
+ // Helper for the forms of IsVarying*; allows subseqent keyframes to vary
+ // by 'tolerance'.
+ bool _IsVarying(double tolerance) const;
+
+ /// Ensure that _data is not shared with any other spline.
+ /// If it is, make our own copy and drop our reference to the shared one.
+ void _Detach();
+
+private:
+ std::shared_ptr<TsSpline_KeyFrames> _data;
+};
+
+TS_API
+std::ostream& operator<<(std::ostream &out, const TsSpline &val);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/spline_KeyFrames.h"
+#include "pxr/base/ts/evalUtils.h"
+#include "pxr/base/ts/keyFrameUtils.h"
+#include "pxr/base/tf/iterator.h"
+#include "pxr/base/tf/mallocTag.h"
+#include "pxr/base/tf/stl.h"
+#include "pxr/base/trace/trace.h"
+#include <limits>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using std::string;
+using std::vector;
+
+TsSpline_KeyFrames::TsSpline_KeyFrames() :
+ _extrapolation(TsExtrapolationHeld, TsExtrapolationHeld)
+{
+}
+
+TsSpline_KeyFrames
+::TsSpline_KeyFrames(TsSpline_KeyFrames const &other,
+ TsKeyFrameMap const *keyFrames)
+ : _extrapolation(other._extrapolation)
+ , _loopParams(other._loopParams)
+{
+ if (keyFrames) {
+ if (_loopParams.GetLooping()) {
+ // If looping, there might be knots hidden under the echos of the
+ // loop that we need to preserve.
+ _normalKeyFrames = other._normalKeyFrames;
+ }
+
+ SetKeyFrames(*keyFrames);
+ } else {
+ _loopedKeyFrames = other._loopedKeyFrames;
+ _normalKeyFrames = other._normalKeyFrames;
+ }
+}
+
+TsSpline_KeyFrames::~TsSpline_KeyFrames()
+{
+}
+
+const TsKeyFrameMap &
+TsSpline_KeyFrames::GetKeyFrames() const
+{
+ if (_loopParams.GetLooping()) {
+ return _loopedKeyFrames;
+ }
+ else {
+ return _normalKeyFrames;
+ }
+}
+
+const TsKeyFrameMap &
+TsSpline_KeyFrames::GetNormalKeyFrames() const
+{
+ return _normalKeyFrames;
+}
+
+void
+TsSpline_KeyFrames::SetKeyFrames(const TsKeyFrameMap &keyFrames)
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::SetKeyFrames");
+ TRACE_FUNCTION();
+
+ if (_loopParams.GetLooping()) {
+ _loopedKeyFrames = keyFrames;
+ _UnrollMaster();
+ // Keep the normal keys in sync; this is so we can write out scene
+ // description (which only refects the normal keys) at any time.
+ // Note we don't update the eval cache for the normal keys; we'll do
+ // this if/when we switch back to normal mode.
+ _SetNormalFromLooped();
+ }
+ else {
+ _normalKeyFrames = keyFrames;
+ }
+}
+
+void
+TsSpline_KeyFrames::SwapKeyFrames(std::vector<TsKeyFrame>* keyFrames)
+{
+ TRACE_FUNCTION();
+
+ if (_loopParams.GetLooping()) {
+ _loopedKeyFrames.swap(*keyFrames);
+ _UnrollMaster();
+ // Keep the normal keys in sync; this is so we can write out scene
+ // description (which only refects the normal keys) at any time.
+ // Note we don't update the eval cache for the normal keys; we'll do
+ // this if/when we switch back to normal mode.
+ _SetNormalFromLooped();
+ }
+ else {
+ _normalKeyFrames.swap(*keyFrames);
+ }
+}
+
+void
+TsSpline_KeyFrames::SetKeyFrame(
+ TsKeyFrame kf,
+ GfInterval *intervalAffected)
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::SetKeyFrame");
+ TsTime t = kf.GetTime();
+
+ if (_loopParams.GetLooping()) {
+ // Get Loop domain intervals
+ GfInterval loopedInterval =
+ _loopParams.GetLoopedInterval();
+ GfInterval masterInterval =
+ _loopParams.GetMasterInterval();
+
+ bool inMaster = masterInterval.Contains(t);
+ // Punt if not in the writable range
+ if (loopedInterval.Contains(t) && !inMaster) {
+ return;
+ }
+
+ _loopedKeyFrames[t] = kf;
+
+ // Keep the normal keys in sync; this is so we can write out scene
+ // description (which only refelcts the normal keys) at any time
+ // Note we don't update the eval cache for the normal keys; we'll do
+ // this if/when we swtich back to normal mode.
+ _normalKeyFrames[t] = kf;
+
+ // The times that we added, including the one passed to us. Note these
+ // will not necessarily be in time order
+ vector<TsTime> times(1, t);
+
+ if (inMaster) {
+ // Iterators for the key to propagate
+ TsKeyFrameMap::iterator k = _loopedKeyFrames.find(t);
+ if (k == _loopedKeyFrames.end())
+ return; // Yikes; just inserted it
+
+ TsKeyFrameMap::iterator k0 = k++;
+ _UnrollKeyFrameRange(&_loopedKeyFrames, k0, k,
+ _loopParams, ×);
+ }
+
+ // Set intervalAffected
+ if (intervalAffected) {
+ TF_FOR_ALL(ti, times) {
+ // For non-looping splines, we already computed the interval
+ // changed, before the key was instered. For looping splines
+ // this is too hard (and not worth it) so we compute here,
+ // afterwards.
+ *intervalAffected |= _GetTimeInterval(*ti);
+ }
+ }
+ } else {
+ // Non-looping
+ if (intervalAffected) {
+ // Optimize the case where the param is empty
+ if (intervalAffected->IsEmpty())
+ *intervalAffected = _FindSetKeyFrameChangedInterval(kf);
+ else {
+ *intervalAffected |= _FindSetKeyFrameChangedInterval(kf);
+ }
+ }
+
+ _normalKeyFrames[t] = kf;
+ }
+}
+
+void
+TsSpline_KeyFrames::RemoveKeyFrame( TsTime t,
+ GfInterval *intervalAffected )
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::RemoveKeyFrame");
+ // Assume none removed
+ if (intervalAffected)
+ *intervalAffected = GfInterval();
+
+ if (_loopParams.GetLooping()) {
+ // Get Loop domain intervals
+ GfInterval loopedInterval =
+ _loopParams.GetLoopedInterval();
+ GfInterval masterInterval =
+ _loopParams.GetMasterInterval();
+
+ bool inMaster = masterInterval.Contains(t);
+ // Punt if not in the writable range
+ if (loopedInterval.Contains(t) && !inMaster) {
+ return;
+ }
+
+ // Error if we've been asked to remove a keyframe that doesn't exist
+ if (_loopedKeyFrames.find( t ) == _loopedKeyFrames.end()) {
+ TF_CODING_ERROR("keyframe does not exist; not removing");
+ return;
+ }
+
+ // Remove the requested time. This will either be in the master
+ // interval, or outside the looped interval
+ if (intervalAffected) {
+ *intervalAffected |=
+ _FindRemoveKeyFrameChangedInterval(t);
+ }
+ _loopedKeyFrames.erase(t);
+
+ // If we removed it from the master interval we now how to iterate
+ // over all the echos and remove it from them too
+ if (inMaster) {
+ // Number of whole iterations to cover the prepeat range
+ int numPrepeats = ceil((masterInterval.GetMin() -
+ loopedInterval.GetMin()) / masterInterval.GetSize());
+ // Number of whole iterations to cover the repeat range
+ int numRepeats = ceil((loopedInterval.GetMax() -
+ masterInterval.GetMax()) / masterInterval.GetSize());
+
+ // Iterate from the first prepeat to the last repeat
+ for (int i = -numPrepeats; i <= numRepeats; i++) {
+ TsTime timeOffset = i * masterInterval.GetSize();
+ // Already removed it from the master interval
+ if (i == 0)
+ continue;
+
+ // Shift time
+ TsTime time = t + timeOffset;
+ // In case the pre/repeat ranges were not multiples of the
+ // period, the first and last iterations may have some knots
+ // outside the range
+ if (!loopedInterval.Contains(time))
+ continue;
+
+ if (intervalAffected) {
+ *intervalAffected |=
+ _FindRemoveKeyFrameChangedInterval(time);
+ }
+ _loopedKeyFrames.erase(time);
+ }
+ }
+
+ } else {
+ // Non-looping
+
+ // Error if we've been asked to remove a keyframe that doesn't exist
+ if (_normalKeyFrames.find( t ) == _normalKeyFrames.end()) {
+ TF_CODING_ERROR("keyframe does not exist; not removing");
+ return;
+ }
+ if (intervalAffected) {
+ *intervalAffected |= _FindRemoveKeyFrameChangedInterval(t);
+ }
+ // Actual removal below
+ }
+
+ // Whether looping or not, remove it from the normal keys to keep them in
+ // sync.
+ _normalKeyFrames.erase(t);
+}
+
+void
+TsSpline_KeyFrames::Clear()
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::Clear");
+ TfReset(_normalKeyFrames);
+ TfReset(_loopedKeyFrames);
+}
+
+void
+TsSpline_KeyFrames::_LoopParamsChanged(bool loopingChanged,
+ bool valueOffsetChanged, bool domainChanged)
+{
+ // Punt if nothing changed
+ if (!(loopingChanged | valueOffsetChanged | domainChanged))
+ return;
+
+ // If we're now looping, then whatever the change was, re-generate the
+ // looped keys from the normal ones
+ if (_loopParams.GetLooping()) {
+ _SetLoopedFromNormal();
+ }
+}
+
+void
+TsSpline_KeyFrames::_SetNormalFromLooped()
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::_SetNormalFromLooped");
+
+ // Get Loop domain intervals
+ GfInterval loopedInterval =
+ _loopParams.GetLoopedInterval();
+ GfInterval masterInterval =
+ _loopParams.GetMasterInterval();
+
+ // Clear the region before the prepeat and copy the corresponding from
+ // the looped keys
+ _normalKeyFrames.erase(
+ _normalKeyFrames.begin(),
+ _normalKeyFrames.lower_bound(loopedInterval.GetMin()));
+ _normalKeyFrames.insert(
+ _loopedKeyFrames.begin(),
+ _loopedKeyFrames.lower_bound(loopedInterval.GetMin()));
+
+ // Clear the masterInterval region and copy the corresponding from the
+ // looped keys
+ _normalKeyFrames.erase(
+ _normalKeyFrames.lower_bound(masterInterval.GetMin()),
+ _normalKeyFrames.lower_bound(masterInterval.GetMax()));
+ _normalKeyFrames.insert(
+ _loopedKeyFrames.lower_bound(masterInterval.GetMin()),
+ _loopedKeyFrames.lower_bound(masterInterval.GetMax()));
+
+ // Clear the region after the repeat and copy the corresponding from
+ // the looped keys
+ _normalKeyFrames.erase(
+ _normalKeyFrames.lower_bound(loopedInterval.GetMax()),
+ _normalKeyFrames.end());
+ _normalKeyFrames.insert(
+ _loopedKeyFrames.lower_bound(loopedInterval.GetMax()),
+ _loopedKeyFrames.end());
+}
+
+void
+TsSpline_KeyFrames::_SetLoopedFromNormal()
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::_SetLoopedFromNormal");
+
+ _loopedKeyFrames = _normalKeyFrames;
+ _UnrollMaster();
+}
+
+bool
+TsSpline_KeyFrames::operator==(const TsSpline_KeyFrames &rhs) const
+{
+ TRACE_FUNCTION();
+
+ if (_extrapolation != rhs._extrapolation ||
+ _loopParams != rhs._loopParams) {
+ return false;
+ }
+
+ // If looping, compare both maps, else just the normal ones
+ bool normalEqual = _normalKeyFrames == rhs._normalKeyFrames;
+ if (!_loopParams.GetLooping()) {
+ return normalEqual;
+ }
+ else {
+ return normalEqual && _loopedKeyFrames == rhs._loopedKeyFrames;
+ }
+}
+
+void
+TsSpline_KeyFrames::BakeSplineLoops()
+{
+ _loopParams.SetLooping(false);
+ _UnrollKeyFrames(&_normalKeyFrames, _loopParams);
+ // Clear the loop params after baking
+ _loopParams = TsLoopParams();
+}
+
+void
+TsSpline_KeyFrames::_UnrollMaster()
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::_UnrollMaster");
+
+ _UnrollKeyFrames(&_loopedKeyFrames, _loopParams);
+}
+
+void
+TsSpline_KeyFrames::_UnrollKeyFrames(TsKeyFrameMap *keyFrames,
+ const TsLoopParams ¶ms)
+{
+ // Get Loop domain intervals
+ GfInterval loopedInterval = params.GetLoopedInterval();
+ GfInterval masterInterval = params.GetMasterInterval();
+
+ // Clear the keys in the prepeat range
+ keyFrames->erase(
+ keyFrames->lower_bound(loopedInterval.GetMin()),
+ keyFrames->lower_bound(masterInterval.GetMin()));
+
+ // Clear the keys in the repeat range
+ keyFrames->erase(
+ keyFrames->lower_bound(masterInterval.GetMax()),
+ keyFrames->lower_bound(loopedInterval.GetMax()));
+
+ // Iterators for the masterInterval keys to propagate
+ TsKeyFrameMap::iterator k0 =
+ keyFrames->lower_bound(masterInterval.GetMin());
+ TsKeyFrameMap::iterator k1 =
+ keyFrames->lower_bound(masterInterval.GetMax());
+
+ _UnrollKeyFrameRange(keyFrames, k0, k1, params);
+}
+
+void
+TsSpline_KeyFrames::_UnrollKeyFrameRange(
+ TsKeyFrameMap *keyFrames,
+ const TsKeyFrameMap::iterator &k0,
+ const TsKeyFrameMap::iterator &k1,
+ const TsLoopParams ¶ms,
+ std::vector<TsTime> *times)
+{
+ // Get Loop domain intervals
+ GfInterval loopedInterval = params.GetLoopedInterval();
+ GfInterval masterInterval = params.GetMasterInterval();
+
+ // Propagate the master keys inside the 'loopedInterval'
+ //
+ // Number of whole iterations to cover the prepeat range; we trim down
+ // below
+ int numPrepeats = ceil((masterInterval.GetMin() - loopedInterval.GetMin()) /
+ masterInterval.GetSize());
+ // Number of whole iterations to cover the repeat range; we trim down
+ // below
+ int numRepeats = ceil((loopedInterval.GetMax() - masterInterval.GetMax()) /
+ masterInterval.GetSize());
+
+ // Iterate from the first prepeat to the last repeat, copying from the
+ // masterInterval and shifting in time and possibly value
+ TsKeyFrameMap newKeyFrames = *keyFrames;
+ for (int i = -numPrepeats; i <= numRepeats; i++) {
+ if (i == 0)
+ continue; // The masterInterval frames are already in place
+ TsTime timeOffset = i * masterInterval.GetSize();
+ double valueOffset = i * params.GetValueOffset();
+ for (TsKeyFrameMap::iterator k = k0; k != k1; k++) {
+ TsKeyFrame key = *k;
+
+ // Shift time
+ TsTime t = key.GetTime() + timeOffset;
+ // In case the pre/repeat ranges were not multiples of the period,
+ // the first and last iterations may have some knots outside the
+ // range
+ if (!loopedInterval.Contains(t))
+ continue;
+ key.SetTime(t);
+
+ // Shift value if a double
+ VtValue v = key.GetValue();
+ if (v.IsHolding<double>()) {
+ key.SetValue(VtValue(v.Get<double>() + valueOffset));
+ // Handle dual valued
+ if (key.GetIsDualValued()) {
+ key.SetLeftValue(
+ VtValue(key.GetLeftValue().Get<double>() + valueOffset));
+ }
+ }
+
+ // Clobber existing
+ newKeyFrames[t] = key;
+
+ // Remember times we changed
+ if (times)
+ times->push_back(t);
+ }
+ }
+ *keyFrames = newKeyFrames;
+}
+
+TsKeyFrameMap *
+TsSpline_KeyFrames::_GetKeyFramesMutable()
+{
+ return const_cast<TsKeyFrameMap *>(&GetKeyFrames());
+}
+
+TsSpline_KeyFrames::_KeyFrameRange
+TsSpline_KeyFrames::_GetKeyFrameRange( TsTime time )
+{
+ // Get the keyframe after time
+ TsKeyFrameMap::iterator i = _GetKeyFramesMutable()->upper_bound( time );
+
+ // Get the keyframe before time
+ TsKeyFrameMap::iterator j = i;
+ if (j != _GetKeyFramesMutable()->begin()) {
+ --j;
+ if (j->GetTime() == time && j != _GetKeyFramesMutable()->begin()) {
+ // There's a keyframe at time so go to the previous keyframe.
+ --j;
+ }
+ }
+
+ return std::make_pair(j, i);
+}
+
+TsSpline_KeyFrames::_KeyFrameRange
+TsSpline_KeyFrames::_GetKeyFrameRange( TsTime leftTime, TsTime rightTime )
+{
+ // Get the keyframe before leftTime
+ TsKeyFrameMap::iterator i = _GetKeyFramesMutable()->lower_bound(
+ leftTime );
+ if (i != _GetKeyFramesMutable()->begin()) {
+ --i;
+ }
+
+ // Get the keyframe after rightTime
+ TsKeyFrameMap::iterator j = _GetKeyFramesMutable()->upper_bound(
+ rightTime );
+
+ return std::make_pair(i, j);
+}
+
+GfInterval
+TsSpline_KeyFrames::_GetTimeInterval( TsTime t )
+{
+ GfInterval result = GfInterval::GetFullInterval();
+
+ if (GetKeyFrames().empty())
+ return result;
+
+ TsKeyFrameMap::const_iterator lower = GetKeyFrames().lower_bound(t);
+ TsKeyFrameMap::const_iterator upper = lower;
+ if (upper != GetKeyFrames().end()) {
+ ++upper;
+ }
+
+ std::pair<TsKeyFrameMap::const_iterator,
+ TsKeyFrameMap::const_iterator> range(
+ GetKeyFrames().lower_bound(t),
+ GetKeyFrames().upper_bound(t));
+
+ // Tighten min bound.
+ if (range.first != GetKeyFrames().begin()) {
+ // Start at previous knot.
+ TsKeyFrameMap::const_iterator prev = range.first;
+ --prev;
+ result.SetMin( prev->GetTime(), prev->GetTime() == t );
+ } else {
+ // No previous knots -- therefore min is unbounded.
+ }
+
+ // Tighten max bound.
+ if (range.second != GetKeyFrames().end()) {
+ result.SetMax( range.second->GetTime(), range.second->GetTime() == t );
+ } else {
+ // No subsequent knots -- therefore max is unbounded.
+ }
+
+ return result;
+}
+
+GfInterval
+TsSpline_KeyFrames::_FindRemoveKeyFrameChangedInterval(TsTime time)
+{
+ // No change if there's no keyframe at the given time.
+ TsKeyFrameMap::const_iterator iter = GetKeyFrames().find(time);
+ if (iter == GetKeyFrames().end()) {
+ return GfInterval();
+ }
+
+ // If the keyframe is redundant, then there's no change
+ const TsKeyFrame &keyFrame = *iter;
+ if (Ts_IsKeyFrameRedundant(GetKeyFrames(), keyFrame)) {
+ return GfInterval();
+ }
+
+ // First assume everything from the previous keyframe to the next keyframe
+ // has changed.
+ GfInterval r = _GetTimeInterval(time);
+
+ _KeyFrameRange keyFrameRange = _GetKeyFrameRange(time);
+
+ // If it's the only key frame and the key frame was not redundant, we
+ // just invalidate the entire interval.
+ if (GetKeyFrames().size() == 1) {
+ return GfInterval::GetFullInterval();
+ }
+
+ // If there is no keyframe to the left, then we do an extrapolation
+ // comparison.
+ if (r.GetMin() == -std::numeric_limits<TsTime>::infinity()) {
+ const TsKeyFrame & nextKeyFrame = *(keyFrameRange.second);
+ // Get the effective extrapolations of each spline on the left side
+ TsExtrapolationType aExtrapLeft =
+ _GetEffectiveExtrapolationType(nextKeyFrame, TsLeft);
+ TsExtrapolationType bExtrapLeft =
+ _GetEffectiveExtrapolationType(keyFrame, TsLeft);
+
+ // We can tighten if the extrapolations of both knots are held and
+ // their left values are the same
+ if (aExtrapLeft == TsExtrapolationHeld &&
+ bExtrapLeft == TsExtrapolationHeld &&
+ nextKeyFrame.GetLeftValue() == keyFrame.GetLeftValue()) {
+
+ r.SetMin(time, /* closed */ false);
+ }
+ } else {
+ // If there is a keyframe to the left that is held, the changed
+ // interval starts at the removed key frame.
+ TsKeyFrameMap::const_iterator
+ it = GetKeyFrames().find(r.GetMin());
+ if (it != GetKeyFrames().end() &&
+ it->GetKnotType() == TsKnotHeld) {
+ r.SetMin(time, /* closed */ true);
+ }
+ }
+ // If there is no keyframe to the right, then we do an extrapolation
+ // comparison.
+ if (r.GetMax() == std::numeric_limits<TsTime>::infinity()) {
+ const TsKeyFrame & prevKeyFrame = *(keyFrameRange.first);
+ // Get the effective extrapolations of each spline on the right side
+ TsExtrapolationType aExtrapRight =
+ _GetEffectiveExtrapolationType(prevKeyFrame, TsRight);
+ TsExtrapolationType bExtrapRight =
+ _GetEffectiveExtrapolationType(keyFrame, TsRight);
+
+ // We can tighten if the extrapolations are the same
+ if (aExtrapRight == TsExtrapolationHeld &&
+ bExtrapRight == TsExtrapolationHeld &&
+ prevKeyFrame.GetValue() == keyFrame.GetValue()) {
+
+ r.SetMax(time, /* closed */ false);
+ }
+ }
+
+ if (r.IsEmpty()) {
+ return GfInterval();
+ }
+ return r;
+}
+
+GfInterval
+TsSpline_KeyFrames::_FindSetKeyFrameChangedInterval(
+ const TsKeyFrame &keyFrame)
+{
+ const TsKeyFrameMap &keyFrames = GetKeyFrames();
+ TsTime time = keyFrame.GetTime();
+
+ // If adding a new key frame that is redundant, nothing changed, just
+ // return an empty interval.
+ if (Ts_IsKeyFrameRedundant(keyFrames, keyFrame)) {
+ if (keyFrames.find(time) == keyFrames.end() ||
+ Ts_IsKeyFrameRedundant(keyFrames, *keyFrames.find(time))) {
+ return GfInterval();
+ }
+ }
+
+ // First assume everything from the previous keyframe to the next keyframe
+ // has changed.
+ GfInterval r = _GetTimeInterval(time);
+
+ // If the spline is empty then just return the entire interval.
+ if (keyFrames.empty()) {
+ return r;
+ }
+
+ // If there is no keyframe to the left, then we do an extrapolation
+ // comparison.
+ if (r.GetMin() == -std::numeric_limits<TsTime>::infinity()) {
+ const TsKeyFrame & firstKeyFrame = *(keyFrames.begin());
+ // Get the effective extrapolations of each spline on the left side
+ TsExtrapolationType aExtrapLeft =
+ _GetEffectiveExtrapolationType(firstKeyFrame, TsLeft);
+ TsExtrapolationType bExtrapLeft =
+ _GetEffectiveExtrapolationType(keyFrame, TsLeft);
+
+ // We can tighten if the extrapolations are the same
+ if (aExtrapLeft == bExtrapLeft) {
+ // if the first keyframes of both splines are the same, then we may
+ // not have any changes to left of the first keyframes
+ if (firstKeyFrame.GetLeftValue() == keyFrame.GetLeftValue()) {
+ // If the extrapolation is held to the left, then there are no
+ // changes before the minimum of the first keyframe times
+ if (aExtrapLeft == TsExtrapolationHeld) {
+ r.SetMin(time, /* closed */ false);
+ }
+ // Otherwise the extrapolation is linear so only if the time and
+ // slopes match, do we not have a change before the first
+ // keyframes
+ else if (firstKeyFrame.GetTime() == time &&
+ firstKeyFrame.GetLeftTangentSlope() ==
+ keyFrame.GetLeftTangentSlope()) {
+ r.SetMin(time, /* closed */ false);
+ }
+ }
+ }
+ } else {
+ // If there is a keyframe to the left that is held, the changed
+ // interval starts at the added key frame.
+ TsKeyFrameMap::const_iterator
+ it = keyFrames.find(r.GetMin());
+ if (it != keyFrames.end() && it->GetKnotType() == TsKnotHeld) {
+ r.SetMin(time, /* closed */ it->GetValue() != keyFrame.GetValue());
+ }
+ }
+ // If there is no keyframe to the right, then we do an extrapolation
+ // comparison.
+ if (r.GetMax() == std::numeric_limits<TsTime>::infinity()) {
+ const TsKeyFrame & lastKeyFrame = *(keyFrames.rbegin());
+ // Get the effective extrapolations of each spline on the right side
+ TsExtrapolationType aExtrapRight =
+ _GetEffectiveExtrapolationType(lastKeyFrame, TsRight);
+ TsExtrapolationType bExtrapRight =
+ _GetEffectiveExtrapolationType(keyFrame, TsRight);
+
+ // We can tighten if the extrapolations are the same
+ if (aExtrapRight == bExtrapRight) {
+ // if the last keyframes of both splines are the same, then we may
+ // not have any changes to right of the last keyframes
+ if (lastKeyFrame.GetValue() == keyFrame.GetValue()) {
+ // If the extrapolation is held to the right, then there are no
+ // changes after the maximum of the last keyframe times
+ if (aExtrapRight == TsExtrapolationHeld) {
+ r.SetMax(time, /* closed */ false);
+ }
+ // Otherwise the extrapolation is linear so only if the time and
+ // slopes match, do we not have a change after the last keyframes
+ else if (lastKeyFrame.GetTime() == time &&
+ lastKeyFrame.GetRightTangentSlope() ==
+ keyFrame.GetRightTangentSlope()) {
+ r.SetMax(time, /* closed */ false);
+ }
+ }
+ }
+ }
+ // If we're replacing an existing keyframe.
+ TsKeyFrameMap::const_iterator it = keyFrames.find(time);
+ if (it != keyFrames.end()) {
+ const TsKeyFrame &k = *(it);
+ _KeyFrameRange keyFrameRange = _GetKeyFrameRange(time);
+ if (k.IsEquivalentAtSide(keyFrame, TsLeft)) {
+ r.SetMin(time, k.GetValue() != keyFrame.GetValue());
+ } else if (keyFrameRange.first->GetTime() != time &&
+ (keyFrameRange.first->GetKnotType() == TsKnotHeld ||
+ (Ts_IsSegmentFlat(*(keyFrameRange.first), k) &&
+ Ts_IsSegmentFlat(*(keyFrameRange.first), keyFrame)))) {
+ r.SetMin(time, k.GetValue() != keyFrame.GetValue());
+ }
+
+ if (k.IsEquivalentAtSide(keyFrame, TsRight)) {
+ // Note that the value *at* this time will not change since the
+ // right values are the same, but since we produce intervals
+ // that contain changed knots, we want an interval that is
+ // closed on the right if the left values are different.
+ r.SetMax(time, k.GetLeftValue() != keyFrame.GetLeftValue());
+ } else if (keyFrameRange.second != keyFrames.end() &&
+ Ts_IsSegmentFlat(k, *(keyFrameRange.second)) &&
+ Ts_IsSegmentFlat(keyFrame, *(keyFrameRange.second))) {
+ r.SetMax(time, k.GetLeftValue() != keyFrame.GetLeftValue());
+ }
+ }
+
+ if (r.IsEmpty()) {
+ return GfInterval();
+ }
+ return r;
+}
+
+TsExtrapolationType
+TsSpline_KeyFrames::_GetEffectiveExtrapolationType(
+ const TsKeyFrame &keyFrame,
+ const TsSide &side) const
+{
+ return Ts_GetEffectiveExtrapolationType(keyFrame, GetExtrapolation(),
+ GetKeyFrames().size() == 1, side);
+}
+
+const TsLoopParams &
+TsSpline_KeyFrames::GetLoopParams() const
+{
+ return _loopParams;
+}
+
+void
+TsSpline_KeyFrames::SetLoopParams(const TsLoopParams ¶ms)
+{
+ TfAutoMallocTag2 tag("Ts", "TsSpline_KeyFrames::SetLoopParams");
+
+ // Note what's changing, to inform _keyframes (don't care about the group)
+ bool loopingChanged = params.GetLooping() != _loopParams.GetLooping();
+ bool valueOffsetChanged =
+ params.GetValueOffset() != _loopParams.GetValueOffset();
+ bool domainChanged = params != _loopParams;
+
+ // Make the change
+ _loopParams = params;
+
+ // Tell _keyframes
+ _LoopParamsChanged(loopingChanged, valueOffsetChanged, domainChanged);
+}
+
+void
+TsSpline_KeyFrames::SetExtrapolation(
+ const TsExtrapolationPair &extrapolation)
+{
+ _extrapolation = extrapolation;
+}
+
+const TsExtrapolationPair &
+TsSpline_KeyFrames::GetExtrapolation() const
+{
+ return _extrapolation;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_SPLINE_KEY_FRAMES_H
+#define PXR_BASE_TS_SPLINE_KEY_FRAMES_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/keyFrame.h"
+#include "pxr/base/ts/keyFrameMap.h"
+#include "pxr/base/ts/loopParams.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/vt/value.h"
+
+#include <vector>
+#include <map>
+#include <typeinfo>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+/// \class TsSpline_KeyFrames
+/// \brief Maintains the keyframes for a spline
+///
+/// The TsSpline_KeyFrames is a private class that holds onto and provides
+/// API for interacting with the spline's keyframes. Its principle duty
+/// is to manage the looping/non-looping representations of the spline. This
+/// class should only be held by TsSpline.
+///
+class TsSpline_KeyFrames
+{
+public:
+ TsSpline_KeyFrames();
+ ~TsSpline_KeyFrames();
+
+ /// Generalized copy constructor.
+ ///
+ /// If \a keyFrames is not NULL, this constructor has the same behavior
+ /// as first copying other, then calling SetKeyFrames with keyFrames.
+ TsSpline_KeyFrames(TsSpline_KeyFrames const &other,
+ TsKeyFrameMap const *keyFrames = NULL);
+
+ /// Gets the looped or unlooped keys, according to whether the spline is
+ /// looping.
+ const TsKeyFrameMap & GetKeyFrames() const;
+
+ /// If looping, just writes to the non unrolled intervals.
+ void SetKeyFrames(const TsKeyFrameMap&);
+
+ /// Replaces the key frames of this spline with keyFrames, and replaces
+ /// the contents of keyFrames with the key frames in this spline. If
+ /// the spline is looping, the data put into keyFrames will be the key
+ /// frames from the looped view of the spline, and hidden keys will be
+ /// preserved when keyFrames is swapped into this spline.
+ void SwapKeyFrames(std::vector<TsKeyFrame>* keyFrames);
+
+ /// If looping, just writes to the non unrolled intervals.
+ void SetKeyFrame( TsKeyFrame kf, GfInterval
+ *intervalAffected=NULL );
+
+ /// If looping, just affects the non unrolled intervals.
+ void RemoveKeyFrame( TsTime t, GfInterval
+ *intervalAffected=NULL );
+
+ /// Clears both maps.
+ void Clear();
+
+ /// Gets the underlying normal keys.
+ const TsKeyFrameMap & GetNormalKeyFrames() const;
+
+ /// Get the loop parameters.
+ const TsLoopParams &GetLoopParams() const;
+
+ /// Sets the loop parameters.
+ void SetLoopParams(const TsLoopParams &loopParams);
+
+ /// Get the left and right extrapolation.
+ const TsExtrapolationPair &GetExtrapolation() const;
+
+ /// Sets the left and right extrapolation.
+ void SetExtrapolation(const TsExtrapolationPair &extrapolation);
+
+ bool operator==(const TsSpline_KeyFrames &rhs) const;
+
+ // Bakes looped key frames out and turns looping off.
+ void BakeSplineLoops();
+
+private:
+ typedef std::pair<TsKeyFrameMap::iterator,
+ TsKeyFrameMap::iterator> _KeyFrameRange;
+
+ // Get a pointer to the keyframes that lets us change them
+ TsKeyFrameMap *_GetKeyFramesMutable();
+
+ // Returns the time interval affected by an edit to a keyframe at
+ // the given time.
+ GfInterval _GetTimeInterval( TsTime time );
+
+ // Copy the normal to the looped and then unroll the master keys
+ void _SetLoopedFromNormal();
+
+ // Copy the master, prepeat and repeated intervals from the looped keys to
+ // the normal keys
+ void _SetNormalFromLooped();
+
+ // Unroll the master interval of the looped keys to itself; clears the
+ // entire unrolled region first
+ void _UnrollMaster();
+
+ void _UnrollKeyFrames(TsKeyFrameMap *keyFrames,
+ const TsLoopParams ¶ms);
+
+ // Unroll the given range of _loopedKeyFrames. If times is given, return
+ // the times that were written. Does not clear the unrolled region before
+ // writing.
+ void _UnrollKeyFrameRange(TsKeyFrameMap *keyFrames,
+ const TsKeyFrameMap::iterator &k0,
+ const TsKeyFrameMap::iterator &k1,
+ const TsLoopParams ¶ms,
+ std::vector<TsTime> *times = NULL);
+
+ // Returns the range of keyframes including time as non-const iterators.
+ // If there is a keyframe at \p time then this is the keyframe before the
+ // keyframe at \p time to the keyframe after that one. If there isn't a
+ // keyframe at \p time then it's the closest keyframes before and after \p
+ // time.
+ _KeyFrameRange _GetKeyFrameRange( TsTime time );
+
+ // Returns the range of keyframes including the time interval as non-const
+ // iterators. These are the key frames from the key frame before (not at)
+ // \p leftTime to the key frame after (not at) \p rightTime.
+ _KeyFrameRange _GetKeyFrameRange( TsTime leftTime, TsTime rightTime );
+
+ // Returns the time interval that will be changed by removing a key frame
+ // at the given \p time.
+ GfInterval _FindRemoveKeyFrameChangedInterval(TsTime time);
+
+ // Returns the time interval that will be changed by setting the given
+ // \p keyFrame on the spline.
+ GfInterval _FindSetKeyFrameChangedInterval(const TsKeyFrame &keyFrame);
+
+ // Determine the effective extrapolation for \p keyframe on \p side
+ TsExtrapolationType _GetEffectiveExtrapolationType(
+ const TsKeyFrame &keyFrame,
+ const TsSide &side) const;
+
+ /// The Spline calls these when the loop params have changed.
+ void _LoopParamsChanged(bool loopingChanged, bool valueOffsetChanged,
+ bool domainChanged);
+
+private:
+ friend class TsKeyFrameEvalUtil;
+ friend class TsSpline;
+
+ TsExtrapolationPair _extrapolation;
+ TsLoopParams _loopParams;
+ TsKeyFrameMap _normalKeyFrames;
+ TsKeyFrameMap _loopedKeyFrames;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+import sys
+from pxr import Ts, Tf, Gf
+
+default_knotType = Ts.KnotLinear
+EPSILON = 1e-6
+
+########################################################################
+
+print('\nTest creating keyframe with float time')
+kf1_time = 0
+kf1 = Ts.KeyFrame( kf1_time )
+assert kf1
+assert kf1.time == kf1_time
+assert kf1.value == 0.0
+assert kf1.knotType == default_knotType
+assert not kf1.isDualValued
+assert eval(repr(kf1)) == kf1
+print('\tPassed')
+
+########################################################################
+
+print('\nTest creating 2nd keyframe')
+kf2_time = 20
+kf2 = Ts.KeyFrame( kf2_time )
+assert kf2
+assert kf2.time == kf2_time
+assert kf2.value == 0.0
+assert kf2.knotType == default_knotType
+assert not kf2.isDualValued
+assert eval(repr(kf2)) == kf2
+print('\tPassed')
+
+########################################################################
+
+print('\nTest creating keyframe with keyword arguments')
+kf3_time = 30
+kf3_value = 0.1234
+kf3_knotType = Ts.KnotBezier
+kf3_leftLen = 0.1
+kf3_leftSlope = 0.2
+kf3_rightLen = 0.3
+kf3_rightSlope = 0.4
+kf3 = Ts.KeyFrame( kf3_time, value = kf3_value, knotType = kf3_knotType,
+ leftLen = kf3_leftLen, leftSlope = kf3_leftSlope,
+ rightLen = kf3_rightLen, rightSlope = kf3_rightSlope )
+assert kf3
+assert kf3.time == kf3_time
+assert Gf.IsClose(kf3.value, kf3_value, EPSILON)
+assert kf3.knotType == kf3_knotType
+assert kf3.supportsTangents
+assert Gf.IsClose(kf3.leftLen, kf3_leftLen, EPSILON)
+assert Gf.IsClose(kf3.leftSlope, kf3_leftSlope, EPSILON)
+assert Gf.IsClose(kf3.rightLen, kf3_rightLen, EPSILON)
+assert Gf.IsClose(kf3.rightSlope, kf3_rightSlope, EPSILON)
+assert eval(repr(kf3)) == kf3
+print('\tPassed')
+
+########################################################################
+
+print('\nTest creating dual-valued keyframe')
+kf4_time = 40
+kf4_value = 0.1234
+kf4_knotType = Ts.KnotHeld
+kf4_leftLen = 0.1
+kf4_leftSlope = 0.2
+kf4_rightLen = 0.3
+kf4_rightSlope = 0.4
+kf4 = Ts.KeyFrame( kf4_time, value = kf4_value, knotType = kf4_knotType,
+ leftLen = kf4_leftLen, leftSlope = kf4_leftSlope,
+ rightLen = kf4_rightLen, rightSlope = kf4_rightSlope )
+assert kf4
+assert kf4.time == kf4_time
+assert Gf.IsClose(kf4.value, kf4_value, EPSILON)
+assert kf4.knotType == kf4_knotType
+assert Gf.IsClose(kf4.leftLen, kf4_leftLen, EPSILON)
+assert Gf.IsClose(kf4.leftSlope, kf4_leftSlope, EPSILON)
+assert Gf.IsClose(kf4.rightLen, kf4_rightLen, EPSILON)
+assert Gf.IsClose(kf4.rightSlope, kf4_rightSlope, EPSILON)
+assert not kf4.isDualValued
+assert eval(repr(kf4)) == kf4
+
+kf4.value = ( 1.234, 4.567 )
+assert kf4.isDualValued
+# Setting to a tuple of the wrong size should fail
+
+assert eval(repr(kf4)) == kf4
+
+try:
+ kf4.value = ( 1.234, )
+ assert 0, "should not have worked"
+except ValueError:
+ pass
+try:
+ kf4.value = ( 1., 2., 3. )
+ assert 0, "should not have worked"
+except ValueError:
+ pass
+# Check that setting a single side of a dual-valued knot works
+kf4.value = 999.
+assert Gf.IsClose(kf4.value, (1.234, 999.), EPSILON)
+print('\tPassed')
+
+print('\nTest convenience get/set value API:')
+kf4.SetValue(2.345, Ts.Left)
+assert eval(repr(kf4)) == kf4
+kf4.SetValue(3.456, Ts.Right)
+assert eval(repr(kf4)) == kf4
+assert Gf.IsClose(kf4.value, (2.345, 3.456), EPSILON)
+assert Gf.IsClose(kf4.GetValue(Ts.Left), 2.345, EPSILON)
+assert Gf.IsClose(kf4.GetValue(Ts.Right), 3.456, EPSILON)
+
+print('\nTest that setting bad type on left value does not work: ')
+print('\n=== EXPECTED ERRORS ===', file=sys.stderr)
+oldVal = kf4.value
+badVal = ('foo', oldVal[1])
+assert badVal != oldVal
+assert kf4.value == oldVal
+try:
+ kf4.value = badVal
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+assert kf4.value == oldVal
+assert kf4.value != badVal
+print('=== END EXPECTED ERRORS ===', file=sys.stderr)
+print('\tPassed')
+
+print("\nTest that single -> dual mirrors the signle value to both sides")
+val1 = 1.23
+val2 = 4.56
+kf = Ts.KeyFrame( 0.0, val1 )
+kf.isDualValued = True
+assert kf.value == (val1, val1)
+kf.value = val2
+assert kf.value == (val1, val2)
+kf.isDualValued = False
+kf.isDualValued = True
+assert kf.value == (val2, val2)
+assert eval(repr(kf)) == kf
+print('\tPassed')
+
+########################################################################
+
+print('\nTest non-interpolatable types: errors expected')
+
+testValues = ['string_value', False]
+
+print('\n=== EXPECTED ERRORS ===', file=sys.stderr)
+
+for testValue in testValues:
+
+ print('\t', type(testValue))
+
+ # Test creating keyframes
+ kf5_time = 50
+ kf5_value = testValue
+ kf5_knotType = Ts.KnotHeld
+ kf5 = Ts.KeyFrame( kf5_time, kf5_value, knotType = Ts.KnotHeld )
+
+ assert eval(repr(kf5)) == kf5
+
+ assert kf5
+ assert kf5.time == kf5_time
+ assert kf5.value == kf5_value
+ assert kf5.knotType == kf5_knotType
+ assert not kf5.isDualValued
+
+ # Test that they can't be dual-valued
+ assert not kf5.isDualValued
+ try:
+ kf5.isDualValued = True
+ except Tf.ErrorException:
+ pass
+ else:
+ assert False
+ try:
+ kf5.value = ('left', 'right')
+ assert False, "should have failed"
+ except TypeError:
+ assert not kf5.isDualValued
+ assert kf5.value == kf5_value
+ pass
+
+ # Test that they can only be Held
+ assert kf5.knotType == Ts.KnotHeld
+ try:
+ kf5.knotType = Ts.KnotLinear
+ assert False, "should have failed"
+ except Tf.ErrorException:
+ pass
+ assert kf5.knotType == Ts.KnotHeld
+
+ # Test that they can't have tangents
+ try:
+ Ts.KeyFrame(0, kf5_value, kf5_value, \
+ Ts.KnotBezier, kf5_value, kf5_value, 0, 0)
+ except Tf.ErrorException:
+ pass
+
+# Test that non-interpolatable types only set held knots
+kf_bool = Ts.KeyFrame( 0.0, True, knotType = Ts.KnotLinear )
+assert kf_bool.knotType is Ts.KnotHeld
+assert eval(repr(kf_bool)) == kf_bool
+kf_bool = Ts.KeyFrame( 0.0, True, knotType = Ts.KnotBezier )
+assert kf_bool.knotType is Ts.KnotHeld
+assert eval(repr(kf_bool)) == kf_bool
+del kf_bool
+
+# Test that interpolatable types w/o tangents only set linear knots
+kf_matrix = Ts.KeyFrame( 0.0, Gf.Matrix4d(), knotType = Ts.KnotBezier )
+assert eval(repr(kf_matrix)) == kf_matrix
+assert kf_matrix.knotType is Ts.KnotLinear
+del kf_matrix
+
+print('=== END EXPECTED ERRORS ===', file=sys.stderr)
+
+print('\tPassed')
+
+########################################################################
+
+print('\nTest keyframe equality / inequality')
+expected = [kf1, kf2, kf3, kf4, kf5]
+for i in range(len(expected)):
+ for j in range(i, len(expected)):
+ if i == j:
+ assert expected[i] == expected[j]
+ else:
+ assert expected[i] != expected[j]
+print('\tPassed')
+
+########################################################################
+
+print('\nTest creating keyframe of bogus type')
+print('\n=== EXPECTED ERRORS ===', file=sys.stderr)
+try:
+ # create a keyframe out of the Ts python module
+ Ts.KeyFrame( 0, Ts )
+ assert False, 'should have failed'
+except Tf.ErrorException:
+ pass
+print('=== END EXPECTED ERRORS ===', file=sys.stderr)
+print('\tPassed')
+
+print('\nTest that you cannot change the type of a keyframe: errors expected')
+print('\n=== EXPECTED ERRORS ===', file=sys.stderr)
+testVal = 0.123
+kf = Ts.KeyFrame( 0, testVal )
+assert kf.value == testVal
+try:
+ kf.value = 'foo'
+ assert False, "expected failure"
+except Tf.ErrorException:
+ pass
+assert kf.value == testVal
+del testVal
+del kf
+print('=== END EXPECTED ERRORS ===', file=sys.stderr)
+print('\tPassed')
+
+print('\nTest that you cannot set a keyframe to a non-Vt.Value: ' \
+ 'errors expected')
+print('\n=== EXPECTED ERRORS ===', file=sys.stderr)
+kf = Ts.KeyFrame( 0, 123.0 )
+testVal = 0.123
+kf = Ts.KeyFrame( 0, testVal )
+assert kf.value == testVal
+try:
+ kf.value = Gf # can't store a python module as a Vt.Value
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+assert kf.value == testVal
+del testVal
+del kf
+print('=== END EXPECTED ERRORS ===', file=sys.stderr)
+print('\tPassed')
+
+########################################################################
+
+print('\nTest that string-value knots do not have tangents: errors expected')
+kf = Ts.KeyFrame( 0, 'foo', Ts.KnotHeld)
+assert not kf.supportsTangents
+# Do the code coverage dance
+print('\n=== EXPECTED ERRORS ===', file=sys.stderr)
+try:
+ kf.leftLen = 0
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ kf.rightLen = 0
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ kf.leftSlope = 0
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ kf.rightSlope = 0
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ assert kf.leftLen == 0
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ assert kf.rightLen == 0
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ assert kf.leftSlope.empty
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ assert kf.rightSlope.empty
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ kf.tangentSymmetryBroken = 0
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+try:
+ assert not kf.tangentSymmetryBroken
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+print('=== END EXPECTED ERRORS ===', file=sys.stderr)
+print('\tPassed')
+
+print('\nTest that setting tangents to value of wrong type does not work: ' \
+ 'errors expected')
+kf = Ts.KeyFrame( 0, 123.0 )
+assert kf.supportsTangents
+assert kf.leftSlope == 0
+print('\n=== EXPECTED ERRORS ===', file=sys.stderr)
+try:
+ kf.leftSlope = 'foo'
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+assert kf.leftSlope == 0
+assert kf.rightSlope == 0
+try:
+ kf.rightSlope = 'foo'
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+assert kf.rightSlope == 0
+print('=== END EXPECTED ERRORS ===', file=sys.stderr)
+assert eval(repr(kf)) == kf
+print('\tPassed')
+
+print('\nTest tangent interface')
+target_time = 1.2
+target_value = 3.4
+kf = Ts.KeyFrame( target_time, target_value )
+target_leftLen = 0.1
+target_leftSlope = 0.2
+target_rightLen = 0.3
+target_rightSlope = 0.4
+kf.leftLen = target_leftLen
+assert eval(repr(kf)) == kf
+kf.rightLen = target_rightLen
+assert eval(repr(kf)) == kf
+kf.leftSlope = target_leftSlope
+assert eval(repr(kf)) == kf
+kf.rightSlope = target_rightSlope
+assert eval(repr(kf)) == kf
+assert Gf.IsClose(kf.leftLen, target_leftLen, EPSILON)
+assert Gf.IsClose(kf.leftSlope, target_leftSlope, EPSILON)
+assert Gf.IsClose(kf.rightLen, target_rightLen, EPSILON)
+assert Gf.IsClose(kf.rightSlope, target_rightSlope, EPSILON)
+kf.knotType = Ts.KnotBezier
+kf.tangentSymmetryBroken = 1
+assert kf.tangentSymmetryBroken
+kf.tangentSymmetryBroken = 0
+assert not kf.tangentSymmetryBroken
+assert eval(repr(kf)) == kf
+print('\tPassed')
+
+print('\nTest whether the test for having tangents works')
+kft_time = 30
+kft_value = 0.1234
+kft_knotType = Ts.KnotBezier
+kft_leftLen = 0.1
+kft_leftSlope = 0.2
+kft_rightLen = 0.3
+kft_rightSlope = 0.4
+kft = Ts.KeyFrame( kft_time, value = kft_value, knotType = kft_knotType,
+ leftLen = kft_leftLen, leftSlope = kft_leftSlope,
+ rightLen = kft_rightLen, rightSlope = kft_rightSlope )
+assert eval(repr(kft)) == kft
+
+kfnt_knotType = Ts.KnotHeld
+kfnt = Ts.KeyFrame( kft_time, value = kft_value, knotType = kfnt_knotType,
+ leftLen = kft_leftLen, leftSlope = kft_leftSlope,
+ rightLen = kft_rightLen, rightSlope = kft_rightSlope )
+assert eval(repr(kfnt)) == kfnt
+
+assert kft.hasTangents
+assert not kfnt.hasTangents
+print('\tPassed')
+
+print('\nTest keyframe ordering')
+knots = [kf1, kf2, kf3, kf4, kf5]
+for i in range(len(knots)):
+ assert knots[i].time == knots[i].time
+ assert knots[i].time <= knots[i].time
+ assert not (knots[i].time < knots[i].time)
+ assert not (knots[i].time > knots[i].time)
+ for j in range(i+1, len(knots)):
+ assert knots[i].time != knots[j].time
+ assert knots[i].time < knots[j].time
+ assert not (knots[j].time < knots[i].time)
+print('\tPassed')
+
+########################################################################
+
+print('\nTest CanSetKnotType')
+
+# Types like doubles support all knot types.
+kf = Ts.KeyFrame(0.0, 0.0, Ts.KnotHeld)
+assert kf.CanSetKnotType(Ts.KnotBezier)
+assert kf.CanSetKnotType(Ts.KnotHeld)
+assert kf.CanSetKnotType(Ts.KnotLinear)
+assert eval(repr(kf)) == kf
+
+# Some types do not support tangents, but are interpolatable.
+kf = Ts.KeyFrame(0.0, Gf.Vec3d(), Ts.KnotHeld)
+assert not kf.CanSetKnotType(Ts.KnotBezier)
+assert kf.CanSetKnotType(Ts.KnotHeld)
+assert kf.CanSetKnotType(Ts.KnotLinear)
+assert eval(repr(kf)) == kf
+
+# Some types do not support interpolation or tangents.
+kf = Ts.KeyFrame(0.0, "foo", Ts.KnotHeld)
+assert not kf.CanSetKnotType(Ts.KnotBezier)
+assert kf.CanSetKnotType(Ts.KnotHeld)
+assert not kf.CanSetKnotType(Ts.KnotLinear)
+assert eval(repr(kf)) == kf
+
+print('\tPassed')
+
+########################################################################
+
+print('\nTest Side based equivalence')
+
+#Control knot
+kf_time = 5
+kf_value = 10.0
+kf_knotType = Ts.KnotBezier
+kf_leftSlope = 0.5
+kf_leftLen = 1
+kf_rightSlope = 0.7
+kf_rightLen = 3
+kf = Ts.KeyFrame(kf_time, kf_value, kf_knotType, kf_leftSlope, kf_rightSlope,
+ kf_leftLen, kf_rightLen)
+assert eval(repr(kf)) == kf
+
+#Same knot (equivalent both sides)
+kfNew = Ts.KeyFrame(kf_time, kf_value, kf_knotType, kf_leftSlope, kf_rightSlope,
+ kf_leftLen, kf_rightLen)
+assert kf == kfNew
+assert kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Same knot with a different time (not equivalent)
+kfNew = Ts.KeyFrame(20, kf_value, kf_knotType, kf_leftSlope, kf_rightSlope,
+ kf_leftLen, kf_rightLen)
+assert kf.time != kfNew.time
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Same knot with a different value (not equivalent)
+kfNew = Ts.KeyFrame(kf_time, 20, kf_knotType, kf_leftSlope, kf_rightSlope,
+ kf_leftLen, kf_rightLen)
+assert kf.value != kfNew.value
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Same knot with a different knot type (not equivalent)
+kfNew = Ts.KeyFrame(kf_time, kf_value, Ts.KnotLinear, kf_leftSlope, kf_rightSlope,
+ kf_leftLen, kf_rightLen)
+assert kf.knotType != kfNew.knotType
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Same knot with a different left tangent slope (right equivalent)
+kfNew = Ts.KeyFrame(kf_time, kf_value, kf_knotType, 0.25, kf_rightSlope,
+ kf_leftLen, kf_rightLen)
+assert kf.leftSlope != kfNew.leftSlope
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Same knot with a different left tangent length (right equivalent)
+kfNew = Ts.KeyFrame(kf_time, kf_value, kf_knotType, kf_leftSlope, kf_rightSlope,
+ 2, kf_rightLen)
+assert kf.leftLen != kfNew.leftLen
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Same knot with a different right tangent slope (left equivalent)
+kfNew = Ts.KeyFrame(kf_time, kf_value, kf_knotType, kf_leftSlope, 0.25,
+ kf_leftLen, kf_rightLen)
+assert kf.rightSlope != kfNew.rightSlope
+assert kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Same knot with a different right tangent length (left equivalent)
+kfNew = Ts.KeyFrame(kf_time, kf_value, kf_knotType, kf_leftSlope, kf_rightSlope,
+ kf_leftLen, 2)
+assert kf.rightLen != kfNew.rightLen
+assert kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Dual valued knots (Both sides equal)
+kfNew = Ts.KeyFrame(kf_time, kf_value, kf_value, kf_knotType, kf_leftSlope,
+ kf_rightSlope, kf_leftLen, kf_rightLen)
+assert kfNew.isDualValued
+assert kf.GetValue(Ts.Left) == kfNew.GetValue(Ts.Left)
+assert kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert kf.GetValue(Ts.Right) == kfNew.GetValue(Ts.Right)
+assert kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Dual valued knots (Left side equal)
+kfNew = Ts.KeyFrame(kf_time, kf_value, 15.0, kf_knotType, kf_leftSlope,
+ kf_rightSlope, kf_leftLen, kf_rightLen)
+assert kfNew.isDualValued
+assert kf.GetValue(Ts.Left) == kfNew.GetValue(Ts.Left)
+assert kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert kf.GetValue(Ts.Right) != kfNew.GetValue(Ts.Right)
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+#Dual valued knots (Right side equal)
+kfNew = Ts.KeyFrame(kf_time, 15.0, kf_value, kf_knotType, kf_leftSlope,
+ kf_rightSlope, kf_leftLen, kf_rightLen)
+assert kfNew.isDualValued
+assert kf.GetValue(Ts.Left) != kfNew.GetValue(Ts.Left)
+assert not kf.IsEquivalentAtSide(kfNew, Ts.Left)
+assert kf.GetValue(Ts.Right) == kfNew.GetValue(Ts.Right)
+assert kf.IsEquivalentAtSide(kfNew, Ts.Right)
+
+print('\tPassed')
+
+########################################################################
+print('\nTest eval(repr(...))')
+kf = Ts.KeyFrame(0.7, 15.0, Ts.KnotBezier,
+ leftSlope = 1.0, rightSlope = 2.0,
+ leftLen = 1.2, rightLen = 3.4)
+
+# Test single and dual valued
+assert eval(repr(kf)) == kf
+kf.value = (3.4,5.6)
+assert eval(repr(kf)) == kf
+
+########################################################################
+#
+print('\nTest SUCCEEDED')
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr import Ts, Gf
+
+EPSILON = 1e-6
+
+def CreateBeforeSpline():
+ beforeSpline = Ts.Spline()
+
+ # Set up the before spline
+ keyFramesBefore = [
+ Ts.KeyFrame(0.0, 0.0, Ts.KnotBezier,
+ leftSlope=0,
+ rightSlope=0,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333),
+ Ts.KeyFrame(1, 2.491507846458525, Ts.KnotBezier,
+ leftSlope=2.992427434859666,
+ rightSlope=2.992427434859666,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333),
+ Ts.KeyFrame(2, 5.38290365439421, Ts.KnotBezier,
+ leftSlope=3.152460322228518,
+ rightSlope=3.152460322228518,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333),
+ Ts.KeyFrame(3, 9.043661089113483, Ts.KnotBezier,
+ leftSlope=4.410088992919291,
+ rightSlope=4.410088992919291,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333),
+ Ts.KeyFrame(4, 14.723785002052717, Ts.KnotBezier,
+ leftSlope=6.333864006799226,
+ rightSlope=6.333864006799226,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333),
+ Ts.KeyFrame(5, 20.71267999005244, Ts.KnotBezier,
+ leftSlope=4.866350481683961,
+ rightSlope=4.866350481683961,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333),
+ Ts.KeyFrame(6, 24.01397216143913, Ts.KnotBezier,
+ leftSlope=0.4713087936971686,
+ rightSlope=0.4713087936971686,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333),
+ Ts.KeyFrame(7, 24.231251085355073, Ts.KnotBezier,
+ leftSlope=0,
+ rightSlope=0,
+ leftLen=0.3333333333333333,
+ rightLen=0.3333333333333333)]
+
+ for kf in keyFramesBefore:
+ beforeSpline.SetKeyFrame(kf)
+
+ return beforeSpline
+
+def CompareWithResults(beforeSpline):
+ # Set up the after keys
+ keyFramesAfter = [
+ Ts.KeyFrame(0.0, 0.0, Ts.KnotBezier,
+ leftSlope=2.491507846458525,
+ rightSlope=2.491507846458525,
+ leftLen=0.3333333333333333,
+ rightLen=0.10008544555664062),
+ Ts.KeyFrame(3.0, 9.043661089113483, Ts.KnotBezier,
+ leftSlope=4.670440673829253,
+ rightSlope=4.670440673829253,
+ leftLen=0.8931911022949219,
+ rightLen=0.8875199291992186),
+ Ts.KeyFrame(5.0, 20.71267999005244, Ts.KnotBezier,
+ leftSlope=4.645093579693207,
+ rightSlope=4.645093579693207,
+ leftLen=0.6600031860351563,
+ rightLen=0.9999050708007813),
+ Ts.KeyFrame(7.0, 24.231251085355073, Ts.KnotBezier,
+ leftSlope=0.2172789239159414,
+ rightSlope=0.2172789239159414,
+ leftLen=0.6857100512695311,
+ rightLen=0.3333333333333333)]
+
+ # Check that the passed-in simplified spline matches the expected key frames
+ for i, t in enumerate(beforeSpline.keys()):
+ assert beforeSpline[t].time == keyFramesAfter[i].time
+ assert beforeSpline[t].knotType == keyFramesAfter[i].knotType
+ assert Gf.IsClose(
+ beforeSpline[t].value, keyFramesAfter[i].value, EPSILON)
+ assert Gf.IsClose(
+ beforeSpline[t].leftLen, keyFramesAfter[i].leftLen, EPSILON)
+ assert Gf.IsClose(
+ beforeSpline[t].rightLen, keyFramesAfter[i].rightLen, EPSILON)
+
+# Test Simplify API. This is not meant to be a deep test of Simplify.
+def TestSimplify():
+
+ beforeSpline0 = CreateBeforeSpline()
+
+ # Test non parallel api
+ Ts.SimplifySpline(beforeSpline0,
+ Gf.MultiInterval(Gf.Interval(0, 7)), 0.003)
+
+ CompareWithResults(beforeSpline0)
+
+ # Test parallel api
+ beforeSpline0 = CreateBeforeSpline()
+ beforeSpline1 = CreateBeforeSpline()
+
+ Ts.SimplifySplinesInParallel([beforeSpline0, beforeSpline1],
+ [Gf.MultiInterval(Gf.Interval(0, 7)),
+ Gf.MultiInterval(Gf.Interval(0, 7))],
+ 0.003)
+
+ CompareWithResults(beforeSpline0)
+ CompareWithResults(beforeSpline1)
+
+ # Test parallel api, with an empty interval list (should process the
+ # wholes splines)
+ beforeSpline0 = CreateBeforeSpline()
+ beforeSpline1 = CreateBeforeSpline()
+
+ Ts.SimplifySplinesInParallel([beforeSpline0, beforeSpline1],
+ [],
+ 0.003)
+
+ CompareWithResults(beforeSpline0)
+ CompareWithResults(beforeSpline1)
+
+TestSimplify()
+
+print('\nTest SUCCEEDED')
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr import Ts, Tf, Gf
+import contextlib
+
+EPSILON = 1e-6
+
+@contextlib.contextmanager
+def _RequiredException(exceptionType):
+ try:
+ # Execute code block.
+ yield
+ except exceptionType:
+ # Got expected exception, continue.
+ pass
+ except:
+ # Got unexpected exception, re-raise.
+ raise
+ else:
+ # Got no exception, raise exception.
+ raise Exception("required exception not raised")
+
+def quatIsClose(a, b):
+ assert(Gf.IsClose(a.real, b.real, EPSILON))
+ assert(Gf.IsClose(a.imaginary, b.imaginary, EPSILON))
+ return True
+
+def createSpline(keys, values, types, tangentSlope=[], tangentLength=[]):
+ lenKeys = len(keys)
+ assert(lenKeys == len(values))
+ assert(lenKeys == len(types))
+ assert(lenKeys == len(types))
+
+ hasTangents = len(tangentSlope) > 0 and len(tangentLength) > 0
+ if hasTangents:
+ assert(2 * lenKeys == len(tangentSlope))
+ assert(2 * lenKeys == len(tangentLength))
+
+ s = Ts.Spline()
+ # add keyframes
+ for index in range(len(keys)):
+ kf = Ts.KeyFrame(keys[index], values[index], types[index])
+ if hasTangents and kf.supportsTangents:
+ kf.leftSlope = tangentSlope[2*index]
+ kf.rightSlope = tangentSlope[2*index+1]
+
+ kf.leftLen = tangentLength[2*index]
+ kf.rightLen = tangentLength[2*index+1]
+ s.SetKeyFrame(kf)
+
+ # check Eval() to make sure keyframes were added correctly
+ for index in range(len(keys)):
+ assert(s.Eval(keys[index]) == values[index])
+
+ return s
+
+default_knotType = Ts.KnotBezier
+
+def verifyNegativeCanResult(res):
+ assert bool(res) == False
+ assert type(res.reasonWhyNot) == str
+ assert len(res.reasonWhyNot) > 0
+
+########################################################################
+# Test API unique to Ts.Spline:
+
+def TestTsSpline():
+ print('\nTest setting an TsSpline as a dict')
+ v = Ts.Spline()
+ v1 = Ts.Spline( {0:0.0, 100:10.0}, Ts.KnotLinear )
+ v2 = Ts.Spline( \
+ [ Ts.KeyFrame(0, 0.0, Ts.KnotLinear),
+ Ts.KeyFrame(100, 10.0, Ts.KnotLinear) ] )
+ assert v1 == v2
+ assert v1 != v
+ assert v2 != v
+ del v1
+ del v2
+ with _RequiredException(TypeError):
+ # Test incorrect time type
+ Ts.Spline( {'blah':0}, Ts.KnotLinear )
+ with _RequiredException(Tf.ErrorException):
+ # Test value type mismatch
+ Ts.Spline( {0:0.0, 1:'blah'}, Ts.KnotLinear )
+ print('\tPassed')
+
+ print('\nTest copy constructor, and constructor w/ extrapolation')
+ v = Ts.Spline( \
+ [ Ts.KeyFrame(0, 0.0, Ts.KnotLinear),
+ Ts.KeyFrame(100, 10.0, Ts.KnotLinear) ],
+ Ts.ExtrapolationHeld,
+ Ts.ExtrapolationLinear )
+ assert v.extrapolation[0] == Ts.ExtrapolationHeld
+ assert v.extrapolation[1] == Ts.ExtrapolationLinear
+ assert v == Ts.Spline(v)
+ assert v == eval(repr(v))
+
+ print('\nTest float Breakdown() on empty spline')
+ del v[:]
+ v.Breakdown(1, Ts.KnotHeld, True, 1.0)
+ assert(1 in v)
+ assert(v.Eval(1) == 0.0)
+ assert v == eval(repr(v))
+ print('\tPassed')
+
+TestTsSpline()
+
+########################################################################
+
+def TestBezierDerivative():
+ range1 = list(range(2,6))
+ range2 = list(range(6,10))
+ rangeTotal = list(range(2, 10))
+
+ print("Start bezier derivative test:")
+ spline = createSpline([1, 10],
+ [0.0, 0.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [1.0, 1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0])
+
+ print("\tTest EvalDerivative at keyframes")
+ spline.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ assert(spline.EvalDerivative(1, Ts.Right) == 1.0)
+ assert(spline.EvalDerivative(1, Ts.Left) == 1.0)
+ assert(spline.EvalDerivative(10, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(10, Ts.Left) == -1.0)
+ spline.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ assert(spline.EvalDerivative(1, Ts.Right) == 1.0)
+ assert(spline.EvalDerivative(1, Ts.Left) == 1.0)
+ assert(spline.EvalDerivative(10, Ts.Right) == -1.0)
+ assert(spline.EvalDerivative(10, Ts.Left) == -1.0)
+
+ print("\tTest EvalDerivative between keyframes")
+ # generate the calculated derivatives
+ calcDerivs = []
+ for time in range1:
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Right))
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Left))
+ for time in range2:
+ calcDerivs.append(0.0)
+ calcDerivs.append(0.0)
+
+ # perform explicit breakdowns at the desired points
+ for time in range1:
+ spline.Breakdown(time, Ts.KnotBezier, False, 1.0)
+
+ for time in range2:
+ spline.Breakdown(time, Ts.KnotBezier, True, 0.9)
+
+ # generate the derivates calculated from breakdowns
+ breakdownDerivs = []
+ for time in rangeTotal:
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Right))
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Left))
+
+ # compare values
+ assert(len(calcDerivs) == len(breakdownDerivs))
+ for index in range(len(calcDerivs)):
+ assert(Gf.IsClose(calcDerivs[index], breakdownDerivs[index], EPSILON))
+
+ print("\tPassed")
+
+TestBezierDerivative()
+
+# ################################################################################
+
+def TestLinearDerivative():
+ print("Start linear derivative test:")
+
+ range1 = list(range(2,5))
+ range2 = list(range(6,10))
+ rangeTotal = (2, 3, 4, 6, 7, 8, 9)
+
+ spline = createSpline([1, 5, 10],
+ [0.0, 1.0, 0.0],
+ [Ts.KnotLinear, Ts.KnotLinear, Ts.KnotLinear])
+
+ print("\tTest EvalDerivative at keyframes")
+ spline.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ assert(spline.EvalDerivative(1, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(1, Ts.Left) == 0.0)
+ assert(spline.EvalDerivative(5, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(5, Ts.Left) == 0.0)
+ assert(spline.EvalDerivative(10, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(10, Ts.Left) == 0.0)
+ spline.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ assert(spline.EvalDerivative(1, Ts.Right) == 0.25)
+ assert(spline.EvalDerivative(1, Ts.Left) == 0.25)
+ assert(spline.EvalDerivative(5, Ts.Right) == 0.25)
+ assert(spline.EvalDerivative(5, Ts.Left) == -0.2)
+ assert(spline.EvalDerivative(10, Ts.Right) == -0.2)
+ assert(spline.EvalDerivative(10, Ts.Left) == -0.2)
+
+ # generate the calculated derivatives
+ calcDerivs = []
+ for time in range1:
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Right))
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Left))
+ for time in range2:
+ calcDerivs.append(-0.2)
+ calcDerivs.append(-0.2)
+
+ # perform explicit breakdowns at the desired points
+ for time in range1:
+ spline.Breakdown(time, Ts.KnotLinear, False, 1.0)
+ for time in range2:
+ spline.Breakdown(time, Ts.KnotLinear, True, 1.0)
+
+ # generate the derivates calculated from breakdowns
+ breakdownDerivs = []
+ for time in rangeTotal:
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Right))
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Left))
+
+ # compare values
+ assert(len(calcDerivs) == len(breakdownDerivs))
+ for index in range(len(calcDerivs)):
+ assert(Gf.IsClose(calcDerivs[index], breakdownDerivs[index], EPSILON))
+
+ print("\tPASSED")
+
+TestLinearDerivative()
+
+# ################################################################################
+
+def TestHeldDerivative():
+ print("Start held derivative test:")
+
+ range1 = list(range(2,5))
+ range2 = list(range(6,10))
+ rangeTotal = (2, 3, 4, 6, 7, 8, 9)
+
+ spline = createSpline([1, 5, 10],
+ [0.0, 1.0, 2.0],
+ [Ts.KnotHeld, Ts.KnotHeld, Ts.KnotHeld])
+
+ print("\tTest EvalDerivative at keyframes")
+ spline.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ assert(spline.EvalDerivative(1, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(1, Ts.Left) == 0.0)
+ assert(spline.EvalDerivative(5, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(5, Ts.Left) == 0.0)
+ assert(spline.EvalDerivative(10, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(10, Ts.Left) == 0.0)
+ spline.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ assert(spline.EvalDerivative(1, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(1, Ts.Left) == 0.0)
+ assert(spline.EvalDerivative(5, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(5, Ts.Left) == 0.0)
+ assert(spline.EvalDerivative(10, Ts.Right) == 0.0)
+ assert(spline.EvalDerivative(10, Ts.Left) == 0.0)
+
+ print("\tTest EvalDerivative between keyframes")
+ # generate the calculated derivatives
+ calcDerivs = []
+ for time in range1:
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Left))
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Right))
+ for time in range2:
+ calcDerivs.append(0.0)
+ calcDerivs.append(0.0)
+
+ # perform explicit breakdowns at the desired points
+ for time in range1:
+ spline.Breakdown(time, Ts.KnotHeld, False, 1.0)
+ for time in range2:
+ spline.Breakdown(time, Ts.KnotHeld, True, 1.0)
+
+ # generate the derivates calculated from breakdowns
+ breakdownDerivs = []
+ for time in rangeTotal:
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Left))
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Right))
+
+ # compare values
+ assert(len(calcDerivs) == len(breakdownDerivs))
+ for index in range(len(calcDerivs)):
+ assert(Gf.IsClose(calcDerivs[index], breakdownDerivs[index], EPSILON))
+
+ print("\tPASSED")
+
+TestHeldDerivative()
+
+################################################################################
+
+def TestBlendDerivative():
+ range1 = list(range(2,5))
+ range2 = list(range(6,10))
+ rangeTotal = (2, 3, 4, 6, 7, 8, 9)
+
+ print("Start blend derivative test:")
+ spline = createSpline([1, 10], [0.0, 0.0], [Ts.KnotBezier, Ts.KnotHeld], [1.0, 1.0, -1.0, -1.0], [1.0, 1.0, 1.0, 1.0])
+
+ print("\tTest EvalDerivative at a keyframe")
+ assert(spline.EvalDerivative(1) == 1.0)
+ assert(spline.EvalDerivative(10) == 0.0)
+
+ print("\tTest EvalDerivative between keyframes")
+ # generate the calculated derivatives
+ calcDerivs = []
+ for time in range1:
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Left))
+ calcDerivs.append(spline.EvalDerivative(time, Ts.Right))
+ for time in range2:
+ calcDerivs.append(0.0)
+ calcDerivs.append(0.0)
+
+ # perform explicit breakdowns at the desired points
+ for time in range1:
+ spline.Breakdown(time, Ts.KnotBezier, False, 1.0)
+ for time in range2:
+ spline.Breakdown(time, Ts.KnotBezier, True, 1.0)
+
+ # generate the derivates calculated from breakdowns
+ breakdownDerivs = []
+ for time in rangeTotal:
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Left))
+ breakdownDerivs.append(spline.EvalDerivative(time, Ts.Right))
+
+ # XXX There might be a bug in Breakdown, so this test is disabled for now
+ # # compare values
+ # assert(len(calcDerivs) == len(breakdownDerivs))
+ # for index in range(len(calcDerivs)):
+ # assert(Gf.IsClose(calcDerivs[index], breakdownDerivs[index]))
+
+ print("\tTest EvalDerivative before first keyframe")
+ assert(spline.EvalDerivative(0) == 0.0)
+
+ print("\tTest EvalDerivative after last keyframe")
+ assert(spline.EvalDerivative(10) == 0.0)
+
+ print("\tPASSED")
+
+TestBlendDerivative()
+
+########################################################################
+def TestQuaternion():
+ print("Start quaternion test:")
+
+ quat1 = Gf.Quatd(0,1,2,3)
+ quat2 = Gf.Quatd(4,5,6,7)
+ quat3 = Gf.Quatd(-1,-2,-3,-4)
+
+ quatZero = Gf.Quatd(0, 0, 0, 0)
+
+ quats = [quat1, quat2, quat3]
+
+ evalFrames = [0, 0.1, 0.5, 1.0, 2.0, 2.3875, 3.0]
+ correctValuesHeld = [
+ quat1,
+ quat1,
+ quat1,
+ quat2,
+ quat2,
+ quat2,
+ quat3]
+ correctValuesLinear = [
+ quat1,
+ Gf.Slerp(0.1, quat1, quat2),
+ Gf.Slerp(0.5, quat1, quat2),
+ quat2,
+ Gf.Slerp(0.5, quat2, quat3),
+ Gf.Slerp((2.3875 - 1.0) / (3.0 - 1.0), quat2, quat3),
+ quat3]
+
+ quatSplineHeld = createSpline([0, 1, 3],
+ [quat1, quat2, quat3],
+ [Ts.KnotHeld, Ts.KnotHeld, Ts.KnotHeld])
+
+ heldSplineValues = list(quatSplineHeld.values())
+ assert(len(heldSplineValues) == len(quats))
+
+ # we expect to receive an exception when accesing slope values on
+ # non-tangential keyframe types
+ with _RequiredException(Tf.ErrorException):
+ heldSplineValues[0].leftSlope
+ with _RequiredException(Tf.ErrorException):
+ heldSplineValues[0].rightSlope
+
+ for ind, key in enumerate(heldSplineValues):
+ assert(key.value == quats[ind])
+
+ for ind in range(len(evalFrames)):
+ frame = evalFrames[ind]
+ assert(quatIsClose(quatSplineHeld.Eval(frame), correctValuesHeld[ind]))
+ assert(quatIsClose(quatSplineHeld.EvalDerivative(frame), quatZero))
+
+ quatSplineLinear = createSpline([0, 1, 3],
+ [quat1, quat2, quat3],
+ [Ts.KnotLinear, Ts.KnotLinear, Ts.KnotLinear])
+ linearSplineValues = list(quatSplineLinear.values())
+ assert(len(linearSplineValues) == len(quats))
+ for ind, key in enumerate(heldSplineValues):
+ assert(key.value == quats[ind])
+
+ for ind, frame in enumerate(evalFrames):
+ assert(quatIsClose(quatSplineLinear.Eval(frame), correctValuesLinear[ind]))
+ assert(quatIsClose(quatSplineLinear.EvalDerivative(frame), quatZero))
+
+ # The current behavior of KeyFrame initialization is that if instantiated with
+ # knots unsupported by the key frame's type, then it will auto-correct it to
+ # a supported type.
+ #
+ # see Ts/KeyFrame.cpp:94
+ quat4 = Gf.Quatd(0,1,2,3)
+ bezierKf = Ts.KeyFrame(0.0, quat4, Ts.KnotBezier)
+ assert(bezierKf.knotType == Ts.KnotLinear)
+
+ # test linear extrapolation
+ kfs = [Ts.KeyFrame(1, Gf.Quatd(1,2,3,4), Ts.KnotLinear), Ts.KeyFrame(2, Gf.Quatd(2,3,4,5), Ts.KnotLinear)]
+ linearExtrapolationSpline = Ts.Spline(kfs, Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ quatExpectedResult = Gf.Quatd(1,2,3,4)
+ assert(quatIsClose(linearExtrapolationSpline.Eval(0), quatExpectedResult))
+ quatExpectedResult = Gf.Quatd(2,3,4,5)
+ assert(quatIsClose(linearExtrapolationSpline.Eval(20), quatExpectedResult))
+
+ # test held extrapolation
+ kfs = [Ts.KeyFrame(1, Gf.Quatd(1,2,3,4), Ts.KnotHeld), Ts.KeyFrame(2, Gf.Quatd(2,3,4,5), Ts.KnotHeld)]
+ heldExtrapolationSpline = Ts.Spline(kfs, Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ quatExpectedResult = Gf.Quatd(1,2,3,4)
+ assert(quatIsClose(heldExtrapolationSpline.Eval(0), quatExpectedResult))
+ quatExpectedResult = Gf.Quatd(2,3,4,5)
+ assert(quatIsClose(linearExtrapolationSpline.Eval(20), quatExpectedResult))
+
+ print("\tPASSED")
+
+TestQuaternion()
+
+########################################################################
+
+def TestBreakdown():
+ print("Start breakdown test:")
+
+ def VerifyBreakdown(t, flatTangents, value=None):
+ spline = createSpline([0, 10],
+ [0.0, 1.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0])
+
+ # Save some values for later verification.
+ ldy = spline.EvalDerivative(t, Ts.Left)
+ rdy = spline.EvalDerivative(t, Ts.Right)
+
+ # Build arg list for Breakdown() call.
+ args = [t, Ts.KnotBezier, flatTangents, 3.0]
+ if (value):
+ args.append(value)
+ else:
+ value = spline.Eval(t)
+
+ # Breakdown at a new frame.
+ assert t not in spline
+ newKf = spline.Breakdown(*args)
+ assert t in spline
+
+ # Verify resulting keyframe value.
+ assert spline[t] == newKf
+ assert spline[t].value == value
+
+ # Verify tangents.
+ if (flatTangents):
+ assert spline[t].leftLen == 3.0
+ assert spline[t].rightLen == 3.0
+ assert spline.EvalDerivative(t, Ts.Left) == 0.0
+ assert spline.EvalDerivative(t, Ts.Right) == 0.0
+ else:
+ assert Gf.IsClose(spline[t].leftSlope, ldy, EPSILON)
+ assert Gf.IsClose(spline[t].rightSlope, rdy, EPSILON)
+
+ print("\tTest breakdown with flat tangents")
+ VerifyBreakdown(7.5, flatTangents=True)
+
+ print("\tTest breakdown with flat tangents, specifying value")
+ VerifyBreakdown(7.5, flatTangents=True, value=12.34)
+
+ print("\tTest breakdown with auto tangents")
+ VerifyBreakdown(2.5, flatTangents=False)
+
+ print("\tTest breakdown with auto tangents, specifying value")
+ VerifyBreakdown(2.5, flatTangents=False, value=12.34)
+
+TestBreakdown()
+
+def TestBreakdownWithExtrapolation():
+ print("Start breakdown with linear extrapolation test:")
+
+ for leftExtrapolation in [
+ Ts.ExtrapolationHeld,
+ Ts.ExtrapolationLinear]:
+ for rightExtrapolation in [
+ Ts.ExtrapolationHeld,
+ Ts.ExtrapolationLinear]:
+
+ spline = Ts.Spline(
+ [Ts.KeyFrame(100.0, 20.0, Ts.KnotBezier,
+ leftSlope=-3.0, rightSlope=-5.0,
+ leftLen=0.9, rightLen=0.9),
+ Ts.KeyFrame(200.0, 30.0, Ts.KnotBezier,
+ leftSlope=4.0, rightSlope=6.0,
+ leftLen=0.9, rightLen=0.9)],
+ leftExtrapolation,
+ rightExtrapolation,
+ Ts.LoopParams())
+
+ for frame in [0.0, 300.0]:
+ breakdownSpline = Ts.Spline(spline)
+ breakdownSpline.Breakdown(
+ frame, Ts.KnotBezier, False, 0.9)
+
+ # Test that the broken down spline evaluates to the same
+ # number as the original spline.
+
+ # Caveat: When we are in held extrapolation and do a breakdown
+ # to left of all knots and the left most knot had a non-flat
+ # tangent, the spline will actually change. It used to be
+ # constant on the interval between the time the breakdown and
+ # the left most knot due to held extrapolation, but now is
+ # influenced by the non-flat tangent of that knot.
+ # This behaviour seems to be desired.
+
+ # Testing for points outside the interval where the splines
+ # might differ.
+ for t in [-100.0, 0.0, 100.0, 150.0,
+ 200.0, 300.0, 400.0]:
+
+ assert(spline.Eval(t) == breakdownSpline.Eval(t))
+
+ # Testing points inside that interval when we don't have held
+ # extrapolation.
+ if leftExtrapolation == Ts.ExtrapolationLinear:
+ t = 50.0
+ assert(spline.Eval(t) == breakdownSpline.Eval(t))
+
+ if rightExtrapolation == Ts.ExtrapolationLinear:
+ t = 250.0
+ assert(spline.Eval(t) == breakdownSpline.Eval(t))
+
+TestBreakdownWithExtrapolation()
+
+def TestBreakdownMultiple():
+ print("Start breakdown multiple test:")
+
+ spline = createSpline([0, 10],
+ [0.0, 1.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0])
+ times = [3.0, 6.0, 12.0, 19.0]
+ values = [0.5, 0.8, 2.0, -9.0]
+ tangentLength = 1.0
+ flatTangents = False
+ spline.Breakdown(times, Ts.KnotBezier, flatTangents, tangentLength, values)
+
+ for i, time in enumerate(times):
+ assert time in spline
+ assert spline[time].value == values[i]
+
+TestBreakdownMultiple()
+
+def TestBreakdownMultipleKnotTypes():
+ print("Start breakdown multiple knot types test:")
+
+ spline = createSpline([0, 10],
+ [0.0, 1.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0])
+ times = [3.0, 6.0, 19.0]
+ values = [0.5, 0.8, -9.0]
+ types = [Ts.KnotBezier, Ts.KnotLinear, Ts.KnotHeld]
+ tangentLength = 1.0
+ flatTangents = False
+ spline.Breakdown(times, types, flatTangents, tangentLength, values)
+
+ for i, time in enumerate(times):
+ assert time in spline
+ assert spline[time].value == values[i]
+ assert spline[time].knotType == types[i]
+
+TestBreakdownMultipleKnotTypes()
+
+
+def TestBug12502():
+ # Verify breakdown behavior on empty splines.
+ print("Start test for breakdown on empty splines:")
+
+ def VerifyFirstBreakdown(s, t, flatTangents, length):
+ s.Breakdown(t, Ts.KnotBezier, flatTangents, length)
+ assert t in spline
+ assert s[t].leftLen == length
+ assert s[t].rightLen == length
+
+ for ft in [True, False]:
+ print("\tTest breakdown with flat tangents:", ft)
+
+ spline = Ts.Spline()
+ assert len(list(spline.keys())) == 0
+
+ # Break down an empty spline with flat tangents. This should create a
+ # keyframe with tangents of the given length.
+ VerifyFirstBreakdown(spline, 5, flatTangents=ft, length=12.34)
+
+ # Subsequent breakdowns should work similarly.
+ VerifyFirstBreakdown(spline, -5, flatTangents=ft, length=34.56)
+ VerifyFirstBreakdown(spline, 10, flatTangents=ft, length=45.67)
+
+ print("\tPASSED")
+
+TestBug12502()
+
+########################################################################
+
+def TestRedundantKeyFrames():
+ def __AssertSplineKeyFrames(spline, baseline):
+ keys = list(spline.keys())
+ assert len(keys) == len(baseline)
+ for index, (key, redundant) in enumerate(baseline):
+ assert keys[index] == key
+ assert spline.IsKeyFrameRedundant(keys[index]) == redundant
+
+ print("Start test for redundant keyframes " \
+ "(frames that don't change the animation).")
+
+ print("\tTest spline with some redundant knots.")
+ spline = createSpline([0, 1, 2, 3, 4, 5],
+ [0.0, 0.0, 1.0, 1.00000001, 1.0, 1.00001],
+ [Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier,
+ Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier])
+ keys = list(spline.keys())
+ assert spline.IsKeyFrameRedundant(keys[0]) == True
+ assert spline.IsKeyFrameRedundant(keys[1]) == False
+ assert spline.IsKeyFrameRedundant(keys[2]) == False
+ # diff < the hard coded epsilon
+ assert spline.IsKeyFrameRedundant(keys[3]) == True
+ # diff to next > the hard coded epsilon
+ assert spline.IsKeyFrameRedundant(keys[4]) == False
+ assert spline.IsKeyFrameRedundant(keys[5]) == False
+ assert spline.IsSegmentFlat(keys[0], keys[1]) == True
+ assert spline.IsSegmentFlat(keys[1], keys[2]) == False
+ assert spline.IsSegmentFlat(keys[2], keys[3]) == True
+ assert spline.IsSegmentFlat(keys[3], keys[4]) == True
+ assert spline.IsSegmentFlat(keys[4], keys[5]) == False
+ assert spline.HasRedundantKeyFrames() == True
+ assert spline.IsVarying() == True
+
+ print("\tTest key frame versions of functions")
+ assert spline.IsKeyFrameRedundant(spline[0]) == True
+ assert spline.IsKeyFrameRedundant(spline[1]) == False
+ assert spline.IsKeyFrameRedundant(spline[2]) == False
+ assert spline.IsKeyFrameRedundant(spline[3]) == True
+ assert spline.IsKeyFrameRedundant(spline[4]) == False
+ assert spline.IsKeyFrameRedundant(spline[5]) == False
+ assert spline.IsSegmentFlat(spline[0], spline[1]) == True
+ assert spline.IsSegmentFlat(spline[1], spline[2]) == False
+ assert spline.IsSegmentFlat(spline[2], spline[3]) == True
+ assert spline.IsSegmentFlat(spline[3], spline[4]) == True
+ assert spline.IsSegmentFlat(spline[4], spline[5]) == False
+ assert spline.HasRedundantKeyFrames() == True
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((1, False), (2, False), (4, False),
+ (5,False)))
+ assert spline.IsVarying() == True
+
+ print("\tTest passing in an interval to ClearRedundantKeyFrames.")
+ spline = createSpline([0, 1, 2, 3],
+ [0.0, 0.0, 0.0, 0.0],
+ [Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier,
+ Ts.KnotBezier])
+ keys = list(spline.keys())
+ assert spline.IsKeyFrameRedundant(keys[0]) == True
+ assert spline.IsKeyFrameRedundant(keys[1]) == True
+ assert spline.IsKeyFrameRedundant(keys[2]) == True
+ assert spline.IsKeyFrameRedundant(keys[3]) == True
+ spline.ClearRedundantKeyFrames(intervals=Gf.MultiInterval(
+ Gf.Interval(1, 2)))
+ __AssertSplineKeyFrames(spline, ((0, True), (3, True)))
+
+ print("\tTest that for looping spline first/last in master interval "\
+ "not removed, even though redundant.")
+ spline = createSpline([0, 10, 20, 30],
+ [0.0, 0.0, 0.0, 0.0],
+ [Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier,
+ Ts.KnotBezier])
+ params = Ts.LoopParams(True, 10, 25, 0, 25, 0.0)
+ spline.loopParams = params
+
+ keys = list(spline.keys())
+ assert spline.IsKeyFrameRedundant(keys[0]) == True
+ assert spline.IsKeyFrameRedundant(keys[1]) == False
+ assert spline.IsKeyFrameRedundant(keys[2]) == True
+ assert spline.IsKeyFrameRedundant(keys[3]) == False
+ assert spline.HasRedundantKeyFrames() == True
+ assert spline.IsVarying() == False
+
+ print("\tTest key frame versions of functions")
+ assert spline.IsKeyFrameRedundant(spline[0]) == True
+ assert spline.IsKeyFrameRedundant(spline[10]) == False
+ assert spline.IsKeyFrameRedundant(spline[20]) == True
+ assert spline.IsKeyFrameRedundant(spline[30]) == False
+ spline.ClearRedundantKeyFrames()
+ # The last two will be redundant, but will not have been removed
+ # since they are in the (non-writable) echo region
+ __AssertSplineKeyFrames(spline, ((10, False), (30, False), (35, True),
+ (55, True)))
+ assert spline.IsVarying() == False
+
+ print("\tTest empty spline.")
+ spline = Ts.Spline()
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == False
+
+ print("\tTest spline with one knot that is redundant.")
+ spline = createSpline([0], [0.0], [Ts.KnotBezier])
+ keys = list(spline.keys())
+ assert spline.IsKeyFrameRedundant(keys[0]) == False
+ assert spline.IsKeyFrameRedundant(keys[0], 0.0) == True
+ assert spline.IsKeyFrameRedundant(spline[0], 0.0) == True
+
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.HasRedundantKeyFrames(0.0) == True
+ assert spline.IsVarying() == False
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((0, False),))
+ assert spline.IsVarying() == False
+
+ print("\tTest that tangent slopes are respected when checking for redundancy.")
+ spline = createSpline([0, 10], [0.0, 0.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0])
+ keys = list(spline.keys())
+ assert spline.IsSegmentFlat(keys[0], keys[1]) == False
+ assert spline.IsKeyFrameRedundant(keys[0]) == False
+ assert spline.IsKeyFrameRedundant(keys[1]) == False
+ assert spline.IsKeyFrameRedundant(keys[1], 0.0) == False
+
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.HasRedundantKeyFrames(0.0) == False
+ assert spline.IsVarying() == True
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((0, False), (10, False)))
+ assert spline.IsVarying() == True
+
+ print("\tTest that tangent slopes that are almost flat are treated as flat.")
+ spline = createSpline([0, 10], [0.0, 0.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [1e-10, 1e-10, 1e-10, 1e-10],
+ [1e-10, 1e-10, 1e-10, 1e-10])
+ keys = list(spline.keys())
+ assert spline.IsSegmentFlat(keys[0], keys[1]) == True
+ assert spline.IsKeyFrameRedundant(keys[0]) == True
+ assert spline.IsKeyFrameRedundant(keys[1]) == True
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((0, False),))
+
+ print("\tTest dual valued knots.")
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(1, 0.0, 0.0, Ts.KnotBezier))
+ spline.SetKeyFrame(Ts.KeyFrame(5, 0.0, 0.0, Ts.KnotBezier))
+ assert spline.IsKeyFrameRedundant(spline[5], 0.0) == True
+ spline.SetKeyFrame(Ts.KeyFrame(6, 0.0, 1.0, Ts.KnotBezier))
+ assert spline.IsKeyFrameRedundant(spline[6]) == False
+ assert spline.IsVarying() == True
+
+ assert spline.HasRedundantKeyFrames() == True
+ assert spline.HasRedundantKeyFrames(0.0) == True
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((6, False),))
+
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 0.0, 0.0, Ts.KnotBezier))
+ assert spline.IsKeyFrameRedundant(spline[0], 0.0) == True
+ assert spline.IsKeyFrameRedundant(spline[0]) == False
+ assert spline.IsVarying() == False
+
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.HasRedundantKeyFrames(0.0) == True
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((0, False),))
+ assert spline.IsVarying() == False
+
+ # single knot with tangents, but held extrapolation
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 1.0, Ts.KnotBezier,
+ 1.0, 1.0, 1.0, 1.0))
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == False
+
+ # same knot value with mixed knot types.
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 1.0, Ts.KnotLinear))
+ spline.SetKeyFrame(Ts.KeyFrame(1, 1.0, Ts.KnotBezier,
+ 1.0, 1.0, 1.0, 1.0))
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == True
+
+ # single knot with broken tangents, left side
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 1.0, Ts.KnotBezier,
+ -1.0, 0.0, 1.0, 0.0))
+ spline.extrapolation = (Ts.ExtrapolationLinear,
+ Ts.ExtrapolationLinear)
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == True
+
+ # single knot with broken tangents, right side
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 1.0, Ts.KnotBezier,
+ 0.0, 1.0, 0.0, 1.0))
+ spline.extrapolation = (Ts.ExtrapolationLinear,
+ Ts.ExtrapolationLinear)
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == True
+
+ # single knot with broken tangents, held extrapolation
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 1.0, Ts.KnotBezier,
+ 0.0, 1.0, 0.0, 1.0))
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == False
+
+ # dual-valued knot, same values
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 1.0, 1.0, Ts.KnotLinear))
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == False
+
+ # dual-valued knot, different values
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, -1.0, 1.0, Ts.KnotLinear))
+ assert spline.HasRedundantKeyFrames() == False
+ assert spline.IsVarying() == True
+
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(0, 0.0, 1.0, Ts.KnotBezier))
+ assert spline.IsKeyFrameRedundant(spline[0]) == False
+ assert spline.IsVarying() == True
+
+ assert spline.HasRedundantKeyFrames() == False
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((0, False),))
+ assert spline.IsVarying() == True
+
+ print("\tTest non-interpolatable key frame types.")
+ spline = createSpline([0, 10, 20], ["foo", "bar", "bar"],
+ [Ts.KnotLinear, Ts.KnotLinear, Ts.KnotLinear])
+ keys = list(spline.keys())
+ assert spline.IsSegmentFlat(keys[0], keys[1]) == False
+ assert spline.IsSegmentFlat(keys[1], keys[2]) == True
+ assert spline.IsKeyFrameRedundant(keys[0]) == False
+ assert spline.IsKeyFrameRedundant(keys[1]) == False
+ assert spline.IsKeyFrameRedundant(keys[2]) == True
+ assert spline.IsVarying() == True
+
+ assert spline.HasRedundantKeyFrames() == True
+ spline.ClearRedundantKeyFrames()
+ __AssertSplineKeyFrames(spline, ((0, False),(10, False)))
+ assert spline.IsVarying() == True
+
+
+ print("\tPASSED")
+
+TestRedundantKeyFrames()
+
+def TestMonotonicSegments():
+ print("Start test for monotonic segments of keyframes")
+
+ #test parabolic shape of the curve
+ print("\tTest monotonic segments.")
+ #Parabolic shape of the curve (a = 0)
+ #10: 10.3684184945756 (bezier, 2.015878, 2.015878, 6.37363, 6.37363),
+ #20: 10.8064368221441 (bezier, -5.034222, -5.034222, 1.42624, 1.42624),
+ spline = createSpline([10, 20], [10.368, 10.806],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [2.0159, 2.015878, -5.0342, -5.0342],
+ [6.37363, 6.37363, 1.42624, 1.42624])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == False
+
+
+
+ #Minimum and a Maximum (hyperbola) first and second knots' values
+ # are relatively close
+ #10: 10.3684184945756 (bezier, 8.001244, 8.001244, 1.92862, 1.92862),
+ #20: 10.8064368221441 (bezier, 3.0872149, 3.0872149, 5.47192, 5.47192),
+ spline = createSpline([10, 20], [10.368, 10.806],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [8.001, 8.001, 3.0872, 3.0872],
+ [1.929, 1.929, 5.4719, 5.4719])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == False
+
+ #Minimum and a Maximum (hyperbola) first and second knots' values
+ # are far apart
+ #10: -0.370986(bezier, 8.00124, 8.00124, 1.9286, 1.9286),
+ #20: 10.806437(bezier, 3.08721, 3.08721, 5.4719, 5.4719),
+ spline = createSpline([10, 20], [-0.370986, 10.806],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [8.001, 8.001, 3.0872, 3.0872],
+ [1.929, 1.929, 5.4719, 5.4719])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == False
+
+ #Almost vertical line, although value increases monotonically
+ # the slope does change
+ #10: -0.37098(bezier, 4.7153, 4.7153, 1.0074, 1.0074),
+ #11: 17.46758(bezier, 4.3937, 4.3937, 0.9674, 0.9674),
+ spline = createSpline([10, 11], [-0.370986, 17.4676],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [4.7153, 4.7153, 4.3937, 4.3937],
+ [1.0074, 1.0074, 0.9674, 0.9674])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == True
+
+
+ #Almost vertical line, although there's a dip at the beginning
+ # the minimum does exist
+ #10: -0.370985(bezier, -10.78057, -10.78057, 0.796848, 0.796848),
+ #11: 17.467586(bezier, 4.3936, 4.3936, 0.9673, 0.9673),
+ spline = createSpline([10, 11], [-0.370986, 17.4676],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [-10.78057, -10.78057, 4.3937, 4.3937],
+ [0.796848, 0.796848, 0.9674, 0.9674])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == False
+
+ # Almost horizontal line, (about a thosand of difference in time
+ # and only 0.1 difference in value), but still monotonically
+ # increasing
+ #-415: 8.8928(bezier, 0.3213, 0.3213, 0.01, 0.01),
+ #795: 8.9012(bezier, 0.747, 0.747, 0.01, 0.01),
+ spline = createSpline([-415, 795], [8.8928, 8.9012],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [0.3213, 0.3213, 0.747, 0.747,],
+ [0.01, 0.01, 0.01, 0.01])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == True
+
+ #Line that loops back on itself (in time) and does have a maximum
+ # and minimum
+ #10: -0.37098(bezier, 4.536694, 4.536694, 7.16636, 7.16636),
+ #11: 17.46758(bezier, 13.002, 13.002, 2.5768, 2.5768),
+ spline = createSpline([10, 11], [-0.370986, 17.4676],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [4.5366, 4.5366, 13.002, 13.002],
+ [7.166, 7.166, 2.5768, 2.5768])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == False
+
+ #Line that loops back on itself (in time) but the value of
+ # which is monotonically increasing
+ #10: -140.99 (bezier, 4.5367, 4.5367, 7.1663, 7.1663),
+ #11: 17.4675 (bezier, 13.002, 13.002, 2.5768, 2.5768),
+ spline = createSpline([10, 11], [-140.99, 17.4675],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [4.5366, 4.5366, 13.002, 13.002],
+ [7.166, 7.166, 2.5768, 2.5768])
+ keys = list(spline.keys())
+ assert spline.IsSegmentValueMonotonic(keys[0], keys[1]) == True
+
+ print("\tPASSED")
+
+TestMonotonicSegments()
+
+def TestVarying():
+ print("Start test for varying")
+
+ print("\tTest value changes.")
+
+ # An empty spline is not varying
+ spline = Ts.Spline()
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ # Nor is a spline with a single knot
+ spline = createSpline([0], [4.5], [Ts.KnotBezier])
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ # A spline with two knots is not varying if the values don't change
+ spline = createSpline([0,1,2], [10.0,10.0,10.0],
+ [Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier])
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ # But is varying if the values do change
+ spline = createSpline([0,1,2], [10.0,12.0,10.0],
+ [Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier])
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+
+ # Test specifying that knots close in value should be considered the same
+ spline = createSpline([0,1,2], [10.0,10.00000001,10.0],
+ [Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier])
+ assert spline.IsVarying();
+ assert not spline.IsVaryingSignificantly();
+
+ # Similar, but the difference is further apart in time
+ spline = createSpline([0,1,2,3], [10.0,10.00000001,10.0,10.00000002],
+ [Ts.KnotBezier, Ts.KnotBezier, Ts.KnotBezier,
+ Ts.KnotBezier])
+ assert spline.IsVarying();
+ assert not spline.IsVaryingSignificantly();
+
+ print("\tTest tangent changes.")
+
+ # Varying can also be achieved by tangent changes
+ spline = createSpline([1, 10],
+ [10.0, 10.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [1, 1, -1, -1],
+ [1, 1, 1, 1])
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+ # But not if the tangents are flat
+ spline = createSpline([1, 10],
+ [10.0, 10.0],
+ [Ts.KnotBezier, Ts.KnotBezier],
+ [0, 0, 0, 0],
+ [1, 1, 1, 1])
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ print("\tTest dual-valued knots.")
+
+ # A spline with a single dual-valued knot with the same values
+ # on both sides does not vary
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(1, 0.0, 0.0, Ts.KnotBezier))
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ # But does vary if the values differ
+ spline = Ts.Spline()
+ spline.SetKeyFrame(Ts.KeyFrame(1, 0.0, 1.0, Ts.KnotBezier))
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+
+ # Test some variations
+ spline.SetKeyFrame(Ts.KeyFrame(1, 1.0, 1.0, Ts.KnotBezier))
+ spline.SetKeyFrame(Ts.KeyFrame(2, 1.0, 0.0, Ts.KnotBezier))
+ spline.SetKeyFrame(Ts.KeyFrame(3, 0.0, 0.0, Ts.KnotBezier))
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+
+ spline.SetKeyFrame(Ts.KeyFrame(1, 0.0, 1.0, Ts.KnotBezier))
+ spline.SetKeyFrame(Ts.KeyFrame(2, 1.0, 1.0, Ts.KnotBezier))
+ spline.SetKeyFrame(Ts.KeyFrame(3, 1.0, 0.0, Ts.KnotBezier))
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+
+ spline.SetKeyFrame(Ts.KeyFrame(1, 1.0, 1.0, Ts.KnotBezier))
+ spline.SetKeyFrame(Ts.KeyFrame(2, 1.0, 1.0, Ts.KnotBezier))
+ spline.SetKeyFrame(Ts.KeyFrame(3, 1.0, 1.0, Ts.KnotBezier))
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ print("\tTest extrapolation.")
+
+ # Create a spline with a single knot and non-flat tangents
+ spline = createSpline([1],
+ [10.0],
+ [Ts.KnotBezier],
+ [1, 1],
+ [1, 1])
+ spline.extrapolation = (Ts.ExtrapolationHeld,
+ Ts.ExtrapolationHeld)
+
+ # With held extrapoluation, this does not vary
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+ spline.extrapolation = (Ts.ExtrapolationLinear,
+ Ts.ExtrapolationLinear)
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+
+ # Make sure this works on a per-side basis
+ spline = createSpline([1],
+ [10.0],
+ [Ts.KnotBezier],
+ [0, 1],
+ [1, 1])
+ spline.extrapolation = (Ts.ExtrapolationLinear,
+ Ts.ExtrapolationHeld)
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+ spline.extrapolation = (Ts.ExtrapolationHeld,
+ Ts.ExtrapolationLinear)
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+
+ spline = createSpline([1],
+ [10.0],
+ [Ts.KnotBezier],
+ [1, 0],
+ [1, 1])
+ spline.extrapolation = (Ts.ExtrapolationLinear,
+ Ts.ExtrapolationHeld)
+ assert spline.IsVarying()
+ assert spline.IsVaryingSignificantly()
+ spline.extrapolation = (Ts.ExtrapolationHeld,
+ Ts.ExtrapolationLinear)
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ # And only if tangents aren't flat
+ spline = createSpline([1],
+ [10.0],
+ [Ts.KnotBezier],
+ [0, 0],
+ [1, 1])
+ spline.extrapolation = (Ts.ExtrapolationLinear,
+ Ts.ExtrapolationLinear)
+ assert not spline.IsVarying()
+ assert not spline.IsVaryingSignificantly()
+
+ print("\tPASSED")
+
+TestVarying()
+
+def TestInfValues():
+ print('Test infinite values')
+
+ kf1 = Ts.KeyFrame(0.0, 0.0, Ts.KnotBezier)
+ kf2 = Ts.KeyFrame(1.0, float('inf'), Ts.KnotBezier)
+ kf3 = Ts.KeyFrame(2.0, 2.0, Ts.KnotBezier)
+ kf4 = Ts.KeyFrame(3.0, float('-inf'), Ts.KnotBezier)
+
+ # Only finite values are interpolatable.
+ assert kf1.isInterpolatable
+ assert not kf2.isInterpolatable
+ assert kf3.isInterpolatable
+ assert not kf4.isInterpolatable
+
+ # Non-interpolatable values should be auto-converted to held knots.
+ assert kf1.knotType == Ts.KnotBezier
+ assert kf2.knotType == Ts.KnotHeld
+ assert kf3.knotType == Ts.KnotBezier
+ assert kf4.knotType == Ts.KnotHeld
+
+ # Interpolatable values can be set to other knot types.
+ assert kf1.CanSetKnotType( Ts.KnotLinear )
+ assert kf3.CanSetKnotType( Ts.KnotLinear )
+
+ # Non-interpolatable values cannot be set to anything but held.
+ verifyNegativeCanResult(kf2.CanSetKnotType(Ts.KnotLinear))
+ verifyNegativeCanResult(kf4.CanSetKnotType(Ts.KnotLinear))
+
+ # Setting a knot to a non-finite value should convert it to held.
+ kfTest = Ts.KeyFrame(0, 0.0, Ts.KnotBezier)
+ assert kfTest.knotType != Ts.KnotHeld
+ kfTest.value = float('inf')
+ assert kfTest.knotType == Ts.KnotHeld
+
+ # Check eval'ing a spline with non-interpolatable values.
+ #
+ # A non-interpolatable keyframe is treated as held,
+ # and also forces the prior keyframe to be treated as held.
+ #
+ s = Ts.Spline([kf1, kf2, kf3, kf4])
+ assert s.Eval(0.0) == 0
+ assert s.Eval(0.5) == 0
+ assert s.Eval(1.0) == float('inf')
+ assert s.Eval(1.5) == float('inf')
+ assert s.Eval(2.0) == 2
+ assert s.Eval(2.5) == 2
+ assert s.Eval(3.0) == float('-inf')
+ assert s.Eval(3.5) == float('-inf')
+ assert s.EvalDerivative(0.0) == 0
+ assert s.EvalDerivative(0.5) == 0
+ assert s.EvalDerivative(1.0) == 0
+ assert s.EvalDerivative(1.5) == 0
+ assert s.EvalDerivative(2.0) == 0
+ assert s.EvalDerivative(2.5) == 0
+ assert s.EvalDerivative(3.0) == 0
+ assert s.EvalDerivative(3.5) == 0
+
+ print("\tPASSED")
+
+TestInfValues()
+
+def TestEvalHeld():
+ print("\nStart EvalHeld test")
+ spline = Ts.Spline([
+ Ts.KeyFrame(5, 2.0, Ts.KnotHeld),
+ Ts.KeyFrame(10, -3.0, Ts.KnotBezier,
+ 0.0, 0.0, 1 + 2/3.0, 3 + 1/3.0),
+ Ts.KeyFrame(20, 16.0, Ts.KnotBezier, 2.0, 3.0, 4.0, 8.0),
+ Ts.KeyFrame(40, 0.0, Ts.KnotLinear),
+ ],
+ Ts.ExtrapolationHeld,
+ Ts.ExtrapolationLinear,
+ )
+ spline.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationLinear)
+
+ print("\tTest Eval and EvalHeld in varying regions")
+ assert spline.Eval(10) == -3.0
+ assert Gf.IsClose(spline.Eval(15), 4.098398216, EPSILON)
+ assert spline.EvalHeld(10) == -3.0
+ assert spline.EvalHeld(15) == -3.0
+
+ print("\tTest that Eval and EvalHeld are the same for held keyframes")
+ assert spline.Eval(0) == 2.0
+ assert spline.Eval(5) == 2.0
+ assert spline.Eval(7) == 2.0
+ assert spline.EvalHeld(0) == 2.0
+ assert spline.EvalHeld(5) == 2.0
+ assert spline.EvalHeld(7) == 2.0
+
+ print("\tTest that the left side of a held frame takes its value from " +
+ "the previous hold")
+ assert spline.Eval(20, Ts.Left) == 16.0
+ assert spline.Eval(20) == 16.0
+ assert spline.EvalHeld(20, Ts.Left) == -3.0
+ assert spline.EvalHeld(20) == 16.0
+
+ print("\tTest that holding ignores extrapolation")
+ assert spline.Eval(40) == 0
+ assert spline.Eval(50) == -8.0
+ assert spline.EvalHeld(40) == 0.0
+ assert spline.EvalHeld(50) == 0.0
+
+ print("\tPASSED")
+
+TestEvalHeld()
+
+def TestHeldAtLeftSideOfFollowingKnot():
+ """
+ Verify that the left evaluated value of a knot that follows a held knot is
+ always the value of the held knot. This verifies the fix for PRES-89202.
+ """
+ s = Ts.Spline([
+ # Held, then held.
+ Ts.KeyFrame(
+ time = 1.0, value = 2.0,
+ knotType = Ts.KnotHeld),
+ Ts.KeyFrame(
+ time = 2.0, value = 3.0,
+ knotType = Ts.KnotHeld),
+
+ # Held, then linear.
+ Ts.KeyFrame(
+ time = 3.0, value = 4.0,
+ knotType = Ts.KnotHeld),
+ Ts.KeyFrame(
+ time = 4.0, value = 5.0,
+ knotType = Ts.KnotLinear),
+
+ # Held, then dual Bezier.
+ Ts.KeyFrame(
+ time = 5.0, value = 6.0,
+ knotType = Ts.KnotHeld),
+ Ts.KeyFrame(
+ time = 6.0, leftValue = 7.0, rightValue = 8.0,
+ knotType = Ts.KnotBezier),
+ ])
+
+ print("\nStart held-then-left test")
+
+ # Held, then held. At the left side of the second knot, that knot's own
+ # value should be ignored. This worked correctly even before PRES-89202 was
+ # fixed.
+ assert s.Eval(time = 1.0, side = Ts.Right) == 2.0
+ assert s.Eval(time = 2.0, side = Ts.Left) == 2.0
+ assert s.Eval(time = 2.0, side = Ts.Right) == 3.0
+
+ # Held, then linear. At the left side of the second knot, that knot's own
+ # value should be ignored.
+ assert s.Eval(time = 3.0, side = Ts.Right) == 4.0
+ assert s.Eval(time = 4.0, side = Ts.Left) == 4.0 # Torture case
+ assert s.Eval(time = 4.0, side = Ts.Right) == 5.0
+
+ # Held, then dual Bezier. At the left side of the second knot, that knot's
+ # own left value should be ignored.
+ assert s.Eval(time = 5.0, side = Ts.Right) == 6.0
+ assert s.Eval(time = 6.0, side = Ts.Left) == 6.0 # Torture case
+ assert s.Eval(time = 6.0, side = Ts.Right) == 8.0
+
+ print("\tPASSED")
+
+TestHeldAtLeftSideOfFollowingKnot()
+
+def TestDoSidesDiffer():
+ """
+ Exercise TsSpline::DoSidesDiffer.
+ """
+ s = Ts.Spline([
+ Ts.KeyFrame(
+ time = 1.0, value = 2.0,
+ knotType = Ts.KnotHeld),
+ Ts.KeyFrame(
+ time = 2.0, leftValue = 3.0, rightValue = 4.0,
+ knotType = Ts.KnotHeld),
+ Ts.KeyFrame(
+ time = 3.0, value = 5.0,
+ knotType = Ts.KnotLinear),
+ Ts.KeyFrame(
+ time = 4.0, value = 6.0,
+ knotType = Ts.KnotHeld),
+ Ts.KeyFrame(
+ time = 5.0, value = 7.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = 0.0, rightSlope = 0.0, leftLen = 0.0, rightLen = 0.0),
+ Ts.KeyFrame(
+ time = 6.0, value = 8.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = 0.0, rightSlope = 0.0, leftLen = 0.0, rightLen = 0.0),
+ Ts.KeyFrame(
+ time = 7.0, leftValue = 9.0, rightValue = 10.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = 0.0, rightSlope = 0.0, leftLen = 0.0, rightLen = 0.0),
+ Ts.KeyFrame(
+ time = 8.0, leftValue = 11.0, rightValue = 11.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = 0.0, rightSlope = 0.0, leftLen = 0.0, rightLen = 0.0),
+ Ts.KeyFrame(
+ time = 9.0, value = 12.0,
+ knotType = Ts.KnotHeld),
+ Ts.KeyFrame(
+ time = 10.0, value = 12.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = 0.0, rightSlope = 0.0, leftLen = 0.0, rightLen = 0.0),
+ ])
+
+ def _TestNonDiff(time):
+ assert not s.DoSidesDiffer(time)
+ assert s.Eval(time, Ts.Left) == s.Eval(time, Ts.Right)
+
+ def _TestDiff(time):
+ assert s.DoSidesDiffer(time)
+ assert s.Eval(time, Ts.Left) != s.Eval(time, Ts.Right)
+
+ print("\nStart DoSidesDiffer test\n")
+
+ # At a non-dual held knot with no preceding knot: false.
+ _TestNonDiff(time = 1.0)
+
+ # Between knots: false.
+ _TestNonDiff(time = 1.5)
+
+ # At a dual-valued held knot: true.
+ _TestDiff(time = 2.0)
+
+ # At a linear knot following a held knot: true.
+ _TestDiff(time = 3.0)
+
+ # At a non-dual held knot: false.
+ _TestNonDiff(time = 4.0)
+
+ # At a Bezier knot following a held knot: true.
+ _TestDiff(time = 5.0)
+
+ # At an ordinary Bezier knot: false.
+ _TestNonDiff(time = 6.0)
+
+ # At a dual-valued Bezier knot: true.
+ _TestDiff(time = 7.0)
+
+ # At a dual-valued Bezier knot with both sides the same: false.
+ _TestNonDiff(time = 8.0)
+
+ # At a Bezier knot following a held knot with same value: false.
+ _TestNonDiff(time = 10.0)
+
+ print("\tPASSED")
+
+TestDoSidesDiffer()
+
+
+print('\nTest SUCCEEDED')
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from __future__ import print_function
+from pxr import Ts, Vt, Gf, Tf
+
+EPSILON = 1e-6
+COARSE_EPSILON = 1e-4
+
+
+v = Ts.Spline()
+
+def _Validate():
+ assert v == eval(repr(v))
+
+########################################################################
+
+print('\nTest basic keyframes interface')
+
+assert len(v) == 0
+
+# Some sample keyframe data.
+kf1_time = 0
+kf1 = Ts.KeyFrame( kf1_time )
+kf2_time = 20
+kf2 = Ts.KeyFrame( kf2_time )
+kf3_time = 30
+kf3_value = 0.1234
+kf3_knotType = Ts.KnotBezier
+kf3_leftLen = 0.1
+kf3_leftSlope = 0.2
+kf3_rightLen = 0.3
+kf3_rightSlope = 0.4
+kf3 = Ts.KeyFrame( kf3_time, value = kf3_value, knotType = kf3_knotType,
+ leftLen = kf3_leftLen, leftSlope = kf3_leftSlope,
+ rightLen = kf3_rightLen, rightSlope = kf3_rightSlope )
+kf4_time = 40
+kf4_value = 0.1234
+kf4_knotType = Ts.KnotHeld
+kf4_leftLen = 0.1
+kf4_leftSlope = 0.2
+kf4_rightLen = 0.3
+kf4_rightSlope = 0.4
+kf4 = Ts.KeyFrame( kf4_time, value = kf4_value, knotType = kf4_knotType,
+ leftLen = kf4_leftLen, leftSlope = kf4_leftSlope,
+ rightLen = kf4_rightLen, rightSlope = kf4_rightSlope )
+kf5_time = 50
+kf5_value = ''
+kf5_knotType = Ts.KnotHeld
+kf5 = Ts.KeyFrame( kf5_time, kf5_value, knotType = Ts.KnotHeld )
+
+expected = [kf1, kf2, kf3, kf4]
+for kf in expected:
+ v.SetKeyFrame(kf)
+ assert v
+ _Validate()
+print(v.frames)
+print(expected)
+assert len(v.frames) == len(expected)
+_Validate()
+# Compare the contents of our list to the contents of the proxy
+for (kf, time) in zip( expected, v.frames ):
+ assert time in v
+ assert v[time] == kf
+# Test contains
+for a in expected:
+ assert a.time in v.frames
+for b in v:
+ assert b in expected
+# Test getitem
+for a in expected:
+ assert v[ a.time ] == a
+# Test accessing keyframe at bogus time
+try:
+ v[ 123456 ]
+ assert 0, 'expected to fail'
+except IndexError:
+ pass
+# Test clear
+v.clear()
+assert len(v) == 0
+_Validate()
+for kf in expected:
+ v.SetKeyFrame(kf)
+_Validate()
+del expected
+print('\tPassed')
+
+########################################################################
+
+print('\nTest heterogeneous keyframe types: errors expected')
+try:
+ v.SetKeyFrame(kf5)
+ assert 0, 'expected to fail'
+except Tf.ErrorException:
+ pass
+_Validate()
+print('\tPassed')
+
+########################################################################
+
+print('\nTest keyframe slicing')
+kf5 = Ts.KeyFrame( kf5_time )
+v.SetKeyFrame(kf5)
+expected = [kf1, kf2, kf3, kf4, kf5]
+assert v[:] == expected
+assert v[:kf3.time] == [kf1, kf2]
+assert v[kf3.time:] == [kf3, kf4, kf5]
+# Test end conditions
+assert v[:-1000] == []
+assert v[1000:] == []
+assert v[-1000:1000] == expected
+# Test invalid slice range (start > stop)
+assert v[1000:-1000] == []
+# Test that stride does not work for slicing
+try:
+ bad = v[-1000:1000 :1.0]
+ assert 0, 'stride should not be allowed'
+except ValueError:
+ pass
+_Validate()
+print('\tPassed')
+
+print('\nTest that non-time slice bounds do not work')
+try:
+ v['foo':'bar']
+ assert 0, 'non-time keys should not work for slicing'
+except ValueError:
+ pass
+try:
+ v[1:'bar']
+ assert 0, 'non-time keys should not work for slicing'
+except ValueError:
+ pass
+_Validate()
+print('\tPassed')
+
+print('\nTest that assignment to keyframes[time] does not work')
+try:
+ v[ 1234 ] = kf1
+ assert 0, 'should not have worked'
+except AttributeError:
+ pass
+_Validate()
+print('\tPassed')
+
+########################################################################
+
+print('\nTest deleting knots')
+assert kf5_time in v.frames
+del v[ kf5.time ]
+assert kf5_time not in v.frames
+_Validate()
+print('\tPassed')
+
+print('\nTest deleting keyframe at bogus time')
+bogus_time = 1234
+assert bogus_time not in v.frames
+try:
+ del v[ bogus_time ]
+ assert 0, 'error expected'
+except Tf.ErrorException:
+ pass
+assert bogus_time not in v.frames
+_Validate()
+print('\tPassed')
+
+print('\nTest deleting knots by slicing')
+# Find some knots to delete; make sure they're valid
+keyframes_to_del = [kf2, kf3, kf4]
+for kf in keyframes_to_del:
+ assert kf.time in v.frames
+ assert v[ kf.time ] == kf
+assert v[ kf2_time : kf5_time ] == keyframes_to_del
+del v[ kf2_time : kf5_time ]
+assert v[ kf2_time : kf5_time ] == []
+assert kf2_time not in v.frames
+assert kf3_time not in v.frames
+assert kf4_time not in v.frames
+_Validate()
+print('\tPassed')
+
+print('\nTest deleting all knots')
+assert len(v.frames) > 0
+del v[:]
+assert len(v.frames) == 0
+_Validate()
+print('\tPassed')
+
+print('\nTest that deleting knots at bogus frames does not work ' \
+ 'errors expected')
+assert 1234 not in v
+try:
+ del v[1234]
+ assert 0, 'expected failure'
+except Tf.ErrorException:
+ pass
+assert 1234 not in v
+print('\tPassed')
+
+print('\nTest that an empty spline returns None for Eval()')
+del v[:]
+assert len(v.frames) == 0
+assert v.Eval(0) is None
+assert v.EvalDerivative(0) is None
+_Validate()
+print('\tPassed')
+
+# Remove these local variables
+del kf1
+del kf2
+del kf3
+del kf4
+del kf5
+
+########################################################################
+
+print('\nTest changing knot times')
+del v[:]
+keyframes = [ Ts.KeyFrame(t) for t in range(10) ]
+for k in keyframes:
+ v.SetKeyFrame(k)
+ _Validate()
+assert keyframes == list(v.values())
+assert [] != list(v.values())
+new_time = 123
+assert new_time not in v.keys()
+del v[ keyframes[0].time ]
+_Validate()
+keyframes[0].time = new_time
+v.SetKeyFrame( keyframes[0] )
+_Validate()
+assert new_time in list(v.keys())
+assert v[new_time] == keyframes[0]
+# We expect that the knot order has changed
+assert keyframes != list(v.values())
+# Sort our expected knots to reflect new expectations
+keyframes.sort(key=lambda x: x.time)
+# Check that the new knot order applies
+assert keyframes == list(v.values())
+del new_time
+_Validate()
+print('\tPassed')
+
+print('\nTest that changing knot times overwrites existing knots')
+keyframes = list(v.values())
+kf0_time = keyframes[0].time
+kf1_time = keyframes[1].time
+oldLen = len(list(v.values()))
+assert len(list(v.values())) == oldLen
+assert keyframes[0].time != keyframes[1].time
+del v[ keyframes[0].time ]
+keyframes[0].time = kf1_time
+assert keyframes[0].time == kf1_time
+_Validate()
+v.SetKeyFrame( keyframes[0] )
+assert len(list(v.values())) == oldLen - 1
+del kf0_time
+del kf1_time
+del oldLen
+_Validate()
+print('\tPassed')
+
+print('\nTest frameRange')
+del v[:]
+assert v.frameRange.isEmpty
+v.SetKeyFrame( Ts.KeyFrame(-10, 0.0) )
+v.SetKeyFrame( Ts.KeyFrame(123, 0.0) )
+assert v.frameRange == Gf.Interval(-10, 123)
+_Validate()
+print('\tPassed')
+
+print('\nTest extrapolation interface')
+x = v.extrapolation
+assert x[0] == Ts.ExtrapolationHeld
+assert x[1] == Ts.ExtrapolationHeld
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationHeld)
+x = v.extrapolation
+assert x[0] == Ts.ExtrapolationLinear
+assert x[1] == Ts.ExtrapolationHeld
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationLinear)
+x = v.extrapolation
+assert x[0] == Ts.ExtrapolationHeld
+assert x[1] == Ts.ExtrapolationLinear
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+x = v.extrapolation
+assert x[0] == Ts.ExtrapolationLinear
+assert x[1] == Ts.ExtrapolationLinear
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+x = v.extrapolation
+assert x[0] == Ts.ExtrapolationHeld
+assert x[1] == Ts.ExtrapolationHeld
+print('\tPassed')
+
+print('\nTest that SetKeyFrames() preserves extrapolation')
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationLinear)
+x = v.extrapolation
+assert x[0] == Ts.ExtrapolationHeld
+assert x[1] == Ts.ExtrapolationLinear
+oldKeyFrames = list(v.values())
+v.SetKeyFrames( [Ts.KeyFrame(0, 0.123)] )
+x = v.extrapolation
+assert x[0] == Ts.ExtrapolationHeld
+assert x[1] == Ts.ExtrapolationLinear
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+v.SetKeyFrames(oldKeyFrames)
+print('\tPassed')
+
+########################################################################
+
+print('\nTest Eval() with held knots of various types')
+# Loop over various types, with sample keyframe values.
+testTypes = {
+ 'string': ['first', 'second', '', ''],
+ 'double': [1.0, 2.0, 0.0, 0.0],
+ 'int':[1,2,0,0],
+ 'GfVec2d': [ Gf.Vec2d( *list(range(0,0+2)) ),
+ Gf.Vec2d( *list(range(2,2+2)) ),
+ Gf.Vec2d(),
+ Gf.Vec2d() ],
+ 'GfVec2f': [ Gf.Vec2f( *list(range(0,0+2)) ),
+ Gf.Vec2f( *list(range(2,2+2)) ),
+ Gf.Vec2f(),
+ Gf.Vec2f() ],
+ 'GfVec3d': [ Gf.Vec3d( *list(range(0,0+3)) ),
+ Gf.Vec3d( *list(range(3,3+3)) ),
+ Gf.Vec3d(),
+ Gf.Vec3d() ],
+ 'GfVec3f': [ Gf.Vec3f( *list(range(0,0+3)) ),
+ Gf.Vec3f( *list(range(3,3+3)) ),
+ Gf.Vec3f(),
+ Gf.Vec3f() ],
+ 'GfVec4d': [ Gf.Vec4d( *list(range(0,0+4)) ),
+ Gf.Vec4d( *list(range(4,4+4)) ),
+ Gf.Vec4d(),
+ Gf.Vec4d() ],
+ 'GfVec4f': [ Gf.Vec4f( *list(range(0,0+4)) ),
+ Gf.Vec4f( *list(range(4,4+4)) ),
+ Gf.Vec4f(),
+ Gf.Vec4f() ],
+ 'GfMatrix2d': [ Gf.Matrix2d( *list(range(0,0+4)) ),
+ Gf.Matrix2d( *list(range(4,4+4)) ),
+ Gf.Matrix2d( 0 ),
+ Gf.Matrix2d( 0 ) ],
+ 'GfMatrix3d': [ Gf.Matrix3d( *list(range(0,0+9)) ),
+ Gf.Matrix3d( *list(range(9,9+9)) ),
+ Gf.Matrix3d( 0 ),
+ Gf.Matrix3d( 0 ) ],
+ 'GfMatrix4d': [ Gf.Matrix4d( *list(range(0,0+16)) ),
+ Gf.Matrix4d( *list(range(16,16+16)) ),
+ Gf.Matrix4d( 0 ),
+ Gf.Matrix4d( 0 ) ]
+ }
+for testType in testTypes:
+ v0, v1, d0, d1 = testTypes[testType]
+ del v[:]
+ t0 = 0
+ t1 = 10
+ kf0 = Ts.KeyFrame(t0, v0, Ts.KnotHeld)
+ kf1 = Ts.KeyFrame(t1, v1, Ts.KnotHeld)
+ if not v.CanSetKeyFrame(kf0) or not v.CanSetKeyFrame(kf1):
+ continue
+ v.SetKeyFrame(kf0)
+ _Validate()
+ v.SetKeyFrame(kf1)
+ _Validate()
+ assert v.typeName == testType
+ assert v.Eval(t0-1) == v0
+ assert v.Eval(t0-0.5) == v0
+ assert v.Eval(t0+0) == v0
+ assert v.Eval(t0+0.5) == v0
+ assert v.Eval(t0+1) == v0
+ assert v.Eval(t1-1) == v0
+ assert v.Eval(t1-0.5) == v0
+ assert v.Eval(t1+0) == v1
+ assert v.Eval(t1+0.5) == v1
+ assert v.Eval(t1+1) == v1
+
+ assert v.EvalDerivative(t0-1) == d0
+ assert v.EvalDerivative(t0-0.5) == d0
+ assert v.EvalDerivative(t0+0) == d0
+ assert v.EvalDerivative(t0+0.5) == d0
+ assert v.EvalDerivative(t0+1) == d0
+ assert v.EvalDerivative(t1-1) == d0
+ assert v.EvalDerivative(t1-0.5) == d0
+ assert v.EvalDerivative(t1+0) == d1
+ assert v.EvalDerivative(t1+0.5) == d1
+ assert v.EvalDerivative(t1+1) == d1
+print('\tPassed')
+
+########################################################################
+
+del v[:]
+
+print('\nTest float Eval() with linear knots')
+v.SetKeyFrame( Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotLinear ) )
+_Validate()
+v.SetKeyFrame( Ts.KeyFrame( 10, value = 20.0, knotType = Ts.KnotLinear ) )
+_Validate()
+assert v.Eval(-5) == 0
+assert v.Eval(2.5) == 5
+assert v.Eval( 5) == 10
+assert v.Eval(7.5) == 15
+assert v.Eval(15) == 20
+assert v.EvalDerivative(-5) == 0
+assert v.EvalDerivative(2.5) == 2
+assert v.EvalDerivative(5) == 2
+assert v.EvalDerivative(7.5) == 2
+assert v.EvalDerivative(15) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Eval(-5) == -10
+assert v.Eval(2.5) == 5
+assert v.Eval( 5) == 10
+assert v.Eval(7.5) == 15
+assert v.Eval(15) == 30
+assert v.EvalDerivative(-5) == 2
+assert v.EvalDerivative(2.5) == 2
+assert v.EvalDerivative(5) == 2
+assert v.EvalDerivative(7.5) == 2
+assert v.EvalDerivative(15) == 2
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print('\tPassed')
+
+print('\nTest float Range() with linear knots')
+assert v.Range(-1, 11) == (0, 20)
+assert v.Range(-1, -1) == (0, 0)
+assert v.Range(-1, 0) == (0, 0)
+assert v.Range(0, 10) == (0, 20)
+assert v.Range(2.5, 7.5) == (5, 15)
+assert v.Range(5, 5) == (10, 10)
+assert v.Range(10, 11) == (20, 20)
+assert v.Range(11, 11) == (20, 20)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Range(-1, 11) == (0, 20)
+assert v.Range(-1, -1) == (0, 0)
+assert v.Range(-1, 0) == (0, 0)
+assert v.Range(0, 10) == (0, 20)
+assert v.Range(2.5, 7.5) == (5, 15)
+assert v.Range(5, 5) == (10, 10)
+assert v.Range(10, 11) == (20, 20)
+assert v.Range(11, 11) == (20, 20)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest extrapolation with one linear knot')
+del v[10]
+assert v.Eval(-5) == 0
+assert v.Eval( 5) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Eval(-5) == 0
+assert v.Eval( 5) == 0
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print('\tPassed')
+
+del v[:]
+
+print('\nTest float Eval() with dual-value linear knots')
+kf1 = Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotLinear )
+kf2 = Ts.KeyFrame( 10, value = 0.0, knotType = Ts.KnotLinear )
+assert not kf1.isDualValued
+assert not kf2.isDualValued
+kf1.value = (10, 0)
+kf2.value = (10, 0)
+assert kf1.isDualValued
+assert kf2.isDualValued
+v.SetKeyFrames( [kf1, kf2] )
+assert v.Eval(-1) == 10
+assert v.Eval( 0) == 0
+assert v.Eval( 1) == 1
+assert v.Eval( 9) == 9
+assert Gf.IsClose(v.Eval( 9.99), 9.99, EPSILON)
+assert v.Eval(10) == 0
+assert v.Eval(11) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Eval(-1) == 10
+assert v.Eval( 0) == 0
+assert v.Eval( 1) == 1
+assert v.Eval( 9) == 9
+assert Gf.IsClose(v.Eval( 9.99), 9.99, EPSILON)
+assert v.Eval(10) == 0
+assert v.Eval(11) == 0
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest float Range() with dual-value linear knots')
+assert v.Range(-1, 11) == (0, 10)
+assert v.Range(-1, -1) == (10, 10)
+assert v.Range(-1, 0) == (0, 10)
+assert v.Range(0, 10) == (0, 10)
+assert v.Range(2.5, 7.5) == (2.5, 7.5)
+assert v.Range(5, 5) == (5, 5)
+assert v.Range(10, 11) == (0, 0)
+assert v.Range(11, 11) == (0, 0)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Range(-1, 11) == (0, 10)
+assert v.Range(-1, -1) == (10, 10)
+assert v.Range(-1, 0) == (0, 10)
+assert v.Range(0, 10) == (0, 10)
+assert v.Range(2.5, 7.5) == (2.5, 7.5)
+assert v.Range(5, 5) == (5, 5)
+assert v.Range(10, 11) == (0, 0)
+assert v.Range(11, 11) == (0, 0)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest extrapolation with one dual-value linear knot')
+del v[10]
+assert v.Eval(-5) == 10
+assert v.Eval( 5) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Eval(-5) == 10
+assert v.Eval( 5) == 0
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print('\tPassed')
+
+del v[:]
+
+print('\nTest float Eval() with Bezier knots w/ zero-length handles')
+v.SetKeyFrame( Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 0, leftSlope = 0, rightLen = 0, rightSlope = 0 ) )
+v.SetKeyFrame( Ts.KeyFrame( 10, value = 10.0, knotType = Ts.KnotBezier,
+ leftLen = 0, leftSlope = 0, rightLen = 0, rightSlope = 0 ) )
+assert Gf.IsClose(v.Eval(-1), 0, EPSILON)
+assert Gf.IsClose(v.Eval(0), 0, EPSILON)
+assert Gf.IsClose(v.Eval(1), 1, EPSILON)
+assert Gf.IsClose(v.Eval(9), 9, EPSILON)
+assert Gf.IsClose(v.Eval(10), 10, EPSILON)
+assert Gf.IsClose(v.Eval(11), 10, EPSILON)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert Gf.IsClose(v.Eval(-1), 0, EPSILON)
+assert Gf.IsClose(v.Eval(0), 0, EPSILON)
+assert Gf.IsClose(v.Eval(1), 1, EPSILON)
+assert Gf.IsClose(v.Eval(9), 9, EPSILON)
+assert Gf.IsClose(v.Eval(10), 10, EPSILON)
+assert Gf.IsClose(v.Eval(11), 10, EPSILON)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest float Range() with Bezier knots w/ zero-length handles')
+assert Gf.IsClose(v.Range(-1, 11), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(-1, -1), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(-1, 0), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(0, 10), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(1, 9), (1, 9), EPSILON)
+assert Gf.IsClose(v.Range(5, 5), (5, 5), EPSILON)
+assert Gf.IsClose(v.Range(10, 11), (10, 10), EPSILON)
+assert Gf.IsClose(v.Range(11, 11), (10, 10), EPSILON)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert Gf.IsClose(v.Range(-1, 11), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(-1, -1), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(-1, 0), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(0, 10), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(1, 9), (1, 9), EPSILON)
+assert Gf.IsClose(v.Range(5, 5), (5, 5), EPSILON)
+assert Gf.IsClose(v.Range(10, 11), (10, 10), EPSILON)
+assert Gf.IsClose(v.Range(11, 11), (10, 10), EPSILON)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest extrapolation with one Bezier knot w/ zero-length handles')
+del v[10]
+assert v.Eval(-5) == 0
+assert v.Eval( 5) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Eval(-5) == 0
+assert v.Eval( 5) == 0
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print('\tPassed')
+
+del v[:]
+
+print('\nTest float Eval() with Bezier knots')
+v.SetKeyFrame( Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 0.5, leftSlope = 0,
+ rightLen = 0.5, rightSlope = 0 ) )
+v.SetKeyFrame( Ts.KeyFrame( 5, value = 10.0, knotType = Ts.KnotBezier,
+ leftLen = 0.5, leftSlope = 0,
+ rightLen = 0.5, rightSlope = 0 ) )
+assert Gf.IsClose(v.Eval(-1), 0, EPSILON)
+assert Gf.IsClose(v.Eval(0), 0, EPSILON)
+assert Gf.IsClose(v.Eval(1), 1.7249, COARSE_EPSILON)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert Gf.IsClose(v.Eval(-1), 0, EPSILON)
+assert Gf.IsClose(v.Eval(0), 0, EPSILON)
+assert Gf.IsClose(v.Eval(1), 1.7249, COARSE_EPSILON)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+del v[:]
+
+print('\nTest float Eval() with Bezier knots (2)')
+v.SetKeyFrame( Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 0.5, leftSlope = 1,
+ rightLen = 0.5, rightSlope = -1 ) )
+v.SetKeyFrame( Ts.KeyFrame( 5, value = 10.0, knotType = Ts.KnotBezier,
+ leftLen = 0.5, leftSlope = 1,
+ rightLen = 0.5, rightSlope = 1 ) )
+v.SetKeyFrame( Ts.KeyFrame( 10, value = 10.0, knotType = Ts.KnotBezier,
+ leftLen = 0.5, leftSlope = -1,
+ rightLen = 0.5, rightSlope = 1 ) )
+v.SetKeyFrame( Ts.KeyFrame( 15, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 0.5, leftSlope = 1,
+ rightLen = 0.5, rightSlope = 1 ) )
+assert v.Eval(-1) == 0
+assert v.Eval(0) == 0
+assert Gf.IsClose(v.Eval(1), 1.4333, COARSE_EPSILON)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Eval(-1) == -1
+assert v.Eval(0) == 0
+assert Gf.IsClose(v.Eval(1), 1.4333, COARSE_EPSILON)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest float Range() with Bezier knots')
+assert Gf.IsClose(v.Range(-1, 16), (-0.018137, 10.375), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(-1, -1), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(-1, 0), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(0, 15), (-0.018137, 10.375), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(1, 9), (1.43337, 10.375), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(1, 1), (1.43337, 1.43337), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(15, 16), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(16, 16), (0, 0), EPSILON)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert Gf.IsClose(v.Range(-1, 16), (-0.018137, 10.375), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(-1, -1), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(-1, 0), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(0, 15), (-0.018137, 10.375), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(1, 9), (1.43337, 10.375), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(1, 1), (1.43337, 1.43337), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(15, 16), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(16, 16), (0, 0), EPSILON)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest extrapolation with one Bezier knot')
+del v[5]
+del v[10]
+del v[15]
+assert v.Eval(-5) == 0
+assert v.Eval( 5) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Eval(-5) == -5
+assert v.Eval( 5) == -5
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print('\tPassed')
+
+print('\nTest float Eval() with Bezier knots (3)')
+del v[:]
+# For code coverage, we construct a case that will exercise different
+# paths of the black box evaluation code.
+v.SetKeyFrame( Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 0.0, leftSlope = 0, rightLen = 0.0, rightSlope = 0 ) )
+v.SetKeyFrame( Ts.KeyFrame( 9, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 6.0, leftSlope = 0, rightLen = 0.0, rightSlope = 0 ) )
+v.Eval(0)
+v.Eval(0.5)
+# And another case...
+del v[:]
+v.SetKeyFrame( Ts.KeyFrame( 1, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 0.0, leftSlope = 0, rightLen = 0.0, rightSlope = 0 ) )
+v.SetKeyFrame( Ts.KeyFrame( 7, value = 0.0, knotType = Ts.KnotBezier,
+ leftLen = 4.0, leftSlope = 0, rightLen = 0.0, rightSlope = 0 ) )
+v.Eval(0)
+v.Eval(0.5)
+_Validate()
+print('\tPassed')
+
+del v[:]
+
+v0 = Gf.Vec3d( 1, 1, 1 )
+v1 = Gf.Vec3d( 11, 21, 31 )
+kf0 = Ts.KeyFrame(0, value = v0, knotType = Ts.KnotLinear)
+kf1 = Ts.KeyFrame(10, value = v1, knotType = Ts.KnotLinear)
+if v.CanSetKeyFrame(kf0):
+ print('\nTest Gf.Vec3d Eval() with linear knots')
+ v.SetKeyFrame(kf0)
+ v.SetKeyFrame(kf1)
+ def blend(a, b, u):
+ return a*(1-u) + b*u
+ assert v.Eval(-1) == v0
+ assert v.Eval(0) == v0
+ assert Gf.IsClose(v.Eval(1), blend( v0, v1, 0.1 ), EPSILON)
+ assert Gf.IsClose(v.Eval(9), blend( v0, v1, 0.9 ), EPSILON)
+ assert v.Eval(10) == v1
+ assert v.Eval(11) == v1
+ v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ assert Gf.IsClose(v.Eval(-1), blend( v0, v1, -0.1 ), EPSILON)
+ assert v.Eval(0) == v0
+ assert Gf.IsClose(v.Eval(1), blend( v0, v1, 0.1 ), EPSILON)
+ assert Gf.IsClose(v.Eval(9), blend( v0, v1, 0.9 ), EPSILON)
+ assert v.Eval(10) == v1
+ assert Gf.IsClose(v.Eval(11), blend( v0, v1, 1.1 ), EPSILON)
+ v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ _Validate()
+ del blend
+ print('\tPassed')
+
+print('\nTest Eval() with multiple keyframes given')
+v.clear()
+kf1 = Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotLinear )
+kf2 = Ts.KeyFrame( 10, value = 0.0, knotType = Ts.KnotLinear )
+v.SetKeyFrame(kf1)
+v.SetKeyFrame(kf2)
+keyframes = list(v[:])
+assert len(keyframes) > 1
+times = [kf.time for kf in keyframes]
+expectedValues = [ v.Eval(t) for t in times ]
+assert expectedValues == list( v.Eval(times) )
+_Validate()
+print('\tPassed')
+
+print('\nTest float Range() with mixed knot types')
+del v[:]
+v.SetKeyFrame( Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotLinear ) )
+v.SetKeyFrame( Ts.KeyFrame( 10, value = 10.0, knotType = Ts.KnotBezier,
+ leftLen = 0.5, leftSlope = 0,
+ rightLen = 0.5, rightSlope = 0 ) )
+assert Gf.IsClose(v.Range(-1, 11), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(-1, 0), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(0, 10), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(1, 9), (1.01184, 9.19740), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(1, 1), (1.01184, 1.01184), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(10, 11), (10, 10), EPSILON)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert Gf.IsClose(v.Range(-1, 11), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(-1, 0), (0, 0), EPSILON)
+assert Gf.IsClose(v.Range(0, 10), (0, 10), EPSILON)
+assert Gf.IsClose(v.Range(1, 9), (1.01184, 9.19740), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(1, 1), (1.01184, 1.01184), COARSE_EPSILON)
+assert Gf.IsClose(v.Range(10, 11), (10, 10), EPSILON)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest float Range() with empty TsValue')
+del v[:]
+assert v.Range(0, 10) == (None, None)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+assert v.Range(0, 10) == (None, None)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+_Validate()
+print('\tPassed')
+
+print('\nTest float Range() with bad time domain: errors expected')
+try:
+ v.Range(10, 0)
+ assert False, "should have failed"
+except Tf.ErrorException:
+ pass
+_Validate()
+print('\tPassed')
+
+del v[:]
+
+# Note that Range() cannot interpolate strings
+kf0 = Ts.KeyFrame( 0, "foo", Ts.KnotHeld )
+kf1 = Ts.KeyFrame( 10, "bar", Ts.KnotHeld )
+if v.CanSetKeyFrame(kf0):
+ print('\nTest string Range()')
+ v.SetKeyFrame(kf0)
+ v.SetKeyFrame(kf1)
+ assert v.Range(0, 10) == (None, None)
+ v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ assert v.Range(0, 10) == (None, None)
+ v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ _Validate()
+ print('\tPassed')
+
+########################################################################
+
+del v[:]
+
+v0 = Vt.DoubleArray(3)
+v0[:] = (1, 1, 1)
+v1 = Vt.DoubleArray(3)
+v1[:] = (10, 20, 30)
+kf0 = Ts.KeyFrame(0, value = v0, knotType = Ts.KnotBezier)
+kf1 = Ts.KeyFrame(10, value = v1, knotType = Ts.KnotBezier)
+if v.CanSetKeyFrame(kf0):
+ print('\nTest Eval() with VtArray<double> and bezflat knots')
+ v.SetKeyFrame(kf0)
+ v.SetKeyFrame(kf1)
+ assert v.Eval(-1) == v0
+ assert v.Eval(0) == v0
+ assert list( v.Eval(5) ) == [5.5, 10.5, 15.5]
+ assert v.Eval(10) == v1
+ assert v.Eval(11) == v1
+ v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ assert Gf.IsClose(list( v.Eval(-1) ), [0.1, -0.9, -1.9], EPSILON)
+ assert v.Eval(0) == v0
+ assert list( v.Eval(5) ) == [5.5, 10.5, 15.5]
+ assert v.Eval(10) == v1
+ assert list( v.Eval(11) ) == [10.9, 21.9, 32.9]
+ v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ v2 = Vt.DoubleArray(3)
+ v2[:] = (0,0,0)
+ v.SetKeyFrame( Ts.KeyFrame(0, value = v2, knotType = Ts.KnotBezier))
+ assert v.Eval(-1) == v2
+ assert v.Eval(0) == v2
+ assert list( v.Eval(5) ) == [5, 10, 15]
+ assert v.Eval(10) == v1
+ assert v.Eval(11) == v1
+ v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ assert list( v.Eval(-1) ) == [-1, -2, -3]
+ assert v.Eval(0) == v2
+ assert list( v.Eval(5) ) == [5, 10, 15]
+ assert v.Eval(10) == v1
+ assert list( v.Eval(11) ) == [11, 22, 33]
+ v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ _Validate()
+ print('\tPassed')
+
+########################################################################
+
+del v[:]
+
+print('\nTest that deleting knots properly affects evaluation')
+v.SetKeyFrame( Ts.KeyFrame( 0, value = 0.0, knotType = Ts.KnotLinear ) )
+v.SetKeyFrame( Ts.KeyFrame( 10, value = 20.0, knotType = Ts.KnotLinear ) )
+# Displace the middle knot
+midKnot = v.ClosestKeyFrame(5)
+midKnot.value = 100
+# Take a sample that depends on the middle knot value
+test_time = 2.5
+test_val = v.Eval(test_time)
+assert v.Eval(test_time) == test_val
+del v[midKnot.time]
+assert v.Eval(test_time) != test_val
+del test_val
+del test_time
+del midKnot
+_Validate()
+print('\tPassed')
+
+########################################################################
+
+del v[:]
+
+print('\nTest "closest keyframe" style access on empty value')
+for t in [-1, 0, 1]:
+ assert not v.ClosestKeyFrame(t)
+ assert not v.ClosestKeyFrameBefore(t)
+ assert not v.ClosestKeyFrameAfter(t)
+_Validate()
+print('\tPassed')
+
+print('\nTest "closest keyframe" style access')
+kf1 = Ts.KeyFrame( -1 )
+kf2 = Ts.KeyFrame( 0 )
+kf3 = Ts.KeyFrame( 1 )
+keyframes = [kf1, kf2, kf3]
+for kf in keyframes:
+ v.SetKeyFrame(kf)
+
+for kf in keyframes:
+ assert v.ClosestKeyFrame( kf.time - 0.1 ) == kf
+ assert v.ClosestKeyFrame( kf.time + 0.0 ) == kf
+ assert v.ClosestKeyFrame( kf.time + 0.1 ) == kf
+
+# Test edge cases
+assert v.ClosestKeyFrameBefore( kf1.time - 0.1 ) is None
+assert v.ClosestKeyFrameBefore( kf2.time - 0.1 ) == kf1
+assert v.ClosestKeyFrameBefore( kf3.time - 0.1 ) == kf2
+assert v.ClosestKeyFrameBefore( kf1.time ) is None
+assert v.ClosestKeyFrameBefore( kf2.time ) == kf1
+assert v.ClosestKeyFrameBefore( kf3.time ) == kf2
+assert v.ClosestKeyFrameBefore( kf1.time + 0.1 ) == kf1
+assert v.ClosestKeyFrameBefore( kf2.time + 0.1 ) == kf2
+assert v.ClosestKeyFrameBefore( kf3.time + 0.1 ) == kf3
+
+# Test edge cases
+assert v.ClosestKeyFrameAfter( kf1.time - 0.1 ) == kf1
+assert v.ClosestKeyFrameAfter( kf2.time - 0.1 ) == kf2
+assert v.ClosestKeyFrameAfter( kf3.time - 0.1 ) == kf3
+assert v.ClosestKeyFrameAfter( kf1.time ) == kf2
+assert v.ClosestKeyFrameAfter( kf2.time ) == kf3
+assert v.ClosestKeyFrameAfter( kf3.time ) is None
+assert v.ClosestKeyFrameAfter( kf1.time + 0.1 ) == kf2
+assert v.ClosestKeyFrameAfter( kf2.time + 0.1 ) == kf3
+assert v.ClosestKeyFrameAfter( kf3.time + 0.1 ) is None
+
+_Validate()
+print('\tPassed')
+
+########################################################################
+# Test breakdown
+
+leftKeyFrames = {
+ Ts.KnotHeld: Ts.KeyFrame(0, value = 0.0, knotType = Ts.KnotHeld),
+ Ts.KnotLinear: Ts.KeyFrame(0, value = 0.0, knotType = Ts.KnotLinear),
+ Ts.KnotBezier: Ts.KeyFrame(0, value = 0.0,
+ knotType = Ts.KnotBezier,
+ leftLen = 2.0, leftSlope = 0.0,
+ rightLen = 2.0, rightSlope = 0.0 )
+ }
+rightKeyFrames = {
+ Ts.KnotHeld: Ts.KeyFrame(12, value = 12.0, knotType = Ts.KnotHeld),
+ Ts.KnotLinear: Ts.KeyFrame(12, value = 12.0, knotType = Ts.KnotLinear),
+ Ts.KnotBezier: Ts.KeyFrame(12, value = 12.0,
+ knotType = Ts.KnotBezier,
+ leftLen = 2.0, leftSlope = 0.0,
+ rightLen = 2.0, rightSlope = 0.0 )
+ }
+breakdownFlatKeyFrames = {
+ Ts.KnotHeld: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotHeld),
+ Ts.KnotLinear: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotLinear),
+ Ts.KnotBezier: Ts.KeyFrame(6, value = 0.0,
+ knotType = Ts.KnotBezier,
+ leftLen = 1.0, leftSlope = 0.0,
+ rightLen = 1.0, rightSlope = 0.0 )
+ }
+breakdownNonFlatKeyFrames = {
+ Ts.KnotHeld: {
+ Ts.KnotHeld: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotHeld),
+ Ts.KnotLinear: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotLinear),
+ Ts.KnotBezier: Ts.KeyFrame(6, value = 0.0,
+ knotType = Ts.KnotBezier,
+ leftLen = 2.0,
+ leftSlope = 0.0,
+ rightLen = 2.0,
+ rightSlope = 0.0 )
+ },
+ Ts.KnotLinear: {
+ Ts.KnotHeld: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotHeld),
+ Ts.KnotLinear: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotLinear),
+ Ts.KnotBezier: Ts.KeyFrame(6, value = 0.0,
+ knotType = Ts.KnotBezier,
+ leftLen = 2.0,
+ leftSlope = 1.0,
+ rightLen = 2.0,
+ rightSlope = 1.0 )
+ },
+ Ts.KnotBezier: {
+ Ts.KnotHeld: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotHeld),
+ Ts.KnotLinear: Ts.KeyFrame(6, value = 0.0, knotType = Ts.KnotLinear),
+ Ts.KnotBezier: Ts.KeyFrame(6, value = 0.0,
+ knotType = Ts.KnotBezier,
+ leftLen = 2.5,
+ leftSlope = 1.2,
+ rightLen = 2.5,
+ rightSlope = 1.2 )
+ }
+ }
+
+for knotType in leftKeyFrames:
+ for breakdownType in breakdownFlatKeyFrames:
+ print('\nTest float Breakdown() with %s knots w/ flat %s knot' % \
+ (knotType, breakdownType))
+ del v[:]
+ v.SetKeyFrame( leftKeyFrames[knotType] )
+ v.SetKeyFrame( rightKeyFrames[knotType] )
+ x = v.Eval(6);
+ v.Breakdown(6, breakdownType, True, 1.0)
+
+ # Verify values
+ assert Gf.IsClose(v.Eval(0), 0, EPSILON)
+ assert Gf.IsClose(v.Eval(6), x, EPSILON)
+ assert Gf.IsClose(v.Eval(12), 12, EPSILON)
+ # Verify key frame
+ breakdownFlatKeyFrames[breakdownType].value = x
+ assert(v[6] == breakdownFlatKeyFrames[breakdownType])
+
+ _Validate()
+ print('\tPassed')
+
+ for breakdownType in breakdownNonFlatKeyFrames:
+ print('\nTest float Breakdown() with %s knots w/ non-flat %s knot' % \
+ (knotType, breakdownType))
+ del v[:]
+ v.SetKeyFrame( leftKeyFrames[knotType] )
+ v.SetKeyFrame( rightKeyFrames[knotType] )
+ x = v.Eval(6);
+ v.Breakdown(6, breakdownType, False, 1.0)
+
+ # Verify values
+ assert Gf.IsClose(v.Eval(0), 0, EPSILON)
+ assert Gf.IsClose(v.Eval(6), x, EPSILON)
+ assert Gf.IsClose(v.Eval(12), 12, EPSILON)
+ # Verify key frame
+ breakdownNonFlatKeyFrames[knotType][breakdownType].value = x
+ assert(v[6] == breakdownNonFlatKeyFrames[knotType][breakdownType])
+
+ _Validate()
+ print('\tPassed')
+
+# Test vectorized breakdown
+types = [Ts.KnotHeld, Ts.KnotLinear, Ts.KnotBezier]
+times = [0.0, 1.0, 2.0, 3.0]
+tangentLength = 100.0
+
+print('\nTest vectorized Breakdown()')
+for type in types:
+ # Reset the spline with some curvature to test
+ del v[:]
+ k1 = v.Breakdown(-1.0, Ts.KnotBezier, True, tangentLength, 0.0)
+ k2 = v.Breakdown(4.0, Ts.KnotBezier, True, tangentLength, 200.0)
+
+ # Save the initial values
+ values = {}
+ for t in times:
+ values[t] = v.Eval(t)
+
+ # Vectorized breakdown. Note that the following loop would produce a
+ # different result because each iteration affects the next:
+ #
+ # for t in times:
+ # b.Breakdown(t, type, True, tangentLength)
+ #
+ breakdownTimes = times + [k1.time, k2.time]
+ result = v.Breakdown(breakdownTimes, type, True, tangentLength)
+
+ print(list(result.keys()))
+ assert set(result.keys()) == set(breakdownTimes)
+ assert result[k1.time] == k1
+ assert result[k2.time] == k2
+
+ for t in times:
+ assert t in v
+ assert v[t].knotType == type
+ assert v[t].value == values[t]
+
+ if (type == Ts.KnotBezier):
+ assert v[t].leftSlope == 0.0
+ assert v[t].rightSlope == 0.0
+ assert v[t].leftLen == tangentLength
+ assert v[t].rightLen == tangentLength
+
+# Test breakdown with multiple values and knot types
+print('\nTest Breakdown() with multiple values and knot types')
+types = [Ts.KnotHeld, Ts.KnotLinear, Ts.KnotBezier]
+times = [0.0, 1.0, 2.0]
+values = [2.0, -2.0, 8.0]
+tangentLength = 10.0
+
+# Reset the spline with some curvature to test
+del v[:]
+k1 = v.Breakdown(-1.0, Ts.KnotBezier, True, tangentLength, 0.0)
+k2 = v.Breakdown(4.0, Ts.KnotBezier, True, tangentLength, 200.0)
+
+breakdownTimes = times + [k1.time, k2.time]
+breakdownTypes = types + [k1.knotType, k2.knotType]
+breakdownValues = values + [k1.value, k2.value]
+result = v.Breakdown(breakdownTimes, breakdownTypes, False, tangentLength, \
+ breakdownValues)
+
+print(list(result.keys()))
+assert set(result.keys()) == set(breakdownTimes)
+assert result[k1.time] == k1
+assert result[k2.time] == k2
+
+for i, t in enumerate(times):
+ assert t in v
+ assert v[t].knotType == types[i]
+ assert v[t].value == values[i]
+
+########################################################################
+# Test Sample()
+
+def _IsAbsClose(a, b, epsilon):
+ return abs(a-b) < epsilon
+
+def _CheckSamples(val, samples, startTime, endTime, tolerance):
+ for i,s in enumerate(samples):
+ assert not s.isBlur
+ assert s.leftTime <= s.rightTime
+ if i != 0:
+ assert samples[i - 1].rightTime <= samples[i].leftTime
+ if s.leftTime >= startTime:
+ assert _IsAbsClose(s.leftValue,
+ val.Eval(s.leftTime, Ts.Right),
+ tolerance)
+ if s.rightTime <= endTime:
+ assert _IsAbsClose(s.rightValue,
+ val.Eval(s.rightTime, Ts.Left),
+ tolerance)
+ if samples:
+ assert samples[0].leftTime <= startTime
+ assert samples[-1].rightTime >= endTime
+
+# Sample to within this error tolerance
+tolerance = 1.0e-3
+
+# Maximum allowed error is not tolerance, it's much larger. This
+# is because Eval() samples differently between frames than at
+# frames and will yield slightly incorrect results but avoid
+# problems with large derivatives. Sample() does not do that.
+maxError = 0.15
+
+print("\nTest Sample() with bad time domain: errors expected\n")
+try:
+ samples = v.Sample(11, -1, 1.0, 1.0, tolerance)
+ assert False, 'exception expected'
+except Tf.ErrorException:
+ pass
+print("\tpassed\n")
+
+print("\nTest Sample() with no knots\n")
+v.clear()
+samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+assert len(samples) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+assert len(samples) == 0
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print("\tpassed\n")
+
+print("\nTest Sample() with empty time domain\n")
+v.SetKeyFrame( Ts.KeyFrame(0, 0.0, Ts.KnotHeld) )
+v.SetKeyFrame( Ts.KeyFrame(10, 10.0, Ts.KnotHeld) )
+samples = v.Sample(3, 3, 1.0, 1.0, tolerance)
+assert len(samples) == 0
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+samples = v.Sample(3, 3, 1.0, 1.0, tolerance)
+assert len(samples) == 0
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print("\tpassed\n")
+
+print("\nTest double Sample() with held knots\n")
+samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 11, maxError)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 11, maxError)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print("\tpassed\n")
+
+v.clear()
+kf0 = Ts.KeyFrame(0, "foo", Ts.KnotHeld)
+if v.CanSetKeyFrame(kf0):
+ print("\nTest string Sample() with held knots\n")
+ v.clear()
+ v.SetKeyFrame( Ts.KeyFrame(0, "foo", Ts.KnotHeld) )
+ v.SetKeyFrame( Ts.KeyFrame(10, "bar", Ts.KnotHeld) )
+ samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+ assert len(samples) == 3
+ assert samples[0].leftValue == "foo"
+ assert samples[1].leftValue == "foo"
+ assert samples[2].leftValue == "bar"
+ assert samples[0].leftValue == samples[0].rightValue
+ assert samples[1].leftValue == samples[1].rightValue
+ assert samples[2].leftValue == samples[2].rightValue
+ v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+ samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+ assert len(samples) == 3
+ assert samples[0].leftValue == "foo"
+ assert samples[1].leftValue == "foo"
+ assert samples[2].leftValue == "bar"
+ assert samples[0].leftValue == samples[0].rightValue
+ assert samples[1].leftValue == samples[1].rightValue
+ assert samples[2].leftValue == samples[2].rightValue
+ v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+ print("\tpassed\n")
+
+print("\nTest Sample() with linear knots\n")
+v.clear()
+v.SetKeyFrame( Ts.KeyFrame(0, 0.0, Ts.KnotLinear) )
+v.SetKeyFrame( Ts.KeyFrame(10, 10.0, Ts.KnotLinear) )
+samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 11, maxError)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+samples = v.Sample(-1, 11, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 11, maxError)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print("\tpassed\n")
+
+print("\nTest Sample() with Bezier knots\n")
+v.clear()
+v.SetKeyFrame( Ts.KeyFrame(0, 0.0, Ts.KnotBezier,
+ 1.0, 0.5, 1.0, 0.5) )
+v.SetKeyFrame( Ts.KeyFrame(5, 10.0, Ts.KnotBezier,
+ 1.0, 0.5, 1.0, 0.5) )
+v.SetKeyFrame( Ts.KeyFrame(10, 10.0, Ts.KnotBezier,
+ -1.0, 0.5, 1.0, 0.5) )
+v.SetKeyFrame( Ts.KeyFrame(15, 0.0, Ts.KnotBezier,
+ 1.0, 0.5, 1.0, 0.5) )
+samples = v.Sample(-1, 16, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 16, maxError)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+samples = v.Sample(-1, 16, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 16, maxError)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print("\tpassed\n")
+
+print("\nTest Sample() with mixed knot types\n")
+v.clear()
+v.SetKeyFrame( Ts.KeyFrame(0, 0.0, Ts.KnotLinear) )
+v.SetKeyFrame( Ts.KeyFrame(5, 5.0, Ts.KnotBezier,
+ 0.0, 0.5, 0.0, 0.5) )
+v.SetKeyFrame( Ts.KeyFrame(10, 10.0, Ts.KnotLinear) )
+samples = v.Sample(-1, 16, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 16, maxError)
+v.extrapolation = (Ts.ExtrapolationLinear, Ts.ExtrapolationLinear)
+samples = v.Sample(-1, 16, 1.0, 1.0, tolerance)
+_CheckSamples(v, samples, -1, 16, maxError)
+v.extrapolation = (Ts.ExtrapolationHeld, Ts.ExtrapolationHeld)
+print("\tpassed\n")
+
+############################################################################
+
+def TestHeldEval():
+ print("\nTest held evaluation:")
+
+ spline = v
+ spline.clear()
+ spline.extrapolation = (Ts.ExtrapolationLinear,
+ Ts.ExtrapolationLinear)
+
+ spline.SetKeyFrame(Ts.KeyFrame(time = 0.0, value = 0.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = -1.0, rightSlope = -1.0,
+ leftLen = 1.0, rightLen = 1.0))
+
+ spline.SetKeyFrame(Ts.KeyFrame(time = 5.0, value = 1.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = 0.0, rightSlope = 0.0,
+ leftLen = 1.0, rightLen = 1.0))
+
+ spline.SetKeyFrame(Ts.KeyFrame(time = 10.0, value = 0.0,
+ knotType = Ts.KnotBezier,
+ leftSlope = 1.0, rightSlope = 1.0,
+ leftLen = 1.0, rightLen = 1.0))
+
+
+ # Check our math for normal bezier interpolation.
+ assert spline.Eval(-2.5) == 2.5
+ assert spline.Eval(2.5) == 0.125
+ assert spline.Eval(7.5) == 0.125
+ assert spline.Eval(12.5) == 2.5
+
+ # Check with held evaluation. Extrapolation and interpolation should both
+ # behave as if "held".
+ assert spline.EvalHeld(-2.5) == 0.0
+ assert spline.EvalHeld(2.5) == 0.0
+ assert spline.EvalHeld(7.5) == 1.0
+ assert spline.EvalHeld(12.5) == 0.0
+
+ # Check held evaluation with side argument.
+ assert spline.EvalHeld(5.0) == 1.0
+ assert spline.EvalHeld(5.0, side=Ts.Right) == 1.0
+ assert spline.EvalHeld(5.0, side=Ts.Left) == 0.0
+
+ assert spline.EvalHeld(10.0) == 0.0
+ assert spline.EvalHeld(10.0, side=Ts.Right) == 0.0
+ assert spline.EvalHeld(10.0, side=Ts.Left) == 1.0
+
+ # Check held evaluation with dual-valued knot. Held evaluation on the
+ # left side of a dual-valued knot should actually give us the value of
+ # the previous keyframe, not the left value.
+ kf = spline[5.0]
+ kf.value = (-1.0, 1.0)
+ spline.SetKeyFrame(kf)
+
+ assert spline.EvalHeld(5.0) == 1.0
+ assert spline.EvalHeld(5.0, side=Ts.Right) == 1.0
+ assert spline.EvalHeld(5.0, side=Ts.Left) == 0.0
+
+ print("\tpassed")
+
+TestHeldEval()
+
+############################################################################
+
+def TestLooping():
+ print("\nTest looping splines:")
+ # value offset used below
+ offset = 2.3
+
+ # Test LoopParams
+ params = Ts.LoopParams(True, 10, 20, 30, 40, offset)
+ assert params.looping == True
+ assert params.GetMasterInterval() == Gf.Interval(10, 30, True, False)
+ assert params.GetLoopedInterval() == Gf.Interval(-20, 70, True, False)
+ assert params.valueOffset == offset
+ # Equality
+ assert params == Ts.LoopParams(True, 10, 20, 30, 40, offset)
+ spline = v
+ spline.extrapolation = (Ts.ExtrapolationHeld,
+ Ts.ExtrapolationHeld)
+ spline.clear()
+ # Add some knots in the master region to be unrolled
+ v.SetKeyFrame( Ts.KeyFrame( 10, value = 10.0, knotType = Ts.KnotLinear ) )
+ v.SetKeyFrame( Ts.KeyFrame( 20, value = 20.0, knotType = Ts.KnotLinear ) )
+ # Add a knot in the region to be unrolled; it will be "hidden"
+ v.SetKeyFrame( Ts.KeyFrame( 35, value = 100.0, knotType = Ts.KnotLinear ) )
+ # Loop it, eval at various places, should be a triangle wave with 1.5
+ # periods of prepeat and 2 periods of repeat, with held extrapolation
+ # beyond
+ v.loopParams = params
+ # Check the keframe count
+ assert len(v.frames) == 9
+ # Check all the keyframe values
+ assert v.Eval(10) == 10
+ assert v.Eval(20) == 20
+ # repeat region from 30 through 70
+ assert v.Eval(30) == 10 + offset
+ assert v.Eval(40) == 20 + offset
+ assert v.Eval(50) == 10 + 2 * offset
+ assert v.Eval(60) == 20 + 2 * offset
+ # held from here on out
+ assert v.Eval(61) == 20 + 2 * offset
+ # prepeat region from -20 through 0
+ assert v.Eval(0) == 20 - offset
+ assert v.Eval(-10) == 10 - offset
+ assert v.Eval(-20) == 20 - 2 * offset
+ # held from here on out
+ assert v.Eval(-21) == 20 - 2 * offset
+ # eval between knots in the unrolled region, this is where there's
+ # a knot "hidden" by the unrolling
+ assert v.Eval(35) == 15 + offset
+
+ # add a key in the master region; should be written
+ v.SetKeyFrame( Ts.KeyFrame( 15, value = 50.0,
+ knotType = Ts.KnotLinear ) )
+ assert v.Eval(15) == 50
+ # because of unrolling, really added 4 frames
+ assert len(v.frames) == 13
+
+ # add keys before and after the pre and repeat; should be written
+ v.SetKeyFrame( Ts.KeyFrame( -200, value = -200.0,
+ knotType = Ts.KnotLinear ) )
+ v.SetKeyFrame( Ts.KeyFrame( 200, value = 200.0,
+ knotType = Ts.KnotLinear ) )
+ assert len(v.frames) == 15
+ assert v.Eval(-200) == -200
+ assert v.Eval(200) == 200
+
+ # add a key in the unrolled region; should be ignored
+ oldVal = v.Eval(35)
+ v.SetKeyFrame( Ts.KeyFrame( 35, value = 50.0,
+ knotType = Ts.KnotLinear ) )
+ assert v.Eval(35) == oldVal
+ assert len(v.frames) == 15
+
+ # remove a key in the master region
+ del v[ 15.0 ]
+ assert v.Eval(15) != 50
+ # because of unrolling, really removed 4 frames
+ assert len(v.frames) == 11
+
+ # turn off looping and see if the "hidden" knot is still there, and
+ # if the one at 15 got removed; should result in a count of 5
+ params.looping = False
+ v.loopParams = params
+ assert len(v.frames) == 5
+ assert v.Eval(35) == 100.0
+
+ # Test baking; start over
+ v.clear()
+ v.SetKeyFrame(Ts.KeyFrame(0.0, 1.0))
+ v.SetKeyFrame(Ts.KeyFrame(1.0, 2.0))
+ assert len(v.frames) == 2
+ # Turn on looping
+ v.loopParams = Ts.LoopParams(True, 0, 2, 2, 1, 2)
+ assert len(v.frames) == 5
+ assert v.loopParams.looping == True
+ # grab keys before baking
+ keysBeforeBaking = v[:]
+ assert len(keysBeforeBaking) == 5
+ # now bake
+ v.BakeSplineLoops()
+ # looping should be off
+ assert v.loopParams.looping == False
+ # and it should have the same keys
+ assert keysBeforeBaking == v[:]
+
+TestLooping()
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/spline.h"
+
+#include "pxr/base/vt/value.h"
+#include "pxr/base/tf/diagnosticLite.h"
+
+#include <thread>
+#include <cstdlib>
+#include <vector>
+#include <functional>
+#include <iostream>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using TestFunction = std::function<void()>;
+
+
+// Execute a function which returns a T and verify
+// that the returned value is equal to expectedResult
+template <typename T>
+void ExecuteAndCompare(
+ const std::function<T()> &function,
+ const T &expectedResult)
+{
+ T result = function();
+ TF_AXIOM(result == expectedResult);
+}
+
+// Set a keyframe to val at time.
+TsSpline SetKeyFrame(TsSpline source, double time, double val) {
+ source.SetKeyFrame(TsKeyFrame(time,val));
+ return source;
+}
+
+// Create a TestFunction which sets some value at some time
+TestFunction CreateSetKeyFrameTest(const TsSpline &baseSpline)
+{
+ double time = rand() % 100;
+ double value = rand()/((double)RAND_MAX);
+ std::function<TsSpline()> f =
+ std::bind(SetKeyFrame,baseSpline,time,value);
+ return std::bind(ExecuteAndCompare<TsSpline>, f, f());
+}
+
+VtValue Eval(TsSpline source, double time)
+{
+ return source.Eval(time);
+}
+
+TestFunction CreateEvalTest(const TsSpline &baseSpline)
+{
+ // Evaluate somewhere between 0 and 10
+ double time = 10.0*(rand()/(double)RAND_MAX);
+ std::function<VtValue()> f = std::bind(Eval, baseSpline, time);
+ return std::bind(ExecuteAndCompare<VtValue>, f, f());
+}
+
+void RunTests(const std::vector<TestFunction> &tests, size_t iterations)
+{
+ const std::thread::id id = std::this_thread::get_id();
+ std::cout << "Running " << iterations
+ << " tests in thread " << id << std::endl;
+
+ for (size_t i=0; i < iterations; i++) {
+ tests[i%tests.size()]();
+ }
+
+ std::cout << "Done running tests in thread " << id << std::endl;
+}
+
+int main() {
+ // Set up a base spline with several key frames already in it
+ // All the operations will be on objects that share a COW representation
+ // with this spline.
+ TsSpline baseSpline;
+ baseSpline.SetKeyFrame(TsKeyFrame(1,1.));
+ baseSpline.SetKeyFrame(TsKeyFrame(5,5.));
+ baseSpline.SetKeyFrame(TsKeyFrame(10,10.));
+
+ // Set up a bunch of tests and their expected results
+ std::vector<TestFunction> tests;
+ for (int i=0; i < 10; i++) {
+ tests.push_back(CreateSetKeyFrameTest(baseSpline));
+ tests.push_back(CreateEvalTest(baseSpline));
+ }
+
+ // Create some threads which will each execute the tests many times
+ size_t numIterations = 100000;
+ std::vector<std::thread> threads;
+ for (size_t i=0; i < 8; i++) {
+ threads.emplace_back(std::bind(RunTests, tests, numIterations));
+ }
+
+ // Wait for all the test threads to complete
+ for (std::thread &thread : threads) {
+ thread.join();
+ }
+ std::cout << "PASSED" << std::endl;
+ return 0;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/diff.h"
+#include "pxr/base/ts/evaluator.h"
+#include "pxr/base/ts/spline.h"
+#include "pxr/base/ts/typeRegistry.h"
+
+#include "pxr/base/gf/range1d.h"
+#include "pxr/base/gf/rotation.h"
+#include "pxr/base/tf/declarePtrs.h"
+#include "pxr/base/tf/diagnosticLite.h"
+#include "pxr/base/tf/weakBase.h"
+#include "pxr/base/tf/stringUtils.h"
+
+#include <limits>
+#include <sstream>
+#include <utility>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using std::string;
+
+static const TsTime inf = std::numeric_limits<TsTime>::infinity();
+
+// Helper class that verifies expected diffs from spline modifications as
+// reported by two sources: the intervalAffected out-param from the spline API,
+// and the TsFindChangedInterval utility.
+//
+class _SplineTester
+{
+public:
+ _SplineTester(const TsSpline & v) :
+ spline(v)
+ {}
+
+ bool SetKeyFrame(const TsKeyFrame &keyFrame,
+ const GfInterval &expectedInterval)
+ {
+ // Make a copy of the previous spline.
+ const TsSpline oldSpline = spline;
+
+ // Make the modification and record intervalAffected.
+ GfInterval actionInterval;
+ spline.SetKeyFrame(keyFrame, &actionInterval);
+
+ // Diff the previous and current splines.
+ const GfInterval diffInterval =
+ TsFindChangedInterval(oldSpline, spline);
+
+ // Verify both intervals are as expected.
+ if (actionInterval == expectedInterval
+ && diffInterval == expectedInterval) {
+ return true;
+ } else {
+ std::cerr << "Failed SetKeyFrame:\n"
+ << " actionInterval: " << actionInterval << "\n"
+ << " diffInterval: " << diffInterval << "\n"
+ << " expectedInterval: " << expectedInterval << "\n"
+ << "Result spline was:\n" << spline
+ << std::endl;
+ return false;
+ }
+ }
+
+ bool RemoveKeyFrame(const TsTime time,
+ const GfInterval &expectedInterval)
+ {
+ // Make a copy of the previous spline.
+ const TsSpline oldSpline = spline;
+
+ // Make the modification and record intervalAffected.
+ GfInterval actionInterval;
+ spline.RemoveKeyFrame(time, &actionInterval);
+
+ // Diff the previous and current splines.
+ const GfInterval diffInterval =
+ TsFindChangedInterval(oldSpline, spline);
+
+ // Verify both intervals are as expected.
+ if (actionInterval == expectedInterval
+ && diffInterval == expectedInterval) {
+ return true;
+ } else {
+ std::cerr << "Failed RemoveKeyFrame:\n"
+ << " actionInterval: " << actionInterval << "\n"
+ << " diffInterval: " << diffInterval << "\n"
+ << " expectedInterval: " << expectedInterval << "\n"
+ << "Result spline was:\n" << spline
+ << std::endl;
+ return false;
+ }
+ }
+
+ bool SetValue(const TsSpline &newValue,
+ const GfInterval &expectedInterval)
+ {
+ // Make a copy of the previous spline.
+ const TsSpline oldSpline = spline;
+
+ // Record the new value. There is no API that returns an
+ // intervalAffected for whole-spline value changes.
+ spline = newValue;
+
+ // Diff the previous and current splines.
+ const GfInterval diffInterval =
+ TsFindChangedInterval(oldSpline, spline);
+
+ // Verify the diff interval is as expected.
+ if (diffInterval == expectedInterval) {
+ return true;
+ } else {
+ std::cerr << "Failed SetValue:\n"
+ << " diffInterval: " << diffInterval << "\n"
+ << " expectedInterval: " << expectedInterval
+ << std::endl;
+ return false;
+ }
+ }
+
+public:
+ TsSpline spline;
+};
+
+
+template <typename T>
+static bool
+_IsClose(const double& a, const double& b,
+ T eps = std::numeric_limits<T>::epsilon())
+{
+ return fabs(a - b) < eps;
+}
+
+template <typename T>
+static bool
+_IsClose(const VtValue& a, const VtValue& b,
+ T eps = std::numeric_limits<T>::epsilon())
+{
+ return fabs(a.Get<T>() - b.Get<T>()) < eps;
+}
+
+template <>
+bool
+_IsClose(const VtValue& a, const VtValue& b, GfVec2d eps)
+{
+ GfVec2d _b = b.Get<GfVec2d>();
+ GfVec2d negB(-_b[0], -_b[1]);
+ GfVec2d diff = a.Get<GfVec2d>() + negB;
+ return fabs(diff[0]) < eps[0] && fabs(diff[1]) < eps[1];
+}
+
+template <typename T>
+static void
+_AssertSamples(const TsSpline & val,
+ const TsSamples & samples,
+ double startTime, double endTime, T tolerance)
+{
+ for (size_t i = 0; i < samples.size(); ++i) {
+ TF_AXIOM(!samples[i].isBlur);
+ TF_AXIOM(samples[i].leftTime <= samples[i].rightTime);
+ if (i != 0) {
+ TF_AXIOM(samples[i - 1].rightTime <= samples[i].leftTime);
+ }
+
+ if (samples[i].leftTime >= startTime) {
+ TF_AXIOM(_IsClose<T>(samples[i].leftValue,
+ val.Eval(samples[i].leftTime, TsRight),
+ tolerance));
+ }
+ if (samples[i].rightTime <= endTime) {
+ TF_AXIOM(_IsClose<T>(samples[i].rightValue,
+ val.Eval(samples[i].rightTime, TsLeft),
+ tolerance));
+ }
+ }
+ if (!samples.empty()) {
+ TF_AXIOM(samples.front().leftTime <= startTime);
+ TF_AXIOM(samples.back().rightTime >= endTime);
+ }
+}
+
+// Helper to verify that raw spline evals match values
+// from the TsEvaluator.
+void _VerifyEvaluator(TsSpline spline)
+{
+ TsEvaluator<double> evaluator(spline);
+ for (TsTime sample = -2.0; sample < 2.0; sample += 0.1) {
+ VtValue rawEvalValue = spline.Eval(sample);
+ TF_AXIOM(_IsClose<double>(!rawEvalValue.IsEmpty() ?
+ rawEvalValue.Get<double>() :
+ TsTraits<double>::zero,
+ evaluator.Eval(sample)));
+ }
+}
+
+void
+_AddSingleKnotSpline(TsTime knotTime, const VtValue &knotValue,
+ std::vector<TsSpline> *splines)
+{
+ TsSpline spline;
+ spline.SetKeyFrame(TsKeyFrame(knotTime, knotValue));
+ splines->push_back(spline);
+}
+
+// Helper function to verify that a setting the value of a spline to multiple
+// single knot splines with the same value but their one keyframe at different
+// times will always cause the same invalidation interval.
+bool _TestSetSingleValueSplines(const TsSpline &testSpline,
+ const VtValue &value,
+ const GfInterval &testInterval)
+{
+ // First we create a list of single knot splines with the same flat value.
+ // We create a full spread a splines that hit all the case of the single
+ // knot being on each key frame, between each key frame, and before and
+ // after the first and last key frames. This should cover every case.
+ std::vector<TsSpline> singleKnotSplines;
+ TsKeyFrameMap keyFrames = testSpline.GetKeyFrames();
+ TsTime prevTime = 0;
+ // Get all the spline's key frames.
+ TF_FOR_ALL(kf, keyFrames) {
+ TsTime time = kf->GetTime();
+ if (singleKnotSplines.empty()) {
+ // The first key frame, so add a spline with a knot before the
+ // first key frame.
+ _AddSingleKnotSpline(time - 5.0, value, &singleKnotSplines);
+ } else {
+ // Add a spline with its knot between the previous key frame and
+ // this key frame.
+ _AddSingleKnotSpline((time - prevTime) / 2.0, value, &singleKnotSplines);
+ }
+ // Add a spline with a knot a this key frame.
+ _AddSingleKnotSpline(time, value, &singleKnotSplines);
+
+ prevTime = time;
+ }
+ // Add the final spline with the knot after the last key frame.
+ _AddSingleKnotSpline(prevTime + 5.0, value, &singleKnotSplines);
+
+ // Test setting each of the single knot spline as the value over the
+ // given spline and make sure each one has the same given invalidation
+ // interval.
+ TF_FOR_ALL(singleKnotSpline, singleKnotSplines) {
+ if (!_SplineTester(testSpline).SetValue(
+ *singleKnotSpline, testInterval)) {
+ std::cerr << "Failed to set single value spline: " << (*singleKnotSpline) << "\n";
+ return false;
+ }
+ }
+ return true;
+}
+
+
+void TestEvaluator()
+{
+ // Empty spline case.
+ TsSpline spline;
+ _VerifyEvaluator(spline);
+
+ // Single knot case
+ spline.SetKeyFrame( TsKeyFrame(-1.0, -1.0, TsKnotBezier) );
+ _VerifyEvaluator(spline);
+
+ // Test evaluation with non-flat tangent.
+ spline.Clear();
+ spline.SetKeyFrame( TsKeyFrame(-1.0, 0.0, TsKnotBezier, 0.0, 0.0, 0.9, 0.9) );
+ spline.SetKeyFrame( TsKeyFrame(0.0, 1.0, TsKnotBezier,
+ 0.168776965344754, 0.168776965344754,
+ 1.85677, 1.85677) );
+ spline.SetKeyFrame( TsKeyFrame(1.0, 0.0, TsKnotBezier, 0.0, 0.0, 0.9, 0.9) );
+ _VerifyEvaluator(spline);
+
+ // Test evaluation with long tangent that causes the spline to be clipped.
+ spline.Clear();
+ spline.SetKeyFrame( TsKeyFrame(-1.0, 0.0, TsKnotBezier, 0.0, 0.0, 0.9, 0.9) );
+ spline.SetKeyFrame( TsKeyFrame(0.0, 1.0, TsKnotBezier,
+ -0.0691717091793238, -0.0691717091793238,
+ 9.49162, 9.49162) );
+ spline.SetKeyFrame( TsKeyFrame(1.0, 0.0, TsKnotBezier, 0.0, 0.0, 0.9, 0.9) );
+ _VerifyEvaluator(spline);
+}
+
+void TestSplineDiff()
+{
+ printf("\nTest spline diffing\n");
+
+ TsSpline initialVal;
+ initialVal.SetKeyFrame( TsKeyFrame(1, VtValue( "bar" )) );
+ _SplineTester tester(initialVal);
+
+ TF_AXIOM(tester.SetKeyFrame( TsKeyFrame(0, VtValue( "blah" )),
+ GfInterval(-inf, 1.0, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( TsKeyFrame(2, VtValue( "papayas" )),
+ GfInterval(2.0, inf, true, false) ));
+ TF_AXIOM(tester.SetKeyFrame( TsKeyFrame(4, VtValue( "navel" )),
+ GfInterval( 4.0, inf, true, false )));
+
+ // Set a kf in the middle
+ TF_AXIOM(tester.SetKeyFrame( TsKeyFrame(3, VtValue( "pippins" )),
+ GfInterval(3.0, 4.0, true, false )));
+
+ // Test setting and removing redundant key frames
+ TF_AXIOM(tester.SetKeyFrame( TsKeyFrame(2.5, VtValue( "papayas" )),
+ GfInterval() ));
+ TF_AXIOM(tester.RemoveKeyFrame( 2.5, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( TsKeyFrame(-1.0, VtValue( "blah" )),
+ GfInterval() ));
+ TF_AXIOM(tester.RemoveKeyFrame( -1.0, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( TsKeyFrame(5.0, VtValue( "navel" )),
+ GfInterval() ));
+ TF_AXIOM(tester.RemoveKeyFrame( 5.0, GfInterval()));
+
+ // Remove middle kf
+ TF_AXIOM(tester.RemoveKeyFrame( 3, GfInterval(3.0, 4.0, true, false )));
+
+ // Remove first kf
+ TF_AXIOM(tester.RemoveKeyFrame( 0, GfInterval(-inf, 1.0, false, false )));
+
+ // Remove last kf
+ TF_AXIOM(tester.RemoveKeyFrame( 4, GfInterval(4.0, inf, true, false )));
+
+ printf("\tpassed\n");
+}
+
+void TestSplineDiff2()
+{
+ printf("\nTest more spline diffing\n");
+
+ _SplineTester tester = _SplineTester(TsSpline());
+
+ // Set a first knot
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(0.0, 0.0),
+ GfInterval::GetFullInterval()));
+
+ // Set a knot on the right side
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(3.0, 1.0),
+ GfInterval(0.0, inf, false, false)));
+
+ // Set a knot in the middle of those
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(2.0, 2.0),
+ GfInterval(0.0, 3.0, false, false)));
+
+ // Set another knot in the middle
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(1.0, 3.0),
+ GfInterval(0.0, 2.0, false, false)));
+
+ // Set the first knot again
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(0.0, 4.0),
+ GfInterval(-inf, 1.0, false, false)));
+}
+
+void TestHeldThenBezier()
+{
+ printf("\nTest held knot followed by Bezier knot\n");
+
+ _SplineTester tester = _SplineTester(TsSpline());
+
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(0.0, 123.0, TsKnotHeld),
+ GfInterval::GetFullInterval()));
+
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(1.0, 1.0, TsKnotBezier),
+ GfInterval(1.0, inf, true, false)));
+
+ TF_AXIOM(tester.RemoveKeyFrame(
+ 1.0,
+ GfInterval(1.0, inf, true, false)));
+
+ printf("\tpassed\n");
+}
+
+void TestRedundantKnots()
+{
+ printf("\nTest redundant knots\n");
+
+ _SplineTester tester = _SplineTester(TsSpline());
+
+ // Add the first knot.
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(1.0, 0.0),
+ GfInterval::GetFullInterval()));
+
+ // Add another knot.
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(2.0, 1.0),
+ GfInterval(1.0, inf, false, false)));
+
+ // Re-adding the same knot should give an empty edit interval.
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(2.0, 1.0),
+ GfInterval()));
+
+ // Changing an existing knot should cause changes.
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(2.0, 0.0),
+ GfInterval(1.0, inf, false, false)));
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(2.0, 1.0),
+ GfInterval(1.0, inf, false, false)));
+
+ // Add some redundant knots.
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(3.0, 1.0),
+ GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame(
+ TsKeyFrame(4.0, 1.0),
+ GfInterval()));
+
+ // Redundant knot removed, edit interval should be empty.
+ TF_AXIOM(tester.RemoveKeyFrame(3.0, GfInterval()));
+
+ // Redundant knot removed at end of spline, interval should be empty.
+ TF_AXIOM(tester.RemoveKeyFrame(4.0, GfInterval()));
+
+ // Removing a non-redundant knot should cause changes.
+ TF_AXIOM(tester.RemoveKeyFrame(2.0, GfInterval(1.0, inf, false, false)));
+
+ // Final knot removed. This may or may not have been redundant, depending
+ // on the fallback value, which is a higher-level concept; the spline
+ // diffing code conservatively reports that the (flat) value may have
+ // changed.
+ TF_AXIOM(tester.RemoveKeyFrame(1.0, GfInterval::GetFullInterval()));
+
+ // Setting flat constant splines should be redundant
+ TsSpline sourceSpline;
+ sourceSpline.SetKeyFrame( TsKeyFrame( 2, VtValue(1.0) ) );
+ TsSpline splineToSet1;
+ splineToSet1.SetKeyFrame( TsKeyFrame( 1, VtValue(0.0) ) );
+ TsSpline splineToSet2;
+ splineToSet2.SetKeyFrame( TsKeyFrame( 3, VtValue(1.0) ) );
+ TsSpline splineToSet3;
+ splineToSet3.SetKeyFrame( TsKeyFrame( 1, VtValue(1.0) ) );
+ splineToSet3.SetKeyFrame( TsKeyFrame( 3, VtValue(1.0) ) );
+ TF_AXIOM(!sourceSpline.IsVarying());
+ TF_AXIOM(!splineToSet1.IsVarying());
+ TF_AXIOM(!splineToSet2.IsVarying());
+ TF_AXIOM(!splineToSet3.IsVarying());
+
+ // Flat spline where values differ, whole interval is changed.
+ tester = _SplineTester(sourceSpline);
+ TF_AXIOM(tester.SetValue(splineToSet1, GfInterval::GetFullInterval()));
+
+ // Flat spline same value at different time, no change
+ tester = _SplineTester(sourceSpline);
+ TF_AXIOM(tester.SetValue(splineToSet2, GfInterval()));
+ tester = _SplineTester(sourceSpline);
+ TF_AXIOM(tester.SetValue(splineToSet3, GfInterval()));
+
+ printf("\tpassed\n");
+}
+
+void TestChangeIntervalsOnAssignment()
+{
+ printf("\nTest change intervals on assignment\n");
+
+ // Create the first spline.
+ TsSpline spline;
+ spline.SetKeyFrame( TsKeyFrame( 1, VtValue( 0.0 ) ) );
+ spline.SetKeyFrame( TsKeyFrame( 2, VtValue( 0.0 ) ) );
+ spline.SetKeyFrame( TsKeyFrame( 3, VtValue( 0.0 ) ) );
+ spline.SetKeyFrame( TsKeyFrame( 4, VtValue( 0.0 ) ) );
+ spline.SetKeyFrame( TsKeyFrame( 5, VtValue( 0.0 ) ) );
+
+ // Create a second spline with only one knot different.
+ TsSpline spline2;
+ spline2.SetKeyFrame( TsKeyFrame( 1, VtValue( 0.0 ) ) );
+ spline2.SetKeyFrame( TsKeyFrame( 2, VtValue( 0.0 ) ) );
+ spline2.SetKeyFrame( TsKeyFrame( 3, VtValue( 1.0 ) ) );
+ spline2.SetKeyFrame( TsKeyFrame( 4, VtValue( 0.0 ) ) );
+ spline2.SetKeyFrame( TsKeyFrame( 5, VtValue( 0.0 ) ) );
+
+ // Change from one spline to the other and verify there is a difference.
+ _SplineTester tester = _SplineTester(spline);
+ TF_AXIOM(tester.SetValue(spline2, GfInterval(2.0, 4.0, false, false)));
+
+ // Make a no-op change and verify there is no difference.
+ TF_AXIOM(tester.SetValue(spline2, GfInterval()));
+
+ printf("\tpassed\n");
+}
+
+void TestChangeIntervalsForKnotEdits()
+{
+ printf("\nTest changed intervals for knot edits\n");
+
+ _SplineTester tester = _SplineTester(TsSpline());
+
+ TF_AXIOM(tester.spline.GetExtrapolation() ==
+ std::make_pair(TsExtrapolationHeld, TsExtrapolationHeld));
+
+ TsKeyFrame kf0( 0, VtValue(1.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf1( 10, VtValue(-1.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf2( 20, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+
+ // Add a knot at time 0, value 1
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval::GetFullInterval()));
+
+ // Add a knot at time 20, value 0
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(0, inf, false, false)));
+
+ // Add a knot at time 10, value -1
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+
+ // First knot updates
+ // Left side tangents
+ kf0.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ kf0.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ // Right side tangents
+ kf0.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+ kf0.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+ // Time only
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval(-inf, 10, false, false)));
+ kf0.SetTime(2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval(-inf, 10, false, false)));
+ kf0.SetTime(-2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval(-inf, 10, false, false)));
+ kf0.SetTime(0);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ // Value only
+ kf0.SetValue(VtValue(2.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ // Dual value (no value change)
+ kf0.SetIsDualValued(true);
+ kf0.SetLeftValue(kf0.GetValue());
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ // Set left value
+ kf0.SetLeftValue(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 0, false, true)));
+ // Set right value
+ kf0.SetValue(VtValue(3.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, true, false)));
+ // Remove dual valued
+ kf0.SetIsDualValued(false);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 0, false, true)));
+ // Change knot type
+ kf0.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+ kf0.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+ kf0.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+
+ // Middle knot updates
+ // Left side tangents
+ kf1.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 10, false, false)));
+ kf1.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 10, false, false)));
+ // Right side tangents
+ kf1.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(10, 20, false, false)));
+ kf1.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(10, 20, false, false)));
+ // Time only
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf1.GetTime(), GfInterval(0, 20, false, false)));
+ kf1.SetTime(12);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf1.GetTime(), GfInterval(0, 20, false, false)));
+ kf1.SetTime(8);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf1.GetTime(), GfInterval(0, 20, false, false)));
+ kf1.SetTime(10);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+ // Value only
+ kf1.SetValue(VtValue(2.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+ // Dual value (no value change)
+ kf1.SetIsDualValued(true);
+ kf1.SetLeftValue(kf1.GetValue());
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval()));
+ // Set left value
+ kf1.SetLeftValue(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 10, false, true)));
+ // Set right value
+ kf1.SetValue(VtValue(3.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(10, 20, true, false)));
+ // Remove dual valued
+ kf1.SetIsDualValued(false);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 10, false, true)));
+ // Change knot type
+ kf1.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+ kf1.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+ kf1.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(0, 20, false, false)));
+
+ // Last knot updates
+ // Left side tangents
+ kf2.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+ kf2.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+ // Right side tangents
+ kf2.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ kf2.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ // Time only
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval(10, inf, false, false)));
+ kf2.SetTime(22);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval(10, inf, false, false)));
+ kf2.SetTime(18);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval(10, inf, false, false)));
+ kf2.SetTime(20);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ // Value only
+ kf2.SetValue(VtValue(2.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ // Dual value (no value change)
+ kf2.SetIsDualValued(true);
+ kf2.SetLeftValue(kf2.GetValue());
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ // Set left value
+ kf2.SetLeftValue(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, true)));
+ // Set right value
+ kf2.SetValue(VtValue(3.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(20, inf, true, false)));
+ // Remove dual valued
+ kf2.SetIsDualValued(false);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, true)));
+ // Change knot type
+ kf2.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+ kf2.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+ kf2.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+
+ // Set linear extrapolation on left
+ tester.spline.SetExtrapolation(
+ TsExtrapolationLinear, TsExtrapolationHeld);
+
+ // First knot updates with linear extrapolation
+ // Left side tangents
+ kf0.SetLeftTangentSlope(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 0, false, false)));
+ kf0.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ // Right side tangents
+ kf0.SetRightTangentSlope(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+ kf0.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+ // Time only
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval(-inf, 10, false, false)));
+ kf0.SetTime(2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval(-inf, 10, false, false)));
+ kf0.SetTime(-2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval(-inf, 10, false, false)));
+ kf0.SetTime(0);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ // Value only
+ kf0.SetValue(VtValue(2.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ // Dual value (no value change)
+ kf0.SetIsDualValued(true);
+ kf0.SetLeftValue(kf0.GetValue());
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ // Set left value
+ kf0.SetLeftValue(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 0, false, true)));
+ // Set right value
+ kf0.SetValue(VtValue(3.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, true, false)));
+ // Remove dual valued
+ kf0.SetIsDualValued(false);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 0, false, true)));
+ // Change knot type
+ kf0.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ kf0.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 10, false, false)));
+ kf0.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(0, 10, false, false)));
+
+ // Set linear extrapolation on right
+ tester.spline.SetExtrapolation(
+ TsExtrapolationLinear, TsExtrapolationLinear);
+
+ // Last knot updates with linear extrapolation
+ // Left side tangents
+ kf2.SetLeftTangentSlope(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+ kf2.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+ // Right side tangents
+ kf2.SetRightTangentSlope(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(20, inf, false, false)));
+ kf2.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ // Time only
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval(10, inf, false, false)));
+ kf2.SetTime(22);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval(10, inf, false, false)));
+ kf2.SetTime(18);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval(10, inf, false, false)));
+ kf2.SetTime(20);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ // Value only
+ kf2.SetValue(VtValue(2.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ // Dual value (no value change)
+ kf2.SetIsDualValued(true);
+ kf2.SetLeftValue(kf2.GetValue());
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ // Set left value
+ kf2.SetLeftValue(VtValue(-1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, true)));
+ // Set right value
+ kf2.SetValue(VtValue(3.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(20, inf, true, false)));
+ // Remove dual valued
+ kf2.SetIsDualValued(false);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, true)));
+ // Change knot type
+ kf2.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ kf2.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, inf, false, false)));
+ kf2.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(10, 20, false, false)));
+
+ printf("\tpassed\n");
+}
+
+void TestChangeIntervalsForKnotEdits2()
+{
+ /* Test six knot spline with flat portions
+ // O---------O
+ // / \
+ // --------O---------O O--------O----------
+ */
+ printf("\nTest changed intervals for knot edits (more)\n");
+
+ _SplineTester tester = _SplineTester(TsSpline());
+
+ TF_AXIOM(tester.spline.GetExtrapolation() ==
+ std::make_pair(TsExtrapolationHeld, TsExtrapolationHeld));
+
+ TsKeyFrame kf0( 0, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf1( 10, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf2( 20, VtValue(1.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf3( 30, VtValue(1.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf4( 40, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf5( 50, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+
+ // Add a knot at time 0, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval::GetFullInterval()));
+
+ // Add a knot at time 10, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval()));
+
+ // Add a knot at time 20, value 1
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval(10, inf, false, false)));
+
+ // Add a knot at time 30, value 1
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval()));
+
+ // Add a knot at time 40, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval(30, inf, false, false)));
+
+ // Add a knot at time 50, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval()));
+
+ // Test adding redundant knots in static sections
+ TsKeyFrame kf0_1( 5, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ // Adding redundant flat knot shouldn't invalidate anything
+ TF_AXIOM(tester.SetKeyFrame( kf0_1, GfInterval()));
+
+ TsKeyFrame kf2_3( 25, VtValue(1.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ // Adding redundant flat knot shouldn't invalidate anything
+ TF_AXIOM(tester.SetKeyFrame( kf2_3, GfInterval()));
+
+ TsKeyFrame kf4_5( 45, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ // Adding redundant flat knot shouldn't invalidate anything
+ TF_AXIOM(tester.SetKeyFrame( kf4_5, GfInterval()));
+
+ // Remove the redundant knots we just added
+ // Removing redundant flat knot shouldn't invalidate anything
+ TF_AXIOM(tester.RemoveKeyFrame( kf0_1.GetTime(), GfInterval()));
+
+ // Removing redundant flat knot shouldn't invalidate anything
+ TF_AXIOM(tester.RemoveKeyFrame( kf2_3.GetTime(), GfInterval()));
+
+ // Removing redundant flat knot shouldn't invalidate anything
+ TF_AXIOM(tester.RemoveKeyFrame( kf4_5.GetTime(), GfInterval()));
+
+ // Change tangent lengths on each knot (flat segments shouldn't change
+ // while non flat segments should
+ kf0.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval()));
+ kf0.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval()));
+
+ kf1.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval()));
+ kf1.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval(10, 20, false, false)));
+
+ kf2.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval(10, 20, false, false)));
+
+ kf2.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval()));
+
+ kf3.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval()));
+
+ kf3.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval(30, 40, false, false)));
+
+ kf4.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval(30, 40, false, false)));
+ kf4.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval()));
+
+ kf5.SetLeftTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval()));
+ kf5.SetRightTangentLength(3);
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval()));
+
+ // Move the whole spline forward five frames
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf1.GetTime(), GfInterval(-inf, 20, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf3.GetTime(), GfInterval(-inf, 40, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf4.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf5.GetTime(), GfInterval::GetFullInterval()));
+ kf0.SetTime(kf0.GetTime() + 5);
+ kf1.SetTime(kf1.GetTime() + 5);
+ kf2.SetTime(kf2.GetTime() + 5);
+ kf3.SetTime(kf3.GetTime() + 5);
+ kf4.SetTime(kf4.GetTime() + 5);
+ kf5.SetTime(kf5.GetTime() + 5);
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval::GetFullInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval(15, inf, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval(35, inf, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval()));
+
+ // Move the whole spline back five frames
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf5.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf4.GetTime(), GfInterval(35, inf, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf3.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval(15, inf, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf1.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval::GetFullInterval()));
+ kf0.SetTime(kf0.GetTime() - 5);
+ kf1.SetTime(kf1.GetTime() - 5);
+ kf2.SetTime(kf2.GetTime() - 5);
+ kf3.SetTime(kf3.GetTime() - 5);
+ kf4.SetTime(kf4.GetTime() - 5);
+ kf5.SetTime(kf5.GetTime() - 5);
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval::GetFullInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval(-inf, 40, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval(-inf, 20, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval()));
+
+ // Change tangent slopes on the outer flat segments
+ kf0.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval(0, 10, false, false)));
+ kf4.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval(40, 50, false, false)));
+
+ // Move the whole spline forward five frames again
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf0.GetTime(), GfInterval(0, 10, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf1.GetTime(), GfInterval(-inf, 20, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf2.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf3.GetTime(), GfInterval(-inf, 40, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf4.GetTime(), GfInterval(40, 50, false, false)));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf5.GetTime(), GfInterval::GetFullInterval()));
+ kf0.SetTime(kf0.GetTime() + 5);
+ kf1.SetTime(kf1.GetTime() + 5);
+ kf2.SetTime(kf2.GetTime() + 5);
+ kf3.SetTime(kf3.GetTime() + 5);
+ kf4.SetTime(kf4.GetTime() + 5);
+ kf5.SetTime(kf5.GetTime() + 5);
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval::GetFullInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval(5, 15, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval(15, inf, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval(35, inf, false, false)));
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval(45, 55, false, false)));
+
+ printf("\tpassed\n");
+}
+
+void TestChangeIntervalsForMixedKnotEdits()
+{
+ printf("\nTest changed intervals for knot edits (mixed knot types)\n");
+
+ _SplineTester tester = _SplineTester(TsSpline());
+
+ TF_AXIOM(tester.spline.GetExtrapolation() ==
+ std::make_pair(TsExtrapolationHeld, TsExtrapolationHeld));
+
+ TsKeyFrame kf0( 0, VtValue(0.0), TsKnotHeld,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf1( 10, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf2( 20, VtValue(0.0), TsKnotLinear,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf3( 30, VtValue(0.0), TsKnotHeld,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf4( 40, VtValue(0.0), TsKnotLinear,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf5( 50, VtValue(0.0), TsKnotBezier,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+ TsKeyFrame kf6( 60, VtValue(0.0), TsKnotHeld,
+ VtValue(0.0), VtValue(0.0), 1, 1 );
+
+ // Add a knot at time 0, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval::GetFullInterval()));
+
+ // Add a knot at time 10, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval()));
+
+ // Add a knot at time 20, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval()));
+
+ // Add a knot at time 30, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval()));
+
+ // Add a knot at time 40, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval()));
+
+ // Add a knot at time 50, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval()));
+
+ // Add a knot at time 60, value 0
+ TF_AXIOM(tester.SetKeyFrame( kf6, GfInterval()));
+
+
+ // Move knots in time only
+ TF_AXIOM(tester.RemoveKeyFrame( kf0.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame( kf1.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame( kf2.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame( kf3.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame( kf4.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame( kf5.GetTime(), GfInterval()));
+ TF_AXIOM(tester.RemoveKeyFrame(
+ kf6.GetTime(), GfInterval::GetFullInterval()));
+ kf0.SetTime(kf0.GetTime() + 5);
+ kf1.SetTime(kf1.GetTime() + 5);
+ kf2.SetTime(kf2.GetTime() + 5);
+ kf3.SetTime(kf3.GetTime() + 5);
+ kf4.SetTime(kf4.GetTime() + 5);
+ kf5.SetTime(kf5.GetTime() + 5);
+ kf6.SetTime(kf6.GetTime() + 5);
+ TF_AXIOM(tester.SetKeyFrame( kf0, GfInterval::GetFullInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf1, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf2, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf3, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf4, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf5, GfInterval()));
+ TF_AXIOM(tester.SetKeyFrame( kf6, GfInterval()));
+
+ // Current key frames
+ // 5 : 0.0 (held)
+ // 15: 0.0 (bezier)
+ // 25: 0.0 (linear)
+ // 35: 0.0 (held)
+ // 45: 0.0 (linear)
+ // 55: 0.0 (bezier)
+ // 65: 0.0 (held)
+
+ // Set tangent slopes
+ kf0.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ kf0.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ kf0.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+ kf0.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval()));
+
+ kf1.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval()));
+ kf1.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval()));
+ kf1.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+ kf1.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+
+ kf2.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ kf2.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ kf2.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+ kf2.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval()));
+
+ kf3.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval()));
+ kf3.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval()));
+ kf3.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval()));
+ kf3.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval()));
+
+ kf4.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf4, GfInterval()));
+ kf4.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf4, GfInterval()));
+ kf4.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf4, GfInterval()));
+ kf4.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf4, GfInterval()));
+
+ kf5.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf5, GfInterval(45, 55, false, false)));
+ kf5.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf5, GfInterval(45, 55, false, false)));
+ kf5.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf5, GfInterval(55, 65, false, false)));
+ kf5.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf5, GfInterval(55, 65, false, false)));
+
+ kf6.SetLeftTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf6, GfInterval()));
+ kf6.SetLeftTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf6, GfInterval()));
+ kf6.SetRightTangentSlope(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf6, GfInterval()));
+ kf6.SetRightTangentLength(2);
+ TF_AXIOM(tester.SetKeyFrame(kf6, GfInterval()));
+
+ // Set values
+ kf0.SetValue(VtValue(1.0));
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(-inf, 15, false, false)));
+ kf1.SetValue(VtValue(2.0));
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, true, false)));
+ kf2.SetValue(VtValue(3.0));
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(15, 35, false, false)));
+ kf3.SetValue(VtValue(4.0));
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval(25, 45, false, false)));
+ kf4.SetValue(VtValue(5.0));
+ TF_AXIOM(tester.SetKeyFrame(kf4, GfInterval(45, 55, true, false)));
+ kf5.SetValue(VtValue(6.0));
+ TF_AXIOM(tester.SetKeyFrame(kf5, GfInterval(45, 65, false, false)));
+ kf6.SetValue(VtValue(7.0));
+ TF_AXIOM(tester.SetKeyFrame(kf6, GfInterval(55, inf, false, false)));
+
+ // Change knot types
+ kf0.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(5, 15, false, false)));
+ kf0.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(5, 15, false, false)));
+ kf0.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(5, 15, false, false)));
+ kf0.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(5, 15, false, false)));
+ kf0.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(5, 15, false, false)));
+ kf0.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf0, GfInterval(5, 15, false, false)));
+
+ kf1.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+ kf1.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+ kf1.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+ kf1.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+ kf1.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+ kf1.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf1, GfInterval(15, 25, false, false)));
+
+ kf2.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(15, 35, false, false)));
+ kf2.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(15, 35, false, false)));
+ kf2.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(15, 35, false, false)));
+ kf2.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(15, 35, false, false)));
+ kf2.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(15, 35, false, false)));
+ kf2.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf2, GfInterval(15, 35, false, false)));
+
+ kf3.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval(25, 45, false, false)));
+ kf3.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval(25, 45, false, false)));
+ kf3.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval(25, 45, false, false)));
+ kf3.SetKnotType(TsKnotLinear);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval(25, 45, false, false)));
+ kf3.SetKnotType(TsKnotBezier);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval(25, 45, false, false)));
+ kf3.SetKnotType(TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(kf3, GfInterval(25, 45, false, false)));
+}
+
+void TestChangedIntervalHeld()
+{
+ printf("\nTest changed interval with held knot\n");
+
+ TsSpline spline;
+ spline.SetKeyFrame(TsKeyFrame(0, VtValue(1.0), TsKnotHeld));
+ spline.SetKeyFrame(TsKeyFrame(1, VtValue(2.0), TsKnotHeld));
+ spline.SetKeyFrame(TsKeyFrame(2, VtValue(3.0), TsKnotBezier));
+ spline.SetKeyFrame(TsKeyFrame(3, VtValue(4.0), TsKnotBezier));
+
+ _SplineTester tester = _SplineTester(spline);
+
+ // Verify that we get the correct range when we change the value of the held
+ // knot. This should be the interval from the held knot to the next knot,
+ // open at the end. The open end means that the left value of the next knot
+ // is affected, but the right value is not.
+ const TsKeyFrame newKf(1, VtValue(2.5), TsKnotHeld);
+ TF_AXIOM(tester.SetKeyFrame(newKf, GfInterval(1, 2, true, false)));
+}
+
+void
+TestIteratorAPI()
+{
+ printf("\nTest iterator API\n");
+
+ TsSpline spline;
+
+ // Test initial conditions
+ TF_AXIOM(spline.empty());
+ TF_AXIOM(spline.begin() == spline.end());
+
+ // Add a bunch of keyframes
+ spline.SetKeyFrame(TsKeyFrame(1.0, 1.0));
+ spline.SetKeyFrame(TsKeyFrame(3.0, 2.0));
+ spline.SetKeyFrame(TsKeyFrame(7.0, 3.0));
+ spline.SetKeyFrame(TsKeyFrame(10.0, 4.0));
+ spline.SetKeyFrame(TsKeyFrame(15.0, 5.0));
+ spline.SetKeyFrame(TsKeyFrame(20.0, 6.0));
+
+ // Test basic container emptyness/size
+ TF_AXIOM(spline.size() == 6);
+ TF_AXIOM(!spline.empty());
+ TF_AXIOM(spline.begin() != spline.end());
+
+ // Test finding iterators at a specific time
+ TF_AXIOM(spline.find(3.0) != spline.end());
+ TF_AXIOM(spline.find(4.0) == spline.end());
+ TF_AXIOM(++spline.find(7.0) == spline.find(10.0));
+ TF_AXIOM(--spline.find(7.0) == spline.find(3.0));
+
+ // Test lower_bound
+ TF_AXIOM(spline.lower_bound(25.0) == spline.end());
+ TF_AXIOM(spline.lower_bound(3.0) == spline.find(3.0));
+ TF_AXIOM(spline.lower_bound(4.0) == spline.find(7.0));
+ TF_AXIOM(spline.lower_bound(-inf) == spline.begin());
+ TF_AXIOM(spline.lower_bound(+inf) == spline.end());
+
+ // Test upper_bound
+ TF_AXIOM(spline.upper_bound(3.0) == spline.find(7.0));
+ TF_AXIOM(spline.upper_bound(4.0) == spline.find(7.0));
+ TF_AXIOM(spline.upper_bound(25.0) == spline.end());
+ TF_AXIOM(spline.upper_bound(-inf) == spline.begin());
+ TF_AXIOM(spline.upper_bound(+inf) == spline.end());
+
+ // Test dereferencing
+ TF_AXIOM(spline.find(7.0)->GetValue().Get<double>() == 3.0);
+ TF_AXIOM(spline.find(10.0)->GetValue().Get<double>() == 4.0);
+
+ // Clear and re-test initial conditions
+ spline.Clear();
+ TF_AXIOM(spline.empty());
+ TF_AXIOM(spline.begin() == spline.end());
+
+ printf("\tpassed\n");
+}
+
+void
+TestSwapKeyFrames()
+{
+ printf("\nTest SwapKeyFrames\n");
+
+ TsSpline spline;
+ std::vector<TsKeyFrame> keyFrames;
+ TsKeyFrame kf0(0.0, 0.0);
+ TsKeyFrame kf1(1.0, 5.0);
+ TsKeyFrame kf1Second(1.0, 9.0);
+
+ // Test trivial case - both empty
+ spline.SwapKeyFrames(&keyFrames);
+
+ TF_AXIOM(spline.empty());
+ TF_AXIOM(keyFrames.empty());
+
+ // Test empty spline, single item vector
+ keyFrames.push_back(kf0);
+ spline.SwapKeyFrames(&keyFrames);
+ TF_AXIOM(spline.size() == 1);
+ TF_AXIOM(spline.find(0.0) != spline.end());
+ TF_AXIOM(keyFrames.empty());
+
+ // Test empty vector, single item spline
+ spline.SwapKeyFrames(&keyFrames);
+ TF_AXIOM(spline.empty());
+ TF_AXIOM(keyFrames.size() == 1);
+
+ // Test items in both, including a frame in each at
+ // same frame
+ spline.SetKeyFrame(kf0);
+ spline.SetKeyFrame(kf1);
+ keyFrames.clear();
+ keyFrames.push_back(kf1Second);
+ TF_AXIOM(spline.size() == 2);
+ TF_AXIOM(spline.find(0.0) != spline.end());
+ TF_AXIOM(spline.find(1.0) != spline.end());
+ TF_AXIOM(spline.find(1.0)->GetValue().Get<double>() == 5.0);
+ TF_AXIOM(keyFrames.size() == 1);
+
+ spline.SwapKeyFrames(&keyFrames);
+ TF_AXIOM(spline.size() == 1);
+ TF_AXIOM(keyFrames.size() == 2);
+ TF_AXIOM(spline.find(1.0)->GetValue().Get<double>() == 9.0);
+}
+
+int
+main(int argc, char **argv)
+{
+ TsSpline val;
+ TsKeyFrame kf;
+
+ TsTypeRegistry &typeRegistry = TsTypeRegistry::GetInstance();
+
+ printf("\nTest supported types\n");
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<double>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<float>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<int>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<bool>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfVec2d>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfVec2f>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfVec3d>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfVec3f>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfVec4d>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfVec4f>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfMatrix2d>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfMatrix3d>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<GfMatrix4d>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find<string>()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find< VtArray<double> >()));
+ TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find< VtArray<float> >()));
+ TF_AXIOM(!typeRegistry.IsSupportedType(TfType::Find<char>()));
+ TF_AXIOM(!typeRegistry.IsSupportedType(TfType::Find<GfRange1d>()));
+ printf("\tpassed\n");
+
+ printf("\nTest that setting left value of an uninterpolatable knot does "
+ "not work:\n\t\terror expected\n");
+ kf = TsKeyFrame(0.0, string("foo"));
+ TF_AXIOM( kf.GetValue().Get<string>() == "foo" );
+ kf.SetLeftValue( VtValue("bar") );
+ TF_AXIOM( kf.GetValue().Get<string>() == "foo" );
+ printf("\tpassed\n");
+
+ printf("\nTest that setting left value of non-dual valued knot does not "
+ "work:\n\t\terror expected\n");
+ kf = TsKeyFrame(0.0, 1.0);
+ TF_AXIOM( kf.GetValue().Get<double>() == 1.0 );
+ kf.SetLeftValue( VtValue(123.0) );
+ TF_AXIOM( kf.GetValue().Get<double>() == 1.0 );
+ printf("\tpassed\n");
+
+ printf("\nTest that initializing a keyframe with an unsupported knot type "
+ "for the given value type causes a supported knot type to be used\n");
+ // GfVec2d is interpolatable but does not support tangents. Expect Linear.
+ kf = TsKeyFrame(0.0, GfVec2d(0), TsKnotBezier);
+ TF_AXIOM(kf.GetKnotType() == TsKnotLinear);
+
+ // std::string is neither interpolatable nor support tangents. Expect Held.
+ kf = TsKeyFrame(0.0, std::string(), TsKnotBezier);
+ TF_AXIOM(kf.GetKnotType() == TsKnotHeld);
+
+ printf("\nTest removing bogus keyframe: errors expected\n");
+ val.Clear();
+ val.RemoveKeyFrame( 123 );
+ printf("\tpassed\n");
+
+ printf("\nTest creating non-held dual-value keyframe for "
+ "non-interpolatable type\n");
+ kf = TsKeyFrame(0.0, string("left"), string("right"), TsKnotLinear);
+ TF_AXIOM(kf.GetKnotType() == TsKnotHeld);
+
+ printf("\nTest interpolation of float\n");
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, float(0), TsKnotLinear) );
+ val.SetKeyFrame( TsKeyFrame(10, float(20), TsKnotLinear) );
+ TF_AXIOM( val.Eval(5).Get<float>() == float(10) );
+ TF_AXIOM( val.Eval(5.5).Get<float>() == float(11) );
+ TF_AXIOM( val.EvalDerivative(5, TsLeft).Get<float>() == float(2) );
+ TF_AXIOM( val.EvalDerivative(5, TsRight).Get<float>() == float(2) );
+ TF_AXIOM( val.EvalDerivative(5.5, TsLeft).Get<float>() == float(2) );
+ TF_AXIOM( val.EvalDerivative(5.5, TsRight).Get<float>() == float(2) );
+ printf("\tpassed\n");
+
+ // Coverage for breakdown of float
+ GfInterval affectedRange;
+ val.Breakdown(5, TsKnotBezier, false, 1.0, VtValue(), &affectedRange);
+
+ // Coverage for constructor
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, float(0), TsKnotLinear) );
+ val.SetKeyFrame( TsKeyFrame(10, float(20), TsKnotLinear) );
+ TF_AXIOM( val.Eval(5).Get<float>() == float(10) );
+ TF_AXIOM( val.Eval(5.5).Get<float>() == float(11) );
+ TF_AXIOM( val.EvalDerivative(0, TsLeft).Get<float>() == float(0) );
+ TF_AXIOM( val.EvalDerivative(0, TsRight).Get<float>() == float(0) );
+ TF_AXIOM( val.EvalDerivative(5, TsLeft).Get<float>() == float(2) );
+ TF_AXIOM( val.EvalDerivative(5, TsRight).Get<float>() == float(2) );
+ TF_AXIOM( val.EvalDerivative(5.5, TsLeft).Get<float>() == float(2) );
+ TF_AXIOM( val.EvalDerivative(5.5, TsRight).Get<float>() == float(2) );
+ TF_AXIOM( val == TsSpline(val.GetKeyFrames()) );
+ TF_AXIOM( val != TsSpline() );
+
+ // Coverage for operator<<().
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, float(0), TsKnotLinear) );
+ val.SetKeyFrame( TsKeyFrame(10, float(20), TsKnotLinear) );
+ TF_AXIOM( !TfStringify(val).empty() );
+
+ // Coverage for float types
+ printf("\nTest GetRange() of float\n");
+ std::pair<VtValue, VtValue> range = val.GetRange(-1, 11);
+ TF_AXIOM( range.first.Get<float>() == 0.0 );
+ TF_AXIOM( range.second.Get<float>() == 20.0 );
+ printf("\tpassed\n");
+
+ printf("\nTest interpolation of int\n");
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, int(0), TsKnotHeld) );
+ val.SetKeyFrame( TsKeyFrame(10, int(20), TsKnotHeld) );
+ TF_AXIOM( val.Eval(5).Get<int>() == int(0) );
+ TF_AXIOM( val.EvalDerivative(5, TsLeft).Get<int>() == int(0) );
+ TF_AXIOM( val.EvalDerivative(5, TsRight).Get<int>() == int(0) );
+ printf("\tpassed\n");
+
+ printf("\nTest construction of various types of keyframes\n");
+ TsKeyFrame(0, VtValue( double(0.123) ) );
+ TsKeyFrame(0, VtValue( float(0.123) ) );
+ TsKeyFrame(0, VtValue( int(0) ) );
+ // For code coverage of unknown types
+ kf = TsKeyFrame(0, 0.123, /* bogus knot type */ TsKnotType(-12345) );
+ printf("\t%s\n", TfStringify(kf).c_str());
+ printf("\tpassed\n");
+
+ printf("\nTest querying left side of non-first held keyframe\n");
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, VtValue( "foo" )) );
+ val.SetKeyFrame( TsKeyFrame(1, VtValue( "bar" )) );
+ val.SetKeyFrame( TsKeyFrame(2, VtValue( "mangoes" )) );
+ val.SetKeyFrame( TsKeyFrame(3, VtValue( "apples" )) );
+ val.SetKeyFrame( TsKeyFrame(4, VtValue( "oranges" )) );
+ TF_AXIOM( val.Eval(1, TsLeft) == "foo" );
+ TF_AXIOM( val.EvalDerivative(1, TsLeft) == "" );
+ printf("\tpassed\n");
+
+ printf("\nTests for code coverage: errors expected\n");
+ GfVec2d vec2dEps = GfVec2d(std::numeric_limits<double>::epsilon(),
+ std::numeric_limits<double>::epsilon());
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame( 0, GfVec2d(0, 0), TsKnotHeld ) );
+ val.SetKeyFrame( TsKeyFrame( 10, GfVec2d(1, 1), TsKnotHeld ) );
+ TF_AXIOM(_IsClose(VtValue(val.Eval(0, TsLeft)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.Eval(0, TsRight)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.Eval(1, TsLeft)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.Eval(1, TsRight)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(0, TsLeft)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(0, TsRight)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(1, TsLeft)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(1, TsRight)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame( 0, GfVec2d(0, 0), TsKnotLinear ) );
+ val.SetKeyFrame( TsKeyFrame( 10, GfVec2d(1, 1), TsKnotLinear ) );
+ TF_AXIOM(_IsClose(VtValue(val.Eval(0, TsLeft)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.Eval(0, TsRight)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.Eval(1, TsLeft)),
+ VtValue(GfVec2d(0.1, 0.1)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.Eval(1, TsRight)),
+ VtValue(GfVec2d(0.1, 0.1)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(0, TsLeft)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(0, TsRight)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(1, TsLeft)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+ TF_AXIOM(_IsClose(VtValue(val.EvalDerivative(1, TsRight)),
+ VtValue(GfVec2d(0.0, 0.0)), vec2dEps));
+
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame( 0, 0.0, TsKnotHeld ) );
+ val.SetKeyFrame( TsKeyFrame( 10, 10.0, TsKnotHeld ) );
+ TF_AXIOM(_IsClose<double>(val.Eval(0, TsRight).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.Eval(0, TsLeft).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.Eval(10, TsRight).Get<double>(), 10.0));
+ TF_AXIOM(_IsClose<double>(val.Eval(10, TsLeft).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(0, TsRight).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(0, TsLeft).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsRight).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsLeft).Get<double>(), 0.0));
+ printf("\tpassed\n");
+
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame( 0, double(0.0), TsKnotLinear ) );
+ val.SetKeyFrame( TsKeyFrame( 10, double(10.0), TsKnotLinear ) );
+ TF_AXIOM(_IsClose<double>(val.Eval(0, TsRight).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.Eval(0, TsLeft).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.Eval(10, TsRight).Get<double>(), 10.0));
+ TF_AXIOM(_IsClose<double>(val.Eval(10, TsLeft).Get<double>(), 10.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(0, TsRight).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(0, TsLeft).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsRight).Get<double>(), 0.0));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsLeft).Get<double>(), 0.0));
+ printf("\tpassed\n");
+
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame( 0, VtValue(0.0), TsKnotLinear ) );
+ val.SetKeyFrame( TsKeyFrame( 10, VtValue(10.0), TsKnotLinear ) );
+ TF_AXIOM(_IsClose<double>(val.Eval(0, TsRight), VtValue(0.0)));
+ TF_AXIOM(_IsClose<double>(val.Eval(0, TsLeft), VtValue(0.0)));
+ TF_AXIOM(_IsClose<double>(val.Eval(10, TsRight), VtValue(10.0)));
+ TF_AXIOM(_IsClose<double>(val.Eval(10, TsLeft), VtValue(10.0)));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(0, TsRight), VtValue(0.0)));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(0, TsLeft), VtValue(0.0)));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsRight), VtValue(0.0)));
+ TF_AXIOM(_IsClose<double>(val.EvalDerivative(10, TsLeft), VtValue(0.0)));
+ printf("\tpassed\n");
+
+ // Test evaluation of cached segments.
+ TestEvaluator();
+
+ // Test spline diffing
+ TestSplineDiff();
+ TestSplineDiff2();
+ TestHeldThenBezier();
+
+ // Test redundant knot detection
+ TestRedundantKnots();
+
+ // Test intervals generated when assigning new splines.
+ TestChangeIntervalsOnAssignment();
+
+ // Test change intervals for edits.
+ TestChangeIntervalsForKnotEdits();
+ TestChangeIntervalsForKnotEdits2();
+ TestChangeIntervalsForMixedKnotEdits();
+ TestChangedIntervalHeld();
+
+ // Sample to within this error tolerance
+ static const double tolerance = 1.0e-3;
+
+ // Maximum allowed error is not tolerance, it's much larger. This
+ // is because Eval() samples differently between frames than at
+ // frames and will yield slightly incorrect results but avoid
+ // problems with large derivatives. Sample() does not do that.
+ static const double maxError = 0.15;
+
+ TsSamples samples;
+
+ // Can't test from Python since we can't set float knots.
+ printf("\nTest float Sample() with held knots\n");
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, float(0.0), TsKnotHeld) );
+ val.SetKeyFrame( TsKeyFrame(10, float(10.0), TsKnotHeld) );
+ samples = val.Sample(-1, 11, 1.0, 1.0, tolerance);
+ _AssertSamples<float>(val, samples, -1, 11, maxError);
+ // Test sampling out of range
+ samples = val.Sample(-300, -200, 1.0, 1.0, tolerance);
+ _AssertSamples<float>(val, samples, -300, -200, maxError);
+ samples = val.Sample(300, 400, 1.0, 1.0, tolerance);
+ _AssertSamples<float>(val, samples, 300, 400, maxError);
+ printf("\tpassed\n");
+
+ printf("\nTest float Eval() on left of keyframe with held knots\n");
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0.5, float(0.0), TsKnotHeld) );
+ val.SetKeyFrame( TsKeyFrame(5.5, float(5.0), TsKnotHeld) );
+ val.SetKeyFrame( TsKeyFrame(10.5, float(10.0), TsKnotHeld) );
+ TF_AXIOM( val.Eval(5.5, TsLeft).Get<float>() == float(0.0) );
+ TF_AXIOM( val.EvalDerivative(5.5, TsLeft).Get<float>() == float(0.0) );
+ TF_AXIOM( val.EvalDerivative(5.5, TsRight).Get<float>() == float(0.0) );
+ printf("\tpassed\n");
+
+ printf("\nTest double tangent symmetry\n");
+ kf = TsKeyFrame(0.0, 0.0, TsKnotBezier, 1.0, 1.0, 1.0, 1.0);
+ TF_AXIOM(!kf.GetTangentSymmetryBroken());
+ kf = TsKeyFrame(0.0, 0.0, TsKnotBezier, 1.0, 1.0, 1.0, 2.0);
+ TF_AXIOM(!kf.GetTangentSymmetryBroken());
+ kf = TsKeyFrame(0.0, 0.0, TsKnotBezier, 1.0, 1.1, 1.0, 1.0);
+ TF_AXIOM(kf.GetTangentSymmetryBroken());
+ TF_AXIOM(kf.GetLeftTangentSlope() != kf.GetRightTangentSlope());
+ kf.SetTangentSymmetryBroken(false);
+ TF_AXIOM(!kf.GetTangentSymmetryBroken());
+ TF_AXIOM(kf.GetLeftTangentSlope() == kf.GetRightTangentSlope());
+ printf("\tpassed\n");
+
+ printf("\nTest float tangent symmetry\n");
+ kf = TsKeyFrame(0.0f, 0.0f, TsKnotBezier, 1.0f, 1.0f, 1.0, 1.0);
+ TF_AXIOM(!kf.GetTangentSymmetryBroken());
+ kf = TsKeyFrame(0.0f, 0.0f, TsKnotBezier, 1.0f, 1.0f, 1.0, 2.0);
+ TF_AXIOM(!kf.GetTangentSymmetryBroken());
+ kf = TsKeyFrame(0.0f, 0.0f, TsKnotBezier, 1.0f, 1.1f, 1.0, 1.0);
+ TF_AXIOM(kf.GetTangentSymmetryBroken());
+ TF_AXIOM(kf.GetLeftTangentSlope() != kf.GetRightTangentSlope());
+ kf.SetTangentSymmetryBroken(false);
+ TF_AXIOM(!kf.GetTangentSymmetryBroken());
+ TF_AXIOM(kf.GetLeftTangentSlope() == kf.GetRightTangentSlope());
+ printf("\tpassed\n");
+
+ // Coverage for ResetTangentSymmetryBroken
+ kf = TsKeyFrame(0.0f, string("foo"), TsKnotHeld);
+ kf.ResetTangentSymmetryBroken();
+
+ // Coverage tests for blur samples
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, 0.0, TsKnotBezier,
+ 1.0, -1.0, 0.5, 0.5) );
+ val.SetKeyFrame( TsKeyFrame(5 - 2.0 * tolerance, 50.0, TsKnotBezier,
+ 0.0, 0.0, 0.5, 0.5) );
+ val.SetKeyFrame( TsKeyFrame(5, 5.0, TsKnotBezier,
+ 0.0, 0.0, 0.5, 0.5) );
+ val.SetKeyFrame( TsKeyFrame(5 + 0.5 * tolerance, 10.0, TsKnotBezier,
+ -1.0, 1.0, 0.5, 0.5) );
+ samples = val.Sample(-1, 16, 1.0, 1.0, tolerance);
+ val.Clear();
+
+ // Get a blur sample due to closely spaced keyframes.
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, 0.0, TsKnotBezier,
+ -1e9, 1e9,
+ tolerance/2.0, tolerance/2.0) );
+ val.SetKeyFrame( TsKeyFrame(1e-3, 1.0, TsKnotBezier,
+ -1e9, 1e9,
+ tolerance/2.0, tolerance/2.0) );
+ samples = val.Sample(-1.0, 1.0,
+ 1.0, 1.0, 1e-9);
+
+ // Coverage of segment blending
+ val.Clear();
+ val.SetKeyFrame( TsKeyFrame(0, 0.0, TsKnotLinear,
+ -1e9, 1e9,
+ tolerance/2.0, tolerance/2.0) );
+ val.SetKeyFrame( TsKeyFrame(1e-3, 1.0, TsKnotBezier,
+ -1e9, 1e9,
+ tolerance/2.0, tolerance/2.0) );
+ samples = val.Sample(-1.0, 1.0,
+ 1.0, 1.0, 1e-9);
+
+ // Coverage of degenerate/extreme tangent handles
+ val.Clear();
+ // Long tangent handles
+ val.SetKeyFrame( TsKeyFrame(0, 0.0, TsKnotBezier,
+ 0.0, 0.0,
+ 10.0, 10.0) );
+ // 0-length tangent handles
+ val.SetKeyFrame( TsKeyFrame(1, 0.0, TsKnotBezier,
+ 0.0, 0.0,
+ 0.0, 0.0) );
+ samples = val.Sample(-1.0, 2.0,
+ 1.0, 1.0, 1e-9);
+
+ string testStrValue("some_string_value");
+
+ // Coverage for OstreamMethods
+ std::ostringstream ss;
+ kf = TsKeyFrame(0.0f, 0.0f, TsKnotBezier, 1.0f, 1.1f, 1.0, 1.0);
+ ss << kf;
+ kf = TsKeyFrame(0.0f, testStrValue, TsKnotHeld);
+ ss << kf;
+
+ // Coverage for operator==()
+ TsKeyFrame kf1 = TsKeyFrame(0.0f, testStrValue, TsKnotHeld);
+ TsKeyFrame kf2 = TsKeyFrame(0.0f, testStrValue, TsKnotHeld);
+ // Different but equal objects, to bypass the *lhs==*rhs test
+ // in operator==()
+ assert (kf1 == kf2);
+
+ TestIteratorAPI();
+
+ // Verify TsFindChangedInterval behavior for dual-valued knots
+ {
+ TsSpline s1;
+ s1.SetKeyFrame( TsKeyFrame(1.0, VtValue(-1.0), VtValue(1.0),
+ TsKnotLinear,
+ VtValue(0.9), VtValue(0.9),
+ TsTime(1.0), TsTime(1.0)) );
+
+ TsSpline s2;
+ s2.SetKeyFrame( TsKeyFrame(1.0, VtValue(14.0), VtValue(1.0),
+ TsKnotLinear,
+ VtValue(0.9), VtValue(0.9),
+ TsTime(1.0), TsTime(1.0)) );
+
+ _SplineTester tester = _SplineTester(TsSpline());
+
+ // 2 splines with a single dual-valued knot at 1.0 that differs on the
+ // left-side value should be detected as different over (-inf, 1.0]
+ tester = _SplineTester(s1);
+ TF_VERIFY(tester.SetValue(
+ s2, GfInterval(-inf, 1.0, false, true)));
+
+ s1.SetKeyFrame( TsKeyFrame(1.0, VtValue(1.0), VtValue(-14.0),
+ TsKnotLinear,
+ VtValue(0.9), VtValue(0.9),
+ TsTime(1.0), TsTime(1.0)) );
+ s2.SetKeyFrame( TsKeyFrame(1.0, VtValue(1.0), VtValue(-1.0),
+ TsKnotLinear,
+ VtValue(0.9), VtValue(0.9),
+ TsTime(1.0), TsTime(1.0)) );
+
+ // 2 splines with a single dual-valued knot at 1.0 that differs on the
+ // right-side value should be detected as different over [1.0, inf)
+ tester = _SplineTester(s1);
+ TF_VERIFY(tester.SetValue(
+ s2, GfInterval(1.0, inf, true, false)));
+ }
+
+ // Verify TsFindChangedInterval behavior in the presence of
+ // redundant held knots.
+ {
+ TsSpline held1;
+ held1.SetKeyFrame( TsKeyFrame(1.0, VtValue(1.0), TsKnotHeld) );
+ held1.SetKeyFrame( TsKeyFrame(3.0, VtValue(1.0), TsKnotHeld) );
+ held1.SetKeyFrame( TsKeyFrame(12.0, VtValue(1.0), TsKnotHeld) );
+
+ TsSpline held2;
+ held2.SetKeyFrame( TsKeyFrame(1.0, VtValue(1.0), TsKnotHeld) );
+ held2.SetKeyFrame( TsKeyFrame(3.0, VtValue(1.0), TsKnotHeld) );
+ held2.SetKeyFrame( TsKeyFrame(12.0, VtValue(1.0), TsKnotHeld) );
+ held2.SetKeyFrame( TsKeyFrame(6.0, VtValue(2.0), TsKnotHeld) );
+
+ // Authoring a new knot in the middle of 2 redundant held knots should
+ // invalidate the interval between the new knot and the next authored
+ // knot.
+ _SplineTester tester = _SplineTester(held1);
+ TF_VERIFY(tester.SetValue(
+ held2, GfInterval(6.0, 12.0, true, false)));
+ }
+
+ // Test spline invalidation against flat single knot splines (essentially
+ // default value invalidation).
+ {
+ TsSpline spline;
+
+ spline.Clear();
+ // All held knots, flat spline by default
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(1.0), TsKnotHeld) );
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(1.0), TsKnotHeld) );
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(1.0), TsKnotHeld) );
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+
+ // Set first knot to a different value 0
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(-inf, 20, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(-inf, 20, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(-inf, 20, false, false)));
+ // Set first knot to dual valued, left 0, right 1
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(0.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(-inf, 10, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(0.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(-inf, 10, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(0.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(-inf, 10, false, true)));
+ // Set first knot to dual valued, left 1, right 0
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(1.0), VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 20, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(1.0), VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 20, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(1.0), VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 20, true, false)));
+ // Set first knot to dual valued, both 1
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(1.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(1.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(10.0, VtValue(1.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+
+ // Set second knot to a different value 0
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 30, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 30, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 30, false, false)));
+ // Set second knot to dual valued, left 0, right 1
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(0.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 20, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(0.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 20, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(0.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(10, 20, false, true)));
+ // Set second knot to dual valued, left 1, right 0
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(1.0), VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 30, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(1.0), VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 30, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(1.0), VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 30, true, false)));
+ // Set second knot to dual valued, both 1
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(1.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(1.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(20.0, VtValue(1.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+
+ // Set third knot to a different value 0
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 40, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 40, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 40, false, false)));
+ // Set third knot to dual valued, left 0, right 1
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(0.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 30, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(0.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 30, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(0.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(20, 30, false, true)));
+ // Set third knot to dual valued, left 1, right 0
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(1.0), VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, 40, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(1.0), VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, 40, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(1.0), VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, 40, true, false)));
+ // Set third knot to dual valued, both 1
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(1.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(1.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(30.0, VtValue(1.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+
+ // Set last knot to a different value 0
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, inf, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, inf, false, false)));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, inf, false, false)));
+ // Set last knot to dual valued, left 0, right 1
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(0.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, 40, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(0.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, 40, false, true)));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(0.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(30, 40, false, true)));
+ // Set last knot to dual valued, left 1, right 0
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(1.0), VtValue(0.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(40, inf, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(1.0), VtValue(0.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(40, inf, true, false)));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(1.0), VtValue(0.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval(40, inf, true, false)));
+ // Set last knot to dual valued, both 1
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(1.0), VtValue(1.0), TsKnotHeld) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(1.0), VtValue(1.0), TsKnotLinear) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ spline.SetKeyFrame( TsKeyFrame(40.0, VtValue(1.0), VtValue(1.0), TsKnotBezier) );
+ TF_AXIOM(_TestSetSingleValueSplines(spline, VtValue(1.0), GfInterval()));
+ }
+
+ TestSwapKeyFrames();
+
+ printf("\nTest SUCCEEDED\n");
+ return 0;
+}
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr.Ts import TsTest_Museum as Museum
+from pxr.Ts import TsTest_AnimXEvaluator as Evaluator
+from pxr.Ts import TsTest_CompareBaseline as CompareBaseline
+from pxr.Ts import TsTest_SampleTimes as STimes
+from pxr.Ts import TsTest_Grapher as Grapher
+from pxr.Ts import TsTest_Comparator as Comparator
+
+import unittest
+
+
+class TsTest_AnimXFramework(unittest.TestCase):
+
+ def test_Grapher(self):
+ """
+ Verify that AnimXEvaluator and Grapher are working.
+ To really be sure, inspect the graph image output.
+ """
+ data1 = Museum.GetData(Museum.TwoKnotBezier)
+ data2 = Museum.GetData(Museum.TwoKnotLinear)
+
+ times = STimes(data1)
+ times.AddStandardTimes()
+
+ samples1 = Evaluator().Eval(data1, times)
+ samples2 = Evaluator().Eval(data2, times)
+
+ grapher = Grapher("test_Grapher")
+ grapher.AddSpline("Bezier", data1, samples1)
+ grapher.AddSpline("Linear", data2, samples2)
+
+ if Grapher.Init():
+ grapher.Write("test_Grapher.png")
+
+ def test_Comparator(self):
+ """
+ Verify that AnimXEvaluator and Comparator are working.
+ To really be sure, inspect the graph image output.
+ """
+ data1 = Museum.GetData(Museum.TwoKnotBezier)
+ data2 = Museum.GetData(Museum.TwoKnotLinear)
+
+ times = STimes(data1)
+ times.AddStandardTimes()
+
+ samples1 = Evaluator().Eval(data1, times)
+ samples2 = Evaluator().Eval(data2, times)
+
+ comparator = Comparator("test_Comparator")
+ comparator.AddSpline("Bezier", data1, samples1)
+ comparator.AddSpline("Linear", data2, samples2)
+
+ self.assertTrue(comparator.GetMaxDiff() < 1.0)
+ if Comparator.Init():
+ comparator.Write("test_Comparator.png")
+
+ def test_Baseline(self):
+ """
+ Verify that AnimXEvaluator and CompareBaseline are working.
+ """
+ data = Museum.GetData(Museum.TwoKnotBezier)
+
+ times = STimes(data)
+ times.AddStandardTimes()
+
+ samples = Evaluator().Eval(data, times)
+
+ self.assertTrue(CompareBaseline("test_Baseline", data, samples))
+
+
+if __name__ == "__main__":
+ unittest.main()
--- /dev/null
+Spline:
+ hermite false
+ preExtrap Held
+ postExtrap Held
+Knots:
+ 1: 1, Curve, preSlope 0, postSlope 1, preLen 0, postLen 0.5, auto false / false
+ 5: 2, Curve, preSlope 0, postSlope 0, preLen 0.5, postLen 0, auto false / false
+-----
+0.2000000 1.0000000
+1.0000000 1.0000000
+1.0199005 1.0187387
+1.0398010 1.0356285
+1.0597015 1.0511517
+1.0796020 1.0656114
+1.0995025 1.0792126
+1.1194030 1.0921016
+1.1393035 1.1043872
+1.1592040 1.1161532
+1.1791045 1.1274651
+1.1990050 1.1383760
+1.2189055 1.1489292
+1.2388060 1.1591606
+1.2587065 1.1691006
+1.2786070 1.1787748
+1.2985075 1.1882055
+1.3184080 1.1974118
+1.3383085 1.2064106
+1.3582090 1.2152165
+1.3781095 1.2238426
+1.3980100 1.2323005
+1.4179104 1.2406008
+1.4378109 1.2487526
+1.4577114 1.2567645
+1.4776119 1.2646441
+1.4975124 1.2723983
+1.5174129 1.2800337
+1.5373134 1.2875559
+1.5572139 1.2949703
+1.5771144 1.3022819
+1.5970149 1.3094952
+1.6169154 1.3166144
+1.6368159 1.3236435
+1.6567164 1.3305861
+1.6766169 1.3374456
+1.6965174 1.3442251
+1.7164179 1.3509276
+1.7363184 1.3575560
+1.7562189 1.3641127
+1.7761194 1.3706002
+1.7960199 1.3770208
+1.8159204 1.3833768
+1.8358209 1.3896701
+1.8557214 1.3959027
+1.8756219 1.4020765
+1.8955224 1.4081931
+1.9154229 1.4142541
+1.9353234 1.4202613
+1.9552239 1.4262160
+1.9751244 1.4321196
+1.9950249 1.4379735
+2.0149254 1.4437790
+2.0348259 1.4495372
+2.0547264 1.4552495
+2.0746269 1.4609168
+2.0945274 1.4665402
+2.1144279 1.4721207
+2.1343284 1.4776594
+2.1542289 1.4831570
+2.1741294 1.4886147
+2.1940299 1.4940331
+2.2139303 1.4994131
+2.2338308 1.5047555
+2.2537313 1.5100610
+2.2736318 1.5153304
+2.2935323 1.5205644
+2.3134328 1.5257636
+2.3333333 1.5309288
+2.3532338 1.5360604
+2.3731343 1.5411591
+2.3930348 1.5462255
+2.4129353 1.5512602
+2.4328358 1.5562636
+2.4527363 1.5612363
+2.4726368 1.5661788
+2.4925373 1.5710916
+2.5124378 1.5759751
+2.5323383 1.5808298
+2.5522388 1.5856561
+2.5721393 1.5904544
+2.5920398 1.5952252
+2.6119403 1.5999688
+2.6318408 1.6046855
+2.6517413 1.6093759
+2.6716418 1.6140401
+2.6915423 1.6186786
+2.7114428 1.6232917
+2.7313433 1.6278797
+2.7512438 1.6324429
+2.7711443 1.6369815
+2.7910448 1.6414960
+2.8109453 1.6459865
+2.8308458 1.6504534
+2.8507463 1.6548968
+2.8706468 1.6593171
+2.8905473 1.6637144
+2.9104478 1.6680891
+2.9303483 1.6724412
+2.9502488 1.6767712
+2.9701493 1.6810791
+2.9900498 1.6853651
+3.0099502 1.6896295
+3.0298507 1.6938724
+3.0497512 1.6980941
+3.0696517 1.7022947
+3.0895522 1.7064743
+3.1094527 1.7106331
+3.1293532 1.7147713
+3.1492537 1.7188890
+3.1691542 1.7229864
+3.1890547 1.7270635
+3.2089552 1.7311206
+3.2288557 1.7351577
+3.2487562 1.7391750
+3.2686567 1.7431726
+3.2885572 1.7471505
+3.3084577 1.7511089
+3.3283582 1.7550479
+3.3482587 1.7589675
+3.3681592 1.7628678
+3.3880597 1.7667490
+3.4079602 1.7706110
+3.4278607 1.7744540
+3.4477612 1.7782779
+3.4676617 1.7820829
+3.4875622 1.7858691
+3.5074627 1.7896363
+3.5273632 1.7933847
+3.5472637 1.7971143
+3.5671642 1.8008252
+3.5870647 1.8045173
+3.6069652 1.8081906
+3.6268657 1.8118452
+3.6467662 1.8154810
+3.6666667 1.8190981
+3.6865672 1.8226964
+3.7064677 1.8262759
+3.7263682 1.8298366
+3.7462687 1.8333784
+3.7661692 1.8369013
+3.7860697 1.8404052
+3.8059701 1.8438901
+3.8258706 1.8473559
+3.8457711 1.8508026
+3.8656716 1.8542299
+3.8855721 1.8576379
+3.9054726 1.8610265
+3.9253731 1.8643954
+3.9452736 1.8677447
+3.9651741 1.8710741
+3.9850746 1.8743834
+4.0049751 1.8776727
+4.0248756 1.8809416
+4.0447761 1.8841899
+4.0646766 1.8874176
+4.0845771 1.8906243
+4.1044776 1.8938098
+4.1243781 1.8969738
+4.1442786 1.9001162
+4.1641791 1.9032365
+4.1840796 1.9063345
+4.2039801 1.9094099
+4.2238806 1.9124623
+4.2437811 1.9154912
+4.2636816 1.9184964
+4.2835821 1.9214773
+4.3034826 1.9244334
+4.3233831 1.9273644
+4.3432836 1.9302695
+4.3631841 1.9331482
+4.3830846 1.9359999
+4.4029851 1.9388238
+4.4228856 1.9416193
+4.4427861 1.9443855
+4.4626866 1.9471215
+4.4825871 1.9498263
+4.5024876 1.9524990
+4.5223881 1.9551384
+4.5422886 1.9577431
+4.5621891 1.9603120
+4.5820896 1.9628433
+4.6019900 1.9653356
+4.6218905 1.9677868
+4.6417910 1.9701950
+4.6616915 1.9725579
+4.6815920 1.9748728
+4.7014925 1.9771368
+4.7213930 1.9793466
+4.7412935 1.9814983
+4.7611940 1.9835876
+4.7810945 1.9856093
+4.8009950 1.9875575
+4.8208955 1.9894249
+4.8407960 1.9912030
+4.8606965 1.9928813
+4.8805970 1.9944468
+4.9004975 1.9958829
+4.9203980 1.9971683
+4.9402985 1.9982742
+5.0000000 2.0000000
+5.8000000 2.0000000
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr.Ts import TsTest_Museum as Museum
+from pxr.Ts import TsTest_MayapyEvaluator as Evaluator
+from pxr.Ts import TsTest_CompareBaseline as CompareBaseline
+from pxr.Ts import TsTest_SampleTimes as STimes
+from pxr.Ts import TsTest_Grapher as Grapher
+from pxr.Ts import TsTest_Comparator as Comparator
+
+import sys, unittest
+
+g_evaluator = None
+
+
+# MayapyEvaluator subclass that writes debug messages to stdout.
+class _TestEvaluator(Evaluator):
+ def _DebugLog(self, msg):
+ sys.stdout.write(msg)
+
+
+class TsTest_MayapyFramework(unittest.TestCase):
+
+ def test_Grapher(self):
+ """
+ Verify that MayapyEvaluator and Grapher are working.
+ To really be sure, inspect the graph image output.
+ """
+ data1 = Museum.GetData(Museum.TwoKnotBezier)
+ data2 = Museum.GetData(Museum.TwoKnotLinear)
+
+ times = STimes(data1)
+ times.AddStandardTimes()
+
+ samples1 = g_evaluator.Eval(data1, times)
+ samples2 = g_evaluator.Eval(data2, times)
+
+ grapher = Grapher("test_Grapher")
+ grapher.AddSpline("Bezier", data1, samples1)
+ grapher.AddSpline("Linear", data2, samples2)
+
+ if Grapher.Init():
+ grapher.Write("test_Grapher.png")
+
+ def test_Comparator(self):
+ """
+ Verify that MayapyEvaluator and Comparator are working.
+ To really be sure, inspect the graph image output.
+ """
+ data1 = Museum.GetData(Museum.TwoKnotBezier)
+ data2 = Museum.GetData(Museum.TwoKnotLinear)
+
+ times = STimes(data1)
+ times.AddStandardTimes()
+
+ samples1 = g_evaluator.Eval(data1, times)
+ samples2 = g_evaluator.Eval(data2, times)
+
+ comparator = Comparator("test_Comparator")
+ comparator.AddSpline("Bezier", data1, samples1)
+ comparator.AddSpline("Linear", data2, samples2)
+
+ self.assertTrue(comparator.GetMaxDiff() < 1.0)
+ if Comparator.Init():
+ comparator.Write("test_Comparator.png")
+
+ def test_Baseline(self):
+ """
+ Verify that MayapyEvaluator and CompareBaseline are working.
+ """
+ data = Museum.GetData(Museum.TwoKnotBezier)
+
+ times = STimes(data)
+ times.AddStandardTimes()
+
+ samples = g_evaluator.Eval(data, times)
+
+ self.assertTrue(CompareBaseline("test_Baseline", data, samples))
+
+ @classmethod
+ def tearDownClass(cls):
+ """
+ Clean up after all tests have run.
+ """
+ g_evaluator.Shutdown()
+
+
+if __name__ == "__main__":
+
+ mayapyPath = sys.argv.pop()
+ g_evaluator = _TestEvaluator(
+ mayapyPath, subprocessDebugFilePath = "debugMayapyDriver.txt")
+
+ unittest.main()
--- /dev/null
+Spline:
+ hermite false
+ preExtrap Held
+ postExtrap Held
+Knots:
+ 1: 1, Curve, preSlope 0, postSlope 1, preLen 0, postLen 0.5, auto false / false
+ 5: 2, Curve, preSlope 0, postSlope 0, preLen 0.5, postLen 0, auto false / false
+-----
+0.2000000 1.0000000
+1.0000000 1.0000000
+1.0199005 1.0187387
+1.0398010 1.0356285
+1.0597015 1.0511517
+1.0796020 1.0656115
+1.0995025 1.0792126
+1.1194030 1.0921016
+1.1393035 1.1043872
+1.1592040 1.1161531
+1.1791045 1.1274651
+1.1990050 1.1383760
+1.2189055 1.1489292
+1.2388060 1.1591606
+1.2587065 1.1691006
+1.2786070 1.1787748
+1.2985075 1.1882055
+1.3184080 1.1974119
+1.3383085 1.2064106
+1.3582090 1.2152165
+1.3781095 1.2238426
+1.3980100 1.2323006
+1.4179104 1.2406007
+1.4378109 1.2487526
+1.4577114 1.2567644
+1.4776119 1.2646440
+1.4975124 1.2723983
+1.5174129 1.2800337
+1.5373134 1.2875559
+1.5572139 1.2949703
+1.5771144 1.3022819
+1.5970149 1.3094952
+1.6169154 1.3166145
+1.6368159 1.3236436
+1.6567164 1.3305862
+1.6766169 1.3374456
+1.6965174 1.3442251
+1.7164179 1.3509276
+1.7363184 1.3575559
+1.7562189 1.3641126
+1.7761194 1.3706002
+1.7960199 1.3770208
+1.8159204 1.3833768
+1.8358209 1.3896701
+1.8557214 1.3959028
+1.8756219 1.4020765
+1.8955224 1.4081931
+1.9154229 1.4142542
+1.9353234 1.4202613
+1.9552239 1.4262159
+1.9751244 1.4321196
+1.9950249 1.4379735
+2.0149254 1.4437790
+2.0348259 1.4495372
+2.0547264 1.4552495
+2.0746269 1.4609168
+2.0945274 1.4665402
+2.1144279 1.4721207
+2.1343284 1.4776594
+2.1542289 1.4831571
+2.1741294 1.4886147
+2.1940299 1.4940331
+2.2139303 1.4994130
+2.2338308 1.5047555
+2.2537313 1.5100610
+2.2736318 1.5153304
+2.2935323 1.5205644
+2.3134328 1.5257636
+2.3333333 1.5309288
+2.3532338 1.5360604
+2.3731343 1.5411591
+2.3930348 1.5462255
+2.4129353 1.5512602
+2.4328358 1.5562636
+2.4527363 1.5612363
+2.4726368 1.5661788
+2.4925373 1.5710916
+2.5124378 1.5759751
+2.5323383 1.5808298
+2.5522388 1.5856561
+2.5721393 1.5904544
+2.5920398 1.5952252
+2.6119403 1.5999688
+2.6318408 1.6046855
+2.6517413 1.6093759
+2.6716418 1.6140401
+2.6915423 1.6186786
+2.7114428 1.6232917
+2.7313433 1.6278797
+2.7512438 1.6324428
+2.7711443 1.6369815
+2.7910448 1.6414960
+2.8109453 1.6459865
+2.8308458 1.6504534
+2.8507463 1.6548968
+2.8706468 1.6593171
+2.8905473 1.6637144
+2.9104478 1.6680891
+2.9303483 1.6724412
+2.9502488 1.6767712
+2.9701493 1.6810791
+2.9900498 1.6853651
+3.0099502 1.6896295
+3.0298507 1.6938724
+3.0497512 1.6980941
+3.0696517 1.7022946
+3.0895522 1.7064743
+3.1094527 1.7106331
+3.1293532 1.7147713
+3.1492537 1.7188890
+3.1691542 1.7229864
+3.1890547 1.7270635
+3.2089552 1.7311206
+3.2288557 1.7351578
+3.2487562 1.7391750
+3.2686567 1.7431726
+3.2885572 1.7471505
+3.3084577 1.7511089
+3.3283582 1.7550479
+3.3482587 1.7589675
+3.3681592 1.7628678
+3.3880597 1.7667490
+3.4079602 1.7706110
+3.4278607 1.7744540
+3.4477612 1.7782779
+3.4676617 1.7820829
+3.4875622 1.7858691
+3.5074627 1.7896363
+3.5273632 1.7933847
+3.5472637 1.7971143
+3.5671642 1.8008252
+3.5870647 1.8045173
+3.6069652 1.8081906
+3.6268657 1.8118452
+3.6467662 1.8154810
+3.6666667 1.8190981
+3.6865672 1.8226964
+3.7064677 1.8262759
+3.7263682 1.8298366
+3.7462687 1.8333784
+3.7661692 1.8369013
+3.7860697 1.8404052
+3.8059701 1.8438901
+3.8258706 1.8473559
+3.8457711 1.8508025
+3.8656716 1.8542299
+3.8855721 1.8576379
+3.9054726 1.8610265
+3.9253731 1.8643954
+3.9452736 1.8677447
+3.9651741 1.8710741
+3.9850746 1.8743835
+4.0049751 1.8776727
+4.0248756 1.8809416
+4.0447761 1.8841900
+4.0646766 1.8874176
+4.0845771 1.8906242
+4.1044776 1.8938097
+4.1243781 1.8969738
+4.1442786 1.9001161
+4.1641791 1.9032365
+4.1840796 1.9063345
+4.2039801 1.9094099
+4.2238806 1.9124623
+4.2437811 1.9154912
+4.2636816 1.9184964
+4.2835821 1.9214773
+4.3034826 1.9244334
+4.3233831 1.9273644
+4.3432836 1.9302695
+4.3631841 1.9331482
+4.3830846 1.9359999
+4.4029851 1.9388238
+4.4228856 1.9416193
+4.4427861 1.9443855
+4.4626866 1.9471215
+4.4825871 1.9498263
+4.5024876 1.9524990
+4.5223881 1.9551384
+4.5422886 1.9577431
+4.5621891 1.9603120
+4.5820896 1.9628434
+4.6019900 1.9653356
+4.6218905 1.9677868
+4.6417910 1.9701950
+4.6616915 1.9725579
+4.6815920 1.9748728
+4.7014925 1.9771368
+4.7213930 1.9793466
+4.7412935 1.9814983
+4.7611940 1.9835876
+4.7810945 1.9856093
+4.8009950 1.9875575
+4.8208955 1.9894249
+4.8407960 1.9912030
+4.8606965 1.9928813
+4.8805970 1.9944468
+4.9004975 1.9958829
+4.9203980 1.9971683
+4.9402985 1.9982742
+5.0000000 2.0000000
+5.8000000 2.0000000
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr.Ts import TsTest_Museum as Museum
+from pxr.Ts import TsTest_MayapyEvaluator as MayapyEvaluator
+from pxr.Ts import TsTest_AnimXEvaluator as AnimXEvaluator
+from pxr.Ts import TsTest_SampleTimes as STimes
+from pxr.Ts import TsTest_Comparator as Comparator
+
+import sys, unittest
+
+g_mayapyEvaluator = None
+
+
+class TsTest_MayapyVsAnimX(unittest.TestCase):
+
+ def test_Basic(self):
+ """
+ Verify mayapy and AnimX evaluation are close in one simple case.
+ To really be sure, inspect the graph image output.
+
+ TODO: can these two backends match exactly?
+ """
+ data = Museum.GetData(Museum.TwoKnotBezier)
+
+ times = STimes(data)
+ times.AddStandardTimes()
+
+ mayapySamples = g_mayapyEvaluator.Eval(data, times)
+ animXSamples = AnimXEvaluator().Eval(data, times)
+
+ comparator = Comparator("test_Basic")
+ comparator.AddSpline("mayapy", data, mayapySamples)
+ comparator.AddSpline("AnimX", data, animXSamples)
+
+ self.assertTrue(comparator.GetMaxDiff() < 1e-7)
+ if Comparator.Init():
+ comparator.Write("test_Basic.png")
+
+ @classmethod
+ def tearDownClass(cls):
+ """
+ Clean up after all tests have run.
+ """
+ g_mayapyEvaluator.Shutdown()
+
+
+if __name__ == "__main__":
+
+ mayapyPath = sys.argv.pop()
+ g_mayapyEvaluator = MayapyEvaluator(mayapyPath)
+
+ unittest.main()
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr.Ts import TsTest_Museum as Museum
+from pxr.Ts import TsTest_TsEvaluator as Evaluator
+from pxr.Ts import TsTest_CompareBaseline as CompareBaseline
+from pxr.Ts import TsTest_SampleTimes as STimes
+from pxr.Ts import TsTest_Grapher as Grapher
+from pxr.Ts import TsTest_Comparator as Comparator
+
+import unittest
+
+
+class TsTest_TsFramework(unittest.TestCase):
+
+ def test_Grapher(self):
+ """
+ Verify that TsEvaluator and Grapher are working.
+ To really be sure, inspect the graph image output.
+ """
+ data1 = Museum.GetData(Museum.TwoKnotBezier)
+ data2 = Museum.GetData(Museum.TwoKnotLinear)
+
+ times = STimes(data1)
+ times.AddStandardTimes()
+
+ samples1 = Evaluator().Eval(data1, times)
+ samples2 = Evaluator().Eval(data2, times)
+
+ grapher = Grapher("test_Grapher")
+ grapher.AddSpline("Bezier", data1, samples1)
+ grapher.AddSpline("Linear", data2, samples2)
+
+ if Grapher.Init():
+ grapher.Write("test_Grapher.png")
+
+ def test_Comparator(self):
+ """
+ Verify that TsEvaluator and Comparator are working.
+ To really be sure, inspect the graph image output.
+ """
+ data1 = Museum.GetData(Museum.TwoKnotBezier)
+ data2 = Museum.GetData(Museum.TwoKnotLinear)
+
+ times = STimes(data1)
+ times.AddStandardTimes()
+
+ samples1 = Evaluator().Eval(data1, times)
+ samples2 = Evaluator().Eval(data2, times)
+
+ comparator = Comparator("test_Comparator")
+ comparator.AddSpline("Bezier", data1, samples1)
+ comparator.AddSpline("Linear", data2, samples2)
+
+ self.assertTrue(comparator.GetMaxDiff() < 1.0)
+ if Comparator.Init():
+ comparator.Write("test_Comparator.png")
+
+ def test_Looping(self):
+ """
+ Verify that Grapher correctly displays loops.
+ To really be sure, inspect the graph image output.
+ """
+ data = Museum.GetData(Museum.SimpleInnerLoop)
+
+ baked = Evaluator().BakeInnerLoops(data)
+
+ times = STimes(baked)
+ times.AddStandardTimes()
+
+ samples = Evaluator().Eval(data, times)
+
+ grapher = Grapher("test_Looping")
+ grapher.AddSpline("Looping", data, samples, baked = baked)
+
+ if Grapher.Init():
+ grapher.Write("test_Looping.png")
+
+ def test_Baseline(self):
+ """
+ Verify that TsEvaluator and CompareBaseline are working.
+ """
+ data = Museum.GetData(Museum.TwoKnotBezier)
+
+ times = STimes(data)
+ times.AddStandardTimes()
+
+ samples = Evaluator().Eval(data, times)
+
+ self.assertTrue(CompareBaseline("test_Baseline", data, samples))
+
+ def test_BaselineFail(self):
+ """
+ Simulate an unintended change in evaluation results, and verify that
+ CompareBaseline catches and reports the difference correctly.
+ """
+ # Pretend this is the data we used. This is the data that's in the
+ # baseline file.
+ data1 = Museum.GetData(Museum.TwoKnotBezier)
+
+ # Actually evaluate using this data.
+ data2 = Museum.GetData(Museum.TwoKnotLinear)
+
+ times = STimes(data1)
+ times.AddStandardTimes()
+
+ samples = Evaluator().Eval(data2, times)
+
+ # Pass the data2 samples, but the data1 input data.
+ # This should trigger a value mismatch and a diff report.
+ self.assertFalse(CompareBaseline("test_BaselineFail", data1, samples))
+
+
+if __name__ == "__main__":
+ unittest.main()
--- /dev/null
+Spline:
+ hermite false
+ preExtrap Held
+ postExtrap Held
+Knots:
+ 1: 1, Curve, preSlope 0, postSlope 1, preLen 0, postLen 0.5, auto false / false
+ 5: 2, Curve, preSlope 0, postSlope 0, preLen 0.5, postLen 0, auto false / false
+-----
+0.2000000 1.0000000
+1.0000000 1.0000000
+1.0199005 1.0187387
+1.0398010 1.0356285
+1.0597015 1.0511517
+1.0796020 1.0656114
+1.0995025 1.0792126
+1.1194030 1.0921016
+1.1393035 1.1043872
+1.1592040 1.1161532
+1.1791045 1.1274651
+1.1990050 1.1383760
+1.2189055 1.1489292
+1.2388060 1.1591606
+1.2587065 1.1691006
+1.2786070 1.1787748
+1.2985075 1.1882055
+1.3184080 1.1974118
+1.3383085 1.2064106
+1.3582090 1.2152165
+1.3781095 1.2238426
+1.3980100 1.2323005
+1.4179104 1.2406008
+1.4378109 1.2487526
+1.4577114 1.2567645
+1.4776119 1.2646441
+1.4975124 1.2723983
+1.5174129 1.2800337
+1.5373134 1.2875559
+1.5572139 1.2949703
+1.5771144 1.3022819
+1.5970149 1.3094952
+1.6169154 1.3166144
+1.6368159 1.3236435
+1.6567164 1.3305861
+1.6766169 1.3374456
+1.6965174 1.3442251
+1.7164179 1.3509276
+1.7363184 1.3575560
+1.7562189 1.3641127
+1.7761194 1.3706002
+1.7960199 1.3770208
+1.8159204 1.3833768
+1.8358209 1.3896701
+1.8557214 1.3959027
+1.8756219 1.4020765
+1.8955224 1.4081931
+1.9154229 1.4142541
+1.9353234 1.4202613
+1.9552239 1.4262160
+1.9751244 1.4321196
+1.9950249 1.4379735
+2.0149254 1.4437790
+2.0348259 1.4495372
+2.0547264 1.4552495
+2.0746269 1.4609168
+2.0945274 1.4665402
+2.1144279 1.4721207
+2.1343284 1.4776594
+2.1542289 1.4831570
+2.1741294 1.4886147
+2.1940299 1.4940331
+2.2139303 1.4994131
+2.2338308 1.5047555
+2.2537313 1.5100610
+2.2736318 1.5153304
+2.2935323 1.5205644
+2.3134328 1.5257636
+2.3333333 1.5309288
+2.3532338 1.5360604
+2.3731343 1.5411591
+2.3930348 1.5462255
+2.4129353 1.5512602
+2.4328358 1.5562636
+2.4527363 1.5612363
+2.4726368 1.5661788
+2.4925373 1.5710916
+2.5124378 1.5759751
+2.5323383 1.5808298
+2.5522388 1.5856561
+2.5721393 1.5904544
+2.5920398 1.5952252
+2.6119403 1.5999688
+2.6318408 1.6046855
+2.6517413 1.6093759
+2.6716418 1.6140401
+2.6915423 1.6186786
+2.7114428 1.6232917
+2.7313433 1.6278797
+2.7512438 1.6324429
+2.7711443 1.6369815
+2.7910448 1.6414960
+2.8109453 1.6459865
+2.8308458 1.6504534
+2.8507463 1.6548968
+2.8706468 1.6593171
+2.8905473 1.6637144
+2.9104478 1.6680891
+2.9303483 1.6724412
+2.9502488 1.6767712
+2.9701493 1.6810791
+2.9900498 1.6853651
+3.0099502 1.6896295
+3.0298507 1.6938724
+3.0497512 1.6980941
+3.0696517 1.7022947
+3.0895522 1.7064743
+3.1094527 1.7106331
+3.1293532 1.7147713
+3.1492537 1.7188890
+3.1691542 1.7229864
+3.1890547 1.7270635
+3.2089552 1.7311206
+3.2288557 1.7351577
+3.2487562 1.7391750
+3.2686567 1.7431726
+3.2885572 1.7471505
+3.3084577 1.7511089
+3.3283582 1.7550479
+3.3482587 1.7589675
+3.3681592 1.7628678
+3.3880597 1.7667490
+3.4079602 1.7706110
+3.4278607 1.7744540
+3.4477612 1.7782779
+3.4676617 1.7820829
+3.4875622 1.7858691
+3.5074627 1.7896363
+3.5273632 1.7933847
+3.5472637 1.7971143
+3.5671642 1.8008252
+3.5870647 1.8045173
+3.6069652 1.8081906
+3.6268657 1.8118452
+3.6467662 1.8154810
+3.6666667 1.8190981
+3.6865672 1.8226964
+3.7064677 1.8262759
+3.7263682 1.8298366
+3.7462687 1.8333784
+3.7661692 1.8369013
+3.7860697 1.8404052
+3.8059701 1.8438901
+3.8258706 1.8473559
+3.8457711 1.8508026
+3.8656716 1.8542299
+3.8855721 1.8576379
+3.9054726 1.8610265
+3.9253731 1.8643954
+3.9452736 1.8677447
+3.9651741 1.8710741
+3.9850746 1.8743834
+4.0049751 1.8776727
+4.0248756 1.8809416
+4.0447761 1.8841899
+4.0646766 1.8874176
+4.0845771 1.8906243
+4.1044776 1.8938098
+4.1243781 1.8969738
+4.1442786 1.9001162
+4.1641791 1.9032365
+4.1840796 1.9063345
+4.2039801 1.9094099
+4.2238806 1.9124623
+4.2437811 1.9154912
+4.2636816 1.9184964
+4.2835821 1.9214773
+4.3034826 1.9244334
+4.3233831 1.9273644
+4.3432836 1.9302695
+4.3631841 1.9331482
+4.3830846 1.9359999
+4.4029851 1.9388238
+4.4228856 1.9416193
+4.4427861 1.9443855
+4.4626866 1.9471215
+4.4825871 1.9498263
+4.5024876 1.9524990
+4.5223881 1.9551384
+4.5422886 1.9577431
+4.5621891 1.9603120
+4.5820896 1.9628433
+4.6019900 1.9653356
+4.6218905 1.9677868
+4.6417910 1.9701950
+4.6616915 1.9725579
+4.6815920 1.9748728
+4.7014925 1.9771368
+4.7213930 1.9793466
+4.7412935 1.9814983
+4.7611940 1.9835876
+4.7810945 1.9856093
+4.8009950 1.9875575
+4.8208955 1.9894249
+4.8407960 1.9912030
+4.8606965 1.9928813
+4.8805970 1.9944468
+4.9004975 1.9958829
+4.9203980 1.9971683
+4.9402985 1.9982742
+5.0000000 2.0000000
+5.8000000 2.0000000
--- /dev/null
+Spline:
+ hermite false
+ preExtrap Held
+ postExtrap Held
+Knots:
+ 1: 1, Curve, preSlope 0, postSlope 1, preLen 0, postLen 0.5, auto false / false
+ 5: 2, Curve, preSlope 0, postSlope 0, preLen 0.5, postLen 0, auto false / false
+-----
+0.2000000 1.0000000
+1.0000000 1.0000000
+1.0199005 1.0187387
+1.0398010 1.0356285
+1.0597015 1.0511517
+1.0796020 1.0656114
+1.0995025 1.0792126
+1.1194030 1.0921016
+1.1393035 1.1043872
+1.1592040 1.1161532
+1.1791045 1.1274651
+1.1990050 1.1383760
+1.2189055 1.1489292
+1.2388060 1.1591606
+1.2587065 1.1691006
+1.2786070 1.1787748
+1.2985075 1.1882055
+1.3184080 1.1974118
+1.3383085 1.2064106
+1.3582090 1.2152165
+1.3781095 1.2238426
+1.3980100 1.2323005
+1.4179104 1.2406008
+1.4378109 1.2487526
+1.4577114 1.2567645
+1.4776119 1.2646441
+1.4975124 1.2723983
+1.5174129 1.2800337
+1.5373134 1.2875559
+1.5572139 1.2949703
+1.5771144 1.3022819
+1.5970149 1.3094952
+1.6169154 1.3166144
+1.6368159 1.3236435
+1.6567164 1.3305861
+1.6766169 1.3374456
+1.6965174 1.3442251
+1.7164179 1.3509276
+1.7363184 1.3575560
+1.7562189 1.3641127
+1.7761194 1.3706002
+1.7960199 1.3770208
+1.8159204 1.3833768
+1.8358209 1.3896701
+1.8557214 1.3959027
+1.8756219 1.4020765
+1.8955224 1.4081931
+1.9154229 1.4142541
+1.9353234 1.4202613
+1.9552239 1.4262160
+1.9751244 1.4321196
+1.9950249 1.4379735
+2.0149254 1.4437790
+2.0348259 1.4495372
+2.0547264 1.4552495
+2.0746269 1.4609168
+2.0945274 1.4665402
+2.1144279 1.4721207
+2.1343284 1.4776594
+2.1542289 1.4831570
+2.1741294 1.4886147
+2.1940299 1.4940331
+2.2139303 1.4994131
+2.2338308 1.5047555
+2.2537313 1.5100610
+2.2736318 1.5153304
+2.2935323 1.5205644
+2.3134328 1.5257636
+2.3333333 1.5309288
+2.3532338 1.5360604
+2.3731343 1.5411591
+2.3930348 1.5462255
+2.4129353 1.5512602
+2.4328358 1.5562636
+2.4527363 1.5612363
+2.4726368 1.5661788
+2.4925373 1.5710916
+2.5124378 1.5759751
+2.5323383 1.5808298
+2.5522388 1.5856561
+2.5721393 1.5904544
+2.5920398 1.5952252
+2.6119403 1.5999688
+2.6318408 1.6046855
+2.6517413 1.6093759
+2.6716418 1.6140401
+2.6915423 1.6186786
+2.7114428 1.6232917
+2.7313433 1.6278797
+2.7512438 1.6324429
+2.7711443 1.6369815
+2.7910448 1.6414960
+2.8109453 1.6459865
+2.8308458 1.6504534
+2.8507463 1.6548968
+2.8706468 1.6593171
+2.8905473 1.6637144
+2.9104478 1.6680891
+2.9303483 1.6724412
+2.9502488 1.6767712
+2.9701493 1.6810791
+2.9900498 1.6853651
+3.0099502 1.6896295
+3.0298507 1.6938724
+3.0497512 1.6980941
+3.0696517 1.7022947
+3.0895522 1.7064743
+3.1094527 1.7106331
+3.1293532 1.7147713
+3.1492537 1.7188890
+3.1691542 1.7229864
+3.1890547 1.7270635
+3.2089552 1.7311206
+3.2288557 1.7351577
+3.2487562 1.7391750
+3.2686567 1.7431726
+3.2885572 1.7471505
+3.3084577 1.7511089
+3.3283582 1.7550479
+3.3482587 1.7589675
+3.3681592 1.7628678
+3.3880597 1.7667490
+3.4079602 1.7706110
+3.4278607 1.7744540
+3.4477612 1.7782779
+3.4676617 1.7820829
+3.4875622 1.7858691
+3.5074627 1.7896363
+3.5273632 1.7933847
+3.5472637 1.7971143
+3.5671642 1.8008252
+3.5870647 1.8045173
+3.6069652 1.8081906
+3.6268657 1.8118452
+3.6467662 1.8154810
+3.6666667 1.8190981
+3.6865672 1.8226964
+3.7064677 1.8262759
+3.7263682 1.8298366
+3.7462687 1.8333784
+3.7661692 1.8369013
+3.7860697 1.8404052
+3.8059701 1.8438901
+3.8258706 1.8473559
+3.8457711 1.8508026
+3.8656716 1.8542299
+3.8855721 1.8576379
+3.9054726 1.8610265
+3.9253731 1.8643954
+3.9452736 1.8677447
+3.9651741 1.8710741
+3.9850746 1.8743834
+4.0049751 1.8776727
+4.0248756 1.8809416
+4.0447761 1.8841899
+4.0646766 1.8874176
+4.0845771 1.8906243
+4.1044776 1.8938098
+4.1243781 1.8969738
+4.1442786 1.9001162
+4.1641791 1.9032365
+4.1840796 1.9063345
+4.2039801 1.9094099
+4.2238806 1.9124623
+4.2437811 1.9154912
+4.2636816 1.9184964
+4.2835821 1.9214773
+4.3034826 1.9244334
+4.3233831 1.9273644
+4.3432836 1.9302695
+4.3631841 1.9331482
+4.3830846 1.9359999
+4.4029851 1.9388238
+4.4228856 1.9416193
+4.4427861 1.9443855
+4.4626866 1.9471215
+4.4825871 1.9498263
+4.5024876 1.9524990
+4.5223881 1.9551384
+4.5422886 1.9577431
+4.5621891 1.9603120
+4.5820896 1.9628433
+4.6019900 1.9653356
+4.6218905 1.9677868
+4.6417910 1.9701950
+4.6616915 1.9725579
+4.6815920 1.9748728
+4.7014925 1.9771368
+4.7213930 1.9793466
+4.7412935 1.9814983
+4.7611940 1.9835876
+4.7810945 1.9856093
+4.8009950 1.9875575
+4.8208955 1.9894249
+4.8407960 1.9912030
+4.8606965 1.9928813
+4.8805970 1.9944468
+4.9004975 1.9958829
+4.9203980 1.9971683
+4.9402985 1.9982742
+5.0000000 2.0000000
+5.8000000 2.0000000
--- /dev/null
+#!/pxrpythonsubst
+
+#
+# Copyright 2023 Pixar
+#
+# Licensed under the Apache License, Version 2.0 (the "Apache License")
+# with the following modification; you may not use this file except in
+# compliance with the Apache License and the following modification to it:
+# Section 6. Trademarks. is deleted and replaced with:
+#
+# 6. Trademarks. This License does not grant permission to use the trade
+# names, trademarks, service marks, or product names of the Licensor
+# and its affiliates, except as required to comply with Section 4(c) of
+# the License and to reproduce the content of the NOTICE file.
+#
+# You may obtain a copy of the Apache License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the Apache License with the above modification is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the Apache License for the specific
+# language governing permissions and limitations under the Apache License.
+#
+
+from pxr.Ts import TsTest_Museum as Museum
+from pxr.Ts import TsTest_MayapyEvaluator as MayapyEvaluator
+from pxr.Ts import TsTest_TsEvaluator as TsEvaluator
+from pxr.Ts import TsTest_SampleBezier as SampleBezier
+from pxr.Ts import TsTest_SampleTimes as STimes
+from pxr.Ts import TsTest_Grapher as Grapher
+from pxr.Ts import TsTest_Comparator as Comparator
+
+import sys, unittest
+
+g_mayapyEvaluator = None
+
+
+class TsTest_TsVsMayapy(unittest.TestCase):
+
+ def test_Basic(self):
+ """
+ Verify Ts and mayapy evaluation are close in one simple case.
+ To really be sure, inspect the graph image output.
+ """
+ data = Museum.GetData(Museum.TwoKnotBezier)
+
+ times = STimes(data)
+ times.AddStandardTimes()
+
+ tsSamples = TsEvaluator().Eval(data, times)
+ mayapySamples = g_mayapyEvaluator.Eval(data, times)
+
+ comparator = Comparator("test_Basic")
+ comparator.AddSpline("Ts", data, tsSamples)
+ comparator.AddSpline("mayapy", data, mayapySamples)
+
+ self.assertTrue(comparator.GetMaxDiff() < 1e-7)
+ if Comparator.Init():
+ comparator.Write("test_Basic.png")
+
+ def test_Recurve(self):
+ """
+ Illustrate the differences among Ts, mayapy, and a pure Bezier in a
+ recurve case (time-regressive curve).
+ """
+ data = Museum.GetData(Museum.Recurve)
+
+ times = STimes(data)
+ times.AddStandardTimes()
+
+ tsSamples = TsEvaluator().Eval(data, times)
+ mayapySamples = g_mayapyEvaluator.Eval(data, times)
+ bezSamples = SampleBezier(data, numSamples = 200)
+
+ grapher = Grapher("test_Recurve")
+ grapher.AddSpline("Ts", data, tsSamples)
+ grapher.AddSpline("mayapy", data, mayapySamples)
+ grapher.AddSpline("Bezier", data, bezSamples)
+
+ if Grapher.Init():
+ grapher.Write("test_Recurve.png")
+
+ def test_Crossover(self):
+ """
+ Illustrate the differences among Ts, mayapy, and a pure Bezier in a
+ crossover case (time-regressive curve).
+ """
+ data = Museum.GetData(Museum.Crossover)
+
+ times = STimes(data)
+ times.AddStandardTimes()
+
+ tsSamples = TsEvaluator().Eval(data, times)
+ mayapySamples = g_mayapyEvaluator.Eval(data, times)
+ bezSamples = SampleBezier(data, numSamples = 200)
+
+ grapher = Grapher("test_Crossover")
+ grapher.AddSpline("Ts", data, tsSamples)
+ grapher.AddSpline("mayapy", data, mayapySamples)
+ grapher.AddSpline("Bezier", data, bezSamples)
+
+ if Grapher.Init():
+ grapher.Write("test_Crossover.png")
+
+ @classmethod
+ def tearDownClass(cls):
+ """
+ Clean up after all tests have run.
+ """
+ g_mayapyEvaluator.Shutdown()
+
+
+if __name__ == "__main__":
+
+ mayapyPath = sys.argv.pop()
+ g_mayapyEvaluator = MayapyEvaluator(mayapyPath)
+
+ unittest.main()
--- /dev/null
+/*!
+\page page_ts_tsTest The TsTest Framework
+
+TsTest is a framework that validates, graphs, and compares spline evaluations.
+It is used to test Ts itself, but it also has other "backends", which perform
+evaluation using other engines.
+
+TsTest lives inside the Ts library. All files prefixed with \c tsTest, \c
+wrapTsTest, or \c TsTest are part of the TsTest framework. They fall into these
+categories:
+
+- <b>Framework.</b> The base implementation of TsTest. Includes data
+ structures, generic evaluation interface, and graphical output.
+
+ - \c tsTest_Evaluator (h, cpp, wrap): generic interface for spline evaluation.
+ Implemented by all backends.
+
+ - \c tsTest_SplineData (h, cpp, wrap): input data structure for the evaluation
+ interface. Provides a generic way of defining the control parameters of a
+ spline: knots, tangents, extrapolation, etc.
+
+ - \c tsTest_SampleTimes (h, cpp, wrap): datatypes and convenience routines to
+ determine a set of times at which to perform sampling.
+
+ - \c tsTest_Types (h, cpp, wrap): datatypes for the evaluation interface.
+
+ - \c tsTest_SampleBezier (h, cpp, wrap): pseudo-evaluator that samples a
+ Bezier spline using the De Casteljau algorithm. Useful for providing
+ "ground truth" in comparison tests.
+
+ - \c TsTest_Grapher (py): takes a spline and evaluation results, and produces
+ a graph image. Requires the Python \c matplotlib module.
+
+ - \c TsTest_Comparator (py): takes two tuples of (spline, evaluation results),
+ draws both on the same graph, and produces a second graph showing the
+ difference curve. Relies on \c TsTest_Grapher.
+
+ - \c tsTest_CompareBaseline (py): test helper that creates baseline files,
+ compares results against baselines, and graphs differences.
+
+- <b>Backends.</b> Implementations of the generic evaluation interface for
+ various spline evaluation engines.
+
+ - \c tsTest_TsEvaluator (h, cpp, wrap): backend for the Ts library.
+
+ - \c tsTest_MayapyEvaluator (py): backend for the `mayapy` interpreter.
+ Produces evaluations using Autodesk Maya. Built optionally, and requires a
+ working installation of Maya.
+
+ - \c tsTest_MayapyDriver (py): helper for `tsTest_MayapyEvaluator`. A
+ script that runs inside `mayapy`.
+
+ - \c tsTest_AnimXEvaluator (h, cpp, wrap): backend for the AnimX library, an
+ open-source Autodesk project that emulates Maya evaluation. Built
+ optionally, and requires a working installation of AnimX.
+
+- <b>Single-Backend Tests.</b> CTest cases that call into one backend to
+ validate behavior.
+
+ - Most tests in \c testenv that start with \c tsTest. For example, \c
+ tsTest_TsFramework.
+
+- <b>Comparison Tests.</b> CTest cases that call into two backends to compare
+ their behavior with identical spline inputs.
+
+ - Tests in \c testenv that start with \c tsTest and include \c Vs. For
+ example, \c tsTest_MayapyVsAnimX.
+
+*/
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_AnimXEvaluator.h"
+
+#include "pxr/base/tf/enum.h"
+#include "pxr/base/tf/registryManager.h"
+#include "pxr/base/tf/diagnostic.h"
+
+// This flag is turned on in all of our AnimX builds.
+// We just hard-code it here.
+// It is apparently necessary in order to make AnimX match Maya precisely.
+#define MAYA_64BIT_TIME_PRECISION
+#include <animx.h>
+
+#include <map>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using Evaluator = TsTest_AnimXEvaluator;
+using STimes = TsTest_SampleTimes;
+using SData = TsTest_SplineData;
+
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(Evaluator::AutoTanAuto);
+ TF_ADD_ENUM_NAME(Evaluator::AutoTanSmooth);
+}
+
+TsTest_AnimXEvaluator::TsTest_AnimXEvaluator(
+ const AutoTanType autoTanType)
+ : _autoTanType(autoTanType)
+{
+}
+
+namespace
+{
+ class _Curve : public adsk::ICurve
+ {
+ public:
+ _Curve(
+ const SData &data,
+ const Evaluator::AutoTanType autoTanType)
+ : _autoTanType(autoTanType)
+ {
+ _isWeighted = !data.GetIsHermite();
+ _preInfinity = _GetInfinity(data.GetPreExtrapolation());
+ _postInfinity = _GetInfinity(data.GetPostExtrapolation());
+
+ if (data.GetKnots().empty())
+ return;
+
+ int i = 0;
+ adsk::TangentType tanType = adsk::TangentType::Global;
+
+ // Translate TsTestUtils knots to AnimX keyframes.
+ for (const SData::Knot &kf : data.GetKnots())
+ {
+ adsk::Keyframe axKf;
+ axKf.time = kf.time;
+ axKf.value = kf.value;
+ axKf.index = i++;
+
+ double preLen = 0, postLen = 0;
+ if (data.GetIsHermite())
+ {
+ // Hermite spline. Tan lengths may be zero and are ignored.
+ // Any nonzero length will allow us to establish a slope in
+ // X and Y, so use length 1.
+ preLen = 1;
+ postLen = 1;
+ }
+ else
+ {
+ // Non-Hermite spline. Use tan lengths as specified.
+ // Multiply by 3.
+ preLen = kf.preLen * 3;
+ postLen = kf.postLen * 3;
+ }
+
+ // Use previous segment type as in-tan type.
+ axKf.tanIn.type = _GetTanType(tanType, kf.preAuto);
+ axKf.tanIn.x = preLen;
+ axKf.tanIn.y = kf.preSlope * preLen;
+
+ // Determine new out-tan type and record that for the next
+ // in-tan.
+ tanType = _GetBaseTanType(kf.nextSegInterpMethod);
+ axKf.tanOut.type = _GetTanType(tanType, kf.postAuto);
+ axKf.tanOut.x = postLen;
+ axKf.tanOut.y = kf.postSlope * postLen;
+
+ // XXX: unimplemented for now.
+ axKf.quaternionW = 0;
+
+ axKf.linearInterpolation =
+ (kf.nextSegInterpMethod == SData::InterpLinear);
+
+ // Store new AnimX keyframe.
+ _kfs[kf.time] = axKf;
+ }
+
+ // Implement linear pre-extrap with explicit linear tangents.
+ if (data.GetPreExtrapolation().method == SData::ExtrapLinear)
+ {
+ adsk::Keyframe &axKf = _kfs.begin()->second;
+ axKf.tanIn.type = adsk::TangentType::Linear;
+ if (_kfs.size() == 1
+ || axKf.tanOut.type == adsk::TangentType::Step)
+ {
+ // Mirror a held (flat) segment.
+ axKf.tanIn.x = 1.0;
+ axKf.tanIn.y = 0.0;
+ }
+ else if (axKf.tanOut.type == adsk::TangentType::Linear)
+ {
+ // Mirror a linear segment.
+ const adsk::Keyframe &axNextKf =
+ (++_kfs.begin())->second;
+ axKf.tanIn.x = axNextKf.time - axKf.time;
+ axKf.tanIn.y = axNextKf.value - axKf.value;
+ }
+ else
+ {
+ // Mirror the tangent to a curved segment.
+ axKf.tanIn.x = axKf.tanOut.x;
+ axKf.tanIn.y = axKf.tanOut.y;
+ }
+ }
+
+ // Implement linear post-extrap with explicit linear tangents.
+ if (data.GetPostExtrapolation().method == SData::ExtrapLinear)
+ {
+ adsk::Keyframe &axKf = _kfs.rbegin()->second;
+ axKf.tanOut.type = adsk::TangentType::Linear;
+ if (_kfs.size() == 1
+ || axKf.tanIn.type == adsk::TangentType::Step)
+ {
+ // Mirror a held (flat) segment.
+ axKf.tanOut.x = 1.0;
+ axKf.tanOut.y = 0.0;
+ }
+ else if (axKf.tanIn.type == adsk::TangentType::Linear)
+ {
+ // Mirror a linear segment.
+ const adsk::Keyframe &axPrevKf =
+ (++_kfs.rbegin())->second;
+ axKf.tanOut.x = axKf.time - axPrevKf.time;
+ axKf.tanOut.y = axKf.value - axPrevKf.value;
+ }
+ else
+ {
+ // Mirror the tangent to a curved segment.
+ axKf.tanOut.x = axKf.tanIn.x;
+ axKf.tanOut.y = axKf.tanIn.y;
+ }
+ }
+ }
+
+ virtual ~_Curve() = default;
+
+ bool keyframeAtIndex(
+ const int index, adsk::Keyframe &keyOut) const override
+ {
+ if (index < 0 || size_t(index) >= _kfs.size())
+ return false;
+
+ auto it = _kfs.begin();
+ for (int i = 0; i < index; i++)
+ it++;
+ keyOut = it->second;
+ return true;
+ }
+
+ // If there is a keyframe at the specified time, return that. If the
+ // time is after the last keyframe, return the last. Otherwise return
+ // the next keyframe after the specified time.
+ bool keyframe(
+ const double time, adsk::Keyframe &keyOut) const override
+ {
+ const auto it = _kfs.lower_bound(time);
+ if (it == _kfs.end())
+ {
+ return last(keyOut);
+ }
+ else
+ {
+ keyOut = it->second;
+ return true;
+ }
+ }
+
+ bool first(adsk::Keyframe &keyOut) const override
+ {
+ if (_kfs.empty())
+ return false;
+
+ keyOut = _kfs.begin()->second;
+ return true;
+ }
+
+ bool last(adsk::Keyframe &keyOut) const override
+ {
+ if (_kfs.empty())
+ return false;
+
+ keyOut = _kfs.rbegin()->second;
+ return true;
+ }
+
+ adsk::InfinityType preInfinityType() const override
+ {
+ return _preInfinity;
+ }
+
+ adsk::InfinityType postInfinityType() const override
+ {
+ return _postInfinity;
+ }
+
+ bool isWeighted() const override
+ {
+ return _isWeighted;
+ }
+
+ unsigned int keyframeCount() const override
+ {
+ return _kfs.size();
+ }
+
+ bool isStatic() const override
+ {
+ // XXX: betting this is just an optimization.
+ return false;
+ }
+
+ private:
+ static adsk::InfinityType _GetInfinity(
+ const SData::Extrapolation &extrap)
+ {
+ // Non-looped modes.
+ if (extrap.method == SData::ExtrapHeld)
+ return adsk::InfinityType::Constant;
+ if (extrap.method == SData::ExtrapLinear)
+ return adsk::InfinityType::Linear;
+
+ // Looped modes.
+ if (extrap.loopMode == SData::LoopRepeat)
+ return adsk::InfinityType::CycleRelative;
+ if (extrap.loopMode == SData::LoopReset)
+ return adsk::InfinityType::Cycle;
+ if (extrap.loopMode == SData::LoopOscillate)
+ return adsk::InfinityType::Oscillate;
+
+ TF_CODING_ERROR("Unexpected extrapolation");
+ return adsk::InfinityType::Constant;
+ }
+
+ static adsk::TangentType _GetBaseTanType(
+ const SData::InterpMethod method)
+ {
+ switch (method)
+ {
+ case SData::InterpHeld: return adsk::TangentType::Step;
+ case SData::InterpLinear: return adsk::TangentType::Linear;
+ case SData::InterpCurve: return adsk::TangentType::Global;
+ }
+
+ TF_CODING_ERROR("Unexpected base tangent type");
+ return adsk::TangentType::Global;
+ }
+
+ adsk::TangentType _GetTanType(
+ const adsk::TangentType tanType,
+ const bool isAuto) const
+ {
+ if (tanType != adsk::TangentType::Global
+ || !isAuto)
+ {
+ return tanType;
+ }
+
+ switch (_autoTanType)
+ {
+ case Evaluator::AutoTanAuto: return adsk::TangentType::Auto;
+ case Evaluator::AutoTanSmooth: return adsk::TangentType::Smooth;
+ }
+
+ TF_CODING_ERROR("Unexpected tangent type");
+ return adsk::TangentType::Global;
+ }
+
+ private:
+ Evaluator::AutoTanType _autoTanType = Evaluator::AutoTanAuto;
+ bool _isWeighted = false;
+ adsk::InfinityType _preInfinity = adsk::InfinityType::Constant;
+ adsk::InfinityType _postInfinity = adsk::InfinityType::Constant;
+ std::map<double, adsk::Keyframe> _kfs;
+ };
+}
+
+TsTest_SampleVec
+TsTest_AnimXEvaluator::Eval(
+ const SData &data,
+ const STimes ×) const
+{
+ static const SData::Features supportedFeatures =
+ SData::FeatureHeldSegments |
+ SData::FeatureLinearSegments |
+ SData::FeatureBezierSegments |
+ SData::FeatureHermiteSegments |
+ SData::FeatureAutoTangents |
+ SData::FeatureExtrapolatingLoops;
+ if (data.GetRequiredFeatures() & ~supportedFeatures)
+ {
+ TF_CODING_ERROR("Unsupported spline features for AnimX");
+ return {};
+ }
+
+ const _Curve curve(data, _autoTanType);
+ TsTest_SampleVec result;
+
+ for (const STimes::SampleTime &sampleTime : times.GetTimes())
+ {
+ // Emulate pre-values by subtracting an arbitrary tiny time delta (but
+ // large enough to avoid Maya snapping it to a knot time). We will
+ // return a sample with a differing time, which will allow the result to
+ // be understood as a small delta rather than an instantaneous change.
+ double time = sampleTime.time;
+ if (sampleTime.pre)
+ time -= 1e-5;
+
+ const double value = adsk::evaluateCurve(time, curve);
+ result.push_back(TsTest_Sample(time, value));
+ }
+
+ return result;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_EXTRAS_BASE_TS_TEST_ANIM_X
+#define PXR_EXTRAS_BASE_TS_TEST_ANIM_X
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/tsTest_Evaluator.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TS_API TsTest_AnimXEvaluator : public TsTest_Evaluator
+{
+public:
+ enum AutoTanType
+ {
+ AutoTanAuto,
+ AutoTanSmooth
+ };
+
+ TsTest_AnimXEvaluator(
+ AutoTanType autoTanType = AutoTanAuto);
+
+ TsTest_SampleVec Eval(
+ const TsTest_SplineData &splineData,
+ const TsTest_SampleTimes &sampleTimes) const override;
+
+private:
+ const AutoTanType _autoTanType;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_Evaluator.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TsTest_SampleVec
+TsTest_Evaluator::Sample(
+ const TsTest_SplineData &splineData,
+ double tolerance) const
+{
+ // Default implementation returns no samples.
+ return {};
+}
+
+TsTest_SplineData
+TsTest_Evaluator::BakeInnerLoops(
+ const TsTest_SplineData &splineData) const
+{
+ // Default implementation returns the input data unmodified.
+ return splineData;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TS_TEST_EVALUATOR_H
+#define PXR_BASE_TS_TS_TEST_EVALUATOR_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+#include "pxr/base/ts/tsTest_SampleTimes.h"
+#include "pxr/base/ts/tsTest_Types.h"
+
+#include <vector>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TS_API TsTest_Evaluator
+{
+public:
+ // Required. Evaluates a spline at the specified times.
+ virtual TsTest_SampleVec Eval(
+ const TsTest_SplineData &splineData,
+ const TsTest_SampleTimes &sampleTimes) const = 0;
+
+ // Optional. Produces samples at implementation-determined times,
+ // sufficient to produce a piecewise linear approximation of the spline with
+ // an absolute value error less than the specified tolerance. Default
+ // implementation returns no samples.
+ virtual TsTest_SampleVec Sample(
+ const TsTest_SplineData &splineData,
+ double tolerance) const;
+
+ // Optional. Produce a copy of splineData with inner loops, if any, baked
+ // out into ordinary knots. Default implementation returns the input data
+ // unmodified.
+ virtual TsTest_SplineData BakeInnerLoops(
+ const TsTest_SplineData &splineData) const;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_Museum.h"
+
+#include "pxr/base/tf/enum.h"
+#include "pxr/base/tf/registryManager.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using SData = TsTest_SplineData;
+
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(TsTest_Museum::TwoKnotBezier);
+ TF_ADD_ENUM_NAME(TsTest_Museum::TwoKnotLinear);
+ TF_ADD_ENUM_NAME(TsTest_Museum::SimpleInnerLoop);
+ TF_ADD_ENUM_NAME(TsTest_Museum::Recurve);
+ TF_ADD_ENUM_NAME(TsTest_Museum::Crossover);
+}
+
+
+static TsTest_SplineData _TwoKnotBezier()
+{
+ SData::Knot knot1;
+ knot1.time = 1.0;
+ knot1.nextSegInterpMethod = SData::InterpCurve;
+ knot1.value = 1.0;
+ knot1.postSlope = 1.0;
+ knot1.postLen = 0.5;
+
+ SData::Knot knot2;
+ knot2.time = 5.0;
+ knot2.nextSegInterpMethod = SData::InterpCurve;
+ knot2.value = 2.0;
+ knot2.preSlope = 0.0;
+ knot2.preLen = 0.5;
+
+ SData data;
+ data.SetKnots({knot1, knot2});
+ return data;
+}
+
+static TsTest_SplineData _TwoKnotLinear()
+{
+ SData::Knot knot1;
+ knot1.time = 1.0;
+ knot1.nextSegInterpMethod = SData::InterpLinear;
+ knot1.value = 1.0;
+
+ SData::Knot knot2;
+ knot2.time = 5.0;
+ knot2.nextSegInterpMethod = SData::InterpLinear;
+ knot2.value = 2.0;
+
+ SData data;
+ data.SetKnots({knot1, knot2});
+ return data;
+}
+
+static TsTest_SplineData _SimpleInnerLoop()
+{
+ SData::Knot knot1;
+ knot1.time = 112.0;
+ knot1.nextSegInterpMethod = SData::InterpCurve;
+ knot1.value = 8.8;
+ knot1.postSlope = 15.0;
+ knot1.postLen = 0.9;
+
+ SData::Knot knot2;
+ knot2.time = 137.0;
+ knot2.nextSegInterpMethod = SData::InterpCurve;
+ knot2.value = 0.0;
+ knot2.preSlope = -5.3;
+ knot2.postSlope = -5.3;
+ knot2.preLen = 1.3;
+ knot2.postLen = 1.8;
+
+ SData::Knot knot3;
+ knot3.time = 145.0;
+ knot3.nextSegInterpMethod = SData::InterpCurve;
+ knot3.value = 8.5;
+ knot3.preSlope = 12.5;
+ knot3.postSlope = 12.5;
+ knot3.preLen = 1.0;
+ knot3.postLen = 1.2;
+
+ SData::Knot knot4;
+ knot4.time = 155.0;
+ knot4.nextSegInterpMethod = SData::InterpCurve;
+ knot4.value = 20.2;
+ knot4.preSlope = -15.7;
+ knot4.postSlope = -15.7;
+ knot4.preLen = 0.7;
+ knot4.postLen = 0.8;
+
+ SData::Knot knot5;
+ knot5.time = 181.0;
+ knot5.nextSegInterpMethod = SData::InterpCurve;
+ knot5.value = 38.2;
+ knot5.preSlope = -9.0;
+ knot5.preLen = 2.0;
+
+ SData::InnerLoopParams lp;
+ lp.enabled = true;
+ lp.protoStart = 137.0;
+ lp.protoEnd = 155.0;
+ lp.preLoopStart = 119.0;
+ lp.postLoopEnd = 173.0;
+ lp.valueOffset = 20.2;
+
+ SData data;
+ data.SetKnots({knot1, knot2, knot3, knot4, knot5});
+ data.SetInnerLoopParams(lp);
+ return data;
+}
+
+static TsTest_SplineData _Recurve()
+{
+ SData::Knot knot1;
+ knot1.time = 145.0;
+ knot1.nextSegInterpMethod = SData::InterpCurve;
+ knot1.value = -5.6;
+ knot1.postSlope = -1.3;
+ knot1.postLen = 3.8;
+
+ SData::Knot knot2;
+ knot2.time = 156.0;
+ knot2.nextSegInterpMethod = SData::InterpCurve;
+ knot2.value = 0.0;
+ knot2.preSlope = -1.3;
+ knot2.postSlope = -1.3;
+ knot2.preLen = 6.2;
+ knot2.postLen = 15.8;
+
+ SData::Knot knot3;
+ knot3.time = 167.0;
+ knot3.nextSegInterpMethod = SData::InterpCurve;
+ knot3.value = 28.8;
+ knot3.preSlope = 0.4;
+ knot3.postSlope = 0.4;
+ knot3.preLen = 16.8;
+ knot3.postLen = 6.0;
+
+ SData::Knot knot4;
+ knot4.time = 185.0;
+ knot4.nextSegInterpMethod = SData::InterpCurve;
+ knot4.value = 0.0;
+ knot4.preSlope = 3.6;
+ knot4.postLen = 5.0;
+
+ SData data;
+ data.SetKnots({knot1, knot2, knot3, knot4});
+ return data;
+}
+
+static TsTest_SplineData _Crossover()
+{
+ SData::Knot knot1;
+ knot1.time = 145.0;
+ knot1.nextSegInterpMethod = SData::InterpCurve;
+ knot1.value = -5.6;
+ knot1.postSlope = -1.3;
+ knot1.postLen = 3.8;
+
+ SData::Knot knot2;
+ knot2.time = 156.0;
+ knot2.nextSegInterpMethod = SData::InterpCurve;
+ knot2.value = 0.0;
+ knot2.preSlope = -1.3;
+ knot2.postSlope = -1.3;
+ knot2.preLen = 6.2;
+ knot2.postLen = 15.8;
+
+ SData::Knot knot3;
+ knot3.time = 167.0;
+ knot3.nextSegInterpMethod = SData::InterpCurve;
+ knot3.value = 28.8;
+ knot3.preSlope = 2.4;
+ knot3.postSlope = 2.4;
+ knot3.preLen = 21.7;
+ knot3.postLen = 5.5;
+
+ SData::Knot knot4;
+ knot4.time = 185.0;
+ knot4.nextSegInterpMethod = SData::InterpCurve;
+ knot4.value = 0.0;
+ knot4.preSlope = 3.6;
+ knot4.postLen = 5.0;
+
+ SData data;
+ data.SetKnots({knot1, knot2, knot3, knot4});
+ return data;
+}
+
+TsTest_SplineData
+TsTest_Museum::GetData(const DataId id)
+{
+ switch (id)
+ {
+ case TwoKnotBezier: return _TwoKnotBezier();
+ case TwoKnotLinear: return _TwoKnotLinear();
+ case SimpleInnerLoop: return _SimpleInnerLoop();
+ case Recurve: return _Recurve();
+ case Crossover: return _Crossover();
+ }
+
+ return {};
+}
+
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TS_TEST_MUSEUM_H
+#define PXR_BASE_TS_TS_TEST_MUSEUM_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+
+// A collection of museum exhibits. These are spline cases that can be used by
+// tests to exercise various behaviors.
+//
+class TS_API TsTest_Museum
+{
+public:
+ enum DataId
+ {
+ TwoKnotBezier,
+ TwoKnotLinear,
+ SimpleInnerLoop,
+ Recurve,
+ Crossover
+ };
+
+ static TsTest_SplineData GetData(DataId id);
+};
+
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_SampleBezier.h"
+#include "pxr/base/gf/vec2d.h"
+#include "pxr/base/gf/math.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using SData = TsTest_SplineData;
+
+// Obtain one sample between knot0 and knot1, at parameter value t.
+// Uses de Casteljau algorithm.
+//
+static TsTest_Sample
+_ComputeSample(
+ const SData::Knot &knot0,
+ const SData::Knot &knot1,
+ const double t)
+{
+ const GfVec2d p0(knot0.time, knot0.value);
+ const GfVec2d tan1(knot0.postLen, knot0.postSlope * knot0.postLen);
+ const GfVec2d p1 = p0 + tan1;
+ const GfVec2d p3(knot1.time, knot1.value);
+ const GfVec2d tan2(-knot1.preLen, -knot1.preSlope * knot1.preLen);
+ const GfVec2d p2 = p3 + tan2;
+
+ const GfVec2d lerp11 = GfLerp(t, p0, p1);
+ const GfVec2d lerp12 = GfLerp(t, p1, p2);
+ const GfVec2d lerp13 = GfLerp(t, p2, p3);
+
+ const GfVec2d lerp21 = GfLerp(t, lerp11, lerp12);
+ const GfVec2d lerp22 = GfLerp(t, lerp12, lerp13);
+
+ const GfVec2d lerp31 = GfLerp(t, lerp21, lerp22);
+
+ return TsTest_Sample(lerp31[0], lerp31[1]);
+}
+
+TsTest_SampleVec
+TsTest_SampleBezier(
+ const SData &splineData,
+ const int numSamples)
+{
+ if (splineData.GetRequiredFeatures() != SData::FeatureBezierSegments)
+ {
+ TF_CODING_ERROR("SampleBezier supports only plain Beziers");
+ return {};
+ }
+
+ const SData::KnotSet &knots = splineData.GetKnots();
+ if (knots.size() < 2)
+ {
+ TF_CODING_ERROR("SampleBezier requires at least two keyframes");
+ return {};
+ }
+
+ // Divide samples equally among segments. Determine increment of 't'
+ // (parameter value on [0, 1]) per sample.
+ const int samplesPerSegment = numSamples / knots.size();
+ const double tPerSample = 1.0 / (samplesPerSegment + 1);
+
+ TsTest_SampleVec result;
+
+ // Process each segment.
+ for (auto knotIt = knots.begin(), knotNextIt = knotIt;
+ ++knotNextIt != knots.end(); knotIt++)
+ {
+ // Divide segment into samples.
+ for (int j = 0; j < samplesPerSegment; j++)
+ {
+ // Sample at this 't' value.
+ const double t = tPerSample * j;
+ result.push_back(_ComputeSample(*knotIt, *knotNextIt, t));
+ }
+ }
+
+ // Add one sample at the end of the last segment.
+ const SData::Knot &lastKnot = *knots.rbegin();
+ result.push_back(TsTest_Sample(lastKnot.time, lastKnot.value));
+
+ return result;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TS_TEST_SAMPLE_BEZIER_H
+#define PXR_BASE_TS_TS_TEST_SAMPLE_BEZIER_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+#include "pxr/base/ts/tsTest_Types.h"
+
+#include <vector>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+// Produces (time, value) samples along a Bezier curve by walking the 't'
+// parameter space. The samples are evenly divided among the segments, and then
+// uniformly in the 't' parameter for each segment. Samples do not necessarily
+// always go forward in time; Bezier segments may form loops that temporarily
+// reverse direction.
+//
+// Only Bezier segments are supported. No extrapolation is performed.
+//
+TS_API
+TsTest_SampleVec
+TsTest_SampleBezier(
+ const TsTest_SplineData &splineData,
+ int numSamples);
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_SampleTimes.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+#include "pxr/base/tf/diagnostic.h"
+
+#include <algorithm>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using SData = TsTest_SplineData;
+
+////////////////////////////////////////////////////////////////////////////////
+
+TsTest_SampleTimes::SampleTime::SampleTime() = default;
+
+TsTest_SampleTimes::SampleTime::SampleTime(
+ double timeIn)
+ : time(timeIn) {}
+
+TsTest_SampleTimes::SampleTime::SampleTime(
+ double timeIn, bool preIn)
+ : time(timeIn), pre(preIn) {}
+
+TsTest_SampleTimes::SampleTime::SampleTime(
+ const SampleTime &other) = default;
+
+TsTest_SampleTimes::SampleTime&
+TsTest_SampleTimes::SampleTime::operator=(
+ const SampleTime &other) = default;
+
+TsTest_SampleTimes::SampleTime&
+TsTest_SampleTimes::SampleTime::operator=(
+ double timeIn)
+{
+ *this = SampleTime(timeIn);
+ return *this;
+}
+
+bool TsTest_SampleTimes::SampleTime::operator<(
+ const SampleTime &other) const
+{
+ return time < other.time
+ || (time == other.time && pre && !other.pre);
+}
+
+bool TsTest_SampleTimes::SampleTime::operator==(
+ const SampleTime &other) const
+{
+ return time == other.time && pre == other.pre;
+}
+
+bool TsTest_SampleTimes::SampleTime::operator!=(
+ const SampleTime &other) const
+{
+ return !(*this == other);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TsTest_SampleTimes::SampleTimeSet
+TsTest_SampleTimes::_GetKnotTimes() const
+{
+ SampleTimeSet result;
+
+ // Examine all knots.
+ bool held = false;
+ for (const SData::Knot &knot : _splineData.GetKnots())
+ {
+ if (held || knot.isDualValued)
+ result.insert(SampleTime(knot.time, /* pre = */ true));
+
+ result.insert(SampleTime(knot.time));
+
+ held = (knot.nextSegInterpMethod == SData::InterpHeld);
+ }
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TsTest_SampleTimes::TsTest_SampleTimes()
+ : _haveSplineData(false)
+{
+}
+
+TsTest_SampleTimes::TsTest_SampleTimes(
+ const TsTest_SplineData &splineData)
+ : _haveSplineData(true),
+ _splineData(splineData)
+{
+}
+
+void TsTest_SampleTimes::AddTimes(
+ const std::vector<double> ×)
+{
+ for (const double time : times)
+ _times.insert(SampleTime(time));
+}
+
+void TsTest_SampleTimes::AddTimes(
+ const std::vector<SampleTime> ×)
+{
+ _times.insert(times.begin(), times.end());
+}
+
+void TsTest_SampleTimes::AddKnotTimes()
+{
+ if (!_haveSplineData)
+ {
+ TF_CODING_ERROR("AddKnotTimes: no spline data");
+ return;
+ }
+
+ const SampleTimeSet knotTimes = _GetKnotTimes();
+ _times.insert(knotTimes.begin(), knotTimes.end());
+}
+
+void TsTest_SampleTimes::AddUniformInterpolationTimes(
+ const int numSamples)
+{
+ if (!_haveSplineData)
+ {
+ TF_CODING_ERROR("AddUniformInterpolationTimes: no spline data");
+ return;
+ }
+
+ if (numSamples < 1)
+ {
+ TF_CODING_ERROR("AddUniformInterpolationTimes: Too few samples");
+ return;
+ }
+
+ const SampleTimeSet knotTimes = _GetKnotTimes();
+ if (knotTimes.size() < 2)
+ {
+ TF_CODING_ERROR("AddUniformInterpolationTimes: Too few knots");
+ return;
+ }
+
+ const double firstTime = knotTimes.begin()->time;
+ const double lastTime = knotTimes.rbegin()->time;
+ const double knotRange = lastTime - firstTime;
+ const double step = knotRange / (numSamples + 1);
+
+ for (int i = 0; i < numSamples - 1; i++)
+ _times.insert(SampleTime(firstTime + i * step));
+}
+
+void TsTest_SampleTimes::AddExtrapolationTimes(
+ const double extrapolationFactor)
+{
+ if (!_haveSplineData)
+ {
+ TF_CODING_ERROR("AddExtrapolationTimes: no spline data");
+ return;
+ }
+
+ if (extrapolationFactor <= 0.0)
+ {
+ TF_CODING_ERROR("AddExtrapolationTimes: invalid factor");
+ return;
+ }
+
+ const SampleTimeSet knotTimes = _GetKnotTimes();
+ if (knotTimes.size() < 2)
+ {
+ TF_CODING_ERROR("AddExtrapolationTimes: too few knots");
+ return;
+ }
+
+ if (_splineData.GetPreExtrapolation().method == SData::ExtrapLoop
+ || _splineData.GetPostExtrapolation().method == SData::ExtrapLoop)
+ {
+ // This technique is too simplistic for extrapolating loops. These
+ // should be baked out by clients before passing spline data.
+ TF_CODING_ERROR("AddExtrapolationTimes: extrapolating loops");
+ return;
+ }
+
+ const double firstTime = knotTimes.begin()->time;
+ const double lastTime = knotTimes.rbegin()->time;
+ const double knotRange = lastTime - firstTime;
+ const double extrap = extrapolationFactor * knotRange;
+
+ _times.insert(SampleTime(firstTime - extrap));
+ _times.insert(SampleTime(lastTime + extrap));
+}
+
+void TsTest_SampleTimes::AddStandardTimes()
+{
+ AddKnotTimes();
+ AddUniformInterpolationTimes(200);
+ AddExtrapolationTimes(0.2);
+}
+
+const TsTest_SampleTimes::SampleTimeSet&
+TsTest_SampleTimes::GetTimes() const
+{
+ return _times;
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TS_TEST_SAMPLE_TIMES_H
+#define PXR_BASE_TS_TS_TEST_SAMPLE_TIMES_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+
+#include <vector>
+#include <set>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+class TsTest_SampleTimes
+{
+public:
+ // A time at which to perform evaluation. Typically just a time, but can
+ // also be a "pre" time, which at a dual-valued knot can differ from the
+ // ordinary value.
+ struct TS_API SampleTime
+ {
+ double time = 0.0;
+ bool pre = false;
+
+ public:
+ SampleTime();
+ SampleTime(double time);
+ SampleTime(double time, bool pre);
+ SampleTime(const SampleTime &other);
+ SampleTime& operator=(const SampleTime &other);
+ SampleTime& operator=(double time);
+ bool operator<(const SampleTime &other) const;
+ bool operator==(const SampleTime &other) const;
+ bool operator!=(const SampleTime &other) const;
+ };
+
+ using SampleTimeSet = std::set<SampleTime>;
+
+public:
+ // DIRECT SPECIFICATION
+
+ // Constructs a SampleTimes object for direct specification of times.
+ TS_API
+ TsTest_SampleTimes();
+
+ // Adds the specified times.
+ TS_API
+ void AddTimes(
+ const std::vector<double> ×);
+
+ // Adds the specified times.
+ TS_API
+ void AddTimes(
+ const std::vector<SampleTime> ×);
+
+ // SPLINE-DRIVEN
+
+ // Constructs a SampleTimes object for specification of times based on the
+ // contents of splineData.
+ TS_API
+ TsTest_SampleTimes(
+ const TsTest_SplineData &splineData);
+
+ // Adds a time for each knot in splineData. For dual-valued knots, adds
+ // both a pre-time and an ordinary time.
+ TS_API
+ void AddKnotTimes();
+
+ // Adds evenly spaced sample times within the frame range of splineData.
+ // The first sample is after the first knot, and the last sample is before
+ // the last knot.
+ TS_API
+ void AddUniformInterpolationTimes(
+ int numSamples);
+
+ // Determines the time range of the knots in splineData, extends it by
+ // extrapolationFactor on each end, and adds one pre-extrapolating and one
+ // post-extrapolating sample. For example, with a time range of 10, and an
+ // extrapolationFactor of 0.25, samples will be added 2.5 time units before
+ // the first knot and 2.5 time units after the last.
+ TS_API
+ void AddExtrapolationTimes(
+ double extrapolationFactor);
+
+ // MACRO
+
+ // Calls AddKnotTimes(), AddUniformInterpolationTimes(200), and
+ // AddExtrapolationTimes(0.2).
+ TS_API
+ void AddStandardTimes();
+
+ // ACCESSORS
+
+ // Returns the set of sample times.
+ TS_API
+ const SampleTimeSet&
+ GetTimes() const;
+
+private:
+ SampleTimeSet _GetKnotTimes() const;
+
+private:
+ const bool _haveSplineData;
+ const TsTest_SplineData _splineData;
+
+ SampleTimeSet _times;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+
+#include "pxr/base/gf/interval.h"
+#include "pxr/base/gf/math.h"
+#include "pxr/base/tf/diagnostic.h"
+#include "pxr/base/tf/enum.h"
+#include "pxr/base/tf/registryManager.h"
+
+#include <algorithm>
+#include <sstream>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(TsTest_SplineData::InterpHeld);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::InterpLinear);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::InterpCurve);
+
+ TF_ADD_ENUM_NAME(TsTest_SplineData::ExtrapHeld);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::ExtrapLinear);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::ExtrapSloped);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::ExtrapLoop);
+
+ TF_ADD_ENUM_NAME(TsTest_SplineData::LoopNone);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::LoopContinue);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::LoopRepeat);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::LoopReset);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::LoopOscillate);
+
+ TF_ADD_ENUM_NAME(TsTest_SplineData::FeatureHeldSegments);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::FeatureLinearSegments);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::FeatureBezierSegments);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::FeatureHermiteSegments);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::FeatureDualValuedKnots);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::FeatureInnerLoops);
+ TF_ADD_ENUM_NAME(TsTest_SplineData::FeatureExtrapolatingLoops);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TsTest_SplineData::Knot::Knot() = default;
+
+TsTest_SplineData::Knot::Knot(
+ const Knot &other) = default;
+
+TsTest_SplineData::Knot&
+TsTest_SplineData::Knot::operator=(
+ const Knot &other) = default;
+
+bool TsTest_SplineData::Knot::operator==(
+ const Knot &other) const
+{
+ return time == other.time
+ && nextSegInterpMethod == other.nextSegInterpMethod
+ && value == other.value
+ && isDualValued == other.isDualValued
+ && preValue == other.preValue
+ && preSlope == other.preSlope
+ && postSlope == other.postSlope
+ && preLen == other.preLen
+ && postLen == other.postLen
+ && preAuto == other.preAuto
+ && postAuto == other.postAuto;
+}
+
+bool TsTest_SplineData::Knot::operator!=(
+ const Knot &other) const
+{
+ return !(*this == other);
+}
+
+bool TsTest_SplineData::Knot::operator<(
+ const Knot &other) const
+{
+ return time < other.time;
+}
+
+TsTest_SplineData::InnerLoopParams::InnerLoopParams() = default;
+
+TsTest_SplineData::InnerLoopParams::InnerLoopParams(
+ const InnerLoopParams &other) = default;
+
+TsTest_SplineData::InnerLoopParams&
+TsTest_SplineData::InnerLoopParams::operator=(
+ const InnerLoopParams &other) = default;
+
+bool TsTest_SplineData::InnerLoopParams::operator==(
+ const InnerLoopParams &other) const
+{
+ return enabled == other.enabled
+ && protoStart == other.protoStart
+ && protoEnd == other.protoEnd
+ && preLoopStart == other.preLoopStart
+ && postLoopEnd == other.postLoopEnd
+ && closedEnd == other.closedEnd
+ && valueOffset == other.valueOffset;
+}
+
+bool TsTest_SplineData::InnerLoopParams::operator!=(
+ const InnerLoopParams &other) const
+{
+ return !(*this == other);
+}
+
+bool TsTest_SplineData::InnerLoopParams::IsValid() const
+{
+ if (!enabled) return true;
+
+ if (protoEnd <= protoStart) return false;
+ if (preLoopStart > protoStart) return false;
+ if (postLoopEnd < protoEnd) return false;
+
+ return true;
+}
+
+TsTest_SplineData::Extrapolation::Extrapolation() = default;
+
+TsTest_SplineData::Extrapolation::Extrapolation(
+ const TsTest_SplineData::ExtrapMethod methodIn)
+ : method(methodIn) {}
+
+TsTest_SplineData::Extrapolation::Extrapolation(
+ const Extrapolation &other) = default;
+
+TsTest_SplineData::Extrapolation&
+TsTest_SplineData::Extrapolation::operator=(
+ const Extrapolation &other) = default;
+
+bool TsTest_SplineData::Extrapolation::operator==(
+ const Extrapolation &other) const
+{
+ return method == other.method
+ && (method != ExtrapSloped
+ || slope == other.slope)
+ && (method != ExtrapLoop
+ || loopMode == other.loopMode);
+}
+
+bool TsTest_SplineData::Extrapolation::operator!=(
+ const Extrapolation &other) const
+{
+ return !(*this == other);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+TsTest_SplineData::TsTest_SplineData() = default;
+
+TsTest_SplineData::TsTest_SplineData(
+ const TsTest_SplineData &other) = default;
+
+TsTest_SplineData&
+TsTest_SplineData::operator=(
+ const TsTest_SplineData &other) = default;
+
+bool TsTest_SplineData::operator==(
+ const TsTest_SplineData &other) const
+{
+ return _isHermite == other._isHermite
+ && _knots == other._knots
+ && _preExtrap == other._preExtrap
+ && _postExtrap == other._postExtrap
+ && _innerLoopParams == other._innerLoopParams;
+}
+
+bool TsTest_SplineData::operator!=(
+ const TsTest_SplineData &other) const
+{
+ return !(*this == other);
+}
+
+void TsTest_SplineData::SetIsHermite(
+ const bool hermite)
+{
+ _isHermite = hermite;
+}
+
+void TsTest_SplineData::AddKnot(
+ const Knot &knot)
+{
+ _knots.erase(knot);
+ _knots.insert(knot);
+}
+
+void TsTest_SplineData::SetKnots(
+ const KnotSet &knots)
+{
+ _knots = knots;
+}
+
+void TsTest_SplineData::SetPreExtrapolation(
+ const Extrapolation &preExtrap)
+{
+ _preExtrap = preExtrap;
+}
+
+void TsTest_SplineData::SetPostExtrapolation(
+ const Extrapolation &postExtrap)
+{
+ _postExtrap = postExtrap;
+}
+
+void TsTest_SplineData::SetInnerLoopParams(
+ const InnerLoopParams ¶ms)
+{
+ _innerLoopParams = params;
+}
+
+bool TsTest_SplineData::GetIsHermite() const
+{
+ return _isHermite;
+}
+
+const TsTest_SplineData::KnotSet&
+TsTest_SplineData::GetKnots() const
+{
+ return _knots;
+}
+
+const TsTest_SplineData::Extrapolation&
+TsTest_SplineData::GetPreExtrapolation() const
+{
+ return _preExtrap;
+}
+
+const TsTest_SplineData::Extrapolation&
+TsTest_SplineData::GetPostExtrapolation() const
+{
+ return _postExtrap;
+}
+
+const TsTest_SplineData::InnerLoopParams&
+TsTest_SplineData::GetInnerLoopParams() const
+{
+ return _innerLoopParams;
+}
+
+TsTest_SplineData::Features
+TsTest_SplineData::GetRequiredFeatures() const
+{
+ Features result = 0;
+
+ for (const Knot &knot : _knots)
+ {
+ switch (knot.nextSegInterpMethod)
+ {
+ case InterpHeld: result |= FeatureHeldSegments; break;
+ case InterpLinear: result |= FeatureLinearSegments; break;
+ case InterpCurve:
+ result |= (_isHermite ?
+ FeatureHermiteSegments : FeatureBezierSegments); break;
+ }
+
+ if (knot.isDualValued)
+ result |= FeatureDualValuedKnots;
+
+ if (knot.preAuto || knot.postAuto)
+ result |= FeatureAutoTangents;
+ }
+
+ if (_innerLoopParams.enabled)
+ result |= FeatureInnerLoops;
+
+ if (_preExtrap.method == ExtrapSloped
+ || _postExtrap.method == ExtrapSloped)
+ {
+ result |= FeatureExtrapolatingSlopes;
+ }
+
+ if (_preExtrap.method == ExtrapLoop
+ || _postExtrap.method == ExtrapLoop)
+ {
+ result |= FeatureExtrapolatingLoops;
+ }
+
+ return result;
+}
+
+static std::string _GetExtrapDesc(
+ const TsTest_SplineData::Extrapolation &e)
+{
+ std::ostringstream ss;
+
+ ss << TfEnum::GetName(e.method).substr(6);
+
+ if (e.method == TsTest_SplineData::ExtrapSloped)
+ ss << " " << e.slope;
+ else if (e.method == TsTest_SplineData::ExtrapLoop)
+ ss << " " << TfEnum::GetName(e.loopMode).substr(4);
+
+ return ss.str();
+}
+
+std::string
+TsTest_SplineData::GetDebugDescription() const
+{
+ std::ostringstream ss;
+
+ ss << "Spline:" << std::endl
+ << " hermite " << (_isHermite ? "true" : "false") << std::endl
+ << " preExtrap " << _GetExtrapDesc(_preExtrap) << std::endl
+ << " postExtrap " << _GetExtrapDesc(_postExtrap) << std::endl;
+
+ if (_innerLoopParams.enabled)
+ {
+ ss << "Loop:" << std::endl
+ << " start " << _innerLoopParams.protoStart
+ << ", end " << _innerLoopParams.protoEnd
+ << ", preStart " << _innerLoopParams.preLoopStart
+ << ", postEnd " << _innerLoopParams.postLoopEnd
+ << ", closed " << _innerLoopParams.closedEnd
+ << ", offset " << _innerLoopParams.valueOffset
+ << std::endl;
+ }
+
+ ss << "Knots:" << std::endl;
+ for (const Knot &knot : _knots)
+ {
+ ss << " " << knot.time << ": "
+ << knot.value
+ << ", " << TfEnum::GetName(knot.nextSegInterpMethod).substr(6);
+
+ if (knot.nextSegInterpMethod == InterpCurve)
+ {
+ ss << ", preSlope " << knot.preSlope
+ << ", postSlope " << knot.postSlope;
+
+ if (!_isHermite)
+ {
+ ss << ", preLen " << knot.preLen
+ << ", postLen " << knot.postLen;
+ }
+
+ ss << ", auto " << (knot.preAuto ? "true" : "false")
+ << " / " << (knot.postAuto ? "true" : "false");
+ }
+
+ ss << std::endl;
+ }
+
+ return ss.str();
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TS_TEST_SPLINE_DATA_H
+#define PXR_BASE_TS_TS_TEST_SPLINE_DATA_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+
+#include <string>
+#include <set>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+// A generic way of encoding spline control parameters. Allows us to pass the
+// same data to different backends (Ts, mayapy, etc) for evaluation.
+//
+class TsTest_SplineData
+{
+public:
+ // Interpolation method for a spline segment.
+ enum InterpMethod
+ {
+ InterpHeld,
+ InterpLinear,
+ InterpCurve
+ };
+
+ // Extrapolation method for the ends of a spline beyond the knots.
+ enum ExtrapMethod
+ {
+ ExtrapHeld,
+ ExtrapLinear,
+ ExtrapSloped,
+ ExtrapLoop
+ };
+
+ // Looping modes.
+ enum LoopMode
+ {
+ LoopNone,
+ LoopContinue, // Used by inner loops. Copy whole knots.
+ LoopRepeat, // Used by extrap loops. Repeat with offset.
+ LoopReset, // Used by extrap loops. Repeat identically.
+ LoopOscillate // Used by extrap loops. Alternate forward / reverse.
+ };
+
+ // Features that may be required by splines.
+ enum Feature
+ {
+ FeatureHeldSegments = 1 << 0,
+ FeatureLinearSegments = 1 << 1,
+ FeatureBezierSegments = 1 << 2,
+ FeatureHermiteSegments = 1 << 3,
+ FeatureAutoTangents = 1 << 4,
+ FeatureDualValuedKnots = 1 << 5,
+ FeatureInnerLoops = 1 << 6,
+ FeatureExtrapolatingLoops = 1 << 7,
+ FeatureExtrapolatingSlopes = 1 << 8
+ };
+ using Features = unsigned int;
+
+ // One knot in a spline.
+ struct TS_API Knot
+ {
+ double time = 0;
+ InterpMethod nextSegInterpMethod = InterpHeld;
+ double value = 0;
+ bool isDualValued = false;
+ double preValue = 0;
+ double preSlope = 0;
+ double postSlope = 0;
+ double preLen = 0;
+ double postLen = 0;
+ bool preAuto = false;
+ bool postAuto = false;
+
+ public:
+ Knot();
+ Knot(
+ const Knot &other);
+ Knot& operator=(
+ const Knot &other);
+ bool operator==(
+ const Knot &other) const;
+ bool operator!=(
+ const Knot &other) const;
+ bool operator<(
+ const Knot &other) const;
+ };
+ using KnotSet = std::set<Knot>;
+
+ // Inner-loop parameters.
+ //
+ // The pre-looping interval is times [preLoopStart, protoStart).
+ // The prototype interval is times [protoStart, protoEnd).
+ // The post-looping interval is times [protoEnd, postLoopEnd],
+ // or, if closedEnd is false, the same interval, but open at the end.
+ // To decline pre-looping or post-looping, make that interval empty.
+ //
+ // The value offset specifies the difference between the value at the starts
+ // of consecutive iterations.
+ //
+ // It is common, but not required, to use a subset of functionality:
+ // - Knots at the start and end of the prototype interval
+ // - Whole numbers of loop iterations
+ // (sizes of looping intervals are multiples of size of proto interval)
+ // - Value offset initially set to original value difference
+ // between ends of prototype interval
+ // - closedEnd true
+ //
+ // A knot exactly at the end of the prototype interval is not part of the
+ // prototype. If there is post-looping, a knot at the end of the prototype
+ // interval is overwritten by a copy of the knot from the start of the
+ // prototype interval.
+ //
+ // Enabling inner looping can change the shape of the prototype interval
+ // (and thus all looped copies), because the first knot is echoed as the
+ // last. Inner looping does not aim to make copies of an existing shape; it
+ // aims to set up for continuity at loop joins.
+ //
+ // If closedEnd is true, and there is a whole number of post-iterations, and
+ // there is a knot at the prototype start time, then a final copy of the
+ // first prototype knot will be echoed at the end of the last
+ // post-iteration.
+ //
+ struct TS_API InnerLoopParams
+ {
+ bool enabled = false;
+ double protoStart = 0;
+ double protoEnd = 0;
+ double preLoopStart = 0;
+ double postLoopEnd = 0;
+ bool closedEnd = true;
+ double valueOffset = 0;
+
+ public:
+ InnerLoopParams();
+ InnerLoopParams(
+ const InnerLoopParams &other);
+ InnerLoopParams& operator=(
+ const InnerLoopParams &other);
+ bool operator==(
+ const InnerLoopParams &other) const;
+ bool operator!=(
+ const InnerLoopParams &other) const;
+
+ bool IsValid() const;
+ };
+
+ // Extrapolation parameters for the ends of a spline beyond the knots.
+ struct TS_API Extrapolation
+ {
+ ExtrapMethod method = ExtrapHeld;
+ double slope = 0;
+ LoopMode loopMode = LoopNone;
+
+ public:
+ Extrapolation();
+ Extrapolation(ExtrapMethod method);
+ Extrapolation(
+ const Extrapolation &other);
+ Extrapolation& operator=(
+ const Extrapolation &other);
+ bool operator==(
+ const Extrapolation &other) const;
+ bool operator!=(
+ const Extrapolation &other) const;
+ };
+
+public:
+ TS_API
+ TsTest_SplineData();
+
+ TS_API
+ TsTest_SplineData(
+ const TsTest_SplineData &other);
+
+ TS_API
+ TsTest_SplineData&
+ operator=(
+ const TsTest_SplineData &other);
+
+ TS_API
+ bool operator==(
+ const TsTest_SplineData &other) const;
+
+ TS_API
+ bool operator!=(
+ const TsTest_SplineData &other) const;
+
+ TS_API
+ void SetIsHermite(bool hermite);
+
+ TS_API
+ void AddKnot(
+ const Knot &knot);
+
+ TS_API
+ void SetKnots(
+ const KnotSet &knots);
+
+ TS_API
+ void SetPreExtrapolation(
+ const Extrapolation &preExtrap);
+
+ TS_API
+ void SetPostExtrapolation(
+ const Extrapolation &postExtrap);
+
+ TS_API
+ void SetInnerLoopParams(
+ const InnerLoopParams ¶ms);
+
+ TS_API
+ bool GetIsHermite() const;
+
+ TS_API
+ const KnotSet&
+ GetKnots() const;
+
+ TS_API
+ const Extrapolation&
+ GetPreExtrapolation() const;
+
+ TS_API
+ const Extrapolation&
+ GetPostExtrapolation() const;
+
+ TS_API
+ const InnerLoopParams&
+ GetInnerLoopParams() const;
+
+ TS_API
+ Features GetRequiredFeatures() const;
+
+ TS_API
+ std::string GetDebugDescription() const;
+
+private:
+ bool _isHermite = false;
+ KnotSet _knots;
+ Extrapolation _preExtrap;
+ Extrapolation _postExtrap;
+ InnerLoopParams _innerLoopParams;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_TsEvaluator.h"
+
+#include "pxr/base/ts/spline.h"
+
+#include "pxr/base/tf/diagnostic.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+using SData = TsTest_SplineData;
+using STimes = TsTest_SampleTimes;
+
+static TsSpline _ConvertToTsSpline(
+ const SData &data)
+{
+ const SData::Features features = data.GetRequiredFeatures();
+ if ((features & SData::FeatureHermiteSegments)
+ || (features & SData::FeatureExtrapolatingLoops)
+ || (features & SData::FeatureAutoTangents))
+ {
+ TF_CODING_ERROR("Unsupported spline features");
+ return TsSpline();
+ }
+
+ const SData::KnotSet &dataKnots = data.GetKnots();
+
+ if (data.GetPreExtrapolation().method == SData::ExtrapSloped
+ && !dataKnots.empty()
+ && dataKnots.begin()->nextSegInterpMethod != SData::InterpCurve)
+ {
+ TF_CODING_ERROR("Unsupported pre-slope");
+ return TsSpline();
+ }
+
+ if (data.GetPostExtrapolation().method == SData::ExtrapSloped
+ && !dataKnots.empty()
+ && dataKnots.rbegin()->nextSegInterpMethod != SData::InterpCurve)
+ {
+ TF_CODING_ERROR("Unsupported post-slope");
+ return TsSpline();
+ }
+
+ TsSpline spline;
+
+ const TsExtrapolationType leftExtrap =
+ (data.GetPreExtrapolation().method == SData::ExtrapHeld ?
+ TsExtrapolationHeld : TsExtrapolationLinear);
+ const TsExtrapolationType rightExtrap =
+ (data.GetPostExtrapolation().method == SData::ExtrapHeld ?
+ TsExtrapolationHeld : TsExtrapolationLinear);
+ spline.SetExtrapolation(leftExtrap, rightExtrap);
+
+ for (const SData::Knot &dataKnot : dataKnots)
+ {
+ TsKeyFrame knot;
+
+ knot.SetTime(dataKnot.time);
+ knot.SetValue(VtValue(dataKnot.value));
+ knot.SetLeftTangentSlope(VtValue(dataKnot.preSlope));
+ knot.SetRightTangentSlope(VtValue(dataKnot.postSlope));
+ knot.SetLeftTangentLength(dataKnot.preLen);
+ knot.SetRightTangentLength(dataKnot.postLen);
+
+ switch (dataKnot.nextSegInterpMethod)
+ {
+ case SData::InterpHeld: knot.SetKnotType(TsKnotHeld); break;
+ case SData::InterpLinear: knot.SetKnotType(TsKnotLinear); break;
+ case SData::InterpCurve: knot.SetKnotType(TsKnotBezier); break;
+ default: TF_CODING_ERROR("Unexpected knot type");
+ }
+
+ if (dataKnot.isDualValued)
+ {
+ knot.SetIsDualValued(true);
+ knot.SetValue(VtValue(dataKnot.preValue), TsLeft);
+ }
+
+ spline.SetKeyFrame(knot);
+ }
+
+ if (data.GetPreExtrapolation().method == SData::ExtrapLinear
+ && !dataKnots.empty()
+ && dataKnots.begin()->nextSegInterpMethod == SData::InterpCurve)
+ {
+ TsKeyFrame knot = *(spline.begin());
+ knot.SetLeftTangentSlope(knot.GetRightTangentSlope());
+ knot.SetLeftTangentLength(1.0);
+ spline.SetKeyFrame(knot);
+ }
+ else if (data.GetPreExtrapolation().method == SData::ExtrapSloped
+ && !dataKnots.empty())
+ {
+ TsKeyFrame knot = *(spline.begin());
+ knot.SetLeftTangentSlope(VtValue(data.GetPreExtrapolation().slope));
+ knot.SetLeftTangentLength(1.0);
+ spline.SetKeyFrame(knot);
+ }
+
+ if (data.GetPostExtrapolation().method == SData::ExtrapLinear
+ && !dataKnots.empty()
+ && dataKnots.rbegin()->nextSegInterpMethod == SData::InterpCurve)
+ {
+ TsKeyFrame knot = *(spline.rbegin());
+ knot.SetRightTangentSlope(knot.GetLeftTangentSlope());
+ knot.SetRightTangentLength(1.0);
+ spline.SetKeyFrame(knot);
+ }
+ else if (data.GetPostExtrapolation().method == SData::ExtrapSloped
+ && !dataKnots.empty())
+ {
+ TsKeyFrame knot = *(spline.rbegin());
+ knot.SetRightTangentSlope(VtValue(data.GetPostExtrapolation().slope));
+ knot.SetRightTangentLength(1.0);
+ spline.SetKeyFrame(knot);
+ }
+
+ const SData::InnerLoopParams &loop = data.GetInnerLoopParams();
+ if (loop.enabled)
+ {
+ // XXX: account for TsLoopParams not yet having the closedEnd feature
+ const double postLenAdj =
+ (loop.closedEnd && loop.postLoopEnd > loop.protoEnd ? 1 : 0);
+
+ spline.SetLoopParams(
+ TsLoopParams(
+ true,
+ loop.protoStart,
+ loop.protoEnd - loop.protoStart,
+ loop.protoStart - loop.preLoopStart,
+ loop.postLoopEnd - loop.protoEnd + postLenAdj,
+ loop.valueOffset));
+ }
+
+ return spline;
+}
+
+static SData _ConvertToSplineData(
+ const TsSpline &spline)
+{
+ SData result;
+
+ auto extrapPair = spline.GetExtrapolation();
+ result.SetPreExtrapolation(
+ extrapPair.first == TsExtrapolationHeld ?
+ SData::ExtrapHeld : SData::ExtrapLinear);
+ result.SetPostExtrapolation(
+ extrapPair.second == TsExtrapolationHeld ?
+ SData::ExtrapHeld : SData::ExtrapLinear);
+
+ for (const TsKeyFrame &knot : spline)
+ {
+ SData::Knot dataKnot;
+
+ dataKnot.time = knot.GetTime();
+ dataKnot.value = knot.GetValue().Get<double>();
+ dataKnot.preSlope = knot.GetLeftTangentSlope().Get<double>();
+ dataKnot.postSlope = knot.GetRightTangentSlope().Get<double>();
+ dataKnot.preLen = knot.GetLeftTangentLength();
+ dataKnot.postLen = knot.GetRightTangentLength();
+
+ switch (knot.GetKnotType())
+ {
+ case TsKnotHeld:
+ dataKnot.nextSegInterpMethod = SData::InterpHeld; break;
+ case TsKnotLinear:
+ dataKnot.nextSegInterpMethod = SData::InterpLinear; break;
+ case TsKnotBezier:
+ dataKnot.nextSegInterpMethod = SData::InterpCurve; break;
+ default: TF_CODING_ERROR("Unexpected knot type");
+ }
+
+ if (knot.GetIsDualValued())
+ {
+ dataKnot.isDualValued = true;
+ dataKnot.preValue = knot.GetLeftValue().Get<double>();
+ }
+
+ result.AddKnot(dataKnot);
+ }
+
+ return result;
+}
+
+TsTest_SampleVec
+TsTest_TsEvaluator::Eval(
+ const SData &splineData,
+ const STimes &sampleTimes) const
+{
+ const TsSpline spline = _ConvertToTsSpline(splineData);
+ if (spline.empty())
+ return {};
+
+ TsTest_SampleVec result;
+
+ for (const STimes::SampleTime time : sampleTimes.GetTimes())
+ {
+ const TsSide side = (time.pre ? TsLeft : TsRight);
+ result.push_back(
+ TsTest_Sample(
+ time.time,
+ spline.Eval(time.time, side).Get<double>()));
+ }
+
+ return result;
+}
+
+TsTest_SampleVec
+TsTest_TsEvaluator::Sample(
+ const SData &splineData,
+ const double tolerance) const
+{
+ const TsSpline spline = _ConvertToTsSpline(splineData);
+ if (spline.empty())
+ return {};
+
+ if (spline.size() < 2)
+ return {};
+
+ const TsSamples samples =
+ spline.Sample(
+ spline.begin()->GetTime(),
+ spline.rbegin()->GetTime(),
+ /* timeScale = */ 1, /* valueScale = */ 1,
+ /* tolerance = */ 1e-6);
+
+ TsTest_SampleVec result;
+
+ for (const TsValueSample &sample : samples)
+ {
+ // XXX: is this a correct interpretation?
+ result.push_back(
+ TsTest_Sample(
+ sample.leftTime,
+ sample.leftValue.Get<double>()));
+ }
+
+ return result;
+}
+
+TsTest_SplineData
+TsTest_TsEvaluator::BakeInnerLoops(
+ const SData &splineData) const
+{
+ if (!splineData.GetInnerLoopParams().enabled)
+ return splineData;
+
+ TsSpline spline = _ConvertToTsSpline(splineData);
+ spline.BakeSplineLoops();
+ return _ConvertToSplineData(spline);
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TS_TEST_TS_EVALUATOR_H
+#define PXR_BASE_TS_TS_TEST_TS_EVALUATOR_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+#include "pxr/base/ts/tsTest_Evaluator.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+// Perform test evaluation using Ts.
+//
+class TS_API TsTest_TsEvaluator : public TsTest_Evaluator
+{
+public:
+ TsTest_SampleVec Eval(
+ const TsTest_SplineData &splineData,
+ const TsTest_SampleTimes &sampleTimes) const override;
+
+ TsTest_SampleVec Sample(
+ const TsTest_SplineData &splineData,
+ double tolerance) const override;
+
+ TsTest_SplineData BakeInnerLoops(
+ const TsTest_SplineData &splineData) const override;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_Types.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TsTest_Sample::TsTest_Sample() = default;
+
+TsTest_Sample::TsTest_Sample(
+ double timeIn, double valueIn)
+ : time(timeIn), value(valueIn) {}
+
+TsTest_Sample::TsTest_Sample(
+ const TsTest_Sample &other) = default;
+
+TsTest_Sample&
+TsTest_Sample::operator=(
+ const TsTest_Sample &other) = default;
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TS_TEST_TYPES_H
+#define PXR_BASE_TS_TS_TEST_TYPES_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+
+#include <vector>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+struct TS_API TsTest_Sample
+{
+ double time = 0;
+ double value = 0;
+
+public:
+ TsTest_Sample();
+ TsTest_Sample(double time, double value);
+ TsTest_Sample(const TsTest_Sample &other);
+ TsTest_Sample& operator=(const TsTest_Sample &other);
+};
+
+using TsTest_SampleVec = std::vector<TsTest_Sample>;
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/typeRegistry.h"
+#include "pxr/base/ts/data.h"
+
+#include "pxr/base/tf/instantiateSingleton.h"
+#include "pxr/base/tf/token.h"
+#include "pxr/base/plug/plugin.h"
+#include "pxr/base/plug/registry.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+TF_INSTANTIATE_SINGLETON(TsTypeRegistry);
+
+TsTypeRegistry::TsTypeRegistry() {
+ // We have to mark this instance as constructed before calling
+ // SubcribeTo<TsTypeRegistry>() below.
+ TfSingleton<TsTypeRegistry>::SetInstanceConstructed(*this);
+
+ // Cause the registry to initialize
+ TfRegistryManager::GetInstance().SubscribeTo<TsTypeRegistry>();
+}
+
+TsTypeRegistry::~TsTypeRegistry() {
+ TfRegistryManager::GetInstance().UnsubscribeFrom<TsTypeRegistry>();
+}
+
+void
+TsTypeRegistry::InitializeDataHolder(
+ Ts_PolymorphicDataHolder *holder,
+ const VtValue &value)
+{
+ static TypedDataFactory const &doubleDataFactory =
+ _dataFactoryMap.find(TfType::Find<double>())->second;
+
+ // Double-valued keyframes are super common, so special-case them here.
+ if (ARCH_LIKELY(value.IsHolding<double>())) {
+ doubleDataFactory(holder, value);
+ return;
+ }
+
+ // Find a data factory for the type held by the VtValue
+ // If it can't be found, see if we haven't yet loaded its plugin.
+ DataFactoryMap::const_iterator i = _dataFactoryMap.find(value.GetType());
+ if (ARCH_UNLIKELY(i == _dataFactoryMap.end())) {
+ PlugPluginPtr plugin =
+ PlugRegistry::GetInstance().GetPluginForType(value.GetType());
+ if (plugin) {
+ plugin->Load();
+ // Try again to see if loading the plugin provided a factory.
+ // Failing that, issue an error.
+ i = _dataFactoryMap.find(value.GetType());
+ }
+ if (i == _dataFactoryMap.end()) {
+ TF_CODING_ERROR("cannot create keyframes of type %s",
+ value.GetTypeName().c_str());
+ holder->New(TsTraits<double>::zero);
+ return;
+ }
+ }
+
+ // Execute the data factory
+ i->second(holder,value);
+}
+
+bool
+TsTypeRegistry::IsSupportedType(const TfType &type) const
+{
+ return (_dataFactoryMap.find(type) != _dataFactoryMap.end());
+}
+
+// Will eventually be handled by TsSpline
+TS_REGISTER_TYPE(double);
+TS_REGISTER_TYPE(float);
+
+// Will eventually be handled by TsLerpSeries
+TS_REGISTER_TYPE(VtArray<double>);
+TS_REGISTER_TYPE(VtArray<float>);
+TS_REGISTER_TYPE(GfVec2d);
+TS_REGISTER_TYPE(GfVec2f);
+TS_REGISTER_TYPE(GfVec3d);
+TS_REGISTER_TYPE(GfVec3f);
+TS_REGISTER_TYPE(GfVec4d);
+TS_REGISTER_TYPE(GfVec4f);
+TS_REGISTER_TYPE(GfMatrix2d);
+TS_REGISTER_TYPE(GfMatrix3d);
+TS_REGISTER_TYPE(GfMatrix4d);
+
+// Will eventually be handled by TsQuatSeries
+TS_REGISTER_TYPE(GfQuatd);
+TS_REGISTER_TYPE(GfQuatf);
+
+// Will eventually be handled by TsHeldSeries
+TS_REGISTER_TYPE(bool);
+TS_REGISTER_TYPE(int);
+TS_REGISTER_TYPE(std::string);
+TS_REGISTER_TYPE(TfToken);
+
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TYPE_REGISTRY_H
+#define PXR_BASE_TS_TYPE_REGISTRY_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/tf/api.h"
+#include "pxr/base/tf/hashmap.h"
+#include "pxr/base/tf/registryManager.h"
+#include "pxr/base/tf/singleton.h"
+#include "pxr/base/ts/data.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+/// \class TsTypeRegistry
+/// \brief Type registry which provides a mapping from dynamically typed
+/// objects to statically typed internal ones.
+///
+/// A new type may be registered by using the TS_REGISTER_TYPE macro. ie:
+///
+/// TS_REGISTER_TYPE(double);
+///
+/// The type will also need to have a traits class defined for it. See Types.h
+/// for example traits classes.
+///
+class TsTypeRegistry {
+ TsTypeRegistry(const TsTypeRegistry&) = delete;
+ TsTypeRegistry& operator=(const TsTypeRegistry&) = delete;
+
+public:
+ /// Return the single instance of TsTypeRegistry
+ TS_API
+ static TsTypeRegistry& GetInstance() {
+ return TfSingleton<TsTypeRegistry>::GetInstance();
+ }
+
+ /// A TypedDataFactory is a function which initializes an
+ /// Ts_PolymorphicDataHolder instance for a given VtValue.
+ typedef void(*TypedDataFactory)(
+ Ts_PolymorphicDataHolder *holder,
+ const VtValue &value);
+
+ /// Map from TfTypes to TypedDataFactories
+ typedef TfHashMap<TfType,TypedDataFactory,TfHash> DataFactoryMap;
+
+ /// Registers a TypedDataFactory for a particular type
+ template <class T>
+ void RegisterTypedDataFactory(TypedDataFactory factory) {
+ _dataFactoryMap[TfType::Find<T>()] = factory;
+ }
+
+ /// Initialize an Ts_PolymorphicDataHolder so that it holds an
+ /// Ts_TypedData of the appropriate type with the provided values.
+ TS_API
+ void InitializeDataHolder(
+ Ts_PolymorphicDataHolder *holder,
+ const VtValue &value);
+
+ /// Returns true if the type of \e value is a type we can make keyframes
+ /// for.
+ TS_API
+ bool IsSupportedType(const TfType &type) const;
+
+private:
+ // Private constructor. Only TfSingleton may create one
+ TsTypeRegistry();
+ virtual ~TsTypeRegistry();
+
+ friend class TfSingleton<TsTypeRegistry>;
+
+ DataFactoryMap _dataFactoryMap;
+};
+
+#define TS_REGISTER_TYPE(TYPE) \
+TF_REGISTRY_FUNCTION(TsTypeRegistry) { \
+ TsTypeRegistry ® = TsTypeRegistry::GetInstance(); \
+ reg.RegisterTypedDataFactory<TYPE>( \
+ [](Ts_PolymorphicDataHolder *holder, const VtValue &value) { \
+ holder->New(value.Get<TYPE>()); \
+ }); \
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/vt/types.h"
+#include "pxr/base/tf/enum.h"
+#include "pxr/base/tf/registryManager.h"
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+const double TsTraits<double>::zero = VtZero<double>();
+const float TsTraits<float>::zero = VtZero<float>();
+const int TsTraits<int>::zero = VtZero<int>();
+const bool TsTraits<bool>::zero = VtZero<bool>();
+const GfVec2d TsTraits<GfVec2d>::zero = VtZero<GfVec2d>();
+const GfVec2f TsTraits<GfVec2f>::zero = VtZero<GfVec2f>();
+const GfVec3d TsTraits<GfVec3d>::zero = VtZero<GfVec3d>();
+const GfVec3f TsTraits<GfVec3f>::zero = VtZero<GfVec3f>();
+const GfVec4d TsTraits<GfVec4d>::zero = VtZero<GfVec4d>();
+const GfVec4f TsTraits<GfVec4f>::zero = VtZero<GfVec4f>();
+const GfQuatf TsTraits<GfQuatf>::zero = VtZero<GfQuatf>();
+const GfQuatd TsTraits<GfQuatd>::zero = VtZero<GfQuatd>();
+
+const GfMatrix2d TsTraits<GfMatrix2d>::zero = VtZero<GfMatrix2d>();
+const GfMatrix3d TsTraits<GfMatrix3d>::zero = VtZero<GfMatrix3d>();
+const GfMatrix4d TsTraits<GfMatrix4d>::zero = VtZero<GfMatrix4d>();
+const std::string TsTraits<std::string>::zero = VtZero<std::string>();
+
+const VtArray<double> TsTraits< VtArray<double> >::zero;
+const VtArray<float> TsTraits< VtArray<float> >::zero;
+
+const TfToken TsTraits<TfToken>::zero;
+
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(TsLeft, "left");
+ TF_ADD_ENUM_NAME(TsRight, "right");
+}
+
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(TsKnotHeld, "held");
+ TF_ADD_ENUM_NAME(TsKnotLinear, "linear");
+ TF_ADD_ENUM_NAME(TsKnotBezier, "bezier");
+}
+
+TF_REGISTRY_FUNCTION(TfEnum)
+{
+ TF_ADD_ENUM_NAME(TsExtrapolationHeld, "held");
+ TF_ADD_ENUM_NAME(TsExtrapolationLinear, "linear");
+}
+
+PXR_NAMESPACE_CLOSE_SCOPE
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_TYPES_H
+#define PXR_BASE_TS_TYPES_H
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/api.h"
+
+#include "pxr/base/gf/vec2d.h"
+#include "pxr/base/gf/vec3d.h"
+#include "pxr/base/gf/vec4d.h"
+#include "pxr/base/gf/vec4i.h"
+#include "pxr/base/gf/quatd.h"
+#include "pxr/base/gf/quatf.h"
+
+#include "pxr/base/gf/matrix2d.h"
+#include "pxr/base/gf/matrix3d.h"
+#include "pxr/base/gf/matrix4d.h"
+#include "pxr/base/gf/interval.h"
+#include "pxr/base/gf/multiInterval.h"
+#include "pxr/base/gf/range1d.h"
+#include "pxr/base/tf/staticTokens.h"
+#include "pxr/base/tf/token.h"
+// Including weakPtrFacade.h before vt/value.h works around a problem
+// finding get_pointer.
+#include "pxr/base/tf/weakPtrFacade.h"
+#include "pxr/base/vt/array.h"
+#include "pxr/base/vt/value.h"
+#include <string>
+#include <map>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+/// The time type used by Ts.
+typedef double TsTime;
+
+/// \brief Keyframe knot types.
+///
+/// These specify the method used to interpolate keyframes.
+/// This enum is registered with TfEnum for conversion to/from std::string.
+///
+enum TsKnotType {
+ TsKnotHeld = 0, //!< A held-value knot; tangents will be ignored
+ TsKnotLinear, //!< A Linear knot; tangents will be ignored
+ TsKnotBezier, //!< A Bezier knot
+
+ TsKnotNumTypes
+};
+
+/// \brief Spline extrapolation types.
+///
+/// These specify the method used to extrapolate splines.
+/// This enum is registered with TfEnum for conversion to/from std::string.
+///
+enum TsExtrapolationType {
+ TsExtrapolationHeld = 0, //!< Held; splines hold values at edges
+ TsExtrapolationLinear, //!< Linear; splines hold slopes at edges
+
+ TsExtrapolationNumTypes
+};
+
+/// \brief A pair of TsExtrapolationTypes indicating left
+/// and right extrapolation in first and second, respectively.
+typedef std::pair<TsExtrapolationType,TsExtrapolationType>
+ TsExtrapolationPair;
+
+/// \brief Dual-value keyframe side.
+enum TsSide {
+ TsLeft,
+ TsRight
+};
+
+/// \brief An individual sample. A sample is either a blur, defining a
+/// rectangle, or linear, defining a line for linear interpolation.
+/// In both cases the sample is half-open on the right.
+typedef struct TsValueSample {
+public:
+ TsValueSample(TsTime inLeftTime, const VtValue& inLeftValue,
+ TsTime inRightTime, const VtValue& inRightValue,
+ bool inBlur = false) :
+ isBlur(inBlur),
+ leftTime(inLeftTime),
+ rightTime(inRightTime),
+ leftValue(inLeftValue),
+ rightValue(inRightValue)
+ {}
+
+public:
+ bool isBlur; //!< True if a blur sample
+ TsTime leftTime; //!< Left side time (inclusive)
+ TsTime rightTime; //!< Right side time (exclusive)
+ VtValue leftValue; //!< Value at left or, for blur, min value
+ VtValue rightValue; //!< Value at right or, for blur, max value
+} TsValueSample;
+
+/// A sequence of samples.
+typedef std::vector<TsValueSample> TsSamples;
+
+// Traits for types used in TsSplines.
+//
+// Depending on a type's traits, different interpolation techniques are
+// available:
+//
+// * if not interpolatable, only TsKnotHeld can be used
+// * if interpolatable, TsKnotHeld and TsKnotLinear can be used
+// * if supportsTangents, any knot type can be used
+//
+template <typename T>
+struct TsTraits {
+ // True if this is a valid value type for splines.
+ // Default is false; set to true for all supported types.
+ static const bool isSupportedSplineValueType = false;
+
+ // True if the type can be interpolated by taking linear combinations.
+ // If this is false, only TsKnotHeld is isSupportedSplineValueType.
+ static const bool interpolatable = true;
+
+ // True if the value can be extrapolated outside of the keyframe
+ // range. If this is false we always use TsExtrapolateHeld behaviour
+ static const bool extrapolatable = false;
+
+ // True if the value type supports tangents.
+ // If true, interpolatable must also be true.
+ static const bool supportsTangents = true;
+
+ // The origin or zero vector for this type.
+ static const T zero;
+};
+
+template <>
+struct TS_API TsTraits<std::string> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = false;
+ static const bool extrapolatable = false;
+ static const bool supportsTangents = false;
+ static const std::string zero;
+};
+
+template <>
+struct TS_API TsTraits<double> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = true;
+ static const double zero;
+};
+
+template <>
+struct TS_API TsTraits<float> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = true;
+ static const float zero;
+};
+
+template <>
+struct TS_API TsTraits<int> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = false;
+ static const bool extrapolatable = false;
+ static const bool supportsTangents = false;
+ static const int zero;
+};
+
+template <>
+struct TS_API TsTraits<bool> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = false;
+ static const bool extrapolatable = false;
+ static const bool supportsTangents = false;
+ static const bool zero;
+};
+
+template <>
+struct TS_API TsTraits<GfVec2d> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfVec2d zero;
+};
+
+template <>
+struct TS_API TsTraits<GfVec2f> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfVec2f zero;
+};
+
+template <>
+struct TS_API TsTraits<GfVec3d> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfVec3d zero;
+};
+
+template <>
+struct TS_API TsTraits<GfVec3f> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfVec3f zero;
+};
+
+template <>
+struct TS_API TsTraits<GfVec4d> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfVec4d zero;
+};
+
+template <>
+struct TS_API TsTraits<GfVec4f> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfVec4f zero;
+};
+
+template <>
+struct TS_API TsTraits<GfQuatd> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = false;
+ static const bool supportsTangents = false;
+ static const GfQuatd zero;
+};
+
+template <>
+struct TS_API TsTraits<GfQuatf> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = false;
+ static const bool supportsTangents = false;
+ static const GfQuatf zero;
+};
+
+template <>
+struct TS_API TsTraits<GfMatrix2d> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfMatrix2d zero;
+};
+
+template <>
+struct TS_API TsTraits<GfMatrix3d> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfMatrix3d zero;
+};
+
+template <>
+struct TS_API TsTraits<GfMatrix4d> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const GfMatrix4d zero;
+};
+
+template <>
+struct TS_API TsTraits< VtArray<double> > {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const VtArray<double> zero;
+};
+
+template <>
+struct TS_API TsTraits< VtArray<float> > {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = true;
+ static const bool extrapolatable = true;
+ static const bool supportsTangents = false;
+ static const bool supportsVaryingShapes = false;
+ static const VtArray<float> zero;
+};
+
+template <>
+struct TS_API TsTraits<TfToken> {
+ static const bool isSupportedSplineValueType = true;
+ static const bool interpolatable = false;
+ static const bool extrapolatable = false;
+ static const bool supportsTangents = false;
+ static const TfToken zero;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/keyFrame.h"
+#include "pxr/base/ts/wrapUtils.h"
+#include "pxr/base/gf/vec3d.h"
+#include "pxr/base/tf/iterator.h"
+#include "pxr/base/tf/makePyConstructor.h"
+#include "pxr/base/tf/pyContainerConversions.h"
+#include "pxr/base/tf/pyEnum.h"
+#include "pxr/base/tf/pyPtrHelpers.h"
+#include "pxr/base/tf/pyResultConversions.h"
+#include "pxr/base/tf/pyUtils.h"
+#include "pxr/base/tf/stringUtils.h"
+#include "pxr/base/vt/array.h"
+#include "pxr/base/vt/valueFromPython.h"
+
+#include <boost/python.hpp>
+#include <boost/function.hpp>
+#include <vector>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+using std::string;
+using std::vector;
+
+
+static Ts_AnnotatedBoolResult
+_CanSetKnotType(const TsKeyFrame &kf, TsKnotType type)
+{
+ std::string reason;
+ const bool canSet = kf.CanSetKnotType(type, &reason);
+ return Ts_AnnotatedBoolResult(canSet, reason);
+}
+
+
+////////////////////////////////////////////////////////////////////////
+// Values
+//
+// For setting and getting values, we want to be able to handle either
+// single values, or 2-tuples of values (for dual-valued knots).
+//
+// Since we wrap these as Python properties, and boost::python doesn't seem
+// to let us override the property setter/getters, we handle the
+// single-vs-tuple value distinction ourselves.
+
+static boost::python::object
+GetValue( TsKeyFrame & kf )
+{
+ if (kf.GetIsDualValued())
+ return boost::python::make_tuple( kf.GetLeftValue(), kf.GetValue() );
+ else
+ return boost::python::object( kf.GetValue() );
+}
+
+static void
+SetValue( TsKeyFrame & kf, const boost::python::object & obj )
+{
+ // Check for a 2-tuple
+ boost::python::extract< vector<VtValue> > tupleExtractor( obj );
+ if (tupleExtractor.check()) {
+ vector< VtValue > vals = tupleExtractor();
+ if (vals.size() != 2) {
+ TfPyThrowValueError("expected exactly 2 elements for tuple");
+ }
+ if (!kf.GetIsDualValued()) {
+ // Automatically promote to a dual-valued knot.
+ kf.SetIsDualValued(true);
+ }
+ // No change unless we're dual valued (i.e. no slicing).
+ if (kf.GetIsDualValued()) {
+ kf.SetLeftValue( vals[0] );
+ kf.SetValue( vals[1] );
+ } else {
+ TfPyThrowTypeError("keyframe cannot be made dual-valued");
+ }
+ return;
+ }
+
+ // Check for a single VtValue
+ boost::python::extract< VtValue > singleValueExtractor( obj );
+ if (singleValueExtractor.check()) {
+ VtValue val = singleValueExtractor();
+ kf.SetValue( val );
+ return;
+ }
+
+ // The above extract<VtValue> will always succeed, since a VtValue
+ // is templated and can store any type. This error is here in case
+ // that ever changes.
+ TfPyThrowTypeError("expected single Vt.Value or pair of Values");
+}
+
+static std::string
+_Repr(const TsKeyFrame &self)
+{
+ std::vector<std::string> args;
+ args.reserve(11);
+
+ // The first three, respectively, four (when dual-valued) arguments are
+ // positional since they are well-established and common to all splines.
+
+ args.push_back(TfPyRepr(self.GetTime()));
+ if (self.GetIsDualValued()) {
+ // Dual-valued knot
+ args.push_back(TfPyRepr(self.GetLeftValue()));
+ }
+ args.push_back(TfPyRepr(self.GetValue()));
+ args.push_back(TfPyRepr(self.GetKnotType()));
+
+ // The remaining arguments are keyword arguments to avoid any ambiguity.
+
+ // Note: We might want to deal with the float types in a more direct way
+ // to improve performance.
+ if (self.SupportsTangents()) {
+ args.push_back("leftSlope=" + TfPyRepr(self.GetLeftTangentSlope()));
+ args.push_back("rightSlope=" + TfPyRepr(self.GetRightTangentSlope()));
+ args.push_back("leftLen=" + TfPyRepr(self.GetLeftTangentLength()));
+ args.push_back("rightLen=" + TfPyRepr(self.GetRightTangentLength()));
+ }
+
+ return TF_PY_REPR_PREFIX + "KeyFrame(" + TfStringJoin(args, ", ") + ")";
+}
+
+
+void wrapKeyFrame()
+{
+ typedef TsKeyFrame This;
+
+ TfPyWrapEnum<TsSide>();
+
+ TfPyWrapEnum<TsKnotType>();
+
+ TfPyContainerConversions::from_python_sequence<
+ std::vector< TsKeyFrame >,
+ TfPyContainerConversions::variable_capacity_policy >();
+
+ TfPyContainerConversions::from_python_sequence<
+ std::vector< TsKnotType >,
+ TfPyContainerConversions::variable_capacity_policy >();
+
+ to_python_converter< std::vector<TsKnotType>,
+ TfPySequenceToPython< std::vector<TsKnotType> > >();
+
+ class_<This>( "KeyFrame", no_init )
+
+ .def( init< const TsTime &,
+ const VtValue &,
+ TsKnotType,
+ const VtValue &,
+ const VtValue &,
+ const TsTime &,
+ const TsTime & >(
+ "",
+ ( arg("time") = 0.0,
+ arg("value") = VtValue(0.0),
+ arg("knotType") = TsKnotLinear,
+ arg("leftSlope") = VtValue(),
+ arg("rightSlope") = VtValue(),
+ arg("leftLen") = 0.0,
+ arg("rightLen") = 0.0 ) ) )
+
+ .def( init< const TsTime &,
+ const VtValue &,
+ const VtValue &,
+ TsKnotType,
+ const VtValue &,
+ const VtValue &,
+ const TsTime &,
+ const TsTime & >(
+ "",
+ ( arg("time"),
+ arg("leftValue"),
+ arg("rightValue"),
+ arg("knotType"),
+ arg("leftSlope") = VtValue(),
+ arg("rightSlope") = VtValue(),
+ arg("leftLen") = 0.0,
+ arg("rightLen") = 0.0 ) ) )
+
+ .def( init<const TsKeyFrame &>() )
+
+ .def("IsEquivalentAtSide", &This::IsEquivalentAtSide)
+
+ .add_property("time",
+ &This::GetTime,
+ &This::SetTime,
+ "The time of this Keyframe.")
+
+ .add_property("value",
+ &::GetValue,
+ &::SetValue,
+ "The value at this Keyframe. If the keyframe is dual-valued, "
+ "this will be a tuple of the (left, right) side values; "
+ "otherwise, it will be the single value. If you assign a "
+ "single value to a dual-valued knot, only the right side "
+ "will be set, and the left side will remain unchanged. If "
+ "you assign a dual-value (tuple) to a single-valued keyframe "
+ "you'll get an exception and the keyframe won't have changed.")
+
+ .def("GetValue",
+ (VtValue (This::*)(TsSide) const) &This::GetValue,
+ "Gets the value at this keyframe on the given side.")
+
+ .def("SetValue",
+ (void (This::*)(VtValue, TsSide)) &This::SetValue,
+ "Sets the value at this keyframe on the given side.")
+
+ .add_property("knotType",
+ &This::GetKnotType,
+ &This::SetKnotType,
+ "The knot type of this Keyframe. It controls how the spline "
+ "is interpolated around this keyframe.")
+
+ .def("CanSetKnotType", &::_CanSetKnotType,
+ "Returns true if the given knot type can be set on this key "
+ "frame. If it returns false, it also returns the reason why not. "
+ "The reason can be accessed like this: "
+ "anim.CanSetKnotType(kf).reasonWhyNot.")
+
+ .add_property("isDualValued",
+ &This::GetIsDualValued,
+ &This::SetIsDualValued,
+ "True if this Keyframe is dual-valued.")
+
+ .add_property("isInterpolatable", &This::IsInterpolatable)
+
+ .add_property("supportsTangents", &This::SupportsTangents)
+
+ .add_property("hasTangents", &This::HasTangents)
+
+ // Slope/length tangent interface
+ .add_property("leftSlope",
+ make_function(&This::GetLeftTangentSlope,
+ return_value_policy<return_by_value>()),
+ &This::SetLeftTangentSlope,
+ "The left tangent's slope.")
+ .add_property("leftLen",
+ &This::GetLeftTangentLength,
+ &This::SetLeftTangentLength,
+ "The left tangent's length.")
+ .add_property("rightSlope",
+ make_function(&This::GetRightTangentSlope,
+ return_value_policy<return_by_value>()),
+ &This::SetRightTangentSlope,
+ "The right tangent's slope.")
+ .add_property("rightLen",
+ &This::GetRightTangentLength,
+ &This::SetRightTangentLength,
+ "The right tangent's length.")
+
+ // Tangent symmetry
+ .add_property("tangentSymmetryBroken",
+ &This::GetTangentSymmetryBroken,
+ &This::SetTangentSymmetryBroken,
+ "Gets and sets whether symmetry between the left/right "
+ "tangents is broken. If true, tangent handles will not "
+ "automatically stay symmetric as they are changed.")
+
+ .def("__repr__", ::_Repr)
+ .def(self == self)
+ .def(self != self)
+ ;
+
+ VtValueFromPython<TsKeyFrame>();
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/loopParams.h"
+#include "pxr/base/tf/pyUtils.h"
+
+#include <boost/python.hpp>
+
+#include <string>
+#include <sstream>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+
+static std::string
+_GetRepr(const TsLoopParams & params)
+{
+ // This takes advantage of our operator<<, which produces a string that
+ // makes a valid Python tuple, or a parenthesized set of args.
+ std::ostringstream result;
+ result << TF_PY_REPR_PREFIX
+ << "LoopParams"
+ << params;
+ return result.str();
+}
+
+
+void wrapLoopParams()
+{
+ typedef TsLoopParams This;
+
+ class_<This>("LoopParams", init<>())
+ .def(init<bool, TsTime, TsTime, TsTime, TsTime, double>())
+
+ .add_property("looping",
+ &This::GetLooping,
+ &This::SetLooping)
+
+ .add_property("start",
+ &This::GetStart)
+
+ .add_property("period",
+ &This::GetPeriod)
+
+ .add_property("preRepeatFrames",
+ &This::GetPreRepeatFrames)
+
+ .add_property("repeatFrames",
+ &This::GetRepeatFrames)
+
+ .def("GetMasterInterval", &This::GetMasterInterval,
+ return_value_policy<return_by_value>())
+
+ .def("GetLoopedInterval", &This::GetLoopedInterval,
+ return_value_policy<return_by_value>())
+
+ .def("IsValid", &This::IsValid)
+
+ .add_property("valueOffset",
+ &This::GetValueOffset,
+ &This::SetValueOffset)
+
+ .def("__repr__", &::_GetRepr)
+
+ .def(self == self)
+ .def(self != self)
+ ;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/simplify.h"
+
+#include "pxr/base/tf/pyContainerConversions.h"
+#include "pxr/base/tf/pyResultConversions.h"
+
+#include <boost/python.hpp>
+#include <boost/python/def.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+static void _SimplifySpline(
+ TsSpline& spline,
+ const GfMultiInterval &intervals,
+ double maxErrorFraction)
+{
+ TsSimplifySpline(&spline, intervals, maxErrorFraction);
+}
+
+static void _SimplifySplinesInParallel(
+ boost::python::list& splines,
+ const std::vector<GfMultiInterval> &intervals,
+ double maxErrorFraction)
+{
+ // Because we are mutating the python 'splines' argument to be consistent
+ // with the non-parallel API, we can't use
+ // TfPyContainerConversions::from_python_sequence() which works only for
+ // const or value arguments so we have to iterate the python list
+ std::vector<TsSpline*> splinePtrs;
+ for(int i=0; i < len(splines); ++i)
+ {
+ boost::python::extract<TsSpline&> extractSpline(splines[i]);
+ if(extractSpline.check())
+ {
+ TsSpline& spline = extractSpline();
+ splinePtrs.push_back(&spline);
+ }
+ else
+ {
+ TfPyThrowTypeError("Expecting type TsSpline in splines.");
+ }
+ }
+ TsSimplifySplinesInParallel(splinePtrs, intervals, maxErrorFraction);
+}
+
+
+void wrapSimplify()
+{
+ TfPyContainerConversions::from_python_sequence<
+ std::vector< GfMultiInterval >,
+ TfPyContainerConversions::variable_capacity_policy >();
+
+ def("SimplifySpline", _SimplifySpline);
+ def("SimplifySplinesInParallel", &_SimplifySplinesInParallel,
+ "(list<Ts.Spline> (mutated for result), list<Gf.MultiInterval>, maxErrorFraction)\n");
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/spline.h"
+
+#include "pxr/base/ts/keyFrameMap.h"
+#include "pxr/base/ts/loopParams.h"
+#include "pxr/base/ts/wrapUtils.h"
+
+#include "pxr/base/tf/iterator.h"
+#include "pxr/base/tf/makePyConstructor.h"
+#include "pxr/base/tf/pyAnnotatedBoolResult.h"
+#include "pxr/base/tf/pyContainerConversions.h"
+#include "pxr/base/tf/pyEnum.h"
+#include "pxr/base/tf/pyPtrHelpers.h"
+#include "pxr/base/tf/pyResultConversions.h"
+#include "pxr/base/tf/pyUtils.h"
+#include "pxr/base/tf/stringUtils.h"
+#include "pxr/base/vt/valueFromPython.h"
+
+#include <boost/python.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+using std::string;
+using std::vector;
+
+static string
+_GetRepr( const TsSpline & val )
+{
+ string repr = TF_PY_REPR_PREFIX + "Spline(";
+ std::pair<TsExtrapolationType, TsExtrapolationType> extrapolation =
+ val.GetExtrapolation();
+ TsLoopParams loopParams = val.GetLoopParams();
+ size_t counter = val.size();
+
+ bool empty = (counter == 0)
+ && (extrapolation.first == TsExtrapolationHeld)
+ && (extrapolation.second == TsExtrapolationHeld)
+ && (!loopParams.IsValid());
+
+ if (!empty) {
+ repr += "[";
+ TF_FOR_ALL(it, val) {
+ repr += TfPyRepr(*it);
+ counter--;
+ repr += counter > 0 ? ", " : "";
+ }
+ repr += "]";
+ repr += string(", ") + TfPyRepr( extrapolation.first );
+ repr += string(", ") + TfPyRepr( extrapolation.second );
+ repr += string(", ") + TfPyRepr( loopParams );
+ }
+
+ repr += ")";
+ return repr;
+}
+
+static TsSpline*
+_ConstructFromKeyFrameDict( boost::python::object & kfDict,
+ TsKnotType knotType )
+{
+ // Given an iterable python object, produces a C++ object suitable
+ // for a C++11-style for-loop.
+ class PyIterableWrapper {
+ public:
+ using Iterator = stl_input_iterator<object>;
+
+ PyIterableWrapper(const object &iterable) : _iterable(iterable) { }
+
+ Iterator begin() {
+ return Iterator(_iterable);
+ }
+ Iterator end() {
+ return Iterator();
+ }
+ private:
+ object _iterable;
+ };
+
+ object items = kfDict.attr("items")();
+
+ vector<TsKeyFrame> keyframes;
+ for (const object &item : PyIterableWrapper(items))
+ {
+ object key = item[0];
+ extract<TsTime> time(key);
+ if (!time.check()) {
+ TfPyThrowTypeError("expected time for keyframe in dict");
+ }
+
+ extract<VtValue> value( item[1] );
+
+ // VtValue can hold any python object, so this always suceeds.
+ // And if this fails in the future, boost.python will throw a
+ // C++ exception, which will be converted to a Python exception.
+
+ keyframes.push_back( TsKeyFrame( time(), value(), knotType ) );
+ }
+
+ return new TsSpline( keyframes );
+}
+
+static vector<VtValue>
+_EvalMultipleTimes( const TsSpline & val,
+ const vector<TsTime> & times )
+{
+ vector<VtValue> result;
+ result.reserve( times.size() );
+ TF_FOR_ALL( it, times )
+ result.push_back( val.Eval(*it) );
+
+ return result;
+}
+
+static vector<VtValue>
+_GetRange( const TsSpline & val,
+ TsTime startTime, TsTime endTime)
+{
+ std::pair<VtValue, VtValue> range = val.GetRange(startTime, endTime);
+ vector<VtValue> result;
+ result.reserve( 2 );
+ result.push_back( range.first );
+ result.push_back( range.second );
+
+ return result;
+}
+
+////////////////////////////////////////////////////////////////////////
+
+static boost::python::object
+_GetClosestKeyFrame( const TsSpline & val, TsTime t )
+{
+ std::optional<TsKeyFrame> kf = val.GetClosestKeyFrame(t);
+ return kf ? object(*kf) : object();
+}
+
+static boost::python::object
+_GetClosestKeyFrameBefore( const TsSpline & val, TsTime t )
+{
+ std::optional<TsKeyFrame> kf = val.GetClosestKeyFrameBefore(t);
+ return kf ? object(*kf) : object();
+}
+
+static boost::python::object
+_GetClosestKeyFrameAfter( const TsSpline & val, TsTime t )
+{
+ std::optional<TsKeyFrame> kf = val.GetClosestKeyFrameAfter(t);
+ return kf ? object(*kf) : object();
+}
+
+////////////////////////////////////////////////////////////////////////
+
+// Helper function to find begin/end iterators for times in slice.
+// Throws exceptions for invalid slices.
+static void
+_SliceKeyframes( const boost::python::slice & index,
+ const TsKeyFrameMap & keyframes,
+ TsKeyFrameMap::const_iterator *begin,
+ TsKeyFrameMap::const_iterator *end )
+{
+ // Prohibit use of 'step'
+ if (!TfPyIsNone(index.step()))
+ TfPyThrowValueError("cannot use 'step' when indexing keyframes");
+
+ // Find begin & end iterators, based on slice bounds
+ if (!TfPyIsNone(index.start())) {
+ boost::python::extract< TsTime > startExtractor( index.start() );
+ if (!startExtractor.check())
+ TfPyThrowValueError("expected time in keyframe slice");
+ TsTime startVal = startExtractor();
+ *begin = keyframes.lower_bound( startVal );
+ } else {
+ *begin = keyframes.begin();
+ }
+ if (!TfPyIsNone(index.stop())) {
+ boost::python::extract< TsTime > stopExtractor( index.stop() );
+ if (!stopExtractor.check())
+ TfPyThrowValueError("expected time in keyframe slice");
+ TsTime stopVal = stopExtractor();
+ *end = keyframes.lower_bound( stopVal );
+ } else {
+ *end = keyframes.end();
+ }
+}
+
+static size_t
+_GetSize( const TsSpline & val )
+{
+ return val.GetKeyFrames().size();
+}
+
+static vector<TsKeyFrame>
+_GetValues( const TsSpline & val )
+{
+ const TsKeyFrameMap &vec = val.GetKeyFrames();
+ return vector<TsKeyFrame>(vec.begin(),vec.end());
+}
+
+static vector<TsTime>
+_GetKeys( const TsSpline & val )
+{
+ const TsKeyFrameMap & kf = val.GetKeyFrames();
+
+ vector<TsTime> result;
+ result.reserve(kf.size());
+ TF_FOR_ALL(it, kf)
+ result.push_back( it->GetTime() );
+
+ return result;
+}
+
+static TsKeyFrame
+_GetItemByKey( const TsSpline & val, const TsTime & key )
+{
+ const TsKeyFrameMap & kf = val.GetKeyFrames();
+
+ TsKeyFrameMap::const_iterator it = kf.find( key );
+
+ if (it == kf.end())
+ TfPyThrowIndexError(TfStringPrintf("no keyframe at time"));
+
+ return *it;
+}
+
+static vector<TsKeyFrame>
+_GetSlice( const TsSpline & val, const boost::python::slice & index )
+{
+ using boost::python::slice;
+
+ const TsKeyFrameMap & kf = val.GetKeyFrames();
+ TsKeyFrameMap::const_iterator begin;
+ TsKeyFrameMap::const_iterator end;
+ _SliceKeyframes( index, kf, &begin, &end );
+
+ // Grab elements in slice bounds
+ vector<TsKeyFrame> result;
+ TsKeyFrameMap::const_iterator it;
+ for (it = begin; it != end && it != kf.end(); ++it) {
+ result.push_back( *it );
+ }
+
+ return result;
+}
+
+static bool
+_ContainsItemWithKey( const TsSpline & val, const TsTime & key )
+{
+ return val.GetKeyFrames().find(key) != val.GetKeyFrames().end();
+}
+
+static void
+_DelItemByKey( TsSpline & val, const TsTime & key )
+{
+ val.RemoveKeyFrame( key );
+}
+
+static void
+_DelSlice( TsSpline & val, const boost::python::slice & index )
+{
+ vector<TsKeyFrame> keyframesToDelete = _GetSlice(val, index);
+
+ TF_FOR_ALL(it, keyframesToDelete)
+ val.RemoveKeyFrame(it->GetTime());
+}
+
+// For __iter__, we copy the current keyframes into a list, and return
+// an iterator for the copy. This provides the desired semantics of
+// being able to iterate over the keyframes, modifying them, without
+// worrying about missing any.
+static PyObject*
+_Iter( const TsSpline & val )
+{
+ vector<TsKeyFrame> kf = _GetValues(val);
+
+ // Convert our vector to a Python list.
+ PyObject *kf_list =
+ TfPySequenceToList::apply< vector<TsKeyFrame> >::type()(kf);
+
+ // Return a native Python iterator over the list.
+ // The iterator will INCREF the list, and free it when iteration is
+ // complete.
+ PyObject *iter = PyObject_GetIter(kf_list);
+
+ // XXX decref on kf_list?
+
+ return iter;
+}
+
+static Ts_AnnotatedBoolResult
+_CanSetKeyFrame(TsSpline &self, const TsKeyFrame &kf)
+{
+ std::string reason;
+ const bool canSet = self.CanSetKeyFrame(kf, &reason);
+ return Ts_AnnotatedBoolResult(canSet, reason);
+}
+
+static void
+_SetKeyFrame( TsSpline & self, const TsKeyFrame & kf )
+{
+ // Wrapper to discard optional intervalAffected argument.
+ self.SetKeyFrame(kf);
+}
+
+static void
+_SetKeyFrames( TsSpline & self,
+ const std::vector<TsKeyFrame> & kf )
+{
+ // XXX: pixar-ism
+ //
+ // We need to save and restore the spline's non-key frame state.
+ // The specification for Clear says that Clear should not affect this
+ // other state, but several implementations of TsSplineInterface
+ // fail to meet this specification.
+
+ TsLoopParams params = self.GetLoopParams();
+ std::pair<TsExtrapolationType, TsExtrapolationType> extrapolation;
+ extrapolation = self.GetExtrapolation();
+ self.Clear();
+ self.SetExtrapolation(extrapolation.first, extrapolation.second);
+ self.SetLoopParams(params);
+
+ TF_FOR_ALL(it, kf) {
+ self.SetKeyFrame(*it);
+ }
+}
+
+// Simple breakdown.
+static object
+_Breakdown1( TsSpline & self, double x, TsKnotType type,
+ bool flatTangents, double tangentLength,
+ const VtValue &v = VtValue())
+{
+ // Wrapper to discard optional intervalAffected argument.
+ std::optional<TsKeyFrame> kf =
+ self.Breakdown(x, type, flatTangents, tangentLength, v);
+
+ return kf ? object(*kf) : object();
+}
+
+BOOST_PYTHON_FUNCTION_OVERLOADS(_Breakdown1_overloads, _Breakdown1, 5, 6);
+
+// Vectorized breakdown.
+static std::map<TsTime,TsKeyFrame>
+_Breakdown2( TsSpline & self, std::set<double> times,
+ TsKnotType type, bool flatTangents, double tangentLength)
+{
+ // Wrapper to discard optional intervalAffected argument.
+ TsKeyFrameMap vec;
+ self.Breakdown(times, type, flatTangents, tangentLength, VtValue(),
+ NULL, &vec);
+ std::map<TsTime,TsKeyFrame> map;
+ TF_FOR_ALL(i, vec) {
+ map.insert(std::make_pair(i->GetTime(),*i));
+ }
+ return map;
+}
+
+// Vectorized breakdown with multiple values
+static std::map<TsTime,TsKeyFrame>
+_Breakdown3( TsSpline & self, std::vector<double> times,
+ TsKnotType type, bool flatTangents, double tangentLength,
+ std::vector<VtValue> values)
+{
+ // Wrapper to discard optional intervalAffected argument.
+ TsKeyFrameMap vec;
+ self.Breakdown(times, type, flatTangents, tangentLength, values,
+ NULL, &vec);
+ std::map<TsTime,TsKeyFrame> map;
+ TF_FOR_ALL(i, vec) {
+ map.insert(std::make_pair(i->GetTime(),*i));
+ }
+ return map;
+}
+
+// Vectorized breakdown with multiple values and knot types
+static std::map<TsTime,TsKeyFrame>
+_Breakdown4( TsSpline & self, std::vector<double> times,
+ std::vector<TsKnotType> types,
+ bool flatTangents, double tangentLength,
+ std::vector<VtValue> values)
+{
+ // Wrapper to discard optional intervalAffected argument.
+ TsKeyFrameMap vec;
+ self.Breakdown(times, types, flatTangents, tangentLength, values,
+ NULL, &vec);
+ std::map<TsTime,TsKeyFrame> map;
+ TF_FOR_ALL(i, vec) {
+ map.insert(std::make_pair(i->GetTime(),*i));
+ }
+ return map;
+}
+
+static void
+_SetExtrapolation(
+ TsSpline & self,
+ const std::pair<TsExtrapolationType, TsExtrapolationType>& x )
+{
+ self.SetExtrapolation(x.first, x.second);
+}
+
+
+// We implement these operators in the Python sense -- value equality --
+// without just implementing and wrapping C++ operator==(), since
+// Python also has the 'is' operator, but in C++ we don't want to
+// gloss over the difference between value and identity comparison.
+static bool
+_Eq( const TsSpline & lhs, const TsSpline & rhs )
+{
+ return (&lhs == &rhs) ||
+ (lhs.GetKeyFrames() == rhs.GetKeyFrames() &&
+ lhs.GetExtrapolation() == rhs.GetExtrapolation() &&
+ lhs.GetLoopParams() == rhs.GetLoopParams());
+}
+static bool
+_Ne( const TsSpline & lhs, const TsSpline & rhs )
+{
+ return !_Eq(lhs, rhs);
+}
+
+// Functions for inspecting redundancy of key frames.
+static bool
+_IsKeyFrameRedundant( const TsSpline & self, const TsTime & key,
+ const VtValue & defaultValue = VtValue() )
+{
+ return self.IsKeyFrameRedundant(_GetItemByKey(self, key), defaultValue);
+}
+
+BOOST_PYTHON_FUNCTION_OVERLOADS(_IsKeyFrameRedundant_overloads,
+ _IsKeyFrameRedundant, 2, 3);
+
+static bool
+_IsKeyFrameRedundant_2( const TsSpline & self,
+ const TsKeyFrame & kf,
+ const VtValue & defaultValue = VtValue() )
+{
+ return self.IsKeyFrameRedundant(kf, defaultValue);
+}
+
+BOOST_PYTHON_FUNCTION_OVERLOADS(_IsKeyFrameRedundant_2_overloads,
+ _IsKeyFrameRedundant_2, 2, 3);
+
+static bool
+_IsSegmentFlat( const TsSpline & self, const TsTime & lkey,
+ const TsTime & rkey )
+{
+ return self.IsSegmentFlat(_GetItemByKey(self, lkey),
+ _GetItemByKey(self, rkey));
+}
+
+static bool
+_IsSegmentFlat_2( const TsSpline & self, const TsKeyFrame & lkf,
+ const TsKeyFrame & rkf )
+{
+ return self.IsSegmentFlat(lkf, rkf);
+}
+
+static bool
+_IsSegmentValueMonotonic( const TsSpline & self,
+ const TsTime & lkey,
+ const TsTime & rkey )
+{
+ return self.IsSegmentValueMonotonic(_GetItemByKey(self, lkey),
+ _GetItemByKey(self, rkey));
+}
+
+static bool
+_IsSegmentValueMonotonic_2( const TsSpline & self,
+ const TsKeyFrame & lkf,
+ const TsKeyFrame & rkf )
+{
+ return self.IsSegmentValueMonotonic(lkf, rkf);
+}
+
+void wrapSpline()
+{
+ using This = TsSpline;
+
+ class_<This>("Spline", no_init)
+ .def( init<>() )
+ .def( init<const TsSpline &>() )
+ .def( init<const vector< TsKeyFrame > &,
+ optional<TsExtrapolationType,
+ TsExtrapolationType,
+ const TsLoopParams &> >())
+ .def( "__init__", make_constructor(&::_ConstructFromKeyFrameDict) )
+
+ .def("__repr__", &::_GetRepr)
+ .def("IsLinear", &This::IsLinear)
+ .def("ClearRedundantKeyFrames", &This::ClearRedundantKeyFrames,
+ "Clears all redundant key frames from the spline\n\n",
+ ( arg("defaultValue") = VtValue(),
+ arg("intervals") = GfMultiInterval(GfInterval(
+ -std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity()))))
+
+ .add_property("typeName", &This::GetTypeName,
+ "Returns the typename of the value type for "
+ "keyframes in this TsSpline. If no keyframes have been "
+ "set, returns None.")
+
+ .def("SetKeyFrames", &::_SetKeyFrames,
+ "SetKeyFrames(keyFrames)\n\n"
+ "keyFrames : sequence<TsKeyFrame>\n\n"
+ "Replaces all of the specified keyframes. Keyframes may be "
+ "specified using any type of Python sequence, such as a list or tuple.")
+ .def("SetKeyFrame", &::_SetKeyFrame)
+ .def("CanSetKeyFrame", &::_CanSetKeyFrame,
+ "CanSetKeyFrame(kf) -> bool\n\n"
+ "kf : TsKeyFrame\n\n"
+ "Returns true if the given keyframe can be set on this "
+ "spline. If it returns false, it also returns the reason why not. "
+ "The reason can be accessed like this: "
+ "anim.CanSetKeyFrame(kf).reasonWhyNot.")
+
+ .def("Breakdown", _Breakdown1, _Breakdown1_overloads())
+ .def("Breakdown", _Breakdown2, return_value_policy<TfPyMapToDictionary>())
+ .def("Breakdown", _Breakdown3, return_value_policy<TfPyMapToDictionary>())
+ .def("Breakdown", _Breakdown4, return_value_policy<TfPyMapToDictionary>())
+
+ .add_property("extrapolation",
+ make_function(&This::GetExtrapolation,
+ return_value_policy< TfPyPairToTuple >()),
+ make_function(&::_SetExtrapolation))
+
+ .add_property("loopParams",
+ make_function(&This::GetLoopParams,
+ return_value_policy<return_by_value>()),
+ &This::SetLoopParams)
+
+ .def("IsTimeLooped", &This::IsTimeLooped,
+ "True if the given time is in the unrolled region of a spline "
+ "that is looping; i.e. not in the master region")
+ .def("BakeSplineLoops", &This::BakeSplineLoops)
+ .def("Eval", &This::Eval,
+ ( arg("time"),
+ arg("side") = TsRight ) )
+ .def("EvalHeld", &This::EvalHeld,
+ ( arg("side") = TsRight ) )
+ .def("EvalDerivative", &This::EvalDerivative,
+ ( arg("side") = TsRight ) )
+ .def("Eval", &::_EvalMultipleTimes,
+ return_value_policy< TfPySequenceToTuple >(),
+ "Eval(times) -> sequence<VtValue>\n\n"
+ "times : tuple<Time>\n\n"
+ "Evaluates this spline at a tuple or list of times, "
+ "returning a tuple of results.")
+ .def("DoSidesDiffer", &This::DoSidesDiffer,
+ (arg("time")))
+
+ .def("Sample", &This::Sample,
+ return_value_policy< TfPySequenceToTuple >() )
+
+ .def("Range", &::_GetRange,
+ return_value_policy< TfPySequenceToTuple >(),
+ "Range(startTime, endTime) -> tuple<VtValue>\n\n"
+ "startTime : Time\n"
+ "endTime : Time\n\n"
+ "The minimum and maximum of this spline returned as a "
+ "tuple pair over the given time domain.")
+
+ .add_property("empty", &This::IsEmpty)
+
+ .add_property("frames",
+ make_function(&::_GetKeys,
+ return_value_policy< TfPySequenceToList >()),
+ "A list of the frames for which keyframes exist.")
+
+ .add_property("frameRange",
+ &This::GetFrameRange,
+ "A list with the first and last frames as elements.")
+
+ .def("GetKeyFramesInMultiInterval", &This::GetKeyFramesInMultiInterval,
+ return_value_policy<TfPySequenceToList>())
+
+ // Keyframes dictionary interface
+ .def("__len__", &::_GetSize)
+ .def("__getitem__", &::_GetItemByKey)
+ .def("__getitem__", &::_GetSlice,
+ return_value_policy<TfPySequenceToList>())
+ .def("has_key", &::_ContainsItemWithKey)
+ .def("__contains__", &::_ContainsItemWithKey)
+ .def("keys", &::_GetKeys,
+ return_value_policy<TfPySequenceToList>())
+ .def("values", &::_GetValues,
+ return_value_policy<TfPySequenceToList>())
+ .def("clear", &This::Clear)
+ .def("__delitem__", &::_DelItemByKey)
+ .def("__delitem__", &::_DelSlice)
+ .def("__iter__", &::_Iter)
+
+ .def("__eq__", &::_Eq)
+ .def("__ne__", &::_Ne)
+
+ .def("ClosestKeyFrame", &::_GetClosestKeyFrame,
+ "ClosestKeyFrame(time) -> TsKeyFrame\n\n"
+ "time : Time\n\n"
+ "Finds the keyframe closest to the given time. "
+ "Returns None if there are no keyframes.")
+ .def("ClosestKeyFrameBefore", &::_GetClosestKeyFrameBefore,
+ "ClosestKeyFrameBefore(time) -> TsKeyFrame\n\n"
+ "time : Time\n\n"
+ "Finds the closest keyframe before the given time. Returns None if "
+ "no such keyframe exists.")
+ .def("ClosestKeyFrameAfter", &::_GetClosestKeyFrameAfter,
+ "ClosestKeyFrameAfter(time) -> TsKeyFrame\n\n"
+ "time : Time\n\n"
+ "Finds the closest keyframe after the given time. Returns None if "
+ "no such keyframe exists.")
+
+ .def("IsKeyFrameRedundant", _IsKeyFrameRedundant,
+ _IsKeyFrameRedundant_overloads(
+ "True if the key frame at the provided time is redundant.\n\n"
+ "If a second parameter is provided it is used as a default value"
+ "for the spline, so that the last knot on a spline can be marked"
+ "redundant if it is equal to the default value.\n"
+ "If the TsTime provided does not refer to a frame that has a"
+ "knot, an exception will be thrown."))
+ .def("IsKeyFrameRedundant", _IsKeyFrameRedundant_2,
+ _IsKeyFrameRedundant_2_overloads(
+ "True if the key frame is redundant.\n\n"
+ "If a second parameter is provided it is used as a default value"
+ "for the spline, so that the last knot on a spline can be marked"
+ "redundant if it is equal to the default value."))
+ .def("HasRedundantKeyFrames", &This::HasRedundantKeyFrames,
+ "True if any key frames are redundant.\n\n",
+ ( arg("defaultValue") = VtValue() ) )
+ .def("IsSegmentFlat", &::_IsSegmentFlat,
+ "True if the segment between the two provided TsTimes is flat."
+ "\n\n"
+ "If either TsTime does not refer to a knot then an exception"
+ " is thrown.")
+ .def("IsSegmentFlat", &::_IsSegmentFlat_2,
+ "True if the segment between the two provided TsKeyFrames is"
+ "flat.")
+ .def("IsSegmentValueMonotonic", &::_IsSegmentValueMonotonic,
+ "True if the segment between the two provided TsTimes is"
+ " monotonically increasing or monotonically decreasing, i.e. no"
+ " extremes are present"
+ "\n\n"
+ "If either TsTime does not refer to a knot then an exception"
+ " is thrown.")
+ .def("IsSegmentValueMonotonic", &::_IsSegmentValueMonotonic_2,
+ "True if the segment between the two provided TsKeyFrames is"
+ " monotonically increasing or monotonically decreasing, i.e. no"
+ " extremes are present")
+ .def("IsVarying", &This::IsVarying,
+ "True if the value of the spline changes over time, "
+ "whether due to differing values among keyframes, "
+ "knot sides, or non-flat tangents")
+ .def("IsVaryingSignificantly", &This::IsVaryingSignificantly,
+ "True if the value of the spline changes over time, "
+ "more than a tiny amount, whether due to differing values among "
+ "keyframes, knot sides, or non-flat tangents")
+ ;
+
+ VtValueFromPython<TsSpline>();
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_AnimXEvaluator.h"
+
+#include "pxr/base/tf/pyEnum.h"
+
+#include <boost/python.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+using This = TsTest_AnimXEvaluator;
+
+
+static This*
+_ConstructEvaluator(
+ const This::AutoTanType autoTanType)
+{
+ return new This(autoTanType);
+}
+
+
+void wrapTsTest_AnimXEvaluator()
+{
+ // First the class object, so we can create a scope for it...
+ class_<This, bases<TsTest_Evaluator>>
+ classObj("TsTest_AnimXEvaluator", no_init);
+
+ // ...then the nested type wrappings, which require the scope...
+ TfPyWrapEnum<This::AutoTanType>();
+
+ // ...then the defs, which must occur after the nested type wrappings.
+ classObj
+
+ .def("__init__",
+ make_constructor(
+ &_ConstructEvaluator, default_call_policies(),
+ (arg("autoTanType") = This::AutoTanAuto)))
+
+ // Wrapping for Eval is inherited from TsTest_Evaluator.
+ ;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_Evaluator.h"
+#include "pxr/base/tf/pyResultConversions.h"
+
+#include <boost/python.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+using This = TsTest_Evaluator;
+
+
+void wrapTsTest_Evaluator()
+{
+ // TsTest_Evaluator is an abstract base class, so wrap without constructors.
+ // Concrete classes need their own wrapping, declaring this class as a base.
+ class_<This, boost::noncopyable>("TsTest_Evaluator", no_init)
+ .def("Eval", &This::Eval,
+ (arg("splineData"),
+ arg("sampleTimes")),
+ return_value_policy<TfPySequenceToList>())
+ .def("Sample", &This::Sample,
+ (arg("splineData"),
+ arg("tolerance")),
+ return_value_policy<TfPySequenceToList>())
+ .def("BakeInnerLoops", &This::BakeInnerLoops,
+ (arg("splineData")))
+ ;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_Museum.h"
+
+#include "pxr/base/tf/pyEnum.h"
+
+#include <boost/python.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+using This = TsTest_Museum;
+
+
+void wrapTsTest_Museum()
+{
+ // First the class object, so we can create a scope for it...
+ class_<This> classObj("TsTest_Museum", no_init);
+ scope classScope(classObj);
+
+ // ...then the nested type wrappings, which require the scope...
+ TfPyWrapEnum<This::DataId>();
+
+ // ...then the defs, which must occur after the nested type wrappings.
+ classObj
+
+ .def("GetData", &This::GetData)
+
+ ;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_SampleBezier.h"
+#include "pxr/base/tf/pyResultConversions.h"
+
+#include <boost/python.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+
+void wrapTsTest_SampleBezier()
+{
+ def("TsTest_SampleBezier", &TsTest_SampleBezier,
+ (arg("splineData"),
+ arg("numSamples")),
+ return_value_policy<TfPySequenceToList>());
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_SampleTimes.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+#include "pxr/base/tf/pyContainerConversions.h"
+#include "pxr/base/tf/pyResultConversions.h"
+#include "pxr/base/tf/stringUtils.h"
+#include "pxr/base/tf/diagnostic.h"
+
+#include <boost/python.hpp>
+#include <sstream>
+#include <string>
+#include <cstdio>
+
+using namespace boost::python;
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using This = TsTest_SampleTimes;
+
+
+// Return a full-precision python repr for a double value.
+static std::string
+_HexFloatRepr(const double num)
+{
+ // XXX: work around std::hexfloat apparently not working in our libstdc++ as
+ // of this writing.
+ char buf[100];
+ sprintf(buf, "float.fromhex('%a')", num);
+ return std::string(buf);
+}
+
+static std::string
+_SampleTimeRepr(const This::SampleTime &st)
+{
+ std::ostringstream result;
+
+ result << "Ts.TsTest_SampleTimes.SampleTime("
+ << _HexFloatRepr(st.time)
+ << ", " << st.pre
+ << ")";
+
+ return result.str();
+}
+
+static This*
+_ConstructSampleTimes(
+ const object ×)
+{
+ This *result = new This();
+
+ std::vector<This::SampleTime> sts;
+ if (!times.is_none())
+ {
+ extract<std::vector<This::SampleTime>> extractor(times);
+ if (extractor.check())
+ sts = extractor();
+ else
+ TF_CODING_ERROR("Unexpected type for times");
+ }
+ result->AddTimes(sts);
+
+ return result;
+}
+
+static std::string
+_SampleTimesRepr(const This ×)
+{
+ std::ostringstream result;
+
+ std::vector<std::string> stStrs;
+ for (const This::SampleTime &st : times.GetTimes())
+ stStrs.push_back(_SampleTimeRepr(st));
+
+ result << "Ts.TsTest_SampleTimes([" << TfStringJoin(stStrs, ", ") << "])";
+
+ return result.str();
+}
+
+
+void wrapTsTest_SampleTimes()
+{
+ // First the class object, so we can create a scope for it...
+ class_<This> classObj("TsTest_SampleTimes", no_init);
+ scope classScope(classObj);
+
+ // ...then the nested type wrappings, which require the scope...
+
+ class_<This::SampleTime>("SampleTime")
+ // Note default init is not suppressed, so automatically created.
+ .def(init<double>())
+ .def(init<double, bool>())
+ .def(init<const This::SampleTime&>())
+ .def("__repr__", &_SampleTimeRepr)
+ .def(self < self)
+ .def(self == self)
+ .def(self != self)
+ .def_readwrite("time", &This::SampleTime::time)
+ .def_readwrite("pre", &This::SampleTime::pre)
+ ;
+
+ TfPyRegisterStlSequencesFromPython<double>();
+ TfPyRegisterStlSequencesFromPython<This::SampleTime>();
+
+ // ...then the defs, which must occur after the nested type wrappings.
+ classObj
+
+ // This serves as a default constructor and a from-repr constructor.
+ .def("__init__",
+ make_constructor(
+ &_ConstructSampleTimes, default_call_policies(),
+ (arg("times") = object())))
+
+ .def("__repr__", &_SampleTimesRepr)
+
+ .def("AddTimes",
+ static_cast<void (This::*)(const std::vector<double>&)>(
+ &This::AddTimes))
+
+ .def("AddTimes",
+ static_cast<void (This::*)(const std::vector<This::SampleTime>&)>(
+ &This::AddTimes))
+
+ .def(init<const TsTest_SplineData&>())
+
+ .def("AddKnotTimes", &This::AddKnotTimes)
+
+ .def("AddUniformInterpolationTimes",
+ &This::AddUniformInterpolationTimes,
+ (arg("numSamples")))
+
+ .def("AddExtrapolationTimes", &This::AddExtrapolationTimes,
+ (arg("extrapolationFactor")))
+
+ .def("AddStandardTimes", &This::AddStandardTimes)
+
+ .def("GetTimes", &This::GetTimes,
+ return_value_policy<TfPySequenceToList>())
+
+ ;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_SplineData.h"
+#include "pxr/base/tf/enum.h"
+#include "pxr/base/tf/pyEnum.h"
+#include "pxr/base/tf/pyContainerConversions.h"
+#include "pxr/base/tf/pyResultConversions.h"
+#include "pxr/base/tf/stringUtils.h"
+#include "pxr/base/tf/diagnostic.h"
+
+#include <boost/python.hpp>
+#include <sstream>
+#include <string>
+#include <cstdio>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+using This = TsTest_SplineData;
+
+
+#define SET_MEMBER(result, type, member, value) \
+ if (!value.is_none()) \
+ { \
+ extract<type> extractor(value); \
+ if (extractor.check()) \
+ result->member = extractor(); \
+ else \
+ TF_CODING_ERROR("Unexpected type for " #member); \
+ }
+
+#define SET_METHOD(result, type, method, value) \
+ if (!value.is_none()) \
+ { \
+ extract<type> extractor(value); \
+ if (extractor.check()) \
+ result->method(extractor()); \
+ else \
+ TF_CODING_ERROR("Unexpected type for " #method); \
+ }
+
+// Return a full-precision python repr for a double value.
+static std::string
+_HexFloatRepr(const double num)
+{
+ // XXX: work around std::hexfloat apparently not working in our libstdc++ as
+ // of this writing.
+ char buf[100];
+ sprintf(buf, "float.fromhex('%a')", num);
+ return std::string(buf);
+}
+
+static This::Knot*
+_ConstructKnot(
+ const object &timeIn,
+ const object &nextSegInterpMethodIn,
+ const object &valueIn,
+ const object &preValueIn,
+ const object &preSlopeIn,
+ const object &postSlopeIn,
+ const object &preLenIn,
+ const object &postLenIn,
+ const object &preAutoIn,
+ const object &postAutoIn)
+{
+ This::Knot *result = new This::Knot();
+
+ SET_MEMBER(result, double, time, timeIn);
+ SET_MEMBER(result, This::InterpMethod,
+ nextSegInterpMethod, nextSegInterpMethodIn);
+ SET_MEMBER(result, double, value, valueIn);
+ SET_MEMBER(result, double, preValue, preValueIn);
+ SET_MEMBER(result, double, preSlope, preSlopeIn);
+ SET_MEMBER(result, double, postSlope, postSlopeIn);
+ SET_MEMBER(result, double, preLen, preLenIn);
+ SET_MEMBER(result, double, postLen, postLenIn);
+ SET_MEMBER(result, bool, preAuto, preAutoIn);
+ SET_MEMBER(result, bool, postAuto, postAutoIn);
+
+ if (!preValueIn.is_none())
+ result->isDualValued = true;
+
+ return result;
+}
+
+static std::string
+_KnotRepr(const This::Knot &kf)
+{
+ std::ostringstream result;
+ result << "Ts.TsTest_SplineData.Knot("
+ << "time = " << _HexFloatRepr(kf.time)
+ << ", nextSegInterpMethod = Ts.TsTest_SplineData."
+ << TfEnum::GetName(kf.nextSegInterpMethod)
+ << ", value = " << _HexFloatRepr(kf.value)
+ << ", preSlope = " << _HexFloatRepr(kf.preSlope)
+ << ", postSlope = " << _HexFloatRepr(kf.postSlope)
+ << ", preLen = " << _HexFloatRepr(kf.preLen)
+ << ", postLen = " << _HexFloatRepr(kf.postLen)
+ << ", preAuto = " << (kf.preAuto ? "True" : "False")
+ << ", postAuto = " << (kf.postAuto ? "True" : "False");
+
+ if (kf.isDualValued)
+ result << ", preValue = " << _HexFloatRepr(kf.preValue);
+
+ result << ")";
+
+ return result.str();
+}
+
+static This::InnerLoopParams*
+_ConstructInnerLoopParams(
+ const object &enabledIn,
+ const object &protoStartIn,
+ const object &protoEndIn,
+ const object &preLoopStartIn,
+ const object &postLoopEndIn,
+ const object &closedEndIn,
+ const object &valueOffsetIn)
+{
+ This::InnerLoopParams *result = new This::InnerLoopParams();
+
+ SET_MEMBER(result, bool, enabled, enabledIn);
+ SET_MEMBER(result, double, protoStart, protoStartIn);
+ SET_MEMBER(result, double, protoEnd, protoEndIn);
+ SET_MEMBER(result, double, preLoopStart, preLoopStartIn);
+ SET_MEMBER(result, double, postLoopEnd, postLoopEndIn);
+ SET_MEMBER(result, bool, closedEnd, closedEndIn);
+ SET_MEMBER(result, double, valueOffset, valueOffsetIn);
+
+ return result;
+}
+
+static std::string
+_InnerLoopParamsRepr(const This::InnerLoopParams &lp)
+{
+ std::ostringstream result;
+
+ result << "Ts.TsTest_SplineData.InnerLoopParams("
+ << "enabled = " << (lp.enabled ? "True" : "False")
+ << ", protoStart = " << _HexFloatRepr(lp.protoStart)
+ << ", protoEnd = " << _HexFloatRepr(lp.protoEnd)
+ << ", preLoopStart = " << _HexFloatRepr(lp.preLoopStart)
+ << ", postLoopEnd = " << _HexFloatRepr(lp.postLoopEnd)
+ << ", closedEnd = " << (lp.closedEnd ? "True" : "False")
+ << ", valueOffset = " << _HexFloatRepr(lp.valueOffset)
+ << ")";
+
+ return result.str();
+}
+
+static This::Extrapolation*
+_ConstructExtrapolation(
+ const This::ExtrapMethod method,
+ const double slope,
+ const This::LoopMode loopMode)
+{
+ This::Extrapolation *result = new This::Extrapolation();
+
+ result->method = method;
+ result->slope = slope;
+ result->loopMode = loopMode;
+
+ return result;
+}
+
+static std::string
+_ExtrapolationRepr(const This::Extrapolation &e)
+{
+ std::ostringstream result;
+
+ result << "Ts.TsTest_SplineData.Extrapolation("
+ << "method = Ts.TsTest_SplineData." << TfEnum::GetName(e.method);
+
+ if (e.method == This::ExtrapSloped)
+ result << ", slope = " << _HexFloatRepr(e.slope);
+ else if (e.method == This::ExtrapLoop)
+ result << ", loopMode = Ts.TsTest_SplineData."
+ << TfEnum::GetName(e.loopMode);
+
+ result << ")";
+
+ return result.str();
+}
+
+static This*
+_ConstructSplineData(
+ const bool isHermite,
+ const object &knots,
+ const object &preExtrap,
+ const object &postExtrap,
+ const object &loopParams)
+{
+ This *result = new This();
+
+ result->SetIsHermite(isHermite);
+
+ SET_METHOD(result, This::KnotSet, SetKnots, knots);
+ SET_METHOD(result, This::InnerLoopParams, SetInnerLoopParams, loopParams);
+ SET_METHOD(result, This::Extrapolation, SetPreExtrapolation, preExtrap);
+ SET_METHOD(result, This::Extrapolation, SetPostExtrapolation, postExtrap);
+
+ return result;
+}
+
+static std::string
+_SplineDataRepr(const This &data)
+{
+ std::ostringstream result;
+
+ result << "Ts.TsTest_SplineData("
+ << "isHermite = " << (data.GetIsHermite() ? "True" : "False")
+ << ", preExtrapolation = "
+ << _ExtrapolationRepr(data.GetPreExtrapolation())
+ << ", postExtrapolation = "
+ << _ExtrapolationRepr(data.GetPostExtrapolation());
+
+ const This::KnotSet &knots = data.GetKnots();
+ if (!knots.empty())
+ {
+ std::vector<std::string> kfStrs;
+ for (const This::Knot &kf : knots)
+ kfStrs.push_back(_KnotRepr(kf));
+
+ result << ", knots = [" << TfStringJoin(kfStrs, ", ") << "]";
+ }
+
+ if (data.GetInnerLoopParams().enabled)
+ {
+ result << ", innerLoopParams = "
+ << _InnerLoopParamsRepr(data.GetInnerLoopParams());
+ }
+
+ result << ")";
+
+ return result.str();
+}
+
+void wrapTsTest_SplineData()
+{
+ // First the class object, so we can create a scope for it...
+ class_<This> classObj("TsTest_SplineData", no_init);
+ scope classScope(classObj);
+
+ // ...then the nested type wrappings, which require the scope...
+
+ TfPyWrapEnum<This::InterpMethod>();
+ TfPyWrapEnum<This::ExtrapMethod>();
+ TfPyWrapEnum<This::LoopMode>();
+ TfPyWrapEnum<This::Feature>();
+
+ class_<This::Knot>("Knot", no_init)
+ .def(init<const This::Knot&>())
+ .def("__init__",
+ make_constructor(
+ &_ConstructKnot, default_call_policies(), (
+ arg("time") = object(),
+ arg("nextSegInterpMethod") = object(),
+ arg("value") = object(),
+ arg("preValue") = object(),
+ arg("preSlope") = object(),
+ arg("postSlope") = object(),
+ arg("preLen") = object(),
+ arg("postLen") = object(),
+ arg("preAuto") = object(),
+ arg("postAuto") = object()
+ )))
+ .def("__repr__", &_KnotRepr)
+ .def(self == self)
+ .def(self != self)
+ .def(self < self)
+ .def_readwrite("time", &This::Knot::time)
+ .def_readwrite(
+ "nextSegInterpMethod", &This::Knot::nextSegInterpMethod)
+ .def_readwrite("value", &This::Knot::value)
+ .def_readwrite("isDualValued", &This::Knot::isDualValued)
+ .def_readwrite("preValue", &This::Knot::preValue)
+ .def_readwrite("preSlope", &This::Knot::preSlope)
+ .def_readwrite("postSlope", &This::Knot::postSlope)
+ .def_readwrite("preLen", &This::Knot::preLen)
+ .def_readwrite("postLen", &This::Knot::postLen)
+ .def_readwrite("preAuto", &This::Knot::preAuto)
+ .def_readwrite("postAuto", &This::Knot::postAuto)
+ ;
+
+ class_<This::InnerLoopParams>("InnerLoopParams", no_init)
+ .def(init<const This::InnerLoopParams&>())
+ .def("__init__",
+ make_constructor(
+ &_ConstructInnerLoopParams, default_call_policies(), (
+ arg("enabled") = object(),
+ arg("protoStart") = object(),
+ arg("protoEnd") = object(),
+ arg("preLoopStart") = object(),
+ arg("postLoopEnd") = object(),
+ arg("closedEnd") = object(),
+ arg("valueOffset") = object()
+ )))
+ .def("__repr__", &_InnerLoopParamsRepr)
+ .def(self == self)
+ .def(self != self)
+ .def_readwrite("enabled", &This::InnerLoopParams::enabled)
+ .def_readwrite("protoStart", &This::InnerLoopParams::protoStart)
+ .def_readwrite("protoEnd", &This::InnerLoopParams::protoEnd)
+ .def_readwrite("preLoopStart", &This::InnerLoopParams::preLoopStart)
+ .def_readwrite("postLoopEnd", &This::InnerLoopParams::postLoopEnd)
+ .def_readwrite("closedEnd", &This::InnerLoopParams::closedEnd)
+ .def_readwrite("valueOffset", &This::InnerLoopParams::valueOffset)
+ .def("IsValid", &This::InnerLoopParams::IsValid)
+ ;
+
+ class_<This::Extrapolation>("Extrapolation", no_init)
+ .def(init<const This::Extrapolation&>())
+ .def("__init__",
+ make_constructor(
+ &_ConstructExtrapolation, default_call_policies(), (
+ arg("method") = This::ExtrapHeld,
+ arg("slope") = 0.0,
+ arg("loopMode") = This::LoopNone
+ )))
+ .def("__repr__", &_ExtrapolationRepr)
+ .def(self == self)
+ .def(self != self)
+ .def_readwrite("method", &This::Extrapolation::method)
+ .def_readwrite("slope", &This::Extrapolation::slope)
+ .def_readwrite("loopMode", &This::Extrapolation::loopMode)
+ ;
+
+ TfPyRegisterStlSequencesFromPython<double>();
+ TfPyRegisterStlSequencesFromPython<This::Knot>();
+
+ // ...then the defs, which must occur after the nested type wrappings.
+ classObj
+
+ .def("__init__",
+ make_constructor(
+ &_ConstructSplineData, default_call_policies(), (
+ arg("isHermite") = false,
+ arg("knots") = object(),
+ arg("preExtrapolation") = object(),
+ arg("postExtrapolation") = object(),
+ arg("innerLoopParams") = object()
+ )))
+
+ .def("__repr__", &_SplineDataRepr)
+
+ .def(self == self)
+ .def(self != self)
+
+ .def("SetIsHermite", &This::SetIsHermite,
+ (arg("isHermite")))
+
+ .def("AddKnot", &This::AddKnot,
+ (arg("knot")))
+
+ .def("SetKnots", &This::SetKnots,
+ (arg("knots")))
+
+ .def("SetPreExtrapolation", &This::SetPreExtrapolation,
+ (arg("preExtrap")))
+
+ .def("SetPostExtrapolation", &This::SetPostExtrapolation,
+ (arg("postExtrap")))
+
+ .def("SetInnerLoopParams", &This::SetInnerLoopParams,
+ (arg("params")))
+
+ .def("GetIsHermite", &This::GetIsHermite)
+
+ .def("GetKnots", &This::GetKnots,
+ return_value_policy<TfPySequenceToList>())
+
+ .def("GetPreExtrapolation", &This::GetPreExtrapolation,
+ return_value_policy<return_by_value>())
+
+ .def("GetPostExtrapolation", &This::GetPostExtrapolation,
+ return_value_policy<return_by_value>())
+
+ .def("GetInnerLoopParams", &This::GetInnerLoopParams,
+ return_value_policy<return_by_value>())
+
+ .def("GetRequiredFeatures", &This::GetRequiredFeatures)
+
+ .def("GetDebugDescription", &This::GetDebugDescription)
+
+ ;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_TsEvaluator.h"
+
+#include <boost/python.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+
+void wrapTsTest_TsEvaluator()
+{
+ class_<TsTest_TsEvaluator, bases<TsTest_Evaluator>>("TsTest_TsEvaluator")
+ // Default init is not suppressed, so automatically created.
+ ;
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/tsTest_Types.h"
+#include "pxr/base/tf/pyContainerConversions.h"
+
+#include <boost/python.hpp>
+#include <sstream>
+#include <string>
+#include <cstdio>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+
+// Return a full-precision python repr for a double value.
+static std::string
+_HexFloatRepr(const double num)
+{
+ // XXX: work around std::hexfloat apparently not working in our libstdc++ as
+ // of this writing.
+ char buf[100];
+ sprintf(buf, "float.fromhex('%a')", num);
+ return std::string(buf);
+}
+
+static std::string
+_SampleRepr(const TsTest_Sample &sample)
+{
+ std::ostringstream result;
+
+ result << "Ts.TsTest_Sample("
+ << _HexFloatRepr(sample.time)
+ << ", " << _HexFloatRepr(sample.value)
+ << ")";
+
+ return result.str();
+}
+
+
+void wrapTsTest_Types()
+{
+ class_<TsTest_Sample>("TsTest_Sample")
+ // Default init is not suppressed, so automatically created.
+ .def(init<double, double>())
+ .def(init<const TsTest_Sample&>())
+ .def("__repr__", &_SampleRepr)
+ .def_readwrite("time", &TsTest_Sample::time)
+ .def_readwrite("value", &TsTest_Sample::value)
+ ;
+
+ to_python_converter<
+ TsTest_SampleVec,
+ TfPySequenceToPython<TsTest_SampleVec>>();
+ TfPyContainerConversions::from_python_sequence<
+ TsTest_SampleVec,
+ TfPyContainerConversions::variable_capacity_policy>();
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#include "pxr/pxr.h"
+#include "pxr/base/ts/types.h"
+#include "pxr/base/ts/wrapUtils.h"
+
+#include "pxr/base/tf/pyContainerConversions.h"
+#include "pxr/base/tf/pyEnum.h"
+
+#include <boost/python.hpp>
+
+PXR_NAMESPACE_USING_DIRECTIVE
+
+using namespace boost::python;
+
+
+static void wrapValueSample()
+{
+ typedef TsValueSample This;
+
+ class_<This>
+ ("ValueSample",
+ "An individual sample. A sample is either a blur, "
+ "defining a rectangle, or linear, defining a line for "
+ "linear interpolation. In both cases the sample is "
+ "half-open on the right.",
+ no_init)
+
+ .def_readonly(
+ "isBlur", &This::isBlur, "True if a blur sample")
+ .def_readonly(
+ "leftTime", &This::leftTime, "Left side time (inclusive)")
+ .def_readonly(
+ "rightTime", &This::rightTime, "Right side time (exclusive)")
+
+ // We need to specify a return_value_policy for VtValues, since
+ // the default policy of add_property is return_internal_reference
+ // but VtValues don't provide an lvalue. Instead, we just copy
+ // them on read.
+ .add_property(
+ "leftValue",
+ make_getter(
+ &This::leftValue, return_value_policy<return_by_value>()),
+ "Value at left or, for blur, min value")
+ .add_property(
+ "rightValue",
+ make_getter(
+ &This::rightValue, return_value_policy<return_by_value>()),
+ "Value at right or, for blur, max value")
+ ;
+}
+
+
+void wrapTypes()
+{
+ TfPyWrapEnum<TsExtrapolationType>();
+
+ TfPyContainerConversions::tuple_mapping_pair<
+ std::pair<TsExtrapolationType, TsExtrapolationType> >();
+
+ TfPyContainerConversions::from_python_sequence<
+ std::set<double> , TfPyContainerConversions::set_policy >();
+
+ wrapValueSample();
+
+ Ts_AnnotatedBoolResult::Wrap<Ts_AnnotatedBoolResult>(
+ "_AnnotatedBoolResult", "reasonWhyNot");
+}
--- /dev/null
+//
+// Copyright 2023 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+// names, trademarks, service marks, or product names of the Licensor
+// and its affiliates, except as required to comply with Section 4(c) of
+// the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+
+#ifndef PXR_BASE_TS_WRAP_UTILS_H
+#define PXR_BASE_TS_WRAP_UTILS_H
+
+#include "pxr/base/tf/pyAnnotatedBoolResult.h"
+
+#include <string>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+
+// Annotated bool result for Python wrappings.
+struct Ts_AnnotatedBoolResult : public TfPyAnnotatedBoolResult<std::string>
+{
+ Ts_AnnotatedBoolResult(bool boolVal, const std::string &reasonWhyNot)
+ : TfPyAnnotatedBoolResult(boolVal, reasonWhyNot) {}
+};
+
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif