Loading...
Searching...
No Matches
splineData.h
1//
2// Copyright 2024 Pixar
3//
4// Licensed under the terms set forth in the LICENSE.txt file available at
5// https://openusd.org/license.
6//
7
8#ifndef PXR_BASE_TS_SPLINE_DATA_H
9#define PXR_BASE_TS_SPLINE_DATA_H
10
11#include "pxr/pxr.h"
12#include "pxr/base/ts/api.h"
13#include "pxr/base/ts/knotData.h"
14#include "pxr/base/ts/types.h"
15#include "pxr/base/ts/typeHelpers.h"
18#include "pxr/base/tf/type.h"
19#include "pxr/base/tf/stl.h"
20
21#include <vector>
22#include <unordered_map>
23#include <algorithm>
24#include <iterator>
25#include <utility>
26#include <cmath>
27
28PXR_NAMESPACE_OPEN_SCOPE
29
30class TsSpline;
31
32
33// Primary data structure for splines. Abstract; subclasses store knot data,
34// which is flexibly typed (double/float/half). This is the unit of data that
35// is managed by shared_ptr, and forms the basis of copy-on-write data sharing.
36//
37struct Ts_SplineData
38{
39public:
40 // If valueType is known, create a TypedSplineData of the specified type.
41 // If valueType is unknown, create a TypedSplineData<double> to store
42 // overall spline parameters in the absence of a value type; this assumes
43 // that when knots arrive, they are most likely to be double-typed. If
44 // overallParamSource is provided, it is a previous overall-only struct, and
45 // our guess about double was wrong, so we are transferring the overall
46 // parameters.
47 static Ts_SplineData* Create(
48 TfType valueType,
49 const Ts_SplineData *overallParamSource = nullptr);
50
51 virtual ~Ts_SplineData();
52
53public:
54 // Virtual interface for typed data.
55
56 virtual TfType GetValueType() const = 0;
57 virtual size_t GetKnotStructSize() const = 0;
58 virtual Ts_SplineData* Clone() const = 0;
59
60 virtual bool operator==(const Ts_SplineData &other) const = 0;
61
62 virtual void ReserveForKnotCount(size_t count) = 0;
63 virtual void PushKnot(
64 const Ts_KnotData *knotData,
65 const VtDictionary &customData) = 0;
66 virtual size_t SetKnot(
67 const Ts_KnotData *knotData,
68 const VtDictionary &customData) = 0;
69
70 virtual Ts_KnotData* CloneKnotAtIndex(size_t index) const = 0;
71 virtual Ts_KnotData* CloneKnotAtTime(TsTime time) const = 0;
72 virtual Ts_KnotData* GetKnotPtrAtIndex(size_t index) = 0;
73 virtual Ts_TypedKnotData<double>
74 GetKnotDataAsDouble(size_t index) const = 0;
75
76 virtual void ClearKnots() = 0;
77 virtual void RemoveKnotAtTime(TsTime time) = 0;
78
79 virtual void ApplyOffsetAndScale(
80 TsTime offset,
81 double scale) = 0;
82
83 virtual bool HasValueBlocks() const = 0;
84 virtual bool HasValueBlockAtTime(TsTime time) const = 0;
85
86public:
87 // Returns whether there is a valid inner-loop configuration. If
88 // firstProtoIndexOut is provided, it receives the index of the first knot
89 // in the prototype.
90 bool HasInnerLoops(
91 size_t *firstProtoIndexOut = nullptr) const;
92
93public:
94 // BITFIELDS - note: for enum-typed bitfields, we declare one bit more than
95 // is minimally needed to represent all declared enum values. For example,
96 // TsCurveType has only two values, so it should be representable in one
97 // bit. However, compilers are free to choose the underlying representation
98 // of enums, and some platforms choose signed values, meaning that we
99 // actually need one bit more, so that we can hold the sign bit. We could
100 // declare the enums with unsigned underlying types, but that runs into a
101 // gcc 9.2 bug. We can spare the extra bit; alignment means there is no
102 // difference in struct size.
103
104 // If true, our subtype is authoritative; we know our value type. If false,
105 // then no value type was provided at initialization, and no knots have been
106 // set. In the latter case, we exist only to store overall parameters, and
107 // we have been presumptively created as TypedSplineData<double>.
108 bool isTyped : 1;
109
110 // Whether ApplyOffsetAndScale applies to values also.
111 bool timeValued : 1;
112
113 // Overall spline parameters.
114 TsCurveType curveType : 2;
115 TsExtrapolation preExtrapolation;
116 TsExtrapolation postExtrapolation;
117 TsLoopParams loopParams;
118
119 // A duplicate of the knot times, so that we can maximize locality while
120 // performing binary searches for knots. This is part of the evaluation hot
121 // path; given an eval time, we must find either the knot at that time, or
122 // the knots before and after that time. The entries in this vector
123 // correspond exactly to the entries in the 'knots' vector in
124 // Ts_TypedSplineData. Times are unique and sorted in ascending order.
125 std::vector<TsTime> times;
126
127 // Custom data for knots, sparsely allocated, keyed by time.
128 std::unordered_map<TsTime, VtDictionary> customData;
129};
130
131
132// Concrete subclass of Ts_SplineData. Templated on T, the value type.
133//
134template <typename T>
135struct Ts_TypedSplineData final :
136 public Ts_SplineData
137{
138public:
139 TfType GetValueType() const override;
140 size_t GetKnotStructSize() const override;
141 Ts_SplineData* Clone() const override;
142
143 bool operator==(const Ts_SplineData &other) const override;
144
145 void ReserveForKnotCount(size_t count) override;
146 void PushKnot(
147 const Ts_KnotData *knotData,
148 const VtDictionary &customData) override;
149 size_t SetKnot(
150 const Ts_KnotData *knotData,
151 const VtDictionary &customData) override;
152
153 Ts_KnotData* CloneKnotAtIndex(size_t index) const override;
154 Ts_KnotData* CloneKnotAtTime(TsTime time) const override;
155 Ts_KnotData* GetKnotPtrAtIndex(size_t index) override;
156 Ts_TypedKnotData<double>
157 GetKnotDataAsDouble(size_t index) const override;
158
159 void ClearKnots() override;
160 void RemoveKnotAtTime(TsTime time) override;
161
162 // Apply offset and scale to all spline data.
163 //
164 // If \p scale is negative, a coding error is generated. This is because
165 // the spline is not only scaled, but also time-reversed. Doing so can
166 // lead to incorrect evaluation results with any scenario where direction
167 // of time is assumed, like dual-value knots, inner looping,
168 // segment interpolation mode assignment, etc.
169 void ApplyOffsetAndScale(
170 TsTime offset,
171 double scale) override;
172
173 bool HasValueBlocks() const override;
174 bool HasValueBlockAtTime(TsTime time) const override;
175
176public:
177 // Per-knot data.
178 std::vector<Ts_TypedKnotData<T>> knots;
179};
180
181
182// Data-access helpers for the Ts implementation. The untyped functions are
183// friends of TsSpline, and retrieve private data pointers.
184
185Ts_SplineData*
186Ts_GetSplineData(TsSpline &spline);
187
188const Ts_SplineData*
189Ts_GetSplineData(const TsSpline &spline);
190
191template <typename T>
192Ts_TypedSplineData<T>*
193Ts_GetTypedSplineData(TsSpline &spline);
194
195template <typename T>
196const Ts_TypedSplineData<T>*
197Ts_GetTypedSplineData(const TsSpline &spline);
198
199
201// TEMPLATE IMPLEMENTATIONS
202
203template <typename T>
204TfType Ts_TypedSplineData<T>::GetValueType() const
205{
206 if (!isTyped)
207 {
208 return TfType();
209 }
210
211 return Ts_GetType<T>();
212}
213
214template <typename T>
215size_t Ts_TypedSplineData<T>::GetKnotStructSize() const
216{
217 return sizeof(Ts_TypedKnotData<T>);
218}
219
220template <typename T>
221Ts_SplineData*
222Ts_TypedSplineData<T>::Clone() const
223{
224 return new Ts_TypedSplineData<T>(*this);
225}
226
227template <typename T>
228bool Ts_TypedSplineData<T>::operator==(
229 const Ts_SplineData &other) const
230{
231 // Compare non-templated data.
232 if (isTyped != other.isTyped
233 || timeValued != other.timeValued
234 || curveType != other.curveType
235 || preExtrapolation != other.preExtrapolation
236 || postExtrapolation != other.postExtrapolation
237 || loopParams != other.loopParams
238 || customData != other.customData)
239 {
240 return false;
241 }
242
243 // Downcast to our value type. If other is not of the same type, we're not
244 // equal.
245 const Ts_TypedSplineData<T>* const typedOther =
246 dynamic_cast<const Ts_TypedSplineData<T>*>(&other);
247 if (!typedOther)
248 {
249 return false;
250 }
251
252 // Compare all knots.
253 return knots == typedOther->knots;
254}
255
256template <typename T>
257void Ts_TypedSplineData<T>::ReserveForKnotCount(
258 const size_t count)
259{
260 times.reserve(count);
261 knots.reserve(count);
262}
263
264template <typename T>
265void Ts_TypedSplineData<T>::PushKnot(
266 const Ts_KnotData* const knotData,
267 const VtDictionary &customDataIn)
268{
269 const Ts_TypedKnotData<T>* const typedKnotData =
270 static_cast<const Ts_TypedKnotData<T>*>(knotData);
271
272 times.push_back(knotData->time);
273 knots.push_back(*typedKnotData);
274
275 if (!customDataIn.empty())
276 {
277 customData[knotData->time] = customDataIn;
278 }
279}
280
281template <typename T>
282size_t Ts_TypedSplineData<T>::SetKnot(
283 const Ts_KnotData* const knotData,
284 const VtDictionary &customDataIn)
285{
286 const Ts_TypedKnotData<T>* const typedKnotData =
287 static_cast<const Ts_TypedKnotData<T>*>(knotData);
288
289 // Use binary search to find insert-or-overwrite position.
290 const auto it =
291 std::lower_bound(times.begin(), times.end(), knotData->time);
292 const size_t idx =
293 it - times.begin();
294 const bool overwrite =
295 (it != times.end() && *it == knotData->time);
296
297 // Insert or overwrite new time and knot data.
298 if (overwrite)
299 {
300 times[idx] = knotData->time;
301 knots[idx] = *typedKnotData;
302 }
303 else
304 {
305 times.insert(it, knotData->time);
306 knots.insert(knots.begin() + idx, *typedKnotData);
307 }
308
309 // Store customData, if any.
310 if (!customDataIn.empty())
311 {
312 customData[knotData->time] = customDataIn;
313 }
314
315 return idx;
316}
317
318template <typename T>
319Ts_KnotData*
320Ts_TypedSplineData<T>::CloneKnotAtIndex(
321 const size_t index) const
322{
323 return new Ts_TypedKnotData<T>(knots[index]);
324}
325
326template <typename T>
327Ts_KnotData*
328Ts_TypedSplineData<T>::CloneKnotAtTime(
329 const TsTime time) const
330{
331 const auto it = std::lower_bound(times.begin(), times.end(), time);
332 if (it == times.end() || *it != time)
333 {
334 return nullptr;
335 }
336
337 const auto knotIt = knots.begin() + (it - times.begin());
338 return new Ts_TypedKnotData<T>(*knotIt);
339}
340
341template <typename T>
342Ts_KnotData*
343Ts_TypedSplineData<T>::GetKnotPtrAtIndex(
344 const size_t index)
345{
346 return &(knots[index]);
347}
348
349// Depending on T, this is either a verbatim copy or an increase in precision.
350template <typename T>
351Ts_TypedKnotData<double>
352Ts_TypedSplineData<T>::GetKnotDataAsDouble(
353 const size_t index) const
354{
355 const Ts_TypedKnotData<T> &in = knots[index];
356 Ts_TypedKnotData<double> out;
357
358 // Use operator= to copy base-class members. This is admittedly weird, but
359 // it will continue working if members are added to the base class.
360 static_cast<Ts_KnotData&>(out) = static_cast<const Ts_KnotData&>(in);
361
362 // Copy derived members individually.
363 out.value = in.value;
364 out.preValue = in.preValue;
365 out.preTanSlope = in.preTanSlope;
366 out.postTanSlope = in.postTanSlope;
367
368 return out;
369}
370
371template <typename T>
372void Ts_TypedSplineData<T>::ClearKnots()
373{
374 times.clear();
375 customData.clear();
376 knots.clear();
377}
378
379template <typename T>
380void Ts_TypedSplineData<T>::RemoveKnotAtTime(
381 const TsTime time)
382{
383 const auto it = std::lower_bound(times.begin(), times.end(), time);
384 if (it == times.end() || *it != time)
385 {
386 TF_CODING_ERROR("Cannot remove nonexistent knot from SplineData");
387 return;
388 }
389
390 const size_t idx = it - times.begin();
391 times.erase(it);
392 customData.erase(time);
393 knots.erase(knots.begin() + idx);
394}
395
396template <typename T>
397static void _ApplyOffsetAndScaleToKnot(
398 Ts_TypedKnotData<T>* const knotData,
399 const TsTime offset,
400 const double scale)
401{
402 // In our private implementation, we must have set a positive scale.
403 TF_VERIFY(scale > 0);
404
405 // Process knot time (absolute).
406 knotData->time = knotData->time * scale + offset;
407
408 // Process tangent widths (relative, strictly positive).
409 knotData->preTanWidth *= scale;
410 knotData->postTanWidth *= scale;
411
412 // Process slopes (inverse relative).
413 knotData->preTanSlope /= scale;
414 knotData->postTanSlope /= scale;
415}
416
417template <typename T>
418void Ts_TypedSplineData<T>::ApplyOffsetAndScale(
419 const TsTime offset,
420 const double scale)
421{
422 if (scale <= 0)
423 {
424 TF_CODING_ERROR("Applying zero or negative scale to spline data, "
425 "collapsing/reversing time and spline representation "
426 "is not allowed.");
427 return;
428 }
429
430 // The spline is changed in the time dimension only.
431 // Different parameters are affected in different ways:
432 // - Absolute times (e.g. knot times): apply scale and offset.
433 // - Relative times (e.g. tan widths): apply scale only.
434 // - Inverse relative (slopes): slope = height/width, so we apply 1/scale.
435
436 // Scale extrapolation slopes if applicable (inverse relative).
437 if (preExtrapolation.mode == TsExtrapSloped)
438 {
439 preExtrapolation.slope /= scale;
440 }
441 if (postExtrapolation.mode == TsExtrapSloped)
442 {
443 postExtrapolation.slope /= scale;
444 }
445
446 // Process inner-loop params.
447 if (loopParams.protoEnd > loopParams.protoStart)
448 {
449 // Process start and end times (absolute).
450 loopParams.protoStart = loopParams.protoStart * scale + offset;
451 loopParams.protoEnd = loopParams.protoEnd * scale + offset;
452 }
453
454 // Process knot-times vector (absolute).
455 for (TsTime &time : times) {
456 time = time * scale + offset;
457 }
458
459 // Process knots. Duplicate the logic that is applied unconditionally, so
460 // that we can rip through the entire vector just once, and we don't have to
461 // do the if-check on each iteration.
462 if (timeValued)
463 {
464 for (Ts_TypedKnotData<T> &knotData : knots)
465 {
466 _ApplyOffsetAndScaleToKnot(&knotData, offset, scale);
467
468 // Process time values (absolute).
469 knotData.value =
470 static_cast<T>(knotData.value * scale + offset);
471 knotData.preValue =
472 static_cast<T>(knotData.preValue * scale + offset);
473 }
474 }
475 else
476 {
477 for (Ts_TypedKnotData<T> &knotData : knots) {
478 _ApplyOffsetAndScaleToKnot(&knotData, offset, scale);
479 }
480 }
481
482 // Re-index custom data. Times are adjusted absolutely.
483 if (!customData.empty())
484 {
485 std::unordered_map<TsTime, VtDictionary> newCustomData;
486 for (const auto &mapPair : customData) {
487 newCustomData[mapPair.first * scale + offset] = mapPair.second;
488 }
489 customData.swap(newCustomData);
490 }
491}
492
493template <typename T>
494bool Ts_TypedSplineData<T>::HasValueBlocks() const
495{
496 if (knots.empty())
497 {
498 return false;
499 }
500
501 if (preExtrapolation.mode == TsExtrapValueBlock
502 || postExtrapolation.mode == TsExtrapValueBlock)
503 {
504 return true;
505 }
506
507 for (const Ts_TypedKnotData<T> &knotData : knots)
508 {
509 if (knotData.nextInterp == TsInterpValueBlock)
510 {
511 return true;
512 }
513 }
514
515 return false;
516}
517
518template <typename T>
519bool Ts_TypedSplineData<T>::HasValueBlockAtTime(
520 const TsTime time) const
521{
522 // If no knots, no blocks.
523 if (knots.empty())
524 {
525 return false;
526 }
527
528 // Find first knot at or after time.
529 const auto lbIt =
530 std::lower_bound(times.begin(), times.end(), time);
531
532 // If time is after all knots, return whether we have blocked
533 // post-extrapolation.
534 if (lbIt == times.end())
535 {
536 return postExtrapolation.mode == TsExtrapValueBlock;
537 }
538
539 // If there is a knot at this time, return whether its segment has blocked
540 // interpolation.
541 if (*lbIt == time)
542 {
543 const auto knotIt = knots.begin() + (lbIt - times.begin());
544 return knotIt->nextInterp == TsInterpValueBlock;
545 }
546
547 // If time is before all knots, return whether we have blocked
548 // pre-extrapolation.
549 if (lbIt == times.begin())
550 {
551 return preExtrapolation.mode == TsExtrapValueBlock;
552 }
553
554 // Between knots. Return whether the segment that we're in has blocked
555 // interpolation.
556 const auto knotIt = knots.begin() + (lbIt - times.begin());
557 return (knotIt - 1)->nextInterp == TsInterpValueBlock;
558}
559
560template <typename T>
561Ts_TypedSplineData<T>*
562Ts_GetTypedSplineData(TsSpline &spline)
563{
564 return static_cast<Ts_TypedSplineData<T>*>(
565 Ts_GetSplineData(spline));
566}
567
568template <typename T>
569const Ts_TypedSplineData<T>*
570Ts_GetTypedSplineData(const TsSpline &spline)
571{
572 return static_cast<Ts_TypedSplineData<T>*>(
573 Ts_GetSplineData(spline));
574}
575
576
577PXR_NAMESPACE_CLOSE_SCOPE
578
579#endif
Low-level utilities for informing users of various internal and external diagnostic conditions.
TfType represents a dynamic runtime type.
Definition: type.h:48
Extrapolation parameters for the ends of a spline beyond the knots.
Definition: types.h:190
Inner-loop parameters.
Definition: types.h:163
A mathematical description of a curved function from time to value.
Definition: spline.h:59
A map with string keys and VtValue values.
Definition: dictionary.h:43
VT_API bool empty() const
true if the VtDictionary's size is 0.
#define TF_CODING_ERROR(fmt, args)
Issue an internal programming error, but continue execution.
Definition: diagnostic.h:68
#define TF_VERIFY(cond, format,...)
Checks a condition and reports an error if it evaluates false.
Definition: diagnostic.h:266