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.
- package/CONTRIBUTING.md +50 -0
- package/README.md +15 -0
- package/dist/fable.js +91 -36
- package/dist/fable.js.map +1 -1
- package/dist/fable.min.js +2 -2
- package/dist/fable.min.js.map +1 -1
- package/dist/indoctrinate_content_staging/Indoctrinate-Catalog-AppData.json +10514 -0
- package/docs/_sidebar.md +1 -1
- package/docs/_topbar.md +6 -0
- package/docs/cover.md +1 -1
- package/docs/css/docuserve.css +73 -0
- package/docs/index.html +1 -1
- package/docs/retold-catalog.json +287 -0
- package/docs/retold-keyword-index.json +43244 -0
- package/example_applications/mathematical_playground/AppData.json +4 -1
- package/example_applications/mathematical_playground/Equations.json +2 -1
- package/package.json +9 -9
- package/source/services/Fable-Service-ExpressionParser/Fable-Service-ExpressionParser-FunctionMap.json +9 -0
- package/source/services/Fable-Service-Math.js +179 -0
- package/test/ExpressionParser_tests.js +44 -0
- package/test/Math_test.js +93 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
60
|
-
"cookie": "^1.
|
|
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.
|
|
64
|
-
"fable-serviceproviderbase": "^3.0.
|
|
65
|
-
"fable-settings": "^3.0.
|
|
66
|
-
"fable-uuid": "^3.0.
|
|
67
|
-
"manyfest": "^1.0.
|
|
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
|
}
|