fable 3.1.55 → 3.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,5 +16,8 @@
16
16
  "X": 10,
17
17
  "Y": 10
18
18
  }
19
- ]
19
+ ],
20
+
21
+ "BezierXValues": [0, 1, 2, 3, 4],
22
+ "BezierYValues": [0, 1, 4, 9, 16]
20
23
  }
@@ -8,6 +8,7 @@
8
8
  { "Equation":"Result = 1 * sqrt(16)", "ExpectedResult":"4" },
9
9
  { "Equation":"Result = sqrt(100 * (C + 30)) + sin(Depth - Width) / 10", "ExpectedResult":"41.32965489638783839821" },
10
10
  { "Equation":"Result = 3.5 + 50 + 10 * 10 / 5 - 1.5", "ExpectedResult":"72" },
11
- { "Equation":"concataddr(Cities[].Name)", "ExpectedResult":"City1City2" }
11
+ { "Equation":"concataddr(Cities[].Name)", "ExpectedResult":"City1City2" },
12
+ { "Equation":"BezierMidpoint = BEZIERPOINT(0, 10, 20, 30, 0.5)", "ExpectedResult":"15" }
12
13
  ]
13
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fable",
3
- "version": "3.1.55",
3
+ "version": "3.1.58",
4
4
  "description": "A service dependency injection, configuration and logging library.",
5
5
  "main": "source/Fable.js",
6
6
  "scripts": {
@@ -50,21 +50,21 @@
50
50
  },
51
51
  "homepage": "https://github.com/stevenvelozo/fable",
52
52
  "devDependencies": {
53
- "quackage": "^1.0.50"
53
+ "quackage": "^1.0.51"
54
54
  },
55
55
  "dependencies": {
56
56
  "async.eachlimit": "^0.5.2",
57
57
  "async.waterfall": "^0.5.2",
58
58
  "big.js": "^7.0.1",
59
- "cachetrax": "^1.0.4",
60
- "cookie": "^1.0.2",
59
+ "cachetrax": "^1.0.5",
60
+ "cookie": "^1.1.1",
61
61
  "data-arithmatic": "^1.0.7",
62
62
  "dayjs": "^1.11.19",
63
- "fable-log": "^3.0.16",
64
- "fable-serviceproviderbase": "^3.0.17",
65
- "fable-settings": "^3.0.14",
66
- "fable-uuid": "^3.0.11",
67
- "manyfest": "^1.0.44",
63
+ "fable-log": "^3.0.17",
64
+ "fable-serviceproviderbase": "^3.0.18",
65
+ "fable-settings": "^3.0.15",
66
+ "fable-uuid": "^3.0.12",
67
+ "manyfest": "^1.0.46",
68
68
  "simple-get": "^4.0.1"
69
69
  }
70
70
  }
@@ -493,5 +493,14 @@
493
493
  "stringgetsegments": {
494
494
  "Name": "Get Segments from a String",
495
495
  "Address": "fable.DataFormat.stringGetSegments"
496
+ },
497
+
498
+ "bezierpoint": {
499
+ "Name": "Evaluate a Point on a Cubic Bezier Curve at Parameter t",
500
+ "Address": "fable.Math.bezierPoint"
501
+ },
502
+ "beziercurvefit": {
503
+ "Name": "Fit a Cubic Bezier Curve to a Set of Data Points",
504
+ "Address": "fable.Math.bezierCurveFit"
496
505
  }
497
506
  }
@@ -1863,6 +1863,185 @@ class FableServiceMath extends libFableServiceBase
1863
1863
  return this.addPrecise(sum, this.multiplyPrecise(b, tmpIndependentVariableVector[i]));
1864
1864
  }, pRegressionCoefficients[0]);
1865
1865
  }
1866
+
1867
+ /**
1868
+ * Evaluate a point on a cubic bezier curve at parameter t.
1869
+ *
1870
+ * B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3
1871
+ *
1872
+ * @param {number|string} pP0 - First control point value
1873
+ * @param {number|string} pP1 - Second control point value
1874
+ * @param {number|string} pP2 - Third control point value
1875
+ * @param {number|string} pP3 - Fourth control point value
1876
+ * @param {number|string} pT - Parameter t in [0,1]
1877
+ *
1878
+ * @return {string} - The bezier curve value at parameter t
1879
+ */
1880
+ bezierPoint(pP0, pP1, pP2, pP3, pT)
1881
+ {
1882
+ let tmpT = this.parsePrecise(pT, 0);
1883
+ let tmpOneMinusT = this.subtractPrecise(1, tmpT);
1884
+
1885
+ // (1-t)^3 * P0
1886
+ let tmpTerm0 = this.multiplyPrecise(this.powerPrecise(tmpOneMinusT, 3), pP0);
1887
+ // 3 * (1-t)^2 * t * P1
1888
+ let tmpTerm1 = this.multiplyPrecise(this.multiplyPrecise(this.multiplyPrecise(3, this.powerPrecise(tmpOneMinusT, 2)), tmpT), pP1);
1889
+ // 3 * (1-t) * t^2 * P2
1890
+ let tmpTerm2 = this.multiplyPrecise(this.multiplyPrecise(this.multiplyPrecise(3, tmpOneMinusT), this.powerPrecise(tmpT, 2)), pP2);
1891
+ // t^3 * P3
1892
+ let tmpTerm3 = this.multiplyPrecise(this.powerPrecise(tmpT, 3), pP3);
1893
+
1894
+ return this.addPrecise(this.addPrecise(tmpTerm0, tmpTerm1), this.addPrecise(tmpTerm2, tmpTerm3));
1895
+ }
1896
+
1897
+ /**
1898
+ * Fit a cubic bezier curve to a set of data points using least-squares optimization.
1899
+ *
1900
+ * Given arrays of X and Y values representing data points, this function finds the four
1901
+ * control points (P0, P1, P2, P3) of a cubic bezier curve that best fits the data.
1902
+ *
1903
+ * The first and last control points are pinned to the first and last data points.
1904
+ * The interior control points (P1, P2) are found by least-squares minimization of
1905
+ * the squared distances between the data points and the curve.
1906
+ *
1907
+ * Parameter t values are assigned by chord-length parameterization: each data point
1908
+ * gets a t value proportional to its cumulative distance along the polyline.
1909
+ *
1910
+ * @param {Array<number|string>} pXValues - Array of x coordinates
1911
+ * @param {Array<number|string>} pYValues - Array of y coordinates
1912
+ *
1913
+ * @return {Array<Array<string>>} - Four control points as [[x0,y0], [x1,y1], [x2,y2], [x3,y3]]
1914
+ */
1915
+ bezierCurveFit(pXValues, pYValues)
1916
+ {
1917
+ if (!Array.isArray(pXValues) || !Array.isArray(pYValues))
1918
+ {
1919
+ this.log.warn('bezierCurveFit: pXValues and pYValues must be arrays');
1920
+ return [[0, 0], [0, 0], [0, 0], [0, 0]];
1921
+ }
1922
+
1923
+ let tmpN = Math.min(pXValues.length, pYValues.length);
1924
+
1925
+ if (tmpN < 2)
1926
+ {
1927
+ this.log.warn('bezierCurveFit: need at least 2 data points');
1928
+ return [[0, 0], [0, 0], [0, 0], [0, 0]];
1929
+ }
1930
+
1931
+ // Pin P0 and P3 to the first and last data points
1932
+ let tmpP0x = this.parsePrecise(pXValues[0], 0);
1933
+ let tmpP0y = this.parsePrecise(pYValues[0], 0);
1934
+ let tmpP3x = this.parsePrecise(pXValues[tmpN - 1], 0);
1935
+ let tmpP3y = this.parsePrecise(pYValues[tmpN - 1], 0);
1936
+
1937
+ if (tmpN === 2)
1938
+ {
1939
+ // With only two points, place control points at 1/3 and 2/3 along the line
1940
+ let tmpP1x = this.addPrecise(tmpP0x, this.dividePrecise(this.subtractPrecise(tmpP3x, tmpP0x), 3));
1941
+ let tmpP1y = this.addPrecise(tmpP0y, this.dividePrecise(this.subtractPrecise(tmpP3y, tmpP0y), 3));
1942
+ let tmpP2x = this.addPrecise(tmpP0x, this.multiplyPrecise(this.dividePrecise(this.subtractPrecise(tmpP3x, tmpP0x), 3), 2));
1943
+ let tmpP2y = this.addPrecise(tmpP0y, this.multiplyPrecise(this.dividePrecise(this.subtractPrecise(tmpP3y, tmpP0y), 3), 2));
1944
+ return [
1945
+ [tmpP0x.toString(), tmpP0y.toString()],
1946
+ [tmpP1x.toString(), tmpP1y.toString()],
1947
+ [tmpP2x.toString(), tmpP2y.toString()],
1948
+ [tmpP3x.toString(), tmpP3y.toString()]
1949
+ ];
1950
+ }
1951
+
1952
+ // Compute chord-length parameterization for t values
1953
+ let tmpDistances = [0];
1954
+ for (let i = 1; i < tmpN; i++)
1955
+ {
1956
+ let tmpDx = this.subtractPrecise(pXValues[i], pXValues[i - 1]);
1957
+ let tmpDy = this.subtractPrecise(pYValues[i], pYValues[i - 1]);
1958
+ let tmpDist = this.sqrtPrecise(this.addPrecise(this.multiplyPrecise(tmpDx, tmpDx), this.multiplyPrecise(tmpDy, tmpDy)));
1959
+ tmpDistances.push(this.addPrecise(tmpDistances[i - 1], tmpDist));
1960
+ }
1961
+
1962
+ let tmpTotalLength = tmpDistances[tmpN - 1];
1963
+ let tmpTValues = [];
1964
+ for (let i = 0; i < tmpN; i++)
1965
+ {
1966
+ if (this.comparePrecise(tmpTotalLength, 0) == 0)
1967
+ {
1968
+ tmpTValues.push(this.dividePrecise(i, tmpN - 1));
1969
+ }
1970
+ else
1971
+ {
1972
+ tmpTValues.push(this.dividePrecise(tmpDistances[i], tmpTotalLength));
1973
+ }
1974
+ }
1975
+
1976
+ // Build the least-squares system for interior control points P1 and P2.
1977
+ // For each data point i with parameter t_i:
1978
+ // B(t_i) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3
1979
+ //
1980
+ // We want to minimize sum of |DataPoint_i - B(t_i)|^2 over P1 and P2.
1981
+ // Let A1(t) = 3*(1-t)^2*t and A2(t) = 3*(1-t)*t^2.
1982
+ // Then: A1(t)*P1 + A2(t)*P2 = DataPoint - (1-t)^3*P0 - t^3*P3
1983
+ //
1984
+ // This gives a 2x2 linear system (solved independently for x and y).
1985
+
1986
+ let tmpC11 = 0, tmpC12 = 0, tmpC22 = 0;
1987
+ let tmpRx1 = 0, tmpRx2 = 0;
1988
+ let tmpRy1 = 0, tmpRy2 = 0;
1989
+
1990
+ for (let i = 0; i < tmpN; i++)
1991
+ {
1992
+ let tmpT = tmpTValues[i];
1993
+ let tmpOneMinusT = this.subtractPrecise(1, tmpT);
1994
+
1995
+ // Basis functions for P1 and P2
1996
+ let tmpA1 = this.multiplyPrecise(this.multiplyPrecise(3, this.powerPrecise(tmpOneMinusT, 2)), tmpT);
1997
+ let tmpA2 = this.multiplyPrecise(this.multiplyPrecise(3, tmpOneMinusT), this.powerPrecise(tmpT, 2));
1998
+
1999
+ // Build normal equations: C * [P1; P2] = R
2000
+ tmpC11 = this.addPrecise(tmpC11, this.multiplyPrecise(tmpA1, tmpA1));
2001
+ tmpC12 = this.addPrecise(tmpC12, this.multiplyPrecise(tmpA1, tmpA2));
2002
+ tmpC22 = this.addPrecise(tmpC22, this.multiplyPrecise(tmpA2, tmpA2));
2003
+
2004
+ // Right-hand side: DataPoint - (1-t)^3*P0 - t^3*P3
2005
+ let tmpB0 = this.powerPrecise(tmpOneMinusT, 3);
2006
+ let tmpB3 = this.powerPrecise(tmpT, 3);
2007
+
2008
+ let tmpResidualX = this.subtractPrecise(this.subtractPrecise(pXValues[i], this.multiplyPrecise(tmpB0, tmpP0x)), this.multiplyPrecise(tmpB3, tmpP3x));
2009
+ let tmpResidualY = this.subtractPrecise(this.subtractPrecise(pYValues[i], this.multiplyPrecise(tmpB0, tmpP0y)), this.multiplyPrecise(tmpB3, tmpP3y));
2010
+
2011
+ tmpRx1 = this.addPrecise(tmpRx1, this.multiplyPrecise(tmpA1, tmpResidualX));
2012
+ tmpRx2 = this.addPrecise(tmpRx2, this.multiplyPrecise(tmpA2, tmpResidualX));
2013
+ tmpRy1 = this.addPrecise(tmpRy1, this.multiplyPrecise(tmpA1, tmpResidualY));
2014
+ tmpRy2 = this.addPrecise(tmpRy2, this.multiplyPrecise(tmpA2, tmpResidualY));
2015
+ }
2016
+
2017
+ // Solve the 2x2 system: [[C11, C12], [C12, C22]] * [P1, P2] = [R1, R2]
2018
+ let tmpDet = this.subtractPrecise(this.multiplyPrecise(tmpC11, tmpC22), this.multiplyPrecise(tmpC12, tmpC12));
2019
+
2020
+ let tmpP1x, tmpP1y, tmpP2x, tmpP2y;
2021
+
2022
+ if (this.comparePrecise(this.absPrecise(tmpDet), '1e-20') < 0)
2023
+ {
2024
+ // Degenerate case: place control points at 1/3 and 2/3 along the line
2025
+ tmpP1x = this.addPrecise(tmpP0x, this.dividePrecise(this.subtractPrecise(tmpP3x, tmpP0x), 3));
2026
+ tmpP1y = this.addPrecise(tmpP0y, this.dividePrecise(this.subtractPrecise(tmpP3y, tmpP0y), 3));
2027
+ tmpP2x = this.addPrecise(tmpP0x, this.multiplyPrecise(this.dividePrecise(this.subtractPrecise(tmpP3x, tmpP0x), 3), 2));
2028
+ tmpP2y = this.addPrecise(tmpP0y, this.multiplyPrecise(this.dividePrecise(this.subtractPrecise(tmpP3y, tmpP0y), 3), 2));
2029
+ }
2030
+ else
2031
+ {
2032
+ tmpP1x = this.dividePrecise(this.subtractPrecise(this.multiplyPrecise(tmpC22, tmpRx1), this.multiplyPrecise(tmpC12, tmpRx2)), tmpDet);
2033
+ tmpP1y = this.dividePrecise(this.subtractPrecise(this.multiplyPrecise(tmpC22, tmpRy1), this.multiplyPrecise(tmpC12, tmpRy2)), tmpDet);
2034
+ tmpP2x = this.dividePrecise(this.subtractPrecise(this.multiplyPrecise(tmpC11, tmpRx2), this.multiplyPrecise(tmpC12, tmpRx1)), tmpDet);
2035
+ tmpP2y = this.dividePrecise(this.subtractPrecise(this.multiplyPrecise(tmpC11, tmpRy2), this.multiplyPrecise(tmpC12, tmpRy1)), tmpDet);
2036
+ }
2037
+
2038
+ return [
2039
+ [tmpP0x.toString(), tmpP0y.toString()],
2040
+ [tmpP1x.toString(), tmpP1y.toString()],
2041
+ [tmpP2x.toString(), tmpP2y.toString()],
2042
+ [tmpP3x.toString(), tmpP3y.toString()]
2043
+ ];
2044
+ }
1866
2045
  }
1867
2046
 
1868
2047
  module.exports = FableServiceMath;
@@ -746,6 +746,50 @@ suite
746
746
  }
747
747
  );
748
748
  test
749
+ (
750
+ 'Bezier Curve Functions',
751
+ (fDone) =>
752
+ {
753
+ let testFable = new libFable();
754
+ let _Parser = testFable.instantiateServiceProviderIfNotExists('ExpressionParser');
755
+
756
+ let tmpResultsObject = {};
757
+ let tmpDestinationObject = {};
758
+
759
+ // Test bezierPoint through the expression parser
760
+ // bezierPoint(P0, P1, P2, P3, t) at t=0 should return P0
761
+ _Parser.solve('PointAtZero = BEZIERPOINT(0, 10, 20, 30, 0)', testFable, tmpResultsObject, false, tmpDestinationObject);
762
+ Expect(tmpDestinationObject.PointAtZero).to.equal('0');
763
+
764
+ // bezierPoint at t=1 should return P3
765
+ _Parser.solve('PointAtOne = BEZIERPOINT(0, 10, 20, 30, 1)', testFable, tmpResultsObject, false, tmpDestinationObject);
766
+ Expect(tmpDestinationObject.PointAtOne).to.equal('30');
767
+
768
+ // bezierPoint at t=0.5 for a linear distribution P0=0, P1=10, P2=20, P3=30
769
+ _Parser.solve('PointAtHalf = BEZIERPOINT(0, 10, 20, 30, 0.5)', testFable, tmpResultsObject, false, tmpDestinationObject);
770
+ Expect(tmpDestinationObject.PointAtHalf).to.equal('15');
771
+
772
+ // Test bezierCurveFit through the expression parser with data arrays
773
+ testFable.AppData =
774
+ {
775
+ XValues: [0, 1, 2, 3],
776
+ YValues: [0, 1, 2, 3]
777
+ };
778
+
779
+ _Parser.solve('FitResult = BEZIERCURVEFIT(AppData.XValues, AppData.YValues)', testFable, tmpResultsObject, false, tmpDestinationObject);
780
+ Expect(tmpDestinationObject.FitResult).to.be.an('array');
781
+ Expect(tmpDestinationObject.FitResult.length).to.equal(4);
782
+ // P0 should be [0, 0]
783
+ Expect(tmpDestinationObject.FitResult[0][0]).to.equal('0');
784
+ Expect(tmpDestinationObject.FitResult[0][1]).to.equal('0');
785
+ // P3 should be [3, 3]
786
+ Expect(tmpDestinationObject.FitResult[3][0]).to.equal('3');
787
+ Expect(tmpDestinationObject.FitResult[3][1]).to.equal('3');
788
+
789
+ return fDone();
790
+ }
791
+ );
792
+ test
749
793
  (
750
794
  'plumbing histogram into aggregation',
751
795
  () =>
package/test/Math_test.js CHANGED
@@ -600,6 +600,99 @@ suite
600
600
  return fDone();
601
601
  }
602
602
  );
603
+
604
+ test
605
+ (
606
+ 'Bezier Point Evaluation',
607
+ function(fDone)
608
+ {
609
+ let testFable = new libFable();
610
+
611
+ // At t=0, should return P0
612
+ Expect(testFable.Math.bezierPoint(0, 10, 20, 30, 0)).to.equal('0');
613
+ // At t=1, should return P3
614
+ Expect(testFable.Math.bezierPoint(0, 10, 20, 30, 1)).to.equal('30');
615
+ // At t=0.5 for a symmetric curve P0=0, P1=10, P2=20, P3=30 (this is actually a line)
616
+ Expect(testFable.Math.bezierPoint(0, 10, 20, 30, '0.5')).to.equal('15');
617
+
618
+ // A known cubic bezier: P0=0, P1=0, P2=100, P3=100 at t=0.5
619
+ // B(0.5) = 0.125*0 + 0.375*0 + 0.375*100 + 0.125*100 = 50
620
+ Expect(testFable.Math.bezierPoint(0, 0, 100, 100, '0.5')).to.equal('50');
621
+
622
+ // Non-symmetric: P0=1, P1=2, P2=4, P3=5 at t=0.25
623
+ // B(0.25) = (0.75)^3*1 + 3*(0.75)^2*0.25*2 + 3*0.75*(0.25)^2*4 + (0.25)^3*5
624
+ // = 0.421875*1 + 3*0.5625*0.25*2 + 3*0.75*0.0625*4 + 0.015625*5
625
+ // = 0.421875 + 0.84375 + 0.5625 + 0.078125 = 1.90625
626
+ Expect(testFable.Math.bezierPoint(1, 2, 4, 5, '0.25')).to.equal('1.90625');
627
+
628
+ return fDone();
629
+ }
630
+ );
631
+
632
+ test
633
+ (
634
+ 'Bezier Curve Fit',
635
+ function(fDone)
636
+ {
637
+ let testFable = new libFable();
638
+
639
+ // Fit a straight line: should get control points roughly on the line
640
+ let tmpLineResult = testFable.Math.bezierCurveFit([0, 1, 2, 3], [0, 1, 2, 3]);
641
+ Expect(tmpLineResult).to.be.an('array');
642
+ Expect(tmpLineResult.length).to.equal(4);
643
+ // P0 should be [0, 0]
644
+ Expect(tmpLineResult[0][0]).to.equal('0');
645
+ Expect(tmpLineResult[0][1]).to.equal('0');
646
+ // P3 should be [3, 3]
647
+ Expect(tmpLineResult[3][0]).to.equal('3');
648
+ Expect(tmpLineResult[3][1]).to.equal('3');
649
+ // Interior control points should be roughly on the line too
650
+ Expect(Number(tmpLineResult[1][0])).to.be.closeTo(1, 0.5);
651
+ Expect(Number(tmpLineResult[1][1])).to.be.closeTo(1, 0.5);
652
+ Expect(Number(tmpLineResult[2][0])).to.be.closeTo(2, 0.5);
653
+ Expect(Number(tmpLineResult[2][1])).to.be.closeTo(2, 0.5);
654
+
655
+ // Two-point degenerate case: should place P1, P2 at 1/3 and 2/3
656
+ let tmpTwoPointResult = testFable.Math.bezierCurveFit([0, 9], [0, 9]);
657
+ Expect(tmpTwoPointResult[0][0]).to.equal('0');
658
+ Expect(tmpTwoPointResult[0][1]).to.equal('0');
659
+ Expect(tmpTwoPointResult[3][0]).to.equal('9');
660
+ Expect(tmpTwoPointResult[3][1]).to.equal('9');
661
+ Expect(tmpTwoPointResult[1][0]).to.equal('3');
662
+ Expect(tmpTwoPointResult[1][1]).to.equal('3');
663
+ Expect(tmpTwoPointResult[2][0]).to.equal('6');
664
+ Expect(tmpTwoPointResult[2][1]).to.equal('6');
665
+
666
+ // Bad input: not arrays
667
+ let tmpBadResult = testFable.Math.bezierCurveFit('bad', 'input');
668
+ Expect(tmpBadResult).to.be.an('array');
669
+ Expect(tmpBadResult.length).to.equal(4);
670
+
671
+ // Bad input: too few points
672
+ let tmpSingleResult = testFable.Math.bezierCurveFit([1], [1]);
673
+ Expect(tmpSingleResult).to.be.an('array');
674
+ Expect(tmpSingleResult.length).to.equal(4);
675
+
676
+ // Curved data: a parabola y = x^2 from 0 to 4
677
+ let tmpXValues = [0, 1, 2, 3, 4];
678
+ let tmpYValues = [0, 1, 4, 9, 16];
679
+ let tmpCurveResult = testFable.Math.bezierCurveFit(tmpXValues, tmpYValues);
680
+ // P0 should be [0, 0], P3 should be [4, 16]
681
+ Expect(tmpCurveResult[0][0]).to.equal('0');
682
+ Expect(tmpCurveResult[0][1]).to.equal('0');
683
+ Expect(tmpCurveResult[3][0]).to.equal('4');
684
+ Expect(tmpCurveResult[3][1]).to.equal('16');
685
+ // The fitted curve should approximate the data at the midpoint
686
+ let tmpMidX = testFable.Math.bezierPoint(tmpCurveResult[0][0], tmpCurveResult[1][0], tmpCurveResult[2][0], tmpCurveResult[3][0], '0.5');
687
+ let tmpMidY = testFable.Math.bezierPoint(tmpCurveResult[0][1], tmpCurveResult[1][1], tmpCurveResult[2][1], tmpCurveResult[3][1], '0.5');
688
+ // At the midpoint parameter, the curve should be somewhere reasonable
689
+ // Note: bezier parameter t=0.5 does not correspond to the geometric midpoint for non-linear data
690
+ Expect(Number(tmpMidX)).to.be.closeTo(2, 1);
691
+ Expect(Number(tmpMidY)).to.be.closeTo(4, 5);
692
+
693
+ return fDone();
694
+ }
695
+ );
603
696
  }
604
697
  );
605
698
  }