From: Pixneb Date: Fri, 12 Jan 2024 18:36:54 +0000 (-0800) Subject: Initial version of Ts library. X-Git-Tag: accepted/tizen/unified/x/20250428.070456~5^2~81 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=0edcf6b884f77415279b26b25aa0caafbb63861c;p=platform%2Fcore%2Fuifw%2FOpenUSD.git Initial version of Ts library. This library implements animation splines. It builds, but isn't used by anything yet. It's a first milestone that will allow us to begin co-developing with external partners and collecting public feedback. Much work remains. See the README for a rough list of tasks. (Internal change: 2310847) (Internal change: 2310875) (Internal change: 2310876) (Internal change: 2311059) (Internal change: 2311623) --- diff --git a/build_scripts/build_usd.py b/build_scripts/build_usd.py index 099ffbc1f..9a012cedc 100644 --- a/build_scripts/build_usd.py +++ b/build_scripts/build_usd.py @@ -1526,7 +1526,32 @@ def InstallEmbree(context, force, buildArgs): 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 @@ -1694,6 +1719,18 @@ def InstallUSD(context, force, buildArgs): 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"') @@ -2017,6 +2054,26 @@ subgroup.add_argument("--materialx", dest="build_materialx", action="store_true" 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: @@ -2161,6 +2218,11 @@ 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(), []) @@ -2231,6 +2293,9 @@ if context.buildImaging: 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 @@ -2359,6 +2424,22 @@ if PYSIDE in requiredDependencies: .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: @@ -2395,6 +2476,8 @@ summaryMsg += """\ Python docs: {buildPythonDocs} Documentation {buildHtmlDocs} Tests {buildTests} + Mayapy Tests: {buildMayapyTests} + AnimX Tests: {buildAnimXTests} Examples {buildExamples} Tutorials {buildTutorials} Tools {buildTools} @@ -2462,6 +2545,8 @@ summaryMsg = summaryMsg.format( 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) diff --git a/cmake/defaults/Options.cmake b/cmake/defaults/Options.cmake index 5b2e1fcd3..3291d9a40 100644 --- a/cmake/defaults/Options.cmake +++ b/cmake/defaults/Options.cmake @@ -47,6 +47,8 @@ option(PXR_ENABLE_HDF5_SUPPORT "Enable HDF5 backend in the Alembic plugin for US 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." @@ -214,4 +216,4 @@ if (${PXR_BUILD_PYTHON_DOCUMENTATION}) "PXR_ENABLE_PYTHON_SUPPORT=OFF") set(PXR_BUILD_PYTHON_DOCUMENTATION "OFF" CACHE BOOL "" FORCE) endif() -endif() \ No newline at end of file +endif() diff --git a/cmake/defaults/Packages.cmake b/cmake/defaults/Packages.cmake index e17fb863a..8bb28dbec 100644 --- a/cmake/defaults/Packages.cmake +++ b/cmake/defaults/Packages.cmake @@ -340,6 +340,10 @@ if(PXR_ENABLE_OSL_SUPPORT) 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 diff --git a/cmake/modules/FindAnimX.cmake b/cmake/modules/FindAnimX.cmake new file mode 100644 index 000000000..1b6c593c0 --- /dev/null +++ b/cmake/modules/FindAnimX.cmake @@ -0,0 +1,36 @@ +# +# 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 +) diff --git a/pxr/base/CMakeLists.txt b/pxr/base/CMakeLists.txt index 55a506403..1f0303f6f 100644 --- a/pxr/base/CMakeLists.txt +++ b/pxr/base/CMakeLists.txt @@ -7,6 +7,7 @@ set(DIRS work plug vt + ts # bin ) diff --git a/pxr/base/ts/CMakeLists.txt b/pxr/base/ts/CMakeLists.txt new file mode 100644 index 000000000..62a2e6a7a --- /dev/null +++ b/pxr/base/ts/CMakeLists.txt @@ -0,0 +1,237 @@ +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() diff --git a/pxr/base/ts/README.md b/pxr/base/ts/README.md new file mode 100644 index 000000000..0691a9b6c --- /dev/null +++ b/pxr/base/ts/README.md @@ -0,0 +1,195 @@ + +# 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. diff --git a/pxr/base/ts/TsTest_Comparator.py b/pxr/base/ts/TsTest_Comparator.py new file mode 100644 index 000000000..f4cb8c052 --- /dev/null +++ b/pxr/base/ts/TsTest_Comparator.py @@ -0,0 +1,84 @@ +# +# 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)) diff --git a/pxr/base/ts/TsTest_CompareBaseline.py b/pxr/base/ts/TsTest_CompareBaseline.py new file mode 100644 index 000000000..53607c910 --- /dev/null +++ b/pxr/base/ts/TsTest_CompareBaseline.py @@ -0,0 +1,278 @@ +# +# 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) diff --git a/pxr/base/ts/TsTest_Grapher.py b/pxr/base/ts/TsTest_Grapher.py new file mode 100644 index 000000000..95a9915f0 --- /dev/null +++ b/pxr/base/ts/TsTest_Grapher.py @@ -0,0 +1,455 @@ +# +# 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() diff --git a/pxr/base/ts/TsTest_MayapyDriver.py b/pxr/base/ts/TsTest_MayapyDriver.py new file mode 100644 index 000000000..b717406ff --- /dev/null +++ b/pxr/base/ts/TsTest_MayapyDriver.py @@ -0,0 +1,380 @@ +# +# 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() diff --git a/pxr/base/ts/TsTest_MayapyEvaluator.py b/pxr/base/ts/TsTest_MayapyEvaluator.py new file mode 100644 index 000000000..11e620eb2 --- /dev/null +++ b/pxr/base/ts/TsTest_MayapyEvaluator.py @@ -0,0 +1,216 @@ +# +# 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) diff --git a/pxr/base/ts/__init__.py b/pxr/base/ts/__init__.py new file mode 100644 index 000000000..0b2ca51e5 --- /dev/null +++ b/pxr/base/ts/__init__.py @@ -0,0 +1,37 @@ +# +# 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 diff --git a/pxr/base/ts/api.h b/pxr/base/ts/api.h new file mode 100644 index 000000000..1ed73f4d8 --- /dev/null +++ b/pxr/base/ts/api.h @@ -0,0 +1,48 @@ +// +// 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 diff --git a/pxr/base/ts/data.cpp b/pxr/base/ts/data.cpp new file mode 100644 index 000000000..9b7e75de4 --- /dev/null +++ b/pxr/base/ts/data.cpp @@ -0,0 +1,79 @@ +// +// 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 + +PXR_NAMESPACE_OPEN_SCOPE + +static const double Ts_slopeDiffMax = 1.0e-4; + +template <> +void +Ts_TypedData::ResetTangentSymmetryBroken() +{ + if (_knotType == TsKnotBezier) { + float slopeDiff = fabs(_GetLeftTangentSlope() - + _GetRightTangentSlope()); + if (slopeDiff >= Ts_slopeDiffMax) { + SetTangentSymmetryBroken(true); + } + } +} + +template <> +void +Ts_TypedData::ResetTangentSymmetryBroken() +{ + if (_knotType == TsKnotBezier) { + double slopeDiff = fabs(_GetLeftTangentSlope() - + _GetRightTangentSlope()); + if (slopeDiff >= Ts_slopeDiffMax) { + SetTangentSymmetryBroken(true); + } + } +} + +template <> +bool +Ts_TypedData::ValueCanBeInterpolated() const +{ + return std::isfinite(_GetRightValue()) && + (!_isDual || std::isfinite(_GetLeftValue())); +} + +template <> +bool +Ts_TypedData::ValueCanBeInterpolated() const +{ + return std::isfinite(_GetRightValue()) && + (!_isDual || std::isfinite(_GetLeftValue())); +} + +template class Ts_TypedData; +template class Ts_TypedData; + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/base/ts/data.h b/pxr/base/ts/data.h new file mode 100644 index 000000000..e3640c3d5 --- /dev/null +++ b/pxr/base/ts/data.h @@ -0,0 +1,981 @@ +// +// 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 +#include + +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 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 +class Ts_TypedData : public Ts_Data { +public: + typedef T ValueType; + typedef Ts_TypedData 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 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::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::extrapolatable) + { + const TsTime dx = right.GetTime() - GetTime(); + const TsTime dxInv = 1.0 / dx; + + const T y1 = GetValue().template Get(); + const T y2 = right.GetLeftValue().template Get(); + 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::zero); + } + } + + VtValue Extrapolate( + const VtValue &value, TsTime dt, const VtValue &slope) const override + { + if constexpr (TsTraits::extrapolatable) + { + const T v = value.template Get(); + const T s = slope.template Get(); + 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; + friend class Ts_EvalCache::interpolatable>; + + // A struct containing all the member variables that depend on type T. + template + struct _Values { + + explicit _Values( + V const& lhv=TsTraits::zero, + V const& rhv=TsTraits::zero, + V const& leftTangentSlope=TsTraits::zero, + V const& rightTangentSlope=TsTraits::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); + static constexpr bool _isSmall = (sizeof(_Values) <= _size); + + // Storage implementation for small types. + struct _LocalStorage + { + _LocalStorage(_Values &&values) + : _data(std::move(values)) {} + + const _Values& Get() const { return _data; } + _Values& GetMutable() { return _data; } + + _Values _data; + }; + + // Storage implementation for large types. + struct _HeapStorage + { + _HeapStorage(_Values &&values) + : _data(new _Values(std::move(values))) {} + + // Copy constructor: deep-copies data. + _HeapStorage(const _HeapStorage &other) + : _data(new _Values(other.Get())) {} + + const _Values& Get() const { return *_data; } + _Values& GetMutable() { return *_data; } + + std::unique_ptr<_Values> _data; + }; + + // Select storage implementation. + using _Storage = + typename std::conditional< + _isSmall, _LocalStorage, _HeapStorage>::type; + + public: + // Construct from _Values rvalue. + explicit _ValuesHolder(_Values &&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& Get() const { return _storage.Get(); } + _Values& 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), + "_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 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 + void New(const T &val) + { + new (&_storage) Ts_TypedData(val); + } + + // Wrapper for general constructor. + template + void New( + const TsTime &t, + bool isDual, + const T &leftValue, + const T &rightValue, + const T &leftTangentSlope, + const T &rightTangentSlope) + { + new (&_storage) Ts_TypedData( + t, isDual, leftValue, rightValue, + leftTangentSlope, rightTangentSlope); + } + + // Copy constructor. + template + void New(const Ts_TypedData &other) + { + new (&_storage) Ts_TypedData(other); + } + + // Explicit destructor. Clients call this method from their destructors, + // and prior to calling New to replace an existing knot. + void Destroy() + { + reinterpret_cast(&_storage)->~Ts_Data(); + } + + // Const accessor. + const Ts_Data* Get() const + { + return reinterpret_cast(&_storage); + } + + // Non-const accessor. + Ts_Data* GetMutable() + { + return reinterpret_cast(&_storage); + } + +private: + // Our buffer is sized for Ts_TypedData. This is always the same size + // regardless of T; see Ts_TypedData::_ValuesHolder. + using _Storage = + typename std::aligned_storage< + sizeof(Ts_TypedData), sizeof(void*)>::type; + +private: + _Storage _storage; +}; + +//////////////////////////////////////////////////////////////////////// +// Ts_TypedData + +template +Ts_TypedData::Ts_TypedData(const T& value) : + _values(_Values(value,value)), + _leftTangentLength(0.0), + _rightTangentLength(0.0), + _knotType(TsKnotHeld), + _isDual(false), + _tangentSymmetryBroken(false) +{ +} + +template +Ts_TypedData::Ts_TypedData( + const TsTime &t, + bool isDual, + const T& leftValue, + const T& rightValue, + const T& leftTangentSlope, + const T& rightTangentSlope) : + _values(_Values(leftValue,rightValue, + leftTangentSlope,rightTangentSlope)), + _leftTangentLength(0.0), + _rightTangentLength(0.0), + _knotType(TsKnotHeld), + _isDual(isDual), + _tangentSymmetryBroken(false) +{ + SetTime(t); +} + +template +void +Ts_TypedData::CloneInto(Ts_PolymorphicDataHolder *holder) const +{ + holder->New(*this); +} + +template +std::shared_ptr +Ts_TypedData::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 const* typedKf2 = + static_cast const*>(kf2); + + // Construct and return a new EvalCache of the appropriate type. + return std::make_shared< + Ts_EvalCache::interpolatable>>(this, typedKf2); +} + +template +std::shared_ptr::interpolatable> > +Ts_TypedData::CreateTypedEvalCache(Ts_Data const* kf2) const +{ + Ts_TypedData const* typedKf2 = + static_cast const*>(kf2); + + return std::shared_ptr::interpolatable> >( + new Ts_EvalCache::interpolatable>(this, typedKf2)); +} + +template +VtValue +Ts_TypedData +::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 const* typedKf2 = + static_cast const*>(kf2); + + return Ts_EvalCache::interpolatable>(this, typedKf2) + .Eval(time); +} + +template +VtValue +Ts_TypedData +::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 const* typedKf2 = + static_cast const*>(kf2); + + return Ts_EvalCache::interpolatable>(this, typedKf2) + .EvalDerivative(time); +} + +template +bool +Ts_TypedData::operator==(const Ts_Data &rhs) const +{ + if (!TsTraits::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 +TsKnotType +Ts_TypedData::GetKnotType() const +{ + return _knotType; +} + +template +void +Ts_TypedData::SetKnotType( TsKnotType knotType ) +{ + std::string reason; + + if (!CanSetKnotType(knotType, &reason)) { + TF_CODING_ERROR(reason); + return; + } + + _knotType = knotType; +} + +template +bool +Ts_TypedData::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::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 +VtValue +Ts_TypedData::GetValue() const +{ + return VtValue(_GetRightValue()); +} + +template +VtValue +Ts_TypedData::GetValueDerivative() const +{ + if (TsTraits::supportsTangents) { + return GetRightTangentSlope(); + } else { + return VtValue(TsTraits::zero); + } +} + +template +void +Ts_TypedData::SetValue( VtValue val ) +{ + VtValue v = val.Cast(); + if (!v.IsEmpty()) { + _SetRightValue(v.Get()); + 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 +bool +Ts_TypedData::GetIsDualValued() const +{ + return _isDual; +} + +template +void +Ts_TypedData::SetIsDualValued( bool isDual ) +{ + if (isDual && !TsTraits::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 +VtValue +Ts_TypedData::GetLeftValue() const +{ + return VtValue(_isDual ? _GetLeftValue() : _GetRightValue()); +} + +template +VtValue +Ts_TypedData::GetLeftValueDerivative() const +{ + if (TsTraits::supportsTangents) { + return GetLeftTangentSlope(); + } else { + return VtValue(TsTraits::zero); + } +} + +template +void +Ts_TypedData::SetLeftValue( VtValue val ) +{ + if (!TsTraits::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(); + if (!v.IsEmpty()) { + _SetLeftValue(v.Get()); + 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 +VtValue +Ts_TypedData::GetZero() const +{ + return VtValue(TsTraits::zero); +} + +template +bool +Ts_TypedData::ValueCanBeInterpolated() const +{ + return TsTraits::interpolatable; +} + +template +bool +Ts_TypedData::HasTangents() const +{ + return TsTraits::supportsTangents && _knotType == TsKnotBezier; +} + +template +bool +Ts_TypedData::ValueTypeSupportsTangents() const +{ + // Oddly, linear and held knots have settable tangents. Animators use + // this when switching Beziers to Held and then back again. + return TsTraits::supportsTangents; +} + +template +VtValue +Ts_TypedData::GetLeftTangentSlope() const +{ + if (!TsTraits::supportsTangents) { + TF_CODING_ERROR("keyframes of type '%s' do not have tangents", + ArchGetDemangled(typeid(ValueType)).c_str()); + return VtValue(); + } + + return VtValue(_GetLeftTangentSlope()); +} + +template +VtValue +Ts_TypedData::GetRightTangentSlope() const +{ + if (!TsTraits::supportsTangents) { + TF_CODING_ERROR("keyframes of type '%s' do not have tangents", + ArchGetDemangled(typeid(ValueType)).c_str() ); + return VtValue(); + } + + return VtValue(_GetRightTangentSlope()); +} + +template +TsTime +Ts_TypedData::GetLeftTangentLength() const +{ + if (!TsTraits::supportsTangents) { + TF_CODING_ERROR("keyframes of type '%s' do not have tangents", + ArchGetDemangled(typeid(ValueType)).c_str()); + return 0; + } + + return _leftTangentLength; +} + +template +TsTime +Ts_TypedData::GetRightTangentLength() const +{ + if (!TsTraits::supportsTangents) { + TF_CODING_ERROR("keyframes of type '%s' do not have tangents", + ArchGetDemangled(typeid(ValueType)).c_str()); + return 0; + } + + return _rightTangentLength; +} + +template +void +Ts_TypedData::SetLeftTangentSlope( VtValue val ) +{ + if (!TsTraits::supportsTangents) { + TF_CODING_ERROR("keyframes of type '%s' do not have tangents", + ArchGetDemangled(typeid(ValueType)).c_str()); + return; + } + + VtValue v = val.Cast(); + if (!v.IsEmpty()) { + _SetLeftTangentSlope(val.Get()); + } else { + TF_CODING_ERROR("cannot convert type '%s' to '%s' to assign to " + "keyframe", val.GetTypeName().c_str(), + ArchGetDemangled(typeid(ValueType)).c_str()); + } +} + +template +void +Ts_TypedData::SetRightTangentSlope( VtValue val ) +{ + if (!TsTraits::supportsTangents) { + TF_CODING_ERROR("keyframes of type '%s' do not have tangents", + ArchGetDemangled(typeid(ValueType)).c_str()); + return; + } + + VtValue v = val.Cast(); + if (!v.IsEmpty()) { + _SetRightTangentSlope(val.Get()); + } 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 +void +Ts_TypedData::SetLeftTangentLength( TsTime newLen ) +{ + if (!TsTraits::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 +void +Ts_TypedData::SetRightTangentLength( TsTime newLen ) +{ + if (!TsTraits::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 +bool +Ts_TypedData::GetTangentSymmetryBroken() const +{ + if (!TsTraits::supportsTangents) { + TF_CODING_ERROR("keyframes of type '%s' do not have tangents", + ArchGetDemangled(typeid(ValueType)).c_str()); + return false; + } + + return _tangentSymmetryBroken; +} + +template +void +Ts_TypedData::SetTangentSymmetryBroken( bool broken ) +{ + if (!TsTraits::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 +void +Ts_TypedData::ResetTangentSymmetryBroken() +{ + // do nothing -- no tangents +} + +// Declare specializations for float and double. +// Definitions are in Data.cpp. +template <> +TS_API void +Ts_TypedData::ResetTangentSymmetryBroken(); + +template <> +TS_API void +Ts_TypedData::ResetTangentSymmetryBroken(); + +template <> +TS_API bool +Ts_TypedData::ValueCanBeInterpolated() const; + +template <> +TS_API bool +Ts_TypedData::ValueCanBeInterpolated() const; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/diff.cpp b/pxr/base/ts/diff.cpp new file mode 100644 index 000000000..f24b6fa13 --- /dev/null +++ b/pxr/base/ts/diff.cpp @@ -0,0 +1,762 @@ +// +// 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 + +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::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::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 diff --git a/pxr/base/ts/diff.h b/pxr/base/ts/diff.h new file mode 100644 index 000000000..3a070cbf3 --- /dev/null +++ b/pxr/base/ts/diff.h @@ -0,0 +1,52 @@ +// +// 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 diff --git a/pxr/base/ts/evalCache.cpp b/pxr/base/ts/evalCache.cpp new file mode 100644 index 000000000..b2c979ad8 --- /dev/null +++ b/pxr/base/ts/evalCache.cpp @@ -0,0 +1,80 @@ +// +// 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::New(const TsKeyFrame &kf1, + const TsKeyFrame &kf2) +{ + return static_cast*>( + Ts_GetKeyFrameData(kf1))-> + CreateTypedEvalCache(Ts_GetKeyFrameData(kf2)); +} + +std::shared_ptr > +Ts_EvalCache::New(const TsKeyFrame &kf1, + const TsKeyFrame &kf2) +{ + return static_cast*>( + Ts_GetKeyFrameData(kf1))-> + CreateTypedEvalCache(Ts_GetKeyFrameData(kf2)); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/base/ts/evalCache.h b/pxr/base/ts/evalCache.h new file mode 100644 index 000000000..984f4e459 --- /dev/null +++ b/pxr/base/ts/evalCache.h @@ -0,0 +1,685 @@ +// +// 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 + +PXR_NAMESPACE_OPEN_SCOPE + +class TsKeyFrame; +template class Ts_TypedData; + +// Bezier data. This holds two beziers (time and value) as both points +// and the coefficients of a cubic polynomial. +template +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 +Ts_Bezier::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 +void +Ts_Bezier::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 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 + static void _SetupBezierGeometry(TsTime* timePoints, T* valuePoints, + const Ts_TypedData* kf1, + const Ts_TypedData* kf2); + + // Compute the time coordinate of the 2nd Bezier control point. This + // synthesizes tangents for held and linear knots. + template + static TsTime _GetBezierPoint2Time(const Ts_TypedData* kf1, + const Ts_TypedData* kf2); + + // Compute the time coordinate of the 3rd Bezier control point. This + // synthesizes tangents for held and linear knots. + template + static TsTime _GetBezierPoint3Time(const Ts_TypedData* kf1, + const Ts_TypedData* kf2); + + // Compute the value coordinate of the 2nd Bezier control point. This + // synthesizes tangents for held and linear knots. + template + static T _GetBezierPoint2Value(const Ts_TypedData* kf1, + const Ts_TypedData* kf2); + + // Compute the value coordinate of the 3rd Bezier control point. This + // synthesizes tangents for held and linear knots. + template + static T _GetBezierPoint3Value(const Ts_TypedData* kf1, + const Ts_TypedData* kf2); + + // Compute the value coordinate of the 4th Bezier control point. This + // synthesizes tangents for held and linear knots. + template + static T _GetBezierPoint4Value(const Ts_TypedData* kf1, + const Ts_TypedData* kf2); +}; + +template ::interpolatable > +class Ts_EvalCache; + +template +class Ts_EvalQuaternionCache : public Ts_UntypedEvalCache { +protected: + static_assert(std::is_same::value + || std::is_same::value + , "T must be Quatd or Quatf"); + Ts_EvalQuaternionCache(const Ts_EvalQuaternionCache * rhs); + Ts_EvalQuaternionCache(const Ts_TypedData* kf1, + const Ts_TypedData* 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* kf1, const Ts_TypedData* kf2); + double _kf1_time, _kf2_time; + T _kf1_value, _kf2_value; + TsKnotType _kf1_knot_type; +}; + +template<> +class Ts_EvalCache final + : public Ts_EvalQuaternionCache { +public: + Ts_EvalCache(const Ts_EvalCache *rhs) : + Ts_EvalQuaternionCache(rhs) {} + Ts_EvalCache(const Ts_TypedData* kf1, + const Ts_TypedData* kf2) : + Ts_EvalQuaternionCache(kf1, kf2) {} + Ts_EvalCache(const TsKeyFrame & kf1, const TsKeyFrame & kf2) : + Ts_EvalQuaternionCache(kf1, kf2) {} + + typedef std::shared_ptr > 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 final + : public Ts_EvalQuaternionCache { +public: + Ts_EvalCache(const Ts_EvalCache *rhs) : + Ts_EvalQuaternionCache(rhs) {} + Ts_EvalCache(const Ts_TypedData* kf1, + const Ts_TypedData* kf2) : + Ts_EvalQuaternionCache(kf1, kf2) {} + Ts_EvalCache(const TsKeyFrame & kf1, const TsKeyFrame & kf2) : + Ts_EvalQuaternionCache(kf1, kf2) {} + + typedef std::shared_ptr > 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 +class Ts_EvalCache final : public Ts_UntypedEvalCache { +public: + Ts_EvalCache(const Ts_EvalCache * rhs); + Ts_EvalCache(const Ts_TypedData* kf1, const Ts_TypedData* 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 > 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 +class Ts_EvalCache final : public Ts_UntypedEvalCache { +public: + Ts_EvalCache(const Ts_EvalCache * rhs); + Ts_EvalCache(const Ts_TypedData* kf1, const Ts_TypedData* 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* GetBezier() const; + + typedef std::shared_ptr > 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* kf1, const Ts_TypedData* kf2); + +private: + bool _interpolate; + + // Value to use when _interpolate is false. + T _value; + + Ts_Bezier _cache; +}; + +//////////////////////////////////////////////////////////////////////// +// Ts_UntypedEvalCache + +template +TsTime +Ts_UntypedEvalCache::_GetBezierPoint2Time(const Ts_TypedData* kf1, + const Ts_TypedData* 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 +TsTime +Ts_UntypedEvalCache::_GetBezierPoint3Time(const Ts_TypedData* kf1, + const Ts_TypedData* 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 +T +Ts_UntypedEvalCache::_GetBezierPoint2Value(const Ts_TypedData* kf1, + const Ts_TypedData* 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 +T +Ts_UntypedEvalCache::_GetBezierPoint3Value(const Ts_TypedData* kf1, + const Ts_TypedData* 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 +T +Ts_UntypedEvalCache::_GetBezierPoint4Value(const Ts_TypedData* kf1, + const Ts_TypedData* 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 +void +Ts_UntypedEvalCache::_SetupBezierGeometry( + TsTime* timePoints, T* valuePoints, + const Ts_TypedData* kf1, const Ts_TypedData* 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 +Ts_EvalCache::Ts_EvalCache(const Ts_EvalCache * rhs) +{ + _value = rhs->_value; +} + +template +Ts_EvalCache::Ts_EvalCache(const Ts_TypedData* kf1, + const Ts_TypedData* kf2) +{ + if (!kf1 || !kf2) { + TF_CODING_ERROR("Constructing an Ts_EvalCache from invalid keyframes"); + return; + } + + _value = kf1->_GetRightValue(); +} + +template +Ts_EvalCache::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 *data = + static_cast const*>(Ts_GetKeyFrameData(kf1)); + + _value = data->_GetRightValue(); +} + +template +VtValue +Ts_EvalCache::Eval(TsTime t) const { + return VtValue(TypedEval(t)); +} + +template +VtValue +Ts_EvalCache::EvalDerivative(TsTime t) const { + return VtValue(TypedEvalDerivative(t)); +} + +template +T +Ts_EvalCache::TypedEval(TsTime) const +{ + return _value; +} + +template +T +Ts_EvalCache::TypedEvalDerivative(TsTime) const +{ + return TsTraits::zero; +} + +//////////////////////////////////////////////////////////////////////// +// Ts_EvalCache interpolatable + +template +Ts_EvalCache::Ts_EvalCache(const Ts_EvalCache * rhs) +{ + _interpolate = rhs->_interpolate; + _value = rhs->_value; + _cache = rhs->_cache; +} + +template +Ts_EvalCache::Ts_EvalCache(const Ts_TypedData* kf1, + const Ts_TypedData* kf2) +{ + _Init(kf1,kf2); +} + +template +Ts_EvalCache::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 const*>(Ts_GetKeyFrameData(kf1)), + static_cast const*>(Ts_GetKeyFrameData(kf2))); +} + +template +void +Ts_EvalCache::_Init( + const Ts_TypedData* kf1, + const Ts_TypedData* 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 +VtValue +Ts_EvalCache::Eval(TsTime t) const { + return VtValue(TypedEval(t)); +} + +template +VtValue +Ts_EvalCache::EvalDerivative(TsTime t) const { + return VtValue(TypedEvalDerivative(t)); +} + +template +T +Ts_EvalCache::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 +T +Ts_EvalCache::TypedEvalDerivative(TsTime time) const +{ + if (!TsTraits::supportsTangents || !_interpolate) { + return TsTraits::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 +const Ts_Bezier* +Ts_EvalCache::GetBezier() const +{ + return &_cache; +} + +template +std::shared_ptr > +Ts_EvalCache::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*>( + Ts_GetKeyFrameData(kf1))-> + CreateTypedEvalCache(Ts_GetKeyFrameData(kf2)); +} + +template +std::shared_ptr > +Ts_EvalCache::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*>( + Ts_GetKeyFrameData(kf1))-> + CreateTypedEvalCache(Ts_GetKeyFrameData(kf2)); +} + +//////////////////////////////////////////////////////////////////////// +// Ts_EvalQuaternionCache + +template +Ts_EvalQuaternionCache::Ts_EvalQuaternionCache( + const Ts_EvalQuaternionCache * 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 +Ts_EvalQuaternionCache::Ts_EvalQuaternionCache( + const Ts_TypedData* kf1, const Ts_TypedData* kf2) +{ + _Init(kf1,kf2); +} + +template +Ts_EvalQuaternionCache::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 const*>(Ts_GetKeyFrameData(kf1)), + static_cast const*>(Ts_GetKeyFrameData(kf2))); +} + +template +void +Ts_EvalQuaternionCache::_Init( + const Ts_TypedData* kf1, + const Ts_TypedData* 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 +VtValue +Ts_EvalQuaternionCache::Eval(TsTime t) const { + return VtValue(TypedEval(t)); +} + +template +T Ts_EvalQuaternionCache::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 +VtValue Ts_EvalQuaternionCache::EvalDerivative(TsTime t) const { + return VtValue(TypedEvalDerivative(t)); +} + +template +T Ts_EvalQuaternionCache::TypedEvalDerivative(TsTime) const { + return TsTraits::zero; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/evalUtils.cpp b/pxr/base/ts/evalUtils.cpp new file mode 100644 index 000000000..6c2540509 --- /dev/null +++ b/pxr/base/ts/evalUtils.cpp @@ -0,0 +1,1100 @@ +// +// 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 + +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() && + kf2LeftVtVal.IsHolding() && + kf1RightTangentSlopeVtVal.IsHolding() && + kf2LeftTangentSlopeVtVal.IsHolding()) + { + monotonic = true; + //get Bezier control points + double x0 = kf1VtVal.Get(); + double x1 = kf1VtVal.Get() + + ( kf1.GetRightTangentSlope().Get() * + kf1.GetRightTangentLength()); + double x2 = kf2LeftVtVal.Get() - + ( kf2.GetLeftTangentSlope().Get() * + kf2.GetLeftTangentLength()); + double x3 = kf2LeftVtVal.Get(); + // 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 +_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 + static std::pair +_GetBezierRange( const Ts_Bezier* bezier, + double startTime, double endTime ) +{ + T min = std::numeric_limits::infinity(); + T max = -std::numeric_limits::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 +static std::pair +_GetSegmentRange(const Ts_EvalCache * cache, + double startTime, double endTime ) +{ + return _GetBezierRange(cache->GetBezier(), startTime, endTime); +} + +template +static std::pair +_GetCurveRange( const TsSpline & val, double startTime, double endTime ) +{ + T min = std::numeric_limits::infinity(); + T max = -std::numeric_limits::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(); + 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(); + 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 cache(*i, *i2); + + std::pair range = + _GetSegmentRange(&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 +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(val, startTime, endTime); + } + else if (TfSafeTypeCompare(t, typeid(float))) { + return _GetCurveRange(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 +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 +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 +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 tmpBezier(timeBezier, valueBezier); + std::pair range = + _GetBezierRange(&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 +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 +static void +_SampleSegment(const Ts_EvalCache::interpolatable> * cache, + double startTime, double endTime, + double timeScale, double valueScale, double tolerance, + TsSamples & samples ) +{ + const Ts_Bezier* 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 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 +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 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 cache(*i, *i2); + + _SampleSegment(&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(val, startTime, endTime, + timeScale, valueScale, tolerance, samples); + } + else if (TfSafeTypeCompare(t, typeid(float))) { + _EvalCurve(val, startTime, endTime, + timeScale, valueScale, tolerance, samples); + } + else { + _EvalLinear(val, startTime, endTime, samples); + } + + return samples; +} + +//////////////////////////////////////////////////////////////////////// +// Breakdown + +template +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 cache(*spline.begin(), *spline.rbegin()); + + // Get the Bezier from the cache + const Ts_Bezier* 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(k, k1, k2, k3); + } + else if (TfSafeTypeCompare(v.GetTypeid(), typeid(float))) { + _Breakdown(k, k1, k2, k3); + } + else { + // No tangents for this value type so nothing to do + } +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/base/ts/evalUtils.h b/pxr/base/ts/evalUtils.h new file mode 100644 index 000000000..5ee330872 --- /dev/null +++ b/pxr/base/ts/evalUtils.h @@ -0,0 +1,84 @@ +// +// 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 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 diff --git a/pxr/base/ts/evaluator.h b/pxr/base/ts/evaluator.h new file mode 100644 index 000000000..0d392c518 --- /dev/null +++ b/pxr/base/ts/evaluator.h @@ -0,0 +1,168 @@ +// +// 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 + +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 +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 > > _segments; + + // The spline being evaluated. + TsSpline _spline; +}; + +template +TsEvaluator::TsEvaluator() +{ +} + +template +TsEvaluator::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 > segmentCache = + Ts_EvalCache::New(*splItr, *iAfterTime); + + if (TF_VERIFY(segmentCache)) { + _segments.push_back(segmentCache); + } + } + + } +} + +template +T +TsEvaluator::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(); + } + + // If we're evaluating an empty spline, fall back to zero. + return TsTraits::zero; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/keyFrame.cpp b/pxr/base/ts/keyFrame.cpp new file mode 100644 index 000000000..0ffef4395 --- /dev/null +++ b/pxr/base/ts/keyFrame.cpp @@ -0,0 +1,426 @@ +// +// 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( 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::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 diff --git a/pxr/base/ts/keyFrame.h b/pxr/base/ts/keyFrame.h new file mode 100644 index 000000000..8eeb70f8c --- /dev/null +++ b/pxr/base/ts/keyFrame.h @@ -0,0 +1,434 @@ +// +// 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 +#include + +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. +/// +/// Note: 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 + TsKeyFrame( const TsTime & time, + const T & val, + TsKnotType knotType = TsKnotLinear, + const T & leftTangentSlope = TsTraits::zero, + const T & rightTangentSlope = TsTraits::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 + TsKeyFrame( const TsTime & time, + const T & lhv, + const T & rhv, + TsKnotType knotType = TsKnotLinear, + const T & leftTangentSlope = TsTraits::zero, + const T & rightTangentSlope = TsTraits::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 +TsKeyFrame::TsKeyFrame( const TsTime & time, + const T & val, + TsKnotType knotType, + const T & leftTangentSlope, + const T & rightTangentSlope, + TsTime leftTangentLength, + TsTime rightTangentLength) +{ + static_assert( TsTraits::isSupportedSplineValueType ); + + _holder.New(time, false /*isDual*/, + val, val, leftTangentSlope, rightTangentSlope); + + _InitializeKnotType(knotType); + _InitializeTangentLength(leftTangentLength,rightTangentLength); +} + +template +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::isSupportedSplineValueType ); + + _holder.New(time, true /*isDual*/, lhv, rhv, + leftTangentSlope, rightTangentSlope); + + _InitializeKnotType(knotType); + _InitializeTangentLength(leftTangentLength,rightTangentLength); +} + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/keyFrameMap.cpp b/pxr/base/ts/keyFrameMap.cpp new file mode 100644 index 000000000..49a99799e --- /dev/null +++ b/pxr/base/ts/keyFrameMap.cpp @@ -0,0 +1,170 @@ +// +// 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 +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 diff --git a/pxr/base/ts/keyFrameMap.h b/pxr/base/ts/keyFrameMap.h new file mode 100644 index 000000000..84ba49c8d --- /dev/null +++ b/pxr/base/ts/keyFrameMap.h @@ -0,0 +1,242 @@ +// +// 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::iterator iterator; + typedef std::vector::const_iterator const_iterator; + typedef std::vector::reverse_iterator reverse_iterator; + typedef std::vector::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& 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 + 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 _data; +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/keyFrameUtils.cpp b/pxr/base/ts/keyFrameUtils.cpp new file mode 100644 index 000000000..fcb5d59f4 --- /dev/null +++ b/pxr/base/ts/keyFrameUtils.cpp @@ -0,0 +1,303 @@ +// +// 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 +Ts_GetClosestKeyFramesSurrounding( + const TsKeyFrameMap &keyframes, + const TsTime targetTime ) +{ + std::pair 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() ) + v0dbl = v0.UncheckedGet(); + else if ( v0.IsHolding() ) + v0dbl = v0.UncheckedGet(); + else + // Not either, so use == + return v0 == v1; + + // Get out the v1 val if a float or double + if ( v1.IsHolding() ) + v1dbl = v1.UncheckedGet(); + else if ( v1.IsHolding() ) + v1dbl = v1.UncheckedGet(); + else + // Not either, so use == + return v0 == v1; + + return TfAbs(v0dbl - v1dbl) < EPS; +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/pxr/base/ts/keyFrameUtils.h b/pxr/base/ts/keyFrameUtils.h new file mode 100644 index 000000000..51e13484f --- /dev/null +++ b/pxr/base/ts/keyFrameUtils.h @@ -0,0 +1,92 @@ +// +// 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 +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 diff --git a/pxr/base/ts/loopParams.cpp b/pxr/base/ts/loopParams.cpp new file mode 100644 index 000000000..9956eda25 --- /dev/null +++ b/pxr/base/ts/loopParams.cpp @@ -0,0 +1,143 @@ +// +// 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_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 diff --git a/pxr/base/ts/loopParams.h b/pxr/base/ts/loopParams.h new file mode 100644 index 000000000..2d96a314b --- /dev/null +++ b/pxr/base/ts/loopParams.h @@ -0,0 +1,110 @@ +// +// 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 +#include + +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 diff --git a/pxr/base/ts/mathUtils.cpp b/pxr/base/ts/mathUtils.cpp new file mode 100644 index 000000000..aff34638a --- /dev/null +++ b/pxr/base/ts/mathUtils.cpp @@ -0,0 +1,223 @@ +// +// 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 diff --git a/pxr/base/ts/mathUtils.h b/pxr/base/ts/mathUtils.h new file mode 100644 index 000000000..62741b24c --- /dev/null +++ b/pxr/base/ts/mathUtils.h @@ -0,0 +1,88 @@ +// +// 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 +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 +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 +T +Ts_EvalCubic(const T c[4], double u) +{ + return u * (u * (u * c[3] + c[2]) + c[1]) + c[0]; +} + +template +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 diff --git a/pxr/base/ts/module.cpp b/pxr/base/ts/module.cpp new file mode 100644 index 000000000..73a4421f6 --- /dev/null +++ b/pxr/base/ts/module.cpp @@ -0,0 +1,48 @@ +// +// 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 +} diff --git a/pxr/base/ts/moduleDeps.cpp b/pxr/base/ts/moduleDeps.cpp new file mode 100644 index 000000000..7abee1c4b --- /dev/null +++ b/pxr/base/ts/moduleDeps.cpp @@ -0,0 +1,50 @@ +// +// 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 + +PXR_NAMESPACE_OPEN_SCOPE + +TF_REGISTRY_FUNCTION(TfScriptModuleLoader) { + // List of direct dependencies for this library. + const std::vector 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 + + diff --git a/pxr/base/ts/pch.h b/pxr/base/ts/pch.h new file mode 100644 index 000000000..646797e89 --- /dev/null +++ b/pxr/base/ts/pch.h @@ -0,0 +1,140 @@ +// +// 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 +#endif +#if defined(ARCH_OS_LINUX) +#include +#include +#endif +#if defined(ARCH_OS_WINDOWS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + + +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef PXR_PYTHON_SUPPORT_ENABLED +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__APPLE__) // Fix breakage caused by Python's pyport.h. +#undef tolower +#undef toupper +#endif +#endif // PXR_PYTHON_SUPPORT_ENABLED +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef PXR_PYTHON_SUPPORT_ENABLED +#include "pxr/base/tf/pySafePython.h" +#endif // PXR_PYTHON_SUPPORT_ENABLED diff --git a/pxr/base/ts/simplify.cpp b/pxr/base/ts/simplify.cpp new file mode 100644 index 000000000..b2b176c6b --- /dev/null +++ b/pxr/base/ts/simplify.cpp @@ -0,0 +1,999 @@ +// +// 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 &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 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 &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 &vals, + const GfInterval &valsInterval) +{ + TRACE_FUNCTION(); + + std::vector 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 v1 = k1.GetValue().Get(); + + 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::TypedSharedPtr cache; + cache = Ts_EvalCache::New(*ki0, *ki1); + const Ts_Bezier *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 &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 &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 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(); + // Set v1 and v2 to v in case the knots don't exist + v1 = v2 = v; + + if (hasLeft) { + v1 = (kIter-1)->GetValue().Get(); + } + + if (hasRight) { + v2 = (kIter+1)->GetValue().Get(); + } + + // 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 v3 = (kIter+2)->GetValue().Get(); + 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 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 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 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()) { + return; + } + + // Compute the spline at every frame in 'valsInterval' for error + // calculation; remember the range + std::vector 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(); + 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 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(); + // Left val at this frame + double vl = keyFrames[i].GetLeftValue().Get(); + // Prev, next if adjacent; init for compiler + double vp=0, vn=0; + + if (leftAdjacent) + vp = keyFrames[i-1].GetValue().Get(); + + if (rightAdjacent) + // Use left value if dual valued + vn = keyFrames[i+1].GetLeftValue().Get(); + + std::optional 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(), + k.GetRightTangentSlope().Get(), + 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 &splines, + const std::vector &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 SplineAndIntervals; + std::vector 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 diff --git a/pxr/base/ts/simplify.h b/pxr/base/ts/simplify.h new file mode 100644 index 000000000..031aeb39a --- /dev/null +++ b/pxr/base/ts/simplify.h @@ -0,0 +1,71 @@ +// +// 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 &splines, + const std::vector& 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 diff --git a/pxr/base/ts/spline.cpp b/pxr/base/ts/spline.cpp new file mode 100644 index 000000000..ddb858802 --- /dev/null +++ b/pxr/base/ts/spline.cpp @@ -0,0 +1,1084 @@ +// +// 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 + +PXR_NAMESPACE_OPEN_SCOPE + +using std::string; + +TF_REGISTRY_FUNCTION(TfType) +{ + TfType::Define(); +} + +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 & 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 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::infinity(), + std::numeric_limits::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 +TsSpline::GetKeyFramesInMultiInterval( + const GfMultiInterval &intervals) const +{ + TRACE_FUNCTION(); + std::vector 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* 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 +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(); + + 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(); + } + } + + // 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(); + } +} + +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 ×, + TsKnotType type, + bool flatTangents, + double tangentLength, + const VtValue &value, + GfInterval *intervalAffected, + TsKeyFrameMap *keyFramesAtTimes) +{ + std::vector timesVec(times.begin(), times.end()); + std::vector values(times.size(), value); + + _BreakdownMultipleValues(timesVec, type, flatTangents, + tangentLength, values, intervalAffected, keyFramesAtTimes); +} + +void +TsSpline::Breakdown( + const std::vector ×, + TsKnotType type, + bool flatTangents, + double tangentLength, + const std::vector &values, + GfInterval *intervalAffected, + TsKeyFrameMap *keyFramesAtTimes) +{ + _BreakdownMultipleValues(times, type, flatTangents, + tangentLength, values, intervalAffected, keyFramesAtTimes); +} + +void +TsSpline::Breakdown( + const std::vector ×, + const std::vector &types, + bool flatTangents, + double tangentLength, + const std::vector &values, + GfInterval *intervalAffected, + TsKeyFrameMap *keyFramesAtTimes) +{ + _BreakdownMultipleKnotTypes(times, types, flatTangents, + tangentLength, values, intervalAffected, keyFramesAtTimes); +} + +void +TsSpline::_BreakdownMultipleValues( + const std::vector ×, + TsKnotType type, + bool flatTangents, + double tangentLength, + const std::vector &values, + GfInterval *intervalAffected, + TsKeyFrameMap *keyFramesAtTimes) +{ + if (times.size() != values.size()) { + TF_CODING_ERROR("Number of times and values do not match"); + return; + } + + std::vector types(times.size(), type); + + _BreakdownMultipleKnotTypes(times, types, flatTangents, + tangentLength, values, intervalAffected, keyFramesAtTimes); +} + +void +TsSpline::_BreakdownMultipleKnotTypes( + const std::vector ×, + const std::vector &types, + bool flatTangents, + double tangentLength, + const std::vector &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 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 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 newData( + new TsSpline_KeyFrames(*_data, &empty)); + _data.swap(newData); + } else { + // Our data is already unique, just set keyframes. + _data->SetKeyFrames(empty); + } +} + +std::optional +TsSpline::GetClosestKeyFrame( TsTime targetTime ) const +{ + const TsKeyFrame *k = Ts_GetClosestKeyFrame(GetKeyFrames(), targetTime); + if (k) { + return *k; + } + return std::optional(); +} + +std::optional +TsSpline::GetClosestKeyFrameBefore( TsTime targetTime ) const +{ + const TsKeyFrame *k = + Ts_GetClosestKeyFrameBefore(GetKeyFrames(), targetTime); + if (k) { + return *k; + } + return std::optional(); +} + +std::optional +TsSpline::GetClosestKeyFrameAfter( TsTime targetTime ) const +{ + const TsKeyFrame *k = + Ts_GetClosestKeyFrameAfter(GetKeyFrames(), targetTime); + if (k) { + return *k; + } + return std::optional(); +} + +// 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(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(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(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(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(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(); + + TRACE_FUNCTION(); + + // Get the extrapolation settings for the whole spline + std::pair 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::infinity(); + double maxDouble = -std::numeric_limits::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(); + minDouble = TfMin(v, minDouble); + maxDouble = TfMax(v, maxDouble); + + // Check other side if dual valued + if (curKeyFrame.GetIsDualValued()) { + double v = curKeyFrame.GetLeftValue().Get(); + 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 +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 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 +_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 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 +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() || + 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 diff --git a/pxr/base/ts/spline.h b/pxr/base/ts/spline.h new file mode 100644 index 000000000..2c2b96a02 --- /dev/null +++ b/pxr/base/ts/spline.h @@ -0,0 +1,619 @@ +// +// 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 +#include +#include +#include +#include +#include + +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 & 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* 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::infinity(), + std::numeric_limits::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 + 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 + 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 & 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 & times, TsKnotType type, + bool flatTangents, double tangentLength, + const std::vector & 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 & times, + const std::vector & types, + bool flatTangents, double tangentLength, + const std::vector & 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 + 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 + 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 + 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 + 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 + 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 ×, + TsKnotType type, bool flatTangents, double tangentLength, + const std::vector &values, + GfInterval *intervalAffected, + TsKeyFrameMap *keyFramesAtTimes); + + void _BreakdownMultipleKnotTypes( const std::vector ×, + const std::vector &types, + bool flatTangents, double tangentLength, + const std::vector &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> _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 _data; +}; + +TS_API +std::ostream& operator<<(std::ostream &out, const TsSpline &val); + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/spline_KeyFrames.cpp b/pxr/base/ts/spline_KeyFrames.cpp new file mode 100644 index 000000000..b31dc3792 --- /dev/null +++ b/pxr/base/ts/spline_KeyFrames.cpp @@ -0,0 +1,816 @@ +// +// 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 + +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* 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 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 *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()) { + key.SetValue(VtValue(v.Get() + valueOffset)); + // Handle dual valued + if (key.GetIsDualValued()) { + key.SetLeftValue( + VtValue(key.GetLeftValue().Get() + 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(&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 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::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::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::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::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 diff --git a/pxr/base/ts/spline_KeyFrames.h b/pxr/base/ts/spline_KeyFrames.h new file mode 100644 index 000000000..6743bfd42 --- /dev/null +++ b/pxr/base/ts/spline_KeyFrames.h @@ -0,0 +1,182 @@ +// +// 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 +#include +#include + +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* 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 _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 *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 diff --git a/pxr/base/ts/testenv/testTsKeyFrame.py b/pxr/base/ts/testenv/testTsKeyFrame.py new file mode 100644 index 000000000..5f98b31fa --- /dev/null +++ b/pxr/base/ts/testenv/testTsKeyFrame.py @@ -0,0 +1,592 @@ +#!/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') diff --git a/pxr/base/ts/testenv/testTsSimplify.py b/pxr/base/ts/testenv/testTsSimplify.py new file mode 100644 index 000000000..e375b4772 --- /dev/null +++ b/pxr/base/ts/testenv/testTsSimplify.py @@ -0,0 +1,154 @@ +#!/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') diff --git a/pxr/base/ts/testenv/testTsSpline.py b/pxr/base/ts/testenv/testTsSpline.py new file mode 100644 index 000000000..2a08f64c0 --- /dev/null +++ b/pxr/base/ts/testenv/testTsSpline.py @@ -0,0 +1,1391 @@ +#!/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') diff --git a/pxr/base/ts/testenv/testTsSplineAPI.py b/pxr/base/ts/testenv/testTsSplineAPI.py new file mode 100644 index 000000000..832fd6f52 --- /dev/null +++ b/pxr/base/ts/testenv/testTsSplineAPI.py @@ -0,0 +1,1397 @@ +#!/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 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() diff --git a/pxr/base/ts/testenv/testTsThreadedCOW.cpp b/pxr/base/ts/testenv/testTsThreadedCOW.cpp new file mode 100644 index 000000000..8d31bfcda --- /dev/null +++ b/pxr/base/ts/testenv/testTsThreadedCOW.cpp @@ -0,0 +1,124 @@ +// +// 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 +#include +#include +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +using TestFunction = std::function; + + +// Execute a function which returns a T and verify +// that the returned value is equal to expectedResult +template +void ExecuteAndCompare( + const std::function &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 f = + std::bind(SetKeyFrame,baseSpline,time,value); + return std::bind(ExecuteAndCompare, 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 f = std::bind(Eval, baseSpline, time); + return std::bind(ExecuteAndCompare, f, f()); +} + +void RunTests(const std::vector &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 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 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; +} diff --git a/pxr/base/ts/testenv/testTs_HardToReach.cpp b/pxr/base/ts/testenv/testTs_HardToReach.cpp new file mode 100644 index 000000000..3236345aa --- /dev/null +++ b/pxr/base/ts/testenv/testTs_HardToReach.cpp @@ -0,0 +1,1878 @@ +// +// 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 +#include +#include + +PXR_NAMESPACE_USING_DIRECTIVE + +using std::string; + +static const TsTime inf = std::numeric_limits::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 +static bool +_IsClose(const double& a, const double& b, + T eps = std::numeric_limits::epsilon()) +{ + return fabs(a - b) < eps; +} + +template +static bool +_IsClose(const VtValue& a, const VtValue& b, + T eps = std::numeric_limits::epsilon()) +{ + return fabs(a.Get() - b.Get()) < eps; +} + +template <> +bool +_IsClose(const VtValue& a, const VtValue& b, GfVec2d eps) +{ + GfVec2d _b = b.Get(); + GfVec2d negB(-_b[0], -_b[1]); + GfVec2d diff = a.Get() + negB; + return fabs(diff[0]) < eps[0] && fabs(diff[1]) < eps[1]; +} + +template +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(samples[i].leftValue, + val.Eval(samples[i].leftTime, TsRight), + tolerance)); + } + if (samples[i].rightTime <= endTime) { + TF_AXIOM(_IsClose(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 evaluator(spline); + for (TsTime sample = -2.0; sample < 2.0; sample += 0.1) { + VtValue rawEvalValue = spline.Eval(sample); + TF_AXIOM(_IsClose(!rawEvalValue.IsEmpty() ? + rawEvalValue.Get() : + TsTraits::zero, + evaluator.Eval(sample))); + } +} + +void +_AddSingleKnotSpline(TsTime knotTime, const VtValue &knotValue, + std::vector *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 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() == 3.0); + TF_AXIOM(spline.find(10.0)->GetValue().Get() == 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 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() == 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() == 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())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find< VtArray >())); + TF_AXIOM(typeRegistry.IsSupportedType(TfType::Find< VtArray >())); + TF_AXIOM(!typeRegistry.IsSupportedType(TfType::Find())); + TF_AXIOM(!typeRegistry.IsSupportedType(TfType::Find())); + 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() == "foo" ); + kf.SetLeftValue( VtValue("bar") ); + TF_AXIOM( kf.GetValue().Get() == "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() == 1.0 ); + kf.SetLeftValue( VtValue(123.0) ); + TF_AXIOM( kf.GetValue().Get() == 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(10) ); + TF_AXIOM( val.Eval(5.5).Get() == float(11) ); + TF_AXIOM( val.EvalDerivative(5, TsLeft).Get() == float(2) ); + TF_AXIOM( val.EvalDerivative(5, TsRight).Get() == float(2) ); + TF_AXIOM( val.EvalDerivative(5.5, TsLeft).Get() == float(2) ); + TF_AXIOM( val.EvalDerivative(5.5, TsRight).Get() == 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(10) ); + TF_AXIOM( val.Eval(5.5).Get() == float(11) ); + TF_AXIOM( val.EvalDerivative(0, TsLeft).Get() == float(0) ); + TF_AXIOM( val.EvalDerivative(0, TsRight).Get() == float(0) ); + TF_AXIOM( val.EvalDerivative(5, TsLeft).Get() == float(2) ); + TF_AXIOM( val.EvalDerivative(5, TsRight).Get() == float(2) ); + TF_AXIOM( val.EvalDerivative(5.5, TsLeft).Get() == float(2) ); + TF_AXIOM( val.EvalDerivative(5.5, TsRight).Get() == 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 range = val.GetRange(-1, 11); + TF_AXIOM( range.first.Get() == 0.0 ); + TF_AXIOM( range.second.Get() == 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(0) ); + TF_AXIOM( val.EvalDerivative(5, TsLeft).Get() == int(0) ); + TF_AXIOM( val.EvalDerivative(5, TsRight).Get() == 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::epsilon(), + std::numeric_limits::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(val.Eval(0, TsRight).Get(), 0.0)); + TF_AXIOM(_IsClose(val.Eval(0, TsLeft).Get(), 0.0)); + TF_AXIOM(_IsClose(val.Eval(10, TsRight).Get(), 10.0)); + TF_AXIOM(_IsClose(val.Eval(10, TsLeft).Get(), 0.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(0, TsRight).Get(), 0.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(0, TsLeft).Get(), 0.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(10, TsRight).Get(), 0.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(10, TsLeft).Get(), 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(val.Eval(0, TsRight).Get(), 0.0)); + TF_AXIOM(_IsClose(val.Eval(0, TsLeft).Get(), 0.0)); + TF_AXIOM(_IsClose(val.Eval(10, TsRight).Get(), 10.0)); + TF_AXIOM(_IsClose(val.Eval(10, TsLeft).Get(), 10.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(0, TsRight).Get(), 0.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(0, TsLeft).Get(), 0.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(10, TsRight).Get(), 0.0)); + TF_AXIOM(_IsClose(val.EvalDerivative(10, TsLeft).Get(), 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(val.Eval(0, TsRight), VtValue(0.0))); + TF_AXIOM(_IsClose(val.Eval(0, TsLeft), VtValue(0.0))); + TF_AXIOM(_IsClose(val.Eval(10, TsRight), VtValue(10.0))); + TF_AXIOM(_IsClose(val.Eval(10, TsLeft), VtValue(10.0))); + TF_AXIOM(_IsClose(val.EvalDerivative(0, TsRight), VtValue(0.0))); + TF_AXIOM(_IsClose(val.EvalDerivative(0, TsLeft), VtValue(0.0))); + TF_AXIOM(_IsClose(val.EvalDerivative(10, TsRight), VtValue(0.0))); + TF_AXIOM(_IsClose(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(val, samples, -1, 11, maxError); + // Test sampling out of range + samples = val.Sample(-300, -200, 1.0, 1.0, tolerance); + _AssertSamples(val, samples, -300, -200, maxError); + samples = val.Sample(300, 400, 1.0, 1.0, tolerance); + _AssertSamples(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(0.0) ); + TF_AXIOM( val.EvalDerivative(5.5, TsLeft).Get() == float(0.0) ); + TF_AXIOM( val.EvalDerivative(5.5, TsRight).Get() == 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; +} diff --git a/pxr/base/ts/testenv/tsTest_AnimXFramework.py b/pxr/base/ts/testenv/tsTest_AnimXFramework.py new file mode 100644 index 000000000..d7a3eca98 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_AnimXFramework.py @@ -0,0 +1,97 @@ +#!/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() diff --git a/pxr/base/ts/testenv/tsTest_AnimXFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt b/pxr/base/ts/testenv/tsTest_AnimXFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt new file mode 100644 index 000000000..73297cb61 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_AnimXFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt @@ -0,0 +1,210 @@ +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 diff --git a/pxr/base/ts/testenv/tsTest_AnimXFramework.testenv/baseline/test_Baseline_TsTestGraph.png b/pxr/base/ts/testenv/tsTest_AnimXFramework.testenv/baseline/test_Baseline_TsTestGraph.png new file mode 100644 index 000000000..da1735075 Binary files /dev/null and b/pxr/base/ts/testenv/tsTest_AnimXFramework.testenv/baseline/test_Baseline_TsTestGraph.png differ diff --git a/pxr/base/ts/testenv/tsTest_MayapyFramework.py b/pxr/base/ts/testenv/tsTest_MayapyFramework.py new file mode 100644 index 000000000..b589f5765 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_MayapyFramework.py @@ -0,0 +1,117 @@ +#!/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() diff --git a/pxr/base/ts/testenv/tsTest_MayapyFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt b/pxr/base/ts/testenv/tsTest_MayapyFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt new file mode 100644 index 000000000..14a27788b --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_MayapyFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt @@ -0,0 +1,210 @@ +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 diff --git a/pxr/base/ts/testenv/tsTest_MayapyFramework.testenv/baseline/test_Baseline_TsTestGraph.png b/pxr/base/ts/testenv/tsTest_MayapyFramework.testenv/baseline/test_Baseline_TsTestGraph.png new file mode 100644 index 000000000..da1735075 Binary files /dev/null and b/pxr/base/ts/testenv/tsTest_MayapyFramework.testenv/baseline/test_Baseline_TsTestGraph.png differ diff --git a/pxr/base/ts/testenv/tsTest_MayapyVsAnimX.py b/pxr/base/ts/testenv/tsTest_MayapyVsAnimX.py new file mode 100644 index 000000000..c739b7551 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_MayapyVsAnimX.py @@ -0,0 +1,76 @@ +#!/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() diff --git a/pxr/base/ts/testenv/tsTest_TsFramework.py b/pxr/base/ts/testenv/tsTest_TsFramework.py new file mode 100644 index 000000000..ba5461e52 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_TsFramework.py @@ -0,0 +1,138 @@ +#!/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() diff --git a/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_BaselineFail_TsTestBaseline.txt b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_BaselineFail_TsTestBaseline.txt new file mode 100644 index 000000000..73297cb61 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_BaselineFail_TsTestBaseline.txt @@ -0,0 +1,210 @@ +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 diff --git a/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_BaselineFail_TsTestGraph.png b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_BaselineFail_TsTestGraph.png new file mode 100644 index 000000000..da1735075 Binary files /dev/null and b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_BaselineFail_TsTestGraph.png differ diff --git a/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt new file mode 100644 index 000000000..73297cb61 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_Baseline_TsTestBaseline.txt @@ -0,0 +1,210 @@ +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 diff --git a/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_Baseline_TsTestGraph.png b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_Baseline_TsTestGraph.png new file mode 100644 index 000000000..da1735075 Binary files /dev/null and b/pxr/base/ts/testenv/tsTest_TsFramework.testenv/baseline/test_Baseline_TsTestGraph.png differ diff --git a/pxr/base/ts/testenv/tsTest_TsVsMayapy.py b/pxr/base/ts/testenv/tsTest_TsVsMayapy.py new file mode 100644 index 000000000..072a1fc93 --- /dev/null +++ b/pxr/base/ts/testenv/tsTest_TsVsMayapy.py @@ -0,0 +1,120 @@ +#!/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() diff --git a/pxr/base/ts/tsTest.dox b/pxr/base/ts/tsTest.dox new file mode 100644 index 000000000..470c3d875 --- /dev/null +++ b/pxr/base/ts/tsTest.dox @@ -0,0 +1,69 @@ +/*! +\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: + +- Framework. 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. + +- Backends. 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. + +- Single-Backend Tests. 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. + +- Comparison Tests. 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. + +*/ diff --git a/pxr/base/ts/tsTest_AnimXEvaluator.cpp b/pxr/base/ts/tsTest_AnimXEvaluator.cpp new file mode 100644 index 000000000..5dfb8ceec --- /dev/null +++ b/pxr/base/ts/tsTest_AnimXEvaluator.cpp @@ -0,0 +1,362 @@ +// +// 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 + +#include + +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 _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 diff --git a/pxr/base/ts/tsTest_AnimXEvaluator.h b/pxr/base/ts/tsTest_AnimXEvaluator.h new file mode 100644 index 000000000..9833945b0 --- /dev/null +++ b/pxr/base/ts/tsTest_AnimXEvaluator.h @@ -0,0 +1,56 @@ +// +// 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 diff --git a/pxr/base/ts/tsTest_Evaluator.cpp b/pxr/base/ts/tsTest_Evaluator.cpp new file mode 100644 index 000000000..eb55bfbb3 --- /dev/null +++ b/pxr/base/ts/tsTest_Evaluator.cpp @@ -0,0 +1,47 @@ +// +// 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 diff --git a/pxr/base/ts/tsTest_Evaluator.h b/pxr/base/ts/tsTest_Evaluator.h new file mode 100644 index 000000000..6080bc58e --- /dev/null +++ b/pxr/base/ts/tsTest_Evaluator.h @@ -0,0 +1,63 @@ +// +// 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 + +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 diff --git a/pxr/base/ts/tsTest_Museum.cpp b/pxr/base/ts/tsTest_Museum.cpp new file mode 100644 index 000000000..40a612dcb --- /dev/null +++ b/pxr/base/ts/tsTest_Museum.cpp @@ -0,0 +1,234 @@ +// +// 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 diff --git a/pxr/base/ts/tsTest_Museum.h b/pxr/base/ts/tsTest_Museum.h new file mode 100644 index 000000000..95e5366b0 --- /dev/null +++ b/pxr/base/ts/tsTest_Museum.h @@ -0,0 +1,57 @@ +// +// 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 diff --git a/pxr/base/ts/tsTest_SampleBezier.cpp b/pxr/base/ts/tsTest_SampleBezier.cpp new file mode 100644 index 000000000..a88132e50 --- /dev/null +++ b/pxr/base/ts/tsTest_SampleBezier.cpp @@ -0,0 +1,107 @@ +// +// 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 diff --git a/pxr/base/ts/tsTest_SampleBezier.h b/pxr/base/ts/tsTest_SampleBezier.h new file mode 100644 index 000000000..7d51f5ce7 --- /dev/null +++ b/pxr/base/ts/tsTest_SampleBezier.h @@ -0,0 +1,53 @@ +// +// 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 + +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 diff --git a/pxr/base/ts/tsTest_SampleTimes.cpp b/pxr/base/ts/tsTest_SampleTimes.cpp new file mode 100644 index 000000000..691903c40 --- /dev/null +++ b/pxr/base/ts/tsTest_SampleTimes.cpp @@ -0,0 +1,227 @@ +// +// 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 + +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 ×) +{ + for (const double time : times) + _times.insert(SampleTime(time)); +} + +void TsTest_SampleTimes::AddTimes( + const std::vector ×) +{ + _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 diff --git a/pxr/base/ts/tsTest_SampleTimes.h b/pxr/base/ts/tsTest_SampleTimes.h new file mode 100644 index 000000000..1b2bce32f --- /dev/null +++ b/pxr/base/ts/tsTest_SampleTimes.h @@ -0,0 +1,134 @@ +// +// 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 +#include + +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; + +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 ×); + + // Adds the specified times. + TS_API + void AddTimes( + const std::vector ×); + + // 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 diff --git a/pxr/base/ts/tsTest_SplineData.cpp b/pxr/base/ts/tsTest_SplineData.cpp new file mode 100644 index 000000000..c02b2d5c9 --- /dev/null +++ b/pxr/base/ts/tsTest_SplineData.cpp @@ -0,0 +1,370 @@ +// +// 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 +#include + +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 diff --git a/pxr/base/ts/tsTest_SplineData.h b/pxr/base/ts/tsTest_SplineData.h new file mode 100644 index 000000000..b65816109 --- /dev/null +++ b/pxr/base/ts/tsTest_SplineData.h @@ -0,0 +1,271 @@ +// +// 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 +#include + +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; + + // 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 diff --git a/pxr/base/ts/tsTest_TsEvaluator.cpp b/pxr/base/ts/tsTest_TsEvaluator.cpp new file mode 100644 index 000000000..a60d6cc0f --- /dev/null +++ b/pxr/base/ts/tsTest_TsEvaluator.cpp @@ -0,0 +1,276 @@ +// +// 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(); + dataKnot.preSlope = knot.GetLeftTangentSlope().Get(); + dataKnot.postSlope = knot.GetRightTangentSlope().Get(); + 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(); + } + + 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())); + } + + 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())); + } + + 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 diff --git a/pxr/base/ts/tsTest_TsEvaluator.h b/pxr/base/ts/tsTest_TsEvaluator.h new file mode 100644 index 000000000..b17d27d15 --- /dev/null +++ b/pxr/base/ts/tsTest_TsEvaluator.h @@ -0,0 +1,53 @@ +// +// 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 diff --git a/pxr/base/ts/tsTest_Types.cpp b/pxr/base/ts/tsTest_Types.cpp new file mode 100644 index 000000000..a6feb79fc --- /dev/null +++ b/pxr/base/ts/tsTest_Types.cpp @@ -0,0 +1,43 @@ +// +// 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 diff --git a/pxr/base/ts/tsTest_Types.h b/pxr/base/ts/tsTest_Types.h new file mode 100644 index 000000000..6c3fc6846 --- /dev/null +++ b/pxr/base/ts/tsTest_Types.h @@ -0,0 +1,51 @@ +// +// 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 + +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; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/typeRegistry.cpp b/pxr/base/ts/typeRegistry.cpp new file mode 100644 index 000000000..80a037567 --- /dev/null +++ b/pxr/base/ts/typeRegistry.cpp @@ -0,0 +1,123 @@ +// +// 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() below. + TfSingleton::SetInstanceConstructed(*this); + + // Cause the registry to initialize + TfRegistryManager::GetInstance().SubscribeTo(); +} + +TsTypeRegistry::~TsTypeRegistry() { + TfRegistryManager::GetInstance().UnsubscribeFrom(); +} + +void +TsTypeRegistry::InitializeDataHolder( + Ts_PolymorphicDataHolder *holder, + const VtValue &value) +{ + static TypedDataFactory const &doubleDataFactory = + _dataFactoryMap.find(TfType::Find())->second; + + // Double-valued keyframes are super common, so special-case them here. + if (ARCH_LIKELY(value.IsHolding())) { + 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::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); +TS_REGISTER_TYPE(VtArray); +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 diff --git a/pxr/base/ts/typeRegistry.h b/pxr/base/ts/typeRegistry.h new file mode 100644 index 000000000..3594d9b02 --- /dev/null +++ b/pxr/base/ts/typeRegistry.h @@ -0,0 +1,107 @@ +// +// 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::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 DataFactoryMap; + + /// Registers a TypedDataFactory for a particular type + template + void RegisterTypedDataFactory(TypedDataFactory factory) { + _dataFactoryMap[TfType::Find()] = 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; + + DataFactoryMap _dataFactoryMap; +}; + +#define TS_REGISTER_TYPE(TYPE) \ +TF_REGISTRY_FUNCTION(TsTypeRegistry) { \ + TsTypeRegistry ® = TsTypeRegistry::GetInstance(); \ + reg.RegisterTypedDataFactory( \ + [](Ts_PolymorphicDataHolder *holder, const VtValue &value) { \ + holder->New(value.Get()); \ + }); \ +} + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/pxr/base/ts/types.cpp b/pxr/base/ts/types.cpp new file mode 100644 index 000000000..ad585e5e5 --- /dev/null +++ b/pxr/base/ts/types.cpp @@ -0,0 +1,75 @@ +// +// 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::zero = VtZero(); +const float TsTraits::zero = VtZero(); +const int TsTraits::zero = VtZero(); +const bool TsTraits::zero = VtZero(); +const GfVec2d TsTraits::zero = VtZero(); +const GfVec2f TsTraits::zero = VtZero(); +const GfVec3d TsTraits::zero = VtZero(); +const GfVec3f TsTraits::zero = VtZero(); +const GfVec4d TsTraits::zero = VtZero(); +const GfVec4f TsTraits::zero = VtZero(); +const GfQuatf TsTraits::zero = VtZero(); +const GfQuatd TsTraits::zero = VtZero(); + +const GfMatrix2d TsTraits::zero = VtZero(); +const GfMatrix3d TsTraits::zero = VtZero(); +const GfMatrix4d TsTraits::zero = VtZero(); +const std::string TsTraits::zero = VtZero(); + +const VtArray TsTraits< VtArray >::zero; +const VtArray TsTraits< VtArray >::zero; + +const TfToken TsTraits::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 diff --git a/pxr/base/ts/types.h b/pxr/base/ts/types.h new file mode 100644 index 000000000..268c613eb --- /dev/null +++ b/pxr/base/ts/types.h @@ -0,0 +1,326 @@ +// +// 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 +#include + +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 + 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 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 +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 > { + static const bool isSupportedSplineValueType = true; + static const bool interpolatable = true; + static const bool extrapolatable = true; + static const bool supportsTangents = false; + static const VtArray zero; +}; + +template <> +struct TS_API TsTraits< VtArray > { + 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 zero; +}; + +template <> +struct TS_API TsTraits { + 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 diff --git a/pxr/base/ts/wrapKeyFrame.cpp b/pxr/base/ts/wrapKeyFrame.cpp new file mode 100644 index 000000000..03df7e301 --- /dev/null +++ b/pxr/base/ts/wrapKeyFrame.cpp @@ -0,0 +1,289 @@ +// +// 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 +#include +#include + +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 > 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 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 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(); + + TfPyWrapEnum(); + + 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, + TfPySequenceToPython< std::vector > >(); + + class_( "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() ) + + .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()), + &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()), + &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(); +} diff --git a/pxr/base/ts/wrapLoopParams.cpp b/pxr/base/ts/wrapLoopParams.cpp new file mode 100644 index 000000000..ea760c97c --- /dev/null +++ b/pxr/base/ts/wrapLoopParams.cpp @@ -0,0 +1,92 @@ +// +// 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 + +#include +#include + +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_("LoopParams", init<>()) + .def(init()) + + .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()) + + .def("GetLoopedInterval", &This::GetLoopedInterval, + return_value_policy()) + + .def("IsValid", &This::IsValid) + + .add_property("valueOffset", + &This::GetValueOffset, + &This::SetValueOffset) + + .def("__repr__", &::_GetRepr) + + .def(self == self) + .def(self != self) + ; +} diff --git a/pxr/base/ts/wrapSimplify.cpp b/pxr/base/ts/wrapSimplify.cpp new file mode 100644 index 000000000..f7e74337a --- /dev/null +++ b/pxr/base/ts/wrapSimplify.cpp @@ -0,0 +1,82 @@ +// +// 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 +#include + +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 &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 splinePtrs; + for(int i=0; i < len(splines); ++i) + { + boost::python::extract 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 (mutated for result), list, maxErrorFraction)\n"); +} diff --git a/pxr/base/ts/wrapSpline.cpp b/pxr/base/ts/wrapSpline.cpp new file mode 100644 index 000000000..9cff885f3 --- /dev/null +++ b/pxr/base/ts/wrapSpline.cpp @@ -0,0 +1,681 @@ +// +// 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 + +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 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; + + PyIterableWrapper(const object &iterable) : _iterable(iterable) { } + + Iterator begin() { + return Iterator(_iterable); + } + Iterator end() { + return Iterator(); + } + private: + object _iterable; + }; + + object items = kfDict.attr("items")(); + + vector keyframes; + for (const object &item : PyIterableWrapper(items)) + { + object key = item[0]; + extract time(key); + if (!time.check()) { + TfPyThrowTypeError("expected time for keyframe in dict"); + } + + extract 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 +_EvalMultipleTimes( const TsSpline & val, + const vector & times ) +{ + vector result; + result.reserve( times.size() ); + TF_FOR_ALL( it, times ) + result.push_back( val.Eval(*it) ); + + return result; +} + +static vector +_GetRange( const TsSpline & val, + TsTime startTime, TsTime endTime) +{ + std::pair range = val.GetRange(startTime, endTime); + vector 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 kf = val.GetClosestKeyFrame(t); + return kf ? object(*kf) : object(); +} + +static boost::python::object +_GetClosestKeyFrameBefore( const TsSpline & val, TsTime t ) +{ + std::optional kf = val.GetClosestKeyFrameBefore(t); + return kf ? object(*kf) : object(); +} + +static boost::python::object +_GetClosestKeyFrameAfter( const TsSpline & val, TsTime t ) +{ + std::optional 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 +_GetValues( const TsSpline & val ) +{ + const TsKeyFrameMap &vec = val.GetKeyFrames(); + return vector(vec.begin(),vec.end()); +} + +static vector +_GetKeys( const TsSpline & val ) +{ + const TsKeyFrameMap & kf = val.GetKeyFrames(); + + vector 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 +_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 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 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 kf = _GetValues(val); + + // Convert our vector to a Python list. + PyObject *kf_list = + TfPySequenceToList::apply< vector >::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 & 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 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 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 +_Breakdown2( TsSpline & self, std::set 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 map; + TF_FOR_ALL(i, vec) { + map.insert(std::make_pair(i->GetTime(),*i)); + } + return map; +} + +// Vectorized breakdown with multiple values +static std::map +_Breakdown3( TsSpline & self, std::vector times, + TsKnotType type, bool flatTangents, double tangentLength, + std::vector values) +{ + // Wrapper to discard optional intervalAffected argument. + TsKeyFrameMap vec; + self.Breakdown(times, type, flatTangents, tangentLength, values, + NULL, &vec); + std::map 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 +_Breakdown4( TsSpline & self, std::vector times, + std::vector types, + bool flatTangents, double tangentLength, + std::vector values) +{ + // Wrapper to discard optional intervalAffected argument. + TsKeyFrameMap vec; + self.Breakdown(times, types, flatTangents, tangentLength, values, + NULL, &vec); + std::map map; + TF_FOR_ALL(i, vec) { + map.insert(std::make_pair(i->GetTime(),*i)); + } + return map; +} + +static void +_SetExtrapolation( + TsSpline & self, + const std::pair& 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_("Spline", no_init) + .def( init<>() ) + .def( init() ) + .def( init &, + optional >()) + .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::infinity(), + std::numeric_limits::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\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()) + .def("Breakdown", _Breakdown3, return_value_policy()) + .def("Breakdown", _Breakdown4, return_value_policy()) + + .add_property("extrapolation", + make_function(&This::GetExtrapolation, + return_value_policy< TfPyPairToTuple >()), + make_function(&::_SetExtrapolation)) + + .add_property("loopParams", + make_function(&This::GetLoopParams, + return_value_policy()), + &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\n\n" + "times : tuple