ntk 8.13.0 → 8.14.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/lib/app.js +30 -2
- package/lib/clipboard.js +16 -5
- package/lib/cliprect.js +128 -0
- package/lib/color.js +21 -0
- package/lib/fontconfig.js +273 -55
- package/lib/renderingcontext_2d.js +111 -16
- package/lib/text/font.js +31 -1
- package/lib/text/fontmanager.js +34 -3
- package/lib/text/fontsource.js +25 -0
- package/lib/text/layout.js +167 -26
- package/lib/text/marks.js +212 -0
- package/package.json +1 -1
package/lib/fontconfig.js
CHANGED
|
@@ -294,6 +294,11 @@ function cacheMatches(fc, out) {
|
|
|
294
294
|
// child whose answer only reaches the event loop.
|
|
295
295
|
const inflight = new Map();
|
|
296
296
|
|
|
297
|
+
// fc -> the prewarm entry of a `best` job: the one face fontconfig picks for a
|
|
298
|
+
// pattern, asked for beside its chain so a layout can take it first
|
|
299
|
+
// (`bestOfPrewarm`)
|
|
300
|
+
const bestInflight = new Map();
|
|
301
|
+
|
|
297
302
|
/**
|
|
298
303
|
* fc-match for a pattern, off the event loop — one child per pattern however
|
|
299
304
|
* many callers ask at once, so a prewarm and an awaiting `matchSorted` share
|
|
@@ -326,11 +331,31 @@ function runFcMatch(fc) {
|
|
|
326
331
|
return entry.promise;
|
|
327
332
|
}
|
|
328
333
|
|
|
329
|
-
// What a prewarm's child runs: fc-match
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
+
// What a prewarm's child runs: an fc-match for each job, side by side, each
|
|
335
|
+
// with its answer in a file and then — once that file is complete — its exit
|
|
336
|
+
// status in another, which is what a synchronous caller waits for
|
|
337
|
+
// (`answerSync`). The arguments are fc-match's own (`fcMatchArgs`, the sort
|
|
338
|
+
// flag first), then a file base, a kind and a pattern for each job: `sorted`
|
|
339
|
+
// asks for the whole fallback chain, `best` for the one face fontconfig
|
|
340
|
+
// would pick, which it answers in half the time, since it sorts nothing and
|
|
341
|
+
// writes one line (`bestOfPrewarm`). The shell exits once every job has.
|
|
342
|
+
// Nothing but shell builtins runs besides fc-match, so a PATH that holds
|
|
343
|
+
// fc-match alone is enough.
|
|
344
|
+
const PREWARM_SCRIPT = [
|
|
345
|
+
...fcMatchArgs.map((_, i) => `a${i}=\${${i + 1}}`),
|
|
346
|
+
`shift ${fcMatchArgs.length}`,
|
|
347
|
+
'while [ $# -gt 2 ]; do ' +
|
|
348
|
+
'if [ "$2" = best ]; then ' +
|
|
349
|
+
`(fc-match ${fcMatchArgs
|
|
350
|
+
.slice(1)
|
|
351
|
+
.map((_, i) => `"$a${i + 1}"`)
|
|
352
|
+
.join(' ')} "$3" > "$1.out" 2> "$1.err"; echo $? > "$1.done") & ` +
|
|
353
|
+
'else ' +
|
|
354
|
+
`(fc-match ${fcMatchArgs.map((_, i) => `"$a${i}"`).join(' ')} "$3" > "$1.out" 2> "$1.err"; ` +
|
|
355
|
+
'echo $? > "$1.done") & ' +
|
|
356
|
+
'fi; shift 3; done',
|
|
357
|
+
'wait'
|
|
358
|
+
].join('; ');
|
|
334
359
|
|
|
335
360
|
// The directory prewarms write into, made on first use: undefined until
|
|
336
361
|
// then, null where it cannot be (no shell, no writable tmpdir), which leaves
|
|
@@ -364,9 +389,9 @@ function prewarmBase() {
|
|
|
364
389
|
return prewarmDir === null ? null : path.join(prewarmDir, String(prewarmCount++));
|
|
365
390
|
}
|
|
366
391
|
|
|
367
|
-
/** The status
|
|
368
|
-
*
|
|
369
|
-
function
|
|
392
|
+
/** The exit status a prewarm left, once it has left it; null while it is
|
|
393
|
+
* still running. */
|
|
394
|
+
function prewarmStatus(base) {
|
|
370
395
|
const fs = builtin('node:fs');
|
|
371
396
|
let done;
|
|
372
397
|
try {
|
|
@@ -377,7 +402,15 @@ function readPrewarm(base) {
|
|
|
377
402
|
// `echo` writes the status and the newline in one go, but a read can land
|
|
378
403
|
// between the file appearing and its bytes arriving
|
|
379
404
|
if (!done.endsWith('\n')) return null;
|
|
380
|
-
|
|
405
|
+
return Number(done);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The status and answer a prewarm left, once it has left them; null while
|
|
409
|
+
* it is still running. */
|
|
410
|
+
function readPrewarm(base) {
|
|
411
|
+
const fs = builtin('node:fs');
|
|
412
|
+
const status = prewarmStatus(base);
|
|
413
|
+
if (status === null) return null;
|
|
381
414
|
let out = '';
|
|
382
415
|
if (status === 0) {
|
|
383
416
|
try {
|
|
@@ -408,38 +441,56 @@ function removePrewarm(base) {
|
|
|
408
441
|
* that, so the layout it was started for spawned fc-match a second time and
|
|
409
442
|
* waited for that one instead.
|
|
410
443
|
*
|
|
411
|
-
*
|
|
444
|
+
* One child for all the patterns handed over, because what a spawn costs is
|
|
445
|
+
* node's: libuv forks the whole process to start one, and a fork copies page
|
|
446
|
+
* tables that grow with the heap. Four spawns took 4.7 ms of the main thread
|
|
447
|
+
* at 130 MB and 33 ms at 260 MB on a Linux desktop; one shell starting the
|
|
448
|
+
* same four took 1.4 and 8, since the shell's own forks cost next to nothing.
|
|
449
|
+
*
|
|
450
|
+
* The answers stay in their files until something asks for one
|
|
451
|
+
* (`answerSync`), so a face no text is set in — a family's italics, as often
|
|
452
|
+
* as not — is never read. Read and parsed as each child exited, a family's
|
|
453
|
+
* answers were 9 ms of the main thread while the connection was still being
|
|
454
|
+
* set up: 634 KB apiece, most of it the coverage of every face fontconfig
|
|
455
|
+
* sorts behind the first.
|
|
456
|
+
*
|
|
457
|
+
* An entry a pattern, in order; null where files cannot be used, and the
|
|
458
|
+
* caller prewarms the old way.
|
|
412
459
|
*/
|
|
413
|
-
function spawnToFiles(
|
|
460
|
+
function spawnToFiles(fcs, bests = []) {
|
|
414
461
|
const cp = childProcess();
|
|
415
|
-
const
|
|
416
|
-
|
|
462
|
+
const all = [...fcs, ...bests];
|
|
463
|
+
const bases = cp ? all.map(() => prewarmBase()) : [];
|
|
464
|
+
if (bases.length === 0 || bases[0] === null) return null;
|
|
465
|
+
const jobs = [];
|
|
466
|
+
all.forEach((fc, i) => jobs.push(bases[i], i < fcs.length ? 'sorted' : 'best', fc));
|
|
417
467
|
let child;
|
|
418
468
|
try {
|
|
419
|
-
child = cp.spawn('/bin/sh', ['-c', PREWARM_SCRIPT,
|
|
469
|
+
child = cp.spawn('/bin/sh', ['-c', PREWARM_SCRIPT, 'ntk-fc-match', ...fcMatchArgs, ...jobs], {
|
|
420
470
|
stdio: 'ignore'
|
|
421
471
|
});
|
|
422
472
|
} catch {
|
|
423
473
|
return null;
|
|
424
474
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
if (entry.answer !== null) resolve(entry.answer);
|
|
436
|
-
else reject(new Error(`fc-match for "${fc}" did not answer`));
|
|
437
|
-
};
|
|
438
|
-
child.once('error', settle);
|
|
439
|
-
child.once('exit', settle);
|
|
475
|
+
// `promise` resolves once the child is gone, with what it left in the
|
|
476
|
+
// files; `exited` tells a synchronous caller there is nothing to wait for
|
|
477
|
+
const entries = all.map((fc, i) => {
|
|
478
|
+
const entry = { promise: null, base: bases[i], answer: undefined, exited: false, gone: null };
|
|
479
|
+
entry.promise = new Promise((resolve) => {
|
|
480
|
+
entry.gone = resolve;
|
|
481
|
+
});
|
|
482
|
+
(i < fcs.length ? inflight : bestInflight).set(fc, entry);
|
|
483
|
+
return entry;
|
|
440
484
|
});
|
|
441
|
-
|
|
442
|
-
|
|
485
|
+
const gone = () => {
|
|
486
|
+
for (const entry of entries) {
|
|
487
|
+
entry.exited = true;
|
|
488
|
+
entry.gone();
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
child.once('error', gone);
|
|
492
|
+
child.once('exit', gone);
|
|
493
|
+
return entries;
|
|
443
494
|
}
|
|
444
495
|
|
|
445
496
|
// the synchronous path's nap while a prewarm finishes
|
|
@@ -451,14 +502,15 @@ const nap = new Int32Array(new SharedArrayBuffer(4));
|
|
|
451
502
|
const PREWARM_WAIT_MS = 3000;
|
|
452
503
|
|
|
453
504
|
/**
|
|
454
|
-
*
|
|
455
|
-
* null if it failed or took too long
|
|
456
|
-
* itself, which is also what reports why.
|
|
505
|
+
* A prewarm's answer, read from its files — waited for synchronously while
|
|
506
|
+
* the child still runs: its stdout, or null if it failed or took too long.
|
|
507
|
+
* The caller then spawns fc-match itself, which is also what reports why.
|
|
457
508
|
*/
|
|
458
509
|
function answerSync(fc, entry) {
|
|
459
510
|
const deadline = performance.now() + PREWARM_WAIT_MS;
|
|
460
511
|
let left = readPrewarm(entry.base);
|
|
461
|
-
|
|
512
|
+
// a child that is gone has left all it ever will
|
|
513
|
+
while (left === null && !entry.exited && performance.now() < deadline) {
|
|
462
514
|
Atomics.wait(nap, 0, 0, 1);
|
|
463
515
|
left = readPrewarm(entry.base);
|
|
464
516
|
}
|
|
@@ -470,6 +522,105 @@ function answerSync(fc, entry) {
|
|
|
470
522
|
return entry.answer;
|
|
471
523
|
}
|
|
472
524
|
|
|
525
|
+
/**
|
|
526
|
+
* The first candidate of a prewarm's answer that ntk can open, read from the
|
|
527
|
+
* head of its file — waited for synchronously while the child still runs.
|
|
528
|
+
* Null where the answer failed, took too long, or holds no such candidate;
|
|
529
|
+
* the caller then reads it whole (`matchSortedSync`), which also reports why.
|
|
530
|
+
*
|
|
531
|
+
* An answer is the whole fallback chain, each face with its coverage: 634 KB
|
|
532
|
+
* for `sans-serif` on a Linux desktop, 2-3 ms to read and parse, where a
|
|
533
|
+
* layout setting text in the face needs its first line. The rest waits in
|
|
534
|
+
* the file for the first character that falls back, which Latin text in a
|
|
535
|
+
* face that covers it never has.
|
|
536
|
+
*/
|
|
537
|
+
function firstOfPrewarm(entry) {
|
|
538
|
+
if (entry.first !== undefined) return entry.first;
|
|
539
|
+
const deadline = performance.now() + PREWARM_WAIT_MS;
|
|
540
|
+
let status = prewarmStatus(entry.base);
|
|
541
|
+
while (status === null && !entry.exited && performance.now() < deadline) {
|
|
542
|
+
Atomics.wait(nap, 0, 0, 1);
|
|
543
|
+
status = prewarmStatus(entry.base);
|
|
544
|
+
}
|
|
545
|
+
// not answered, or not yet: the whole read is what decides, and reports
|
|
546
|
+
if (status !== 0) return null;
|
|
547
|
+
entry.first = firstCandidate(`${entry.base}.out`);
|
|
548
|
+
return entry.first;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The face a prewarm's `best` job found for a pattern, if ntk can open it —
|
|
553
|
+
* waited for synchronously while the job still runs. Null where it failed,
|
|
554
|
+
* took too long or named a face ntk cannot open; the caller then takes the
|
|
555
|
+
* chain's head (`firstOfPrewarm`).
|
|
556
|
+
*
|
|
557
|
+
* A `best` job asks fontconfig for the face it would pick, which it answers
|
|
558
|
+
* in about half the time the whole chain takes (15 ms against 30 on a Linux
|
|
559
|
+
* desktop): no sort, one line. A layout that asks for a family warmed while
|
|
560
|
+
* its component rendered waits on the prewarm, and the chain was most of
|
|
561
|
+
* that wait.
|
|
562
|
+
*/
|
|
563
|
+
function bestOfPrewarm(entry) {
|
|
564
|
+
if (entry.first !== undefined) return entry.first;
|
|
565
|
+
const deadline = performance.now() + PREWARM_WAIT_MS;
|
|
566
|
+
let left = readPrewarm(entry.base);
|
|
567
|
+
while (left === null && !entry.exited && performance.now() < deadline) {
|
|
568
|
+
Atomics.wait(nap, 0, 0, 1);
|
|
569
|
+
left = readPrewarm(entry.base);
|
|
570
|
+
}
|
|
571
|
+
if (left === null) return null;
|
|
572
|
+
removePrewarm(entry.base);
|
|
573
|
+
const [best] = left.status === 0 ? parseMatches(left.out) : [];
|
|
574
|
+
entry.first = best ?? null;
|
|
575
|
+
return entry.first;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// how much of an answer's head a read takes at a time: a line is a face's
|
|
579
|
+
// path, names and coverage, a few KB at most
|
|
580
|
+
const HEAD_CHUNK = 16384;
|
|
581
|
+
|
|
582
|
+
/** The first line of an fc-match answer naming a face ntk can open, parsed;
|
|
583
|
+
* null if the file has none, or cannot be read. */
|
|
584
|
+
function firstCandidate(file) {
|
|
585
|
+
const fs = builtin('node:fs');
|
|
586
|
+
let fd;
|
|
587
|
+
try {
|
|
588
|
+
fd = fs.openSync(file, 'r');
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const decoder = new TextDecoder();
|
|
593
|
+
try {
|
|
594
|
+
let pending = new Uint8Array(0);
|
|
595
|
+
const chunk = new Uint8Array(HEAD_CHUNK);
|
|
596
|
+
for (;;) {
|
|
597
|
+
const read = fs.readSync(fd, chunk, 0, HEAD_CHUNK, null);
|
|
598
|
+
let bytes = pending;
|
|
599
|
+
if (read > 0) {
|
|
600
|
+
bytes = new Uint8Array(pending.length + read);
|
|
601
|
+
bytes.set(pending);
|
|
602
|
+
bytes.set(chunk.subarray(0, read), pending.length);
|
|
603
|
+
}
|
|
604
|
+
let start = 0;
|
|
605
|
+
// a newline never falls inside a UTF-8 sequence, so whole lines decode
|
|
606
|
+
for (let nl = bytes.indexOf(10, start); nl !== -1; nl = bytes.indexOf(10, start)) {
|
|
607
|
+
const [candidate] = parseMatches(decoder.decode(bytes.subarray(start, nl)));
|
|
608
|
+
if (candidate) return candidate;
|
|
609
|
+
start = nl + 1;
|
|
610
|
+
}
|
|
611
|
+
if (read <= 0) {
|
|
612
|
+
const [candidate] = parseMatches(decoder.decode(bytes.subarray(start)));
|
|
613
|
+
return candidate ?? null;
|
|
614
|
+
}
|
|
615
|
+
pending = bytes.slice(start);
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
return null;
|
|
619
|
+
} finally {
|
|
620
|
+
fs.closeSync(fd);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
473
624
|
/**
|
|
474
625
|
* Seed the match cache for a pattern ahead of time, off the event loop.
|
|
475
626
|
*
|
|
@@ -491,22 +642,54 @@ function answerSync(fc, entry) {
|
|
|
491
642
|
* `matchSorted` is the reporting variant: same spawn, same cache, but it
|
|
492
643
|
* awaits an answer and so has somewhere to put a failure.
|
|
493
644
|
*
|
|
494
|
-
* @returns {Promise<void>} resolves once the
|
|
495
|
-
*
|
|
645
|
+
* @returns {Promise<void>} resolves once the answer is ready for the first
|
|
646
|
+
* layout that asks — in the cache, or in the prewarm's files — or the
|
|
647
|
+
* attempt abandoned
|
|
496
648
|
*/
|
|
497
649
|
export function prewarm(pattern = {}) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
650
|
+
return warm([patternFor(pattern)])[0];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* `prewarm` for several patterns at once, started together in one child: the
|
|
655
|
+
* faces a caller knows text will be set in besides a family's four — a menu's
|
|
656
|
+
* medium. Patterns already cached or running are not asked again.
|
|
657
|
+
*
|
|
658
|
+
* @returns {Promise<void>} once every pattern's answer is ready or given
|
|
659
|
+
* up on; never rejects
|
|
660
|
+
*/
|
|
661
|
+
export function prewarmPatterns(patterns) {
|
|
662
|
+
return Promise.all(warm(patterns.map(patternFor))).then(() => {});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* `prewarm` for a list of fontconfig patterns: the ones neither cached nor
|
|
667
|
+
* already running start together, in one child (`spawnToFiles`). A promise a
|
|
668
|
+
* pattern, resolving once its answer is ready or given up on; none rejects.
|
|
669
|
+
*/
|
|
670
|
+
function warm(fcs, best = null) {
|
|
671
|
+
if (unavailable) return fcs.map(() => Promise.resolve());
|
|
672
|
+
const fresh = [...new Set(fcs)].filter((fc) => !sortedCache.has(fc) && !inflight.has(fc));
|
|
673
|
+
// the best face only beside its own chain: once that is running or read,
|
|
674
|
+
// the chain's head answers as soon as a best job would
|
|
675
|
+
const bests = best !== null && fresh.includes(best) && !bestInflight.has(best) ? [best] : [];
|
|
676
|
+
if (fresh.length > 0) spawnToFiles(fresh, bests);
|
|
677
|
+
return fcs.map((fc) => {
|
|
678
|
+
if (sortedCache.has(fc)) return Promise.resolve();
|
|
679
|
+
// a prewarm's answer waits in its files for the first ask
|
|
680
|
+
const running = inflight.get(fc);
|
|
681
|
+
if (running?.base) return running.promise;
|
|
682
|
+
const pending = running?.promise ?? runFcMatch(fc);
|
|
683
|
+
return pending.then(
|
|
684
|
+
(out) => {
|
|
685
|
+
if (!sortedCache.has(fc)) {
|
|
686
|
+
const list = parseMatches(out);
|
|
687
|
+
if (list.length > 0) sortedCache.set(fc, list);
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
() => {}
|
|
691
|
+
);
|
|
692
|
+
});
|
|
510
693
|
}
|
|
511
694
|
|
|
512
695
|
// the faces a family is asked for in: regular, bold, and both in italic
|
|
@@ -524,14 +707,20 @@ const facesWarmed = new Set();
|
|
|
524
707
|
* A document asks for its family's faces one at a time, as layout reaches
|
|
525
708
|
* the first bold word, the first emphasis, and each ask that misses is a
|
|
526
709
|
* synchronous fc-match on the way to the first frame: a Markdown document
|
|
527
|
-
* waited on five of them, 600 ms on XQuartz. Started together
|
|
528
|
-
* by side, and each later ask finds its answer there.
|
|
710
|
+
* waited on five of them, 600 ms on XQuartz. Started together, from one
|
|
711
|
+
* child, they run side by side, and each later ask finds its answer there.
|
|
529
712
|
*/
|
|
530
713
|
export function prewarmFaces(pattern = {}) {
|
|
531
714
|
const family = pattern.family || 'sans-serif';
|
|
532
715
|
if (facesWarmed.has(family) || unavailable) return;
|
|
533
716
|
facesWarmed.add(family);
|
|
534
|
-
|
|
717
|
+
// the face a layout will ask for first gets a `best` job beside the chains:
|
|
718
|
+
// the one being asked for, or, warmed ahead, the regular
|
|
719
|
+
const first = patternFor(pattern.weight === undefined && pattern.style === undefined ? { family, weight: 400, style: 'normal' } : pattern);
|
|
720
|
+
warm(
|
|
721
|
+
FACES.map(([weight, style]) => patternFor({ family, weight, style })),
|
|
722
|
+
first
|
|
723
|
+
);
|
|
535
724
|
}
|
|
536
725
|
|
|
537
726
|
/**
|
|
@@ -561,9 +750,10 @@ export async function matchSorted(pattern = {}) {
|
|
|
561
750
|
let out;
|
|
562
751
|
const pending = inflight.get(fc);
|
|
563
752
|
if (pending?.base) {
|
|
564
|
-
// a prewarm
|
|
565
|
-
// a spawn of our own, which is what reports why
|
|
566
|
-
|
|
753
|
+
// a prewarm for the pattern: its answer once the child is gone, or — if
|
|
754
|
+
// it left none — a spawn of our own, which is what reports why
|
|
755
|
+
await pending.promise;
|
|
756
|
+
out = answerSync(fc, pending) ?? undefined;
|
|
567
757
|
}
|
|
568
758
|
if (out === undefined) {
|
|
569
759
|
try {
|
|
@@ -617,6 +807,34 @@ export function matchSortedSync(pattern) {
|
|
|
617
807
|
return cacheMatches(fc, out);
|
|
618
808
|
}
|
|
619
809
|
|
|
810
|
+
/**
|
|
811
|
+
* The best face for a pattern: `matchSortedSync(pattern)[0]`, without reading
|
|
812
|
+
* the rest of the fallback chain where a prewarm's answer is waiting in its
|
|
813
|
+
* file (`firstOfPrewarm`). A layout setting text in a face asks for this;
|
|
814
|
+
* the first character it has to fall back for reads the chain whole.
|
|
815
|
+
*
|
|
816
|
+
* @returns {{path, postscriptName, family: string, families: string[],
|
|
817
|
+
* charset: string}}
|
|
818
|
+
*/
|
|
819
|
+
export function matchFirstSync(pattern) {
|
|
820
|
+
const fc = patternFor(pattern);
|
|
821
|
+
const cached = sortedCache.get(fc);
|
|
822
|
+
if (cached) return cached[0];
|
|
823
|
+
if (unavailable) throw noFontsError(unavailable);
|
|
824
|
+
prewarmFaces(pattern);
|
|
825
|
+
const best = bestInflight.get(fc);
|
|
826
|
+
if (best) {
|
|
827
|
+
const first = bestOfPrewarm(best);
|
|
828
|
+
if (first) return first;
|
|
829
|
+
}
|
|
830
|
+
const pending = inflight.get(fc);
|
|
831
|
+
if (pending?.base) {
|
|
832
|
+
const first = firstOfPrewarm(pending);
|
|
833
|
+
if (first) return first;
|
|
834
|
+
}
|
|
835
|
+
return matchSortedSync(pattern)[0];
|
|
836
|
+
}
|
|
837
|
+
|
|
620
838
|
/**
|
|
621
839
|
* Resolve a font pattern ({family, weight, style}) to the best matching font
|
|
622
840
|
* file. Returns { path, postscriptName, family, families } or throws if
|
|
@@ -29,6 +29,13 @@ import { formatForDepth } from "./pictformat.js";
|
|
|
29
29
|
import Pixmap from "./pixmap.js";
|
|
30
30
|
import { REGION_DOCS, regionId } from "./region.js";
|
|
31
31
|
import { routeRaster } from "./rasterize.js";
|
|
32
|
+
import {
|
|
33
|
+
clipRingToRect,
|
|
34
|
+
clipTrianglesToRect,
|
|
35
|
+
rectsOnSurface,
|
|
36
|
+
WIRE_REACH,
|
|
37
|
+
withinRect,
|
|
38
|
+
} from "./cliprect.js";
|
|
32
39
|
import {
|
|
33
40
|
blurCoverage,
|
|
34
41
|
cachedShadow,
|
|
@@ -1029,6 +1036,13 @@ class RenderingContext2d {
|
|
|
1029
1036
|
}
|
|
1030
1037
|
|
|
1031
1038
|
_stylePicture(value) {
|
|
1039
|
+
if (typeof value === "string") {
|
|
1040
|
+
// by the string, where the app keeps that index (App#solidPictureOf);
|
|
1041
|
+
// an app that only answers solidPicture — a host's own, a test's
|
|
1042
|
+
// stand-in — takes the long way below
|
|
1043
|
+
const app = this.window.app;
|
|
1044
|
+
if (app.solidPictureOf) return app.solidPictureOf(value);
|
|
1045
|
+
}
|
|
1032
1046
|
if (typeof value === "string" || Array.isArray(value)) {
|
|
1033
1047
|
const c = parseColor(value);
|
|
1034
1048
|
return this.createSolidPicture(c[0], c[1], c[2], c[3]);
|
|
@@ -1868,9 +1882,37 @@ class RenderingContext2d {
|
|
|
1868
1882
|
});
|
|
1869
1883
|
}
|
|
1870
1884
|
|
|
1871
|
-
_rasterizePolys(
|
|
1885
|
+
_rasterizePolys(
|
|
1886
|
+
picture,
|
|
1887
|
+
shapes,
|
|
1888
|
+
rule,
|
|
1889
|
+
dx = 0,
|
|
1890
|
+
dy = 0,
|
|
1891
|
+
w = this.width,
|
|
1892
|
+
h = this.height,
|
|
1893
|
+
) {
|
|
1872
1894
|
if (!shapes.length) return;
|
|
1873
|
-
|
|
1895
|
+
// Trapezoids are 16.16 fixed point, and a coordinate past 32,767 throws
|
|
1896
|
+
// out of the paint (lib/cliprect.js). A shape that reaches that far is
|
|
1897
|
+
// cut to the picture first, a pixel to spare on every side: nothing
|
|
1898
|
+
// outside the picture is ever composited. One that does not goes as it
|
|
1899
|
+
// came, for the server to clip.
|
|
1900
|
+
const x0 = -dx - 1;
|
|
1901
|
+
const y0 = -dy - 1;
|
|
1902
|
+
const x1 = w - dx + 1;
|
|
1903
|
+
const y1 = h - dy + 1;
|
|
1904
|
+
let fitted = null;
|
|
1905
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
1906
|
+
const shape = shapes[i];
|
|
1907
|
+
if (withinRect(shape, -WIRE_REACH - dx, -WIRE_REACH - dy, WIRE_REACH - dx, WIRE_REACH - dy)) {
|
|
1908
|
+
if (fitted) fitted.push(shape);
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (!fitted) fitted = shapes.slice(0, i);
|
|
1912
|
+
const ring = clipRingToRect(shape, x0, y0, x1, y1);
|
|
1913
|
+
if (ring.length >= 6) fitted.push(ring);
|
|
1914
|
+
}
|
|
1915
|
+
const traps = trapezoidize(fitted ?? shapes, dx, dy, [], rule);
|
|
1874
1916
|
// stay under the server's maximum request size
|
|
1875
1917
|
const chunk = 4000 * 6;
|
|
1876
1918
|
for (let i = 0; i < traps.length; i += chunk) {
|
|
@@ -2465,21 +2507,49 @@ class RenderingContext2d {
|
|
|
2465
2507
|
// cuts above are meant to keep the extruder off that path; this keeps
|
|
2466
2508
|
// a future miss from being catastrophic. A sum is finite iff every
|
|
2467
2509
|
// term is.
|
|
2510
|
+
//
|
|
2511
|
+
// A triangle wholly off the surface is left out as well: it adds
|
|
2512
|
+
// nothing on either route — the rasterizer would walk its rows only for
|
|
2513
|
+
// its edges to cancel on the border column, and the server would be
|
|
2514
|
+
// sent it only to clip it away — and a zoomed graph has hundreds of
|
|
2515
|
+
// edges past the window every frame.
|
|
2516
|
+
const right = this.width + 1;
|
|
2517
|
+
const bottom = this.height + 1;
|
|
2468
2518
|
let w = 0;
|
|
2469
2519
|
for (let i = 0; i < tris.length; i += 6) {
|
|
2470
|
-
const
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
if (
|
|
2520
|
+
const ax = tris[i];
|
|
2521
|
+
const ay = tris[i + 1];
|
|
2522
|
+
const bx = tris[i + 2];
|
|
2523
|
+
const by = tris[i + 3];
|
|
2524
|
+
const cx = tris[i + 4];
|
|
2525
|
+
const cy = tris[i + 5];
|
|
2526
|
+
if (!Number.isFinite(ax + ay + bx + by + cx + cy)) continue;
|
|
2527
|
+
if (
|
|
2528
|
+
(ax < -1 && bx < -1 && cx < -1) ||
|
|
2529
|
+
(ay < -1 && by < -1 && cy < -1) ||
|
|
2530
|
+
(ax > right && bx > right && cx > right) ||
|
|
2531
|
+
(ay > bottom && by > bottom && cy > bottom)
|
|
2532
|
+
) {
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2478
2535
|
if (w < i) for (let k = 0; k < 6; k++) tris[w + k] = tris[i + k];
|
|
2479
2536
|
w += 6;
|
|
2480
2537
|
}
|
|
2481
2538
|
if (w < tris.length) tris.length = w;
|
|
2482
2539
|
if (!tris.length) return;
|
|
2540
|
+
// Triangles are 16.16 fixed point, and the source origin a direct stroke
|
|
2541
|
+
// hands the server is 16 bits: a stroke that reaches that far is cut to
|
|
2542
|
+
// the surface first (lib/cliprect.js), a pixel to spare on every side, so
|
|
2543
|
+
// what the surface shows is drawn exactly as before. One that does not
|
|
2544
|
+
// goes as it came, for the server to clip.
|
|
2545
|
+
const edgeX = this.width + 1;
|
|
2546
|
+
const edgeY = this.height + 1;
|
|
2547
|
+
if (!withinRect(tris, -WIRE_REACH, -WIRE_REACH, WIRE_REACH, WIRE_REACH)) {
|
|
2548
|
+
const cut = clipTrianglesToRect(tris, -1, -1, edgeX, edgeY);
|
|
2549
|
+
tris.length = 0;
|
|
2550
|
+
for (let i = 0; i < cut.length; i++) tris.push(cut[i]);
|
|
2551
|
+
if (!tris.length) return;
|
|
2552
|
+
}
|
|
2483
2553
|
|
|
2484
2554
|
const op = this._op();
|
|
2485
2555
|
// round-cap/join disks overlap the stroke body; overlapping coverage
|
|
@@ -3018,6 +3088,20 @@ class RenderingContext2d {
|
|
|
3018
3088
|
* for the whole list and then draws the rectangles through here */
|
|
3019
3089
|
_fillRect(x, y, w, h) {
|
|
3020
3090
|
if (matIsIdentity(this._m)) {
|
|
3091
|
+
// X coordinates and sizes are 16 bits, and a coordinate past 32,767
|
|
3092
|
+
// throws out of the paint (lib/cliprect.js): a rectangle that reaches
|
|
3093
|
+
// past the surface is filled as the part of it the surface has.
|
|
3094
|
+
if (x < 0 || y < 0 || x + w > this.width || y + h > this.height) {
|
|
3095
|
+
const x0 = Math.max(x, 0);
|
|
3096
|
+
const y0 = Math.max(y, 0);
|
|
3097
|
+
const x1 = Math.min(x + w, this.width);
|
|
3098
|
+
const y1 = Math.min(y + h, this.height);
|
|
3099
|
+
if (!(x1 > x0 && y1 > y0)) return;
|
|
3100
|
+
x = x0;
|
|
3101
|
+
y = y0;
|
|
3102
|
+
w = x1 - x0;
|
|
3103
|
+
h = y1 - y0;
|
|
3104
|
+
}
|
|
3021
3105
|
if (!prepareStyle(this._backgroundPicture, this._m)) return;
|
|
3022
3106
|
const op = this._op();
|
|
3023
3107
|
// A rectangular clip is a smaller rectangle to fill, not a mask — this
|
|
@@ -3113,6 +3197,10 @@ class RenderingContext2d {
|
|
|
3113
3197
|
}
|
|
3114
3198
|
if (this._hasPolyClip) return;
|
|
3115
3199
|
if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
|
|
3200
|
+
// the part of each the surface has, as `_fillRect` fills it: the wire's
|
|
3201
|
+
// coordinates are 16 bits
|
|
3202
|
+
const onSurface = rectsOnSurface(flat, this.width, this.height);
|
|
3203
|
+
if (!onSurface.length) return;
|
|
3116
3204
|
const R = this.Render;
|
|
3117
3205
|
if (clip) this._setPictureClip(clip);
|
|
3118
3206
|
// globalAlpha folds into the premultiplied colour — scaling all four
|
|
@@ -3122,12 +3210,14 @@ class RenderingContext2d {
|
|
|
3122
3210
|
const color = this._foldedColor(this._fillStyle);
|
|
3123
3211
|
// stay under the server's maximum request size
|
|
3124
3212
|
const chunk = 10000 * 4;
|
|
3125
|
-
for (let i = 0; i <
|
|
3213
|
+
for (let i = 0; i < onSurface.length; i += chunk) {
|
|
3126
3214
|
R.FillRectangles(
|
|
3127
3215
|
this._op(),
|
|
3128
3216
|
this._dst(),
|
|
3129
3217
|
color,
|
|
3130
|
-
i === 0 &&
|
|
3218
|
+
i === 0 && onSurface.length <= chunk
|
|
3219
|
+
? onSurface
|
|
3220
|
+
: onSurface.slice(i, i + chunk),
|
|
3131
3221
|
);
|
|
3132
3222
|
}
|
|
3133
3223
|
if (clip) this._invalidatePictureClip();
|
|
@@ -3166,10 +3256,13 @@ class RenderingContext2d {
|
|
|
3166
3256
|
let x1 = -Infinity;
|
|
3167
3257
|
let y1 = -Infinity;
|
|
3168
3258
|
for (let i = 0; i < flat.length; i += 4) {
|
|
3169
|
-
|
|
3170
|
-
const
|
|
3171
|
-
const
|
|
3172
|
-
const
|
|
3259
|
+
// the part of it on the surface: the wire's coordinates are 16 bits
|
|
3260
|
+
const left = Math.trunc(flat[i]);
|
|
3261
|
+
const top = Math.trunc(flat[i + 1]);
|
|
3262
|
+
const x = Math.max(0, left);
|
|
3263
|
+
const y = Math.max(0, top);
|
|
3264
|
+
const w = Math.min(this.width, left + Math.trunc(flat[i + 2])) - x;
|
|
3265
|
+
const h = Math.min(this.height, top + Math.trunc(flat[i + 3])) - y;
|
|
3173
3266
|
if (w <= 0 || h <= 0) continue;
|
|
3174
3267
|
rects.push(x, y, w, h);
|
|
3175
3268
|
if (x < x0) x0 = x;
|
|
@@ -3953,6 +4046,8 @@ class RenderingContext2d {
|
|
|
3953
4046
|
entry.rule,
|
|
3954
4047
|
-bb.x,
|
|
3955
4048
|
-bb.y,
|
|
4049
|
+
bb.w,
|
|
4050
|
+
bb.h,
|
|
3956
4051
|
);
|
|
3957
4052
|
}
|
|
3958
4053
|
R.Composite(
|