huangcm
2024-12-18 9d29be7f7249789d6ffd0440067187a9f040c2cd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
 
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,
            }).ready().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(catchException(done, () => {
            // 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(catchException(done, () => {
            // 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();
                });
            });
        }));
    });
});