ntk 8.17.8 → 8.18.0
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/README.md +4 -0
- package/lib/index.js +2 -0
- package/lib/precise.js +452 -0
- package/lib/rasterize.js +21 -8
- package/lib/renderingcontext_2d.js +53 -21
- package/lib/text/brotlidictionary.js +99 -0
- package/lib/text/font.js +16 -6
- package/lib/text/woff2.js +102 -0
- package/lib/trapezoid.js +111 -45
- package/lib/vendor/fontkit.js +14379 -0
- package/package.json +12 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Keeps brotli's static dictionary out of every app's startup (issue #427).
|
|
2
|
+
//
|
|
3
|
+
// The fontkit build ntk carries (lib/vendor/fontkit.js) imports brotli's
|
|
4
|
+
// decoder at the top, for WOFF2, and the decoder's last line is `BrotliDictionary.init()`, at module scope.
|
|
5
|
+
// `dec/dictionary.js` requires `dec/dictionary-data.js` to have something to
|
|
6
|
+
// init from: RFC 7932's 122,784-byte dictionary written out as a 756 KB array
|
|
7
|
+
// literal. Compiling and running it is 8 of the 10 ms the decoder takes to
|
|
8
|
+
// load on an M1 Pro — the issue measured the decoder at 16 ms on an i7-7700K
|
|
9
|
+
// — and `import 'ntk'` paid it in every app, including the ones that never
|
|
10
|
+
// open a WOFF2.
|
|
11
|
+
//
|
|
12
|
+
// The decoder reads the dictionary's fields as it decompresses, and nothing
|
|
13
|
+
// else does; `init()` is the one thing done at load. So before fontkit loads,
|
|
14
|
+
// a stand-in for `dec/dictionary.js` goes into `require.cache`: its `init()`
|
|
15
|
+
// does nothing, and the first read of a field — the first WOFF2 decompressed
|
|
16
|
+
// — drops the stand-in from the cache, requires the real module and copies
|
|
17
|
+
// its fields over, so the decoder holds the real values from then on.
|
|
18
|
+
//
|
|
19
|
+
// Why that module and not a shallower one: before ESM evaluates anything, it
|
|
20
|
+
// pre-parses the CommonJS it imports for export names, which leaves loader-
|
|
21
|
+
// owned placeholders in `require.cache` for `decompress.js` and, through
|
|
22
|
+
// what reads as a re-export, `dec/decode.js` — replace one and the import
|
|
23
|
+
// dies with ERR_INTERNAL_ASSERTION. `dec/dictionary.js` is only reached by
|
|
24
|
+
// `require`, while the decoder evaluates, so it is still free when this runs.
|
|
25
|
+
//
|
|
26
|
+
// Each step stands down where it cannot apply: outside Node, in a bundle
|
|
27
|
+
// (which carries its own copy of brotli and never consults `require.cache`),
|
|
28
|
+
// where brotli does not resolve, where something required the
|
|
29
|
+
// dictionary first, and for a brotli other than the 1.3 this was written
|
|
30
|
+
// against. A brotli release that loads the dictionary lazily itself makes
|
|
31
|
+
// this redundant, and the version check retires it.
|
|
32
|
+
//
|
|
33
|
+
// Where the runtime has a Brotli of its own, a WOFF2 ntk opens is not
|
|
34
|
+
// decompressed by this decoder at all (./woff2.js), so with the stand-in in
|
|
35
|
+
// place the dictionary never loads. Without it, it would still load with
|
|
36
|
+
// fontkit, used or not.
|
|
37
|
+
import { builtin, nodeRequire } from '../builtin.js';
|
|
38
|
+
|
|
39
|
+
// What the decoder reads off brotli 1.3's `dec/dictionary.js`.
|
|
40
|
+
const FIELDS = [
|
|
41
|
+
'dictionary',
|
|
42
|
+
'offsetsByLength',
|
|
43
|
+
'sizeBitsByLength',
|
|
44
|
+
'minDictionaryWordLength',
|
|
45
|
+
'maxDictionaryWordLength',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Put the stand-in for fontkit's `brotli/dec/dictionary.js` into
|
|
50
|
+
* `require.cache`, unless it cannot or should not be.
|
|
51
|
+
* @returns {boolean} whether it was installed
|
|
52
|
+
*/
|
|
53
|
+
export function deferBrotliDictionary() {
|
|
54
|
+
try {
|
|
55
|
+
const require = nodeRequire();
|
|
56
|
+
if (!require) return false;
|
|
57
|
+
const { Module } = builtin('node:module');
|
|
58
|
+
// brotli is ntk's own dependency, for the fontkit build in lib/vendor/,
|
|
59
|
+
// so it resolves from here to the copy that build imports
|
|
60
|
+
if (!/^1\.3\./.test(require('brotli/package.json').version)) return false;
|
|
61
|
+
const file = require.resolve('brotli/dec/dictionary.js');
|
|
62
|
+
const cache = require.cache;
|
|
63
|
+
if (cache[file]) return false;
|
|
64
|
+
|
|
65
|
+
const standIn = new Module(file);
|
|
66
|
+
standIn.filename = file;
|
|
67
|
+
standIn.loaded = true;
|
|
68
|
+
const exports = (standIn.exports = { init() {} });
|
|
69
|
+
const load = () => {
|
|
70
|
+
if (cache[file] === standIn) delete cache[file];
|
|
71
|
+
const real = require(file);
|
|
72
|
+
real.init();
|
|
73
|
+
for (const key of Object.keys(real)) {
|
|
74
|
+
Object.defineProperty(exports, key, {
|
|
75
|
+
value: real[key],
|
|
76
|
+
writable: true,
|
|
77
|
+
enumerable: true,
|
|
78
|
+
configurable: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return real;
|
|
82
|
+
};
|
|
83
|
+
for (const key of FIELDS) {
|
|
84
|
+
Object.defineProperty(exports, key, {
|
|
85
|
+
get: () => load()[key],
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
cache[file] = standIn;
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
// no node:module, no import.meta.url (a CommonJS bundle), or a layout
|
|
94
|
+
// that does not resolve: the dictionary loads with fontkit, as before
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
deferBrotliDictionary();
|
package/lib/text/font.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Must stay above fontkit, whose import loads brotli's decoder: this keeps
|
|
2
|
+
// the decoder's static dictionary from loading with it (see the module).
|
|
3
|
+
import './brotlidictionary.js';
|
|
4
|
+
import { builtin } from '../builtin.js';
|
|
3
5
|
import { flatten, rasterizePath } from '../rasterize.js';
|
|
6
|
+
// the windowkit fork's build, which ntk carries (scripts/vendor-fontkit.mjs)
|
|
7
|
+
import * as fontkit from '../vendor/fontkit.js';
|
|
4
8
|
import { NO_ANCHOR, tolerateAnchors, watchAnchors } from './anchors.js';
|
|
5
9
|
import { marksCover, standInMarks } from './marks.js';
|
|
6
10
|
import { pairGlyphs, pairsCover } from './pairs.js';
|
|
7
11
|
import { sfntOf } from './sfnt.js';
|
|
12
|
+
import { opened, useNativeBrotli } from './woff2.js';
|
|
13
|
+
|
|
14
|
+
// a WOFF2 decompresses with the runtime's Brotli where it has one (see the module)
|
|
15
|
+
useNativeBrotli(fontkit);
|
|
8
16
|
|
|
9
17
|
// Axis coordinates are rounded to this many decimals before anything is
|
|
10
18
|
// instantiated or cached. Every distinct coordinate is a font in its own
|
|
@@ -228,16 +236,18 @@ export default class Font {
|
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
static loadSync(path, postscriptName) {
|
|
231
|
-
|
|
239
|
+
// what fontkit's openSync does, which the build ntk carries leaves out:
|
|
240
|
+
// it is the one that imports node:fs
|
|
241
|
+
let fk = fontkit.create(builtin('node:fs').readFileSync(path));
|
|
232
242
|
// select the face of a .ttc/.dfont collection (first face by default);
|
|
233
|
-
// don't pass the name to
|
|
243
|
+
// don't pass the name to create — for single variable fonts it would
|
|
234
244
|
// be misread as a named variation instance
|
|
235
245
|
if (fk.fonts) {
|
|
236
246
|
fk =
|
|
237
247
|
(postscriptName && fk.fonts.find((f) => f.postscriptName === postscriptName)) ||
|
|
238
248
|
fk.fonts[0];
|
|
239
249
|
}
|
|
240
|
-
return new Font(instantiable(fk), path, postscriptName);
|
|
250
|
+
return new Font(instantiable(opened(fk, path)), path, postscriptName);
|
|
241
251
|
}
|
|
242
252
|
|
|
243
253
|
/**
|
|
@@ -258,7 +268,7 @@ export default class Font {
|
|
|
258
268
|
(postscriptName && fk.fonts.find((f) => f.postscriptName === postscriptName)) ||
|
|
259
269
|
fk.fonts[0];
|
|
260
270
|
}
|
|
261
|
-
const font = new Font(instantiable(fk), null, postscriptName);
|
|
271
|
+
const font = new Font(instantiable(opened(fk)), null, postscriptName);
|
|
262
272
|
font.key = key ?? `mem:#${fk.postscriptName || ''}`;
|
|
263
273
|
return font;
|
|
264
274
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// A WOFF2's font data, decompressed by the runtime's own Brotli.
|
|
2
|
+
//
|
|
3
|
+
// A WOFF2 keeps every table in one Brotli stream. The fontkit build ntk
|
|
4
|
+
// carries (lib/vendor/fontkit.js) is the fork's browser build, and it
|
|
5
|
+
// decompresses that with brotli.js, a decoder written in JavaScript. Node,
|
|
6
|
+
// Bun and Deno each carry a native one in `node:zlib` — every Node ntk runs
|
|
7
|
+
// on, and Bun since 1.1.8 — which gives the same bytes about six times as
|
|
8
|
+
// fast: the twenty KaTeX faces and the variable fixture decompress in 3.9 ms
|
|
9
|
+
// instead of 21.4 on an M1 Pro. It also never reaches brotli.js's static
|
|
10
|
+
// dictionary, which ./brotlidictionary.js keeps out of startup so that only a
|
|
11
|
+
// WOFF2 pays for it. On these runtimes, none does.
|
|
12
|
+
//
|
|
13
|
+
// The fork takes a decompressor from outside, `setBrotliDecompressor`
|
|
14
|
+
// (windowkit/fontkit#6), and checks what comes back against the size the
|
|
15
|
+
// table directory gives, as Google's reference decoder does. So this is one
|
|
16
|
+
// function, handed over once: zlib's Brotli where the runtime has one, looked
|
|
17
|
+
// for at the first WOFF2 rather than at import, and the build's own brotli.js
|
|
18
|
+
// where it has none, in a browser.
|
|
19
|
+
//
|
|
20
|
+
// What fontkit does not do is say so when a file is damaged. It decompresses
|
|
21
|
+
// at a WOFF2's first table read, and swallows a table's errors: the table
|
|
22
|
+
// reads as undefined. So a damaged WOFF2 opened as a font with no tables,
|
|
23
|
+
// and failed at the first glyph drawn, with brotli.js's own message. `opened`
|
|
24
|
+
// decompresses it as it is opened, calling fontkit's `_decompress` as
|
|
25
|
+
// ./sfnt.js does, and a damaged file is an error naming it, at the call that
|
|
26
|
+
// named it. Font sources skip a face that throws there (./fontsource.js),
|
|
27
|
+
// and a match moves on to the next candidate (./fontmanager.js).
|
|
28
|
+
import { builtin } from '../builtin.js';
|
|
29
|
+
|
|
30
|
+
// `node:zlib`, or null where it has no Brotli; undefined until first asked
|
|
31
|
+
let zlib;
|
|
32
|
+
|
|
33
|
+
function nativeBrotli() {
|
|
34
|
+
if (zlib === undefined) {
|
|
35
|
+
zlib = null;
|
|
36
|
+
try {
|
|
37
|
+
const z = builtin('node:zlib');
|
|
38
|
+
if (typeof z?.brotliDecompressSync === 'function') zlib = z;
|
|
39
|
+
} catch {
|
|
40
|
+
// no builtins to reach: brotli.js it is
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return zlib;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Test seam: what decompresses a WOFF2 — a `node:zlib`-shaped module, `null`
|
|
48
|
+
* for the build's brotli.js, or `undefined` to look for the runtime's again.
|
|
49
|
+
*/
|
|
50
|
+
export function setNativeBrotli(module) {
|
|
51
|
+
zlib = module;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hand `fontkit` the runtime's Brotli for WOFF2, keeping the decompressor it
|
|
56
|
+
* had for where there is none.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} fontkit the build ntk carries
|
|
59
|
+
*/
|
|
60
|
+
export function useNativeBrotli(fontkit) {
|
|
61
|
+
const own = fontkit.setBrotliDecompressor(decompress);
|
|
62
|
+
|
|
63
|
+
function decompress(buffer, size) {
|
|
64
|
+
const z = nativeBrotli();
|
|
65
|
+
if (!z) return own(buffer, size);
|
|
66
|
+
let bytes;
|
|
67
|
+
try {
|
|
68
|
+
bytes = z.brotliDecompressSync(buffer, { maxOutputLength: Math.max(size, 1) });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err?.code !== 'ERR_BUFFER_TOO_LARGE') throw err;
|
|
71
|
+
throw new RangeError(`it holds more than the ${size} bytes its table directory gives`, {
|
|
72
|
+
cause: err,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// A plain Uint8Array, as brotli.js returns, not zlib's Buffer: a Buffer's
|
|
76
|
+
// slice() is a view, and what is read out of this expects a copy —
|
|
77
|
+
// ./sfnt.js rewrites `head` in the copy it slices.
|
|
78
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `fk`, with its font data decompressed now if it is a WOFF2, so that a
|
|
84
|
+
* damaged file is an error naming it rather than a font with no tables.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} fk a fontkit font
|
|
87
|
+
* @param {string} [where] the file it was read from
|
|
88
|
+
* @returns {object} `fk`
|
|
89
|
+
*/
|
|
90
|
+
export function opened(fk, where) {
|
|
91
|
+
if (fk?.type !== 'WOFF2' || typeof fk._decompress !== 'function') return fk;
|
|
92
|
+
try {
|
|
93
|
+
fk._decompress();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${where ? `${where}: ` : ''}${err.message} — the file is damaged or cut short; ` +
|
|
97
|
+
'browsers reject it too.',
|
|
98
|
+
{ cause: err }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return fk;
|
|
102
|
+
}
|
package/lib/trapezoid.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
// Trapezoidal decomposition of flattened closed polygons (non-zero winding
|
|
2
2
|
// by default, even-odd optionally), producing XRender AddTraps data.
|
|
3
3
|
//
|
|
4
|
-
// A horizontal slab sweep
|
|
5
|
-
// coordinates,
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
4
|
+
// A horizontal slab sweep. Slab boundaries are the sorted unique vertex y
|
|
5
|
+
// coordinates, and where two edges cross inside a slab the slab is split at
|
|
6
|
+
// the crossing, so that within every slab the active edges keep one
|
|
7
|
+
// left-to-right order from top to bottom. Spans between them are then true
|
|
8
|
+
// trapezoids, and self-intersecting input — a pentagram, two crossed bars,
|
|
9
|
+
// overlapping composite-glyph contours — is filled exactly by its rule.
|
|
10
|
+
// Spans where the rule says "inside" become trapezoids; a span whose
|
|
11
|
+
// left/right edges continue through consecutive slabs is merged into a
|
|
12
|
+
// single tall trapezoid, which is what keeps the output at roughly one
|
|
13
|
+
// trapezoid per outline edge (plus a few per crossing).
|
|
14
|
+
//
|
|
15
|
+
// Crossings are found the way a Bentley-Ottmann sweep finds them: the first
|
|
16
|
+
// change of order below a line is always between two edges adjacent on it,
|
|
17
|
+
// so each step takes the earliest crossing of an adjacent pair whose order
|
|
18
|
+
// is reversed at the slab's bottom, cuts there and swaps the pair. Every
|
|
19
|
+
// swap removes one inversion of the bottom order, so the loop ends however
|
|
20
|
+
// the floats round. Differences within a hair of the input's magnitude —
|
|
21
|
+
// collinear edges, crossings right at a slab's top or bottom — are rounding,
|
|
22
|
+
// not geometry: they cause no split and cut no sliver.
|
|
11
23
|
//
|
|
12
24
|
// Output is the flat number list node-x11's Render.AddTraps expects:
|
|
13
25
|
// 6 values per trapezoid — top spanfix (left x, right x, y) then bottom
|
|
@@ -26,6 +38,8 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
|
|
|
26
38
|
// collect non-horizontal edges, normalized to y0 < y1 with winding dir
|
|
27
39
|
const edges = [];
|
|
28
40
|
const ys = [];
|
|
41
|
+
// the largest coordinate magnitude, which sets the rounding noise
|
|
42
|
+
let reach = 1;
|
|
29
43
|
for (const poly of polys) {
|
|
30
44
|
const n = poly.length / 2;
|
|
31
45
|
for (let i = 0; i < n; ++i) {
|
|
@@ -35,15 +49,22 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
|
|
|
35
49
|
const bx = poly[j * 2] + dx;
|
|
36
50
|
const by = poly[j * 2 + 1] + dy;
|
|
37
51
|
if (ay === by) continue;
|
|
52
|
+
const slope = (bx - ax) / (by - ay);
|
|
38
53
|
edges.push(
|
|
39
54
|
ay < by
|
|
40
|
-
? { x0: ax, y0: ay,
|
|
41
|
-
: { x0: bx, y0: by,
|
|
55
|
+
? { x0: ax, y0: ay, y1: by, slope, dir: 1, xt: 0, xb: 0, xc: 0 }
|
|
56
|
+
: { x0: bx, y0: by, y1: ay, slope, dir: -1, xt: 0, xb: 0, xc: 0 }
|
|
42
57
|
);
|
|
43
58
|
ys.push(ay, by);
|
|
59
|
+
const m = Math.max(Math.abs(ax), Math.abs(ay), Math.abs(bx), Math.abs(by));
|
|
60
|
+
if (m > reach) reach = m;
|
|
44
61
|
}
|
|
45
62
|
}
|
|
46
63
|
if (edges.length === 0) return out;
|
|
64
|
+
// ~256 ulps of the largest coordinate: far above what evaluating an edge
|
|
65
|
+
// at a height rounds by, far below a 16.16 fixed-point step at any
|
|
66
|
+
// coordinate that fits one
|
|
67
|
+
const eps = reach * 2 ** -44;
|
|
47
68
|
|
|
48
69
|
ys.sort((a, b) => a - b);
|
|
49
70
|
edges.sort((a, b) => a.y0 - b.y0);
|
|
@@ -72,52 +93,97 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
|
|
|
72
93
|
continue;
|
|
73
94
|
}
|
|
74
95
|
|
|
75
|
-
// x at slab top/bottom for each active edge; sort left-to-right
|
|
96
|
+
// x at slab top/bottom for each active edge; sort left-to-right just
|
|
97
|
+
// below the top (edges leaving one point go by where they are headed)
|
|
76
98
|
for (const e of active) {
|
|
77
|
-
|
|
78
|
-
e.
|
|
79
|
-
e.xb = e.x0 + (yb - e.y0) * inv;
|
|
99
|
+
e.xt = e.x0 + (ya - e.y0) * e.slope;
|
|
100
|
+
e.xb = e.x0 + (yb - e.y0) * e.slope;
|
|
80
101
|
}
|
|
81
|
-
active.sort((a, b) => a.
|
|
102
|
+
active.sort((a, b) => a.xt - b.xt || a.xb - b.xb);
|
|
82
103
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (p.l === left && p.r === e && out[p.at + 5] === ya) {
|
|
101
|
-
out[p.at + 3] = left.xb;
|
|
102
|
-
out[p.at + 4] = e.xb;
|
|
103
|
-
out[p.at + 5] = yb;
|
|
104
|
-
spans.push(p);
|
|
105
|
-
merged = true;
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
104
|
+
// cut the slab at each crossing, top to bottom; `y` is where the part
|
|
105
|
+
// still to emit starts, and `xt` each edge's x there
|
|
106
|
+
let y = ya;
|
|
107
|
+
for (;;) {
|
|
108
|
+
let yc = yb;
|
|
109
|
+
let k = -1;
|
|
110
|
+
for (let i = 0; i + 1 < active.length; ++i) {
|
|
111
|
+
const a = active[i];
|
|
112
|
+
const b = active[i + 1];
|
|
113
|
+
// how far `a` ends up right of `b`, and how far left of it it starts
|
|
114
|
+
const over = a.xb - b.xb;
|
|
115
|
+
if (!(over > eps)) continue;
|
|
116
|
+
const gap = b.xt - a.xt;
|
|
117
|
+
const at = gap > 0 ? y + (yb - y) * (gap / (gap + over)) : y;
|
|
118
|
+
if (at < yc) {
|
|
119
|
+
yc = at;
|
|
120
|
+
k = i;
|
|
108
121
|
}
|
|
109
|
-
if (!merged && (e.xa > left.xa || e.xb > left.xb)) {
|
|
110
|
-
spans.push({ l: left, r: e, at: out.length });
|
|
111
|
-
out.push(left.xa, e.xa, ya, left.xb, e.xb, yb);
|
|
112
|
-
}
|
|
113
|
-
left = null;
|
|
114
122
|
}
|
|
123
|
+
if (k < 0 || yc >= yb - eps) {
|
|
124
|
+
prevSpans = emitSpans(active, y, yb, true, evenodd, prevSpans, out);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
if (yc > y + eps) {
|
|
128
|
+
for (const e of active) e.xc = e.x0 + (yc - e.y0) * e.slope;
|
|
129
|
+
prevSpans = emitSpans(active, y, yc, false, evenodd, prevSpans, out);
|
|
130
|
+
for (const e of active) e.xt = e.xc;
|
|
131
|
+
y = yc;
|
|
132
|
+
}
|
|
133
|
+
const a = active[k];
|
|
134
|
+
active[k] = active[k + 1];
|
|
135
|
+
active[k + 1] = a;
|
|
115
136
|
}
|
|
116
|
-
prevSpans = spans;
|
|
117
137
|
}
|
|
118
138
|
return out;
|
|
119
139
|
}
|
|
120
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Emit a span per maximal interior interval of the slab `ya`..`yb`;
|
|
143
|
+
* interiority is decided by the fill rule (non-zero winding, or crossing
|
|
144
|
+
* parity for even-odd). Each edge is at `xt` at the top, and at the bottom
|
|
145
|
+
* at `xb` when this is the `last` part of its vertex slab, else at `xc`. A
|
|
146
|
+
* span bounded by the same two edges as one ending at `ya` extends that
|
|
147
|
+
* trapezoid instead.
|
|
148
|
+
* @returns the spans, for the next slab to merge with
|
|
149
|
+
*/
|
|
150
|
+
function emitSpans(active, ya, yb, last, evenodd, prevSpans, out) {
|
|
151
|
+
const spans = [];
|
|
152
|
+
let winding = 0;
|
|
153
|
+
let crossings = 0;
|
|
154
|
+
let left = null;
|
|
155
|
+
for (const e of active) {
|
|
156
|
+
const wasInside = evenodd ? (crossings & 1) === 1 : winding !== 0;
|
|
157
|
+
winding += e.dir;
|
|
158
|
+
crossings++;
|
|
159
|
+
const isInside = evenodd ? (crossings & 1) === 1 : winding !== 0;
|
|
160
|
+
if (!wasInside && isInside) {
|
|
161
|
+
left = e;
|
|
162
|
+
} else if (wasInside && !isInside && left) {
|
|
163
|
+
const lb = last ? left.xb : left.xc;
|
|
164
|
+
const rb = last ? e.xb : e.xc;
|
|
165
|
+
// merge with a span from the previous slab bounded by the same edges
|
|
166
|
+
let merged = false;
|
|
167
|
+
for (const p of prevSpans) {
|
|
168
|
+
if (p.l === left && p.r === e && out[p.at + 5] === ya) {
|
|
169
|
+
out[p.at + 3] = lb;
|
|
170
|
+
out[p.at + 4] = rb;
|
|
171
|
+
out[p.at + 5] = yb;
|
|
172
|
+
spans.push(p);
|
|
173
|
+
merged = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!merged && (e.xt > left.xt || rb > lb)) {
|
|
178
|
+
spans.push({ l: left, r: e, at: out.length });
|
|
179
|
+
out.push(left.xt, e.xt, ya, lb, rb, yb);
|
|
180
|
+
}
|
|
181
|
+
left = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return spans;
|
|
185
|
+
}
|
|
186
|
+
|
|
121
187
|
/** area covered by a trapezoid list (useful for tests/diagnostics) */
|
|
122
188
|
export function trapArea(traps) {
|
|
123
189
|
let area = 0;
|