[PathKit] Move from experimental to modules
Add in Code of Conduct and Contributing.md as well.
Docs-Preview: https://skia.org/?cl=150901
Bug: skia:8216
Change-Id: Ia881124f653617ad0b7b91f40ba21de2c13220a6
Reviewed-on: https://skia-review.googlesource.com/150901
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
diff --git a/modules/pathkit/tests/pathops.spec.js b/modules/pathkit/tests/pathops.spec.js
new file mode 100644
index 0000000..1453c22
--- /dev/null
+++ b/modules/pathkit/tests/pathops.spec.js
@@ -0,0 +1,230 @@
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
+
+var dumpErrors = false;
+var container;
+
+function getViewBox(path) {
+ let bounds = path.getBounds();
+ return `${(bounds.fLeft-2)*.95} ${(bounds.fTop-2)*.95} ${(bounds.fRight+2)*1.05} ${(bounds.fBottom+2)*1.05}`;
+}
+
+function addSVG(testName, expectedPath, actualPath, message) {
+ if (!dumpErrors) {
+ return;
+ }
+ if (!container) {
+ let styleEl = document.createElement('style');
+ document.head.appendChild(styleEl);
+ let sheet = styleEl.sheet;
+ sheet.insertRule(`svg {
+ border: 1px solid #DDD;
+ max-width: 45%;
+ vertical-align: top;
+ }`, 0);
+
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ }
+
+ let thisTest = document.createElement('div');
+ thisTest.innerHTML = `
+ <h2>Failed test ${testName}</h2>
+
+ <div>${message}</div>
+
+ <svg class='expected' viewBox='${getViewBox(expectedPath)}'>
+ <path stroke=black fill=white stroke-width=0.01 d="${expectedPath.toSVGString()}"></path>
+ </svg>
+
+ <svg class='actual' viewBox='${getViewBox(actualPath)}'>
+ <path stroke=black fill=white stroke-width=0.01 d="${actualPath.toSVGString()}"></path>
+ </svg>
+`;
+ container.appendChild(thisTest);
+
+}
+
+const TOLERANCE = 0.0001;
+
+function diffPaths(expected, actual) {
+ // Look through commands and see if they are within tolerance.
+ let eCmds = expected.toCmds(), aCmds = actual.toCmds();
+ if (eCmds.length !== aCmds.length) {
+ //console.log(`Expected: ${JSON.stringify(eCmds)} and Actual: ${JSON.stringify(aCmds)}`);
+ return `Different amount of verbs. Expected had ${eCmds.length}, Actual had ${aCmds.length}`;
+ }
+ for(let idx = 0; idx < eCmds.length; idx++){
+ let eCmd = eCmds[idx], aCmd = aCmds[idx];
+ if (eCmd.length !== aCmd.length) {
+ // Should never happen, means WASM code is returning bad ops.
+ return `Command index ${idx} differs in num arguments. Expected had ${eCmd.length}, Actual had ${aCmd.length}`;
+ }
+ let eVerb = eCmd[0], aVerb = aCmd[0];
+ if (eVerb !== aVerb) {
+ return `Command index ${idx} differs. Expected had ${eVerb}, Actual had ${aVerb}`;
+ }
+ for (let arg = 1; arg < eCmd.length; arg++) {
+ if (Math.abs(eCmd[arg] - aCmd[arg]) > TOLERANCE) {
+ return `Command index ${idx} has different argument for verb ${eVerb} at position ${arg}. Expected had ${eCmd[arg]}, Actual had ${aCmd[arg]}`
+ }
+ }
+ }
+ return null;
+}
+
+describe('PathKit\'s PathOps Behavior', function() {
+ // Note, don't try to print the PathKit object - it can cause Karma/Jasmine to lock up.
+ var PathKit = null;
+ var PATHOP_MAP = {};
+ var FILLTYPE_MAP = {};
+ const LoadPathKit = new Promise(function(resolve, reject) {
+ if (PathKit) {
+ resolve();
+ } else {
+ PathKitInit({
+ locateFile: (file) => '/pathkit/'+file,
+ }).then((_PathKit) => {
+ PathKit = _PathKit;
+ PATHOP_MAP = {
+ 'kIntersect_SkPathOp':PathKit.PathOp.INTERSECT,
+ 'kDifference_SkPathOp':PathKit.PathOp.DIFFERENCE,
+ 'kUnion_SkPathOp': PathKit.PathOp.UNION,
+ 'kXOR_SkPathOp': PathKit.PathOp.XOR,
+ 'kXOR_PathOp': PathKit.PathOp.XOR,
+ 'kReverseDifference_SkPathOp': PathKit.PathOp.REVERSE_DIFFERENCE,
+ };
+ FILLTYPE_MAP = {
+ 'kWinding_FillType':PathKit.FillType.WINDING,
+ 'kEvenOdd_FillType':PathKit.FillType.EVENODD,
+ 'kInverseWinding_FillType': PathKit.FillType.INVERSE_WINDING,
+ 'kInverseEvenOdd_FillType': PathKit.FillType.INVERSE_EVENODD,
+ };
+ resolve();
+ });
+ }
+ });
+
+ function getFillType(str) {
+ let e = FILLTYPE_MAP[str];
+ expect(e).toBeTruthy(`Could not find FillType Enum for ${str}`);
+ return e;
+ }
+
+ function getPathOp(str) {
+ let e = PATHOP_MAP[str];
+ expect(e).toBeTruthy(`Could not find PathOp Enum for ${str}`);
+ return e;
+ }
+
+ it('combines two paths with .op() and matches what we see from C++', function(done) {
+ LoadPathKit.then(() => {
+ // Test JSON created with:
+ // ./out/Clang/pathops_unittest -J ./modules/pathkit/tests/PathOpsOp.json -m PathOpsOp$
+ fetch('/base/tests/PathOpsOp.json').then((r) => {
+ r.json().then((json)=>{
+ expect(json).toBeTruthy();
+ let testNames = Object.keys(json);
+ // Assert we loaded a non-zero amount of tests, i.e. the JSON is valid.
+ expect(testNames.length > 0).toBeTruthy();
+ testNames.sort();
+ for (testName of testNames) {
+ let test = json[testName];
+
+ let path1 = PathKit.FromCmds(test.p1);
+ expect(path1).not.toBeNull(`path1 error when loading cmds '${test.p1}'`);
+ path1.setFillType(getFillType(test.fillType1));
+
+ let path2 = PathKit.FromCmds(test.p2);
+ expect(path2).not.toBeNull(`path2 error when loading cmds '${test.p2}'`);
+ path2.setFillType(getFillType(test.fillType2));
+
+ let combined = path1.op(path2, getPathOp(test.op));
+
+ if (test.expectSuccess === 'no') {
+ expect(combined).toBeNull(`Test ${testName} should have not created output, but did`);
+ } else {
+ expect(combined).not.toBeNull();
+ let expected = PathKit.FromCmds(test.out);
+ // Do a tolerant match.
+ let diff = diffPaths(expected, combined);
+ if (test.expectMatch === 'yes'){
+ // Check fill type
+ expect(combined.getFillType().value).toEqual(getFillType(test.fillTypeOut).value);
+ // diff should be null if the paths are identical (modulo rounding)
+ if (diff) {
+ expect(`[${testName}] ${diff}`).toBe('');
+ addSVG('[PathOps] ' + testName, expected, combined, diff);
+ }
+ } else if (test.expectMatch === 'flaky') {
+ // Don't worry about it, at least it didn't crash.
+ } else {
+ if (!diff) {
+ expect(`[${testName}] was expected to have paths that differed`).not.toBe('');
+ }
+ }
+ expected.delete();
+ }
+ // combined === path1, so we only have to delete one.
+ path1.delete();
+ path2.delete();
+ }
+ done();
+ });
+ });
+ });
+ });
+
+ it('simplifies a path with .simplify() and matches what we see from C++', function(done) {
+ LoadPathKit.then(() => {
+ // Test JSON created with:
+ // ./out/Clang/pathops_unittest -J ./modules/pathkit/tests/PathOpsSimplify.json -m PathOpsSimplify$
+ fetch('/base/tests/PathOpsSimplify.json').then((r) => {
+ r.json().then((json)=>{
+ expect(json).toBeTruthy();
+ let testNames = Object.keys(json);
+ // Assert we loaded a non-zero amount of tests, i.e. the JSON is valid.
+ expect(testNames.length > 0).toBeTruthy();
+ testNames.sort();
+ for (testName of testNames) {
+ let test = json[testName];
+
+ let path = PathKit.FromCmds(test.path);
+ expect(path).not.toBeNull(`path1 error when loading cmds '${test.path}'`);
+ path.setFillType(getFillType(test.fillType));
+
+ let simplified = path.simplify();
+
+ if (test.expectSuccess === 'no') {
+ expect(simplified).toBeNull(`Test ${testName} should have not created output, but did`);
+ } else {
+ expect(simplified).not.toBeNull();
+ let expected = PathKit.FromCmds(test.out);
+ // Do a tolerant match.
+ let diff = diffPaths(expected, simplified);
+ if (test.expectMatch === 'yes'){
+ // Check fill type
+ expect(simplified.getFillType().value).toEqual(getFillType(test.fillTypeOut).value);
+ // diff should be null if the paths are identical (modulo rounding)
+ if (diff) {
+ expect(`[${testName}] ${diff}`).toBe('');
+ addSVG('[Simplify] ' + testName, expected, simplified, diff);
+ }
+ } else if (test.expectMatch === 'flaky') {
+ // Don't worry about it, at least it didn't crash.
+ } else {
+ if (!diff) {
+ expect(`[${testName}] was expected to not match output`).not.toBe('');
+ }
+ }
+ expected.delete();
+ }
+ // simplified === path, so we only have to delete one.
+ path.delete();
+ }
+ done();
+ });
+ });
+ });
+ });
+});