diff options
Diffstat (limited to 'third_party/rust/aa-stroke/src/bezierflattener.rs')
-rw-r--r-- | third_party/rust/aa-stroke/src/bezierflattener.rs | 828 |
1 files changed, 828 insertions, 0 deletions
diff --git a/third_party/rust/aa-stroke/src/bezierflattener.rs b/third_party/rust/aa-stroke/src/bezierflattener.rs new file mode 100644 index 0000000000..fd1ab21839 --- /dev/null +++ b/third_party/rust/aa-stroke/src/bezierflattener.rs @@ -0,0 +1,828 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +#![allow(non_snake_case)] + +use std::ops::{Sub, Mul, Add, AddAssign, SubAssign, MulAssign, Div}; + +macro_rules! IFC { + ($e: expr) => { + assert_eq!($e, S_OK); + } +} + +pub type HRESULT = i32; + +pub const S_OK: i32 = 0; +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GpPointR { + pub x: f64, + pub y: f64 +} + +impl Sub for GpPointR { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + GpPointR { x: self.x - rhs.x, y: self.y - rhs.y } + } +} + +impl Add for GpPointR { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + GpPointR { x: self.x + rhs.x, y: self.y + rhs.y } + } +} + +impl AddAssign for GpPointR { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl SubAssign for GpPointR { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl MulAssign<f64> for GpPointR { + fn mul_assign(&mut self, rhs: f64) { + *self = *self * rhs; + } +} + + +impl Mul<f64> for GpPointR { + type Output = Self; + + fn mul(self, rhs: f64) -> Self::Output { + GpPointR { x: self.x * rhs, y: self.y * rhs } + } +} + +impl Div<f64> for GpPointR { + type Output = Self; + + fn div(self, rhs: f64) -> Self::Output { + GpPointR { x: self.x / rhs, y: self.y / rhs } + } +} + + +impl Mul for GpPointR { + type Output = f64; + + fn mul(self, rhs: Self) -> Self::Output { + self.x * rhs.x + self.y * rhs.y + } +} + +impl GpPointR { + pub fn ApproxNorm(&self) -> f64 { + self.x.abs().max(self.y.abs()) + } + pub fn Norm(&self) -> f64 { + self.x.hypot(self.y) + } +} + +// Relative to this is relative to the tolerance squared. In other words, a vector +// whose length is less than .01*tolerance will be considered 0 +const SQ_LENGTH_FUZZ: f64 = 1.0e-4; + +// Some of these constants need further thinking + +//const FUZZ: f64 = 1.0e-6; // Relative 0 +// Minimum allowed tolerance - should probably be adjusted to the size of the +// geometry we are rendering, but for now --- + +/* +const FUZZ_DOUBLE: f64 = 1.0e-12; // Double-precision relative 0 +const MIN_TOLERANCE: f64 = 1.0e-6; +const DEFAULT_FLATTENING_TOLERANCE: f64 = 0.25;*/ +const TWICE_MIN_BEZIER_STEP_SIZE: f64 = 1.0e-3; // The step size in the Bezier flattener should + // never go below half this amount. +//+----------------------------------------------------------------------------- +// + +// +// $TAG ENGR + +// $Module: win_mil_graphics_geometry +// $Keywords: +// +// $Description: +// Definition of CBezierFlattener. +// +// $ENDTAG +// +//------------------------------------------------------------------------------ + +//+----------------------------------------------------------------------------- +// +// Class: +// CFlatteningSink +// +// Synopsis: +// Callback interface for the results of curve flattening +// +// Notes: +// Methods are implemented rather than pure, for callers who do not use all +// of them. +// +//------------------------------------------------------------------------------ +// +// Definition of CFlatteningSink +// +//------------------------------------------------------------------------------ +/* +struct CFlatteningSink +{ +public: + CFlatteningSink() {} + + virtual ~CFlatteningSink() {} + + virtual HRESULT Begin( + __in_ecount(1) const GpPointR &) + // First point (transformed) + { + // Do nothing stub, should not be called + RIP("Base class Begin called"); + return E_NOTIMPL; + } + + virtual HRESULT AcceptPoint( + __in_ecount(1) const GpPointR &pt, + // The point + IN GpReal t, + // Parameter we're at + __out_ecount(1) bool &fAborted) + // Set to true to signal aborting + { + UNREFERENCED_PARAMETER(pt); + UNREFERENCED_PARAMETER(t); + UNREFERENCED_PARAMETER(fAborted); + + // Do nothing stub, should not be called + RIP("Base class AcceptPoint called"); + return E_NOTIMPL; + } + + virtual HRESULT AcceptPointAndTangent( + __in_ecount(1) const GpPointR &, + //The point + __in_ecount(1) const GpPointR &, + //The tangent there + IN bool fLast) // Is this the last point on the curve? + { + // Do nothing stub, should not be called + RIP("Base class AcceptPointAndTangent called"); + return E_NOTIMPL; + } +}; + + + +*/ +#[derive(Clone, Debug)] + +pub struct CBezier +{ + /* +public: + CBezier() + { + } + + CBezier( + __in_ecount(4) const GpPointR *pPt) + // The defining Bezier points + { + Assert(pPt); + memcpy(&m_ptB, pPt, 4 * sizeof(GpPointR)); + } + + CBezier( + __in_ecount(1) const CBezier &other) + // Another Bezier to copy + { + Copy(other); + } + + void Copy( + __in_ecount(1) const CBezier &other) + // Another Bezier to copy + { + memcpy(&m_ptB, other.m_ptB, 4 * sizeof(GpPointR)); + } + + void Initialize( + __in_ecount(1) const GpPointR &ptFirst, + // The first Bezier point + __in_ecount(3) const GpPointR *pPt) + // The remaining 3 Bezier points + { + m_ptB[0] = ptFirst; + memcpy(m_ptB + 1, pPt, 3 * sizeof(GpPointR)); + } + + __outro_ecount(1) const GpPointR &GetControlPoint(__range(0, 3) UINT i) const + { + Assert(i < 4); + return m_ptB[i]; + } + + __outro_ecount(1) const GpPointR &GetFirstPoint() const + { + return m_ptB[0]; + } + + __outro_ecount(1) const GpPointR &GetLastPoint() const + { + return m_ptB[3]; + } + + void GetPoint( + _In_ double t, + // Parameter value + __out_ecount(1) GpPointR &pt) const; + // Point there + + void GetPointAndDerivatives( + __in double t, + // Parameter value + __out_ecount(3) GpPointR *pValues) const; + // Point, first derivative and second derivative there + + void TrimToStartAt( + IN double t); // Parameter value + + void TrimToEndAt( + IN double t); // Parameter value + + bool TrimBetween( + __in double rStart, + // Parameter value for the new start, must be between 0 and 1 + __in double rEnd); + // Parameter value for the new end, must be between 0 and 1 + + bool operator ==(__in_ecount(1) const CBezier &other) const + { + return (m_ptB[0] == other.m_ptB[0]) && + (m_ptB[1] == other.m_ptB[1]) && + (m_ptB[2] == other.m_ptB[2]) && + (m_ptB[3] == other.m_ptB[3]); + } + + void AssertEqualOrNaN(__in_ecount(1) const CBezier &other) const + { + m_ptB[0].AssertEqualOrNaN(other.m_ptB[0]); + m_ptB[1].AssertEqualOrNaN(other.m_ptB[1]); + m_ptB[2].AssertEqualOrNaN(other.m_ptB[2]); + m_ptB[3].AssertEqualOrNaN(other.m_ptB[3]); + } + +protected: + */ + // Data + m_ptB: [GpPointR; 4], + // The defining Bezier points +} + +impl CBezier { + pub fn new(curve: [GpPointR; 4]) -> Self { + Self { m_ptB: curve } + } + + pub fn is_degenerate(&self) -> bool { + self.m_ptB[0] == self.m_ptB[1] && + self.m_ptB[0] == self.m_ptB[2] && + self.m_ptB[0] == self.m_ptB[3] + } +} + +pub trait CFlatteningSink { + fn AcceptPointAndTangent(&mut self, + pt: &GpPointR, + // The point + vec: &GpPointR, + // The tangent there + fLast: bool + // Is this the last point on the curve? + ) -> HRESULT; + + fn AcceptPoint(&mut self, + pt: &GpPointR, + // The point + t: f64, + // Parameter we're at + fAborted: &mut bool, + lastPoint: bool + ) -> HRESULT; +} + +//+----------------------------------------------------------------------------- +// +// Class: +// CBezierFlattener +// +// Synopsis: +// Generates a polygonal apprximation to a given Bezier curve +// +//------------------------------------------------------------------------------ +pub struct CBezierFlattener<'a> +{ + bezier: CBezier, + // Flattening defining data + m_pSink: &'a mut dyn CFlatteningSink, // The recipient of the flattening data + m_rTolerance: f64, // Prescribed tolerance + m_fWithTangents: bool, // Generate tangent vectors if true + m_rQuarterTolerance: f64,// Prescribed tolerance/4 (for doubling the step) + m_rFuzz: f64, // Computational zero + + // Flattening working data + m_ptE: [GpPointR; 4], // The moving basis of the curve definition + m_cSteps: i32, // The number of steps left to the end of the curve + m_rParameter: f64, // Parameter value + m_rStepSize: f64, // Steps size in parameter domain +} +impl<'a> CBezierFlattener<'a> { + /*fn new( + __in_ecount_opt(1) CFlatteningSink *pSink, + // The reciptient of the flattened data + IN GpReal rTolerance) + // Flattening tolerance + { + Initialize(pSink, rTolerance); + }*/ +/* + void SetTarget(__in_ecount_opt(1) CFlatteningSink *pSink) + { + m_pSink = pSink; + } + + void Initialize( + __in_ecount_opt(1) CFlatteningSink *pSink, + // The reciptient of the flattened data + IN GpReal rTolerance); + // Flattening tolerance + + void SetPoint( + __in UINT i, + // index of the point (must be between 0 and 3) + __in_ecount(1) const GpPointR &pt) + // point value + { + Assert(i < 4); + m_ptB[i] = pt; + } + + HRESULT GetFirstTangent( + __out_ecount(1) GpPointR &vecTangent) const; + // Tangent vector there + + GpPointR GetLastTangent() const; + + HRESULT Flatten( + IN bool fWithTangents); // Return tangents with the points if true + +private: + // Disallow copy constructor + CBezierFlattener(__in_ecount(1) const CBezierFlattener &) + { + RIP("CBezierFlattener copy constructor reached."); + } + +protected: +*/ +/* fn Step( + __out_ecount(1) bool &fAbort); // Set to true if flattening should be aborted + + fn HalveTheStep(); + + fn TryDoubleTheStep();*/ + +} + + + + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + + +//+----------------------------------------------------------------------------- +// + +// +// $TAG ENGR + +// $Module: win_mil_graphics_geometry +// $Keywords: +// +// $Description: +// Implementation of CBezierFlattener. +// +// $ENDTAG +// +//------------------------------------------------------------------------------ + +impl<'a> CBezierFlattener<'a> { +///////////////////////////////////////////////////////////////////////////////// +// +// Implementation of CBezierFlattener + +//+----------------------------------------------------------------------------- +// +// Member: +// CBezierFlattener::Initialize +// +// Synopsis: +// Initialize the sink and tolerance +// +//------------------------------------------------------------------------------ +pub fn new(bezier: &CBezier, + pSink: &'a mut dyn CFlatteningSink, + // The reciptient of the flattened data + rTolerance: f64) // Flattening tolerance + -> Self +{ + let mut result = CBezierFlattener { + bezier: bezier.clone(), + // Flattening defining data + m_pSink: pSink, // The recipient of the flattening data + m_rTolerance: 0., // Prescribed tolerance + m_fWithTangents: false, // Generate tangent vectors if true + m_rQuarterTolerance: 0.,// Prescribed tolerance/4 (for doubling the step) + m_rFuzz: 0., // Computational zero + + // Flattening working data + m_ptE: [GpPointR { x: 0., y: 0.}; 4], // The moving basis of the curve definition + m_cSteps: 0, // The number of steps left to the end of the curve + m_rParameter: 0., // Parameter value + m_rStepSize: 0., // Steps size in parameter domain + }; + + // If rTolerance == NaN or less than 0, we'll treat it as 0. + result.m_rTolerance = if rTolerance >= 0.0 { rTolerance } else { 0.0 }; + result.m_rFuzz = rTolerance * rTolerance * SQ_LENGTH_FUZZ; + + // The error is tested on max(|e2|, |e2|), which represent 6 times the actual error, so: + result.m_rTolerance *= 6.; + result.m_rQuarterTolerance = result.m_rTolerance * 0.25; + result +} + +//+----------------------------------------------------------------------------- +// +// Member: +// CBezierFlattener::Flatten +// +// Synopsis: +// Flatten this curve +// +// Notes: + +// The algorithm is described in detail in the 1995 patent # 5367617 "System and +// method of hybrid forward differencing to render Bezier splines" to be found +// on the Microsoft legal dept. web site (LCAWEB). Additional references are: +// Lien, Shantz and Vaughan Pratt, "Adaptive Forward Differencing for +// Rendering Curves and Surfaces", Computer Graphics, July 1987 +// Chang and Shantz, "Rendering Trimmed NURBS with Adaptive Forward +// Differencing", Computer Graphics, August 1988 +// Foley and Van Dam, "Fundamentals of Interactive Computer Graphics" +// +// The basic idea is to replace the Bernstein basis (underlying Bezier curves) +// with the Hybrid Forward Differencing (HFD) basis which is more efficient at +// for flattening. Each one of the 3 actions - Step, Halve and Double (step +// size) this basis affords very efficient formulas for computing coefficients +// for the new interval. +// +// The coefficients of the HFD basis are defined in terms of the Bezier +// coefficients as follows: +// +// e0 = p0, e1 = p3 - p0, e2 = 6(p1 - 2p2 + p3), e3 = 6(p0 - 2p1 + p2), +// +// but formulas may be easier to understand by going through the power basis +// representation: f(t) = a*t + b*t + c * t^2 + d * t^3. +// +// The conversion is then: +// e0 = a +// e1 = f(1) - f(0) = b + c + d +// e2 = f"(1) = 2c + 6d +// e3 = f"(0) = 2c +// +// This is inverted to: +// a = e0 +// c = e3 / 2 +// d = (e2 - 2c) / 6 = (e2 - e3) / 6 +// b = e1 - c - d = e1 - e2 / 6 - e3 / 3 +// +// a, b, c, d for the new (halved, doubled or forwarded) interval are derived +// and then converted to e0, e1, e2, e3 using these relationships. +// +// An exact integer version is implemented in Bezier.h and Bezier.cpp. +// +//------------------------------------------------------------------------------ + + +pub fn Flatten(&mut self, + fWithTangents: bool) // Return tangents with the points if true + -> HRESULT +{ + + let hr = S_OK; + let mut fAbort = false; + + /*if (!self.m_pSink) + { + return E_UNEXPECTED; + }*/ + + self.m_fWithTangents = fWithTangents; + + self.m_cSteps = 1; + + self.m_rParameter = 0.; + self.m_rStepSize = 1.; + + // Compute the HFD basis + self.m_ptE[0] = self.bezier.m_ptB[0]; + self.m_ptE[1] = self.bezier.m_ptB[3] - self.bezier.m_ptB[0]; + self.m_ptE[2] = (self.bezier.m_ptB[1] - self.bezier.m_ptB[2] * 2. + self.bezier.m_ptB[3]) * 6.; // The second derivative at curve end + self.m_ptE[3] = (self.bezier.m_ptB[0] - self.bezier.m_ptB[1] * 2. + self.bezier.m_ptB[2]) * 6.; // The second derivative at curve start + + // Determine the initial step size + self.m_cSteps = 1; + while ((self.m_ptE[2].ApproxNorm() > self.m_rTolerance) || (self.m_ptE[3].ApproxNorm() > self.m_rTolerance)) && + (self.m_rStepSize > TWICE_MIN_BEZIER_STEP_SIZE) + + { + self.HalveTheStep(); + } + + while self.m_cSteps > 1 + { + IFC!(self.Step(&mut fAbort)); + if fAbort { + return hr; + } + + // E[3] was already tested as E[2] in the previous step + if self.m_ptE[2].ApproxNorm() > self.m_rTolerance && + self.m_rStepSize > TWICE_MIN_BEZIER_STEP_SIZE + { + // Halving the step once is provably sufficient (see Notes above), so --- + self.HalveTheStep(); + } + else + { + // --- but the step can possibly be more than doubled, hence the while loop + while self.TryDoubleTheStep() { + continue; + } + } + } + + // Last point + if self.m_fWithTangents + { + IFC!(self.m_pSink.AcceptPointAndTangent(&self.bezier.m_ptB[3], &self.GetLastTangent(), true /* last point */)); + } + else + { + IFC!(self.m_pSink.AcceptPoint(&self.bezier.m_ptB[3], 1., &mut fAbort, true)); + } + + return hr; +} +//+----------------------------------------------------------------------------- +// +// Member: +// CBezierFlattener::Step +// +// Synopsis: +// Step forward on the polygonal approximation of the curve +// +// Notes: +// Taking a step means replacing a,b,c,d by coefficients of g(t) = f(t+1). +// Express those in terms of a,b,c,d and convert to e0, e1, e2, e3 to get: +// +// New e0 = e0 + e1 +// New e1 = e1 + e2 +// New e2 = 2e2 - e3 +// New e3 = e2 +// +// The patent application (see above) explains why. +// +// Getting a tangent vector is a minor enhancement along the same lines: +// f'(0) = b = 6e1 - e2 - 2e3. +// +//------------------------------------------------------------------------------ + +fn Step(&mut self, + fAbort: &mut bool) -> HRESULT // Set to true if flattening should be aborted, untouched otherwise +{ + let hr = S_OK; + + // Compute the basis for the same curve on the next interval + let mut pt; + + self.m_ptE[0] += self.m_ptE[1]; + pt = self.m_ptE[2]; + self.m_ptE[1] += pt; + self.m_ptE[2] += pt; self.m_ptE[2] -= self.m_ptE[3]; + self.m_ptE[3] = pt; + + // Increment the parameter + self.m_rParameter += self.m_rStepSize; + + // Generate the start point of the new interval + if self.m_fWithTangents + { + // Compute the tangent there + pt = self.m_ptE[1] * 6. - self.m_ptE[2] - self.m_ptE[3] * 2.; // = twice the derivative at E[0] + IFC!(self.m_pSink.AcceptPointAndTangent(&self.m_ptE[0], &pt, false /* not the last point */)); + } + else + { + IFC!(self.m_pSink.AcceptPoint(&self.m_ptE[0], self.m_rParameter, fAbort, false)); + } + + self.m_cSteps-=1; + return hr; +} +//+----------------------------------------------------------------------------- +// +// Member: +// CBezierFlattener::HalveTheStep +// +// Synopsis: +// Halve the size of the step +// +// Notes: +// Halving the step means replacing a,b,c,d by coefficients of g(t) = +// f(t/2). Experss those in terms of a,b,c,d and convert to e0, e1, e2, e3 +// to get: +// +// New e0 = e0 +// New e1 = (e1 - e2) / 2 +// New e2 = (e2 + e3) / 8 +// New e3 = e3 / 4 +// +// The patent application (see above) explains why. +// +//------------------------------------------------------------------------------ +fn HalveTheStep(&mut self) +{ + self.m_ptE[2] += self.m_ptE[3]; self.m_ptE[2] *= 0.125; + self.m_ptE[1] -= self.m_ptE[2]; self.m_ptE[1] *= 0.5; + self.m_ptE[3] *= 0.25; + + self.m_cSteps *= 2; // Double the number of steps left + self.m_rStepSize *= 0.5; +} +//+----------------------------------------------------------------------------- +// +// Member: +// CBezierFlattener::TryDoubleTheStep +// +// Synopsis: +// Double the step size if possible within tolerance. +// +// Notes: +// Coubling the step means replacing a,b,c,d by coefficients of g(t) = +// f(2t). Experss those in terms of a,b,c,d and convert to e0, e1, e2, e3 +// to get: +// +// New e0 = e0 +// New e1 = 2e1 + e2 +// New e2 = 8e2 - 4e3 +// New e3 = 4e3 +// +// The patent application (see above) explains why. Note also that these +// formulas are the inverse of those for halving the step. +// +//------------------------------------------------------------------------------ +fn +TryDoubleTheStep(&mut self) -> bool +{ + let mut fDoubled = 0 == (self.m_cSteps & 1); + if fDoubled + { + let ptTemp = self.m_ptE[2] * 2. - self.m_ptE[3]; + + fDoubled = (self.m_ptE[3].ApproxNorm() <= self.m_rQuarterTolerance) && + (ptTemp.ApproxNorm() <= self.m_rQuarterTolerance); + + if fDoubled + { + self.m_ptE[1] *= 2.; self.m_ptE[1] += self.m_ptE[2]; + self.m_ptE[3] *= 4.; + self.m_ptE[2] = ptTemp * 4.; + + self.m_cSteps /= 2; // Halve the number of steps left + self.m_rStepSize *= 2.; + } + } + + return fDoubled; +} +//+----------------------------------------------------------------------------- +// +// Member: +// CBezierFlattener::GetFirstTangent +// +// Synopsis: +// Get the tangent at curve start +// +// Return: +// WGXERR_ZEROVECTOR if the tangent vector has practically 0 length +// +// Notes: +// This method can return an error if all the points are bunched together. +// The idea is that the caller will detect that, abandon this curve, and +// never call GetLasttangent, which can therefore be presumed to succeed. +// The failure here is benign. +// +//------------------------------------------------------------------------------ +#[allow(dead_code)] +fn GetFirstTangent(&self) -> Option<GpPointR> // Tangent vector there + +{ + + let mut vecTangent = self.bezier.m_ptB[1] - self.bezier.m_ptB[0]; + if vecTangent * vecTangent > self.m_rFuzz + { + return Some(vecTangent); // - we're done + } + // Zero first derivative, go for the second + vecTangent = self.bezier.m_ptB[2] - self.bezier.m_ptB[0]; + if vecTangent * vecTangent > self.m_rFuzz + { + return Some(vecTangent); // - we're done + } + // Zero second derivative, go for the third + vecTangent = self.bezier.m_ptB[3] - self.bezier.m_ptB[0]; + + if vecTangent * vecTangent <= self.m_rFuzz + { + return None; + } + + return Some(vecTangent); // no RRETURN, error is expected +} +//+----------------------------------------------------------------------------- +// +// Member: +// CBezierFlattener::GetLastTangent +// +// Synopsis: +// Get the tangent at curve end +// +// Return: +// The tangent +// +// Notes: +// This method has no error return while GetFirstTangent returns +// WGXERR_ZEROVECTOR if the tangent is zero. The idea is that we should +// only fail if all the control points coincide, that should have been +// detected at GetFirstTangent, and then we should have not be called. +// +//------------------------------------------------------------------------------ +fn GetLastTangent(&self) -> GpPointR +{ + let mut vecTangent = self.bezier.m_ptB[3] - self.bezier.m_ptB[2]; + + // If the curve is degenerate, we should have detected it at curve-start, skipped this curve + // altogether and not be here. But the test in GetFirstTangent is for the point-differences + // 1-0, 2-0 and 3-0, while here it is for points 3-2, 3-1 and 3-0, which is not quite the same. + // Still, In a disk of radius r no 2 points are more than 2r apart. The tests are done with + // squared distance, and m_rFuzz is the minimal accepted squared distance. GetFirstTangent() + // succeeded, so there is a pair of points whose squared distance is greater than m_rfuzz. + // So the squared radius of a disk about point 3 that contains the remaining points must be + // at least m_rFuzz/4. Allowing some margin for arithmetic error: + + let rLastTangentFuzz = self.m_rFuzz/8.; + + if vecTangent * vecTangent <= rLastTangentFuzz + { + // Zero first derivative, go for the second + vecTangent = self.bezier.m_ptB[3] - self.bezier.m_ptB[1]; + if vecTangent * vecTangent <= rLastTangentFuzz + { + // Zero second derivative, go for the third + vecTangent = self.bezier.m_ptB[3] - self.bezier.m_ptB[0]; + } + } + + debug_assert! (!(vecTangent * vecTangent < rLastTangentFuzz)); // Ignore NaNs + + return vecTangent; +} +} |