ntk 6.6.1 → 7.0.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 +108 -0
- package/lib/index.js +2 -0
- package/lib/path.js +196 -6
- package/lib/renderingcontext_2d.js +458 -8
- package/lib/shapeglyphs.js +327 -0
- package/lib/window.js +495 -56
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -8,6 +8,41 @@ import { ShmUploader } from './shm-upload.js';
|
|
|
8
8
|
import FontManager from './text/fontmanager.js';
|
|
9
9
|
import Window from './window.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Frame interval used before the display has been asked, in ms — and the one
|
|
13
|
+
* kept on a server that cannot answer.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_FRAME_INTERVAL = 16;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The range a refresh rate has to fall in to be paced against, in Hz. Below
|
|
19
|
+
* this a display is not something a UI should be throttled to, and above it
|
|
20
|
+
* the numbers are not describing a display at all.
|
|
21
|
+
*/
|
|
22
|
+
const MIN_REFRESH_RATE = 20;
|
|
23
|
+
const MAX_REFRESH_RATE = 1000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A RandR mode's vertical refresh rate in Hz, or 0 when it does not describe
|
|
27
|
+
* one.
|
|
28
|
+
*
|
|
29
|
+
* `dot_clock / (h_total * v_total)`, with the two flags that make a frame
|
|
30
|
+
* something other than one pass down the mode, exactly as xrandr computes it:
|
|
31
|
+
* an interlaced mode paints half the lines per pass, a doublescan mode paints
|
|
32
|
+
* each line twice.
|
|
33
|
+
*
|
|
34
|
+
* The zero check is not defensive: a virtual output has no pixel clock to
|
|
35
|
+
* report and Xvfb — which is what CI runs against — fills all three fields
|
|
36
|
+
* with zeroes, so the arithmetic yields NaN rather than a rate.
|
|
37
|
+
*/
|
|
38
|
+
function modeRate(mode) {
|
|
39
|
+
if (!mode || !mode.dot_clock || !mode.h_total || !mode.v_total) return 0;
|
|
40
|
+
let vTotal = mode.v_total;
|
|
41
|
+
if (mode.modeflags & 0x10) vTotal /= 2; // Interlace
|
|
42
|
+
if (mode.modeflags & 0x20) vTotal *= 2; // DoubleScan
|
|
43
|
+
return mode.dot_clock / (mode.h_total * vTotal);
|
|
44
|
+
}
|
|
45
|
+
|
|
11
46
|
/**
|
|
12
47
|
* A connection to an X server. Owns the underlying node-x11 client
|
|
13
48
|
* (`app.X`) and acts as a factory for windows and pixmaps.
|
|
@@ -59,6 +94,79 @@ export default class App {
|
|
|
59
94
|
});
|
|
60
95
|
}
|
|
61
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The fastest refresh rate any active output is running at, in Hz, or
|
|
99
|
+
* `null` until the display has been asked — and on a server with no RandR
|
|
100
|
+
* or no mode worth pacing to.
|
|
101
|
+
*
|
|
102
|
+
* The fastest rather than the one this or that window is on, because what
|
|
103
|
+
* it feeds is a rate *ceiling* (see `frameInterval`), and a window paced
|
|
104
|
+
* faster than its own monitor is bounded by the next gate along rather than
|
|
105
|
+
* drawing more than anyone can see. Tracking which output every window
|
|
106
|
+
* overlaps would cost a per-window RandR lookup plus change tracking, to
|
|
107
|
+
* make a ceiling slightly tighter.
|
|
108
|
+
*/
|
|
109
|
+
get refreshRate() {
|
|
110
|
+
if (this._refreshProbe === undefined) this._probeRefreshRate();
|
|
111
|
+
return this._refreshRate ?? null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The frame interval windows on this connection take by default, in ms:
|
|
116
|
+
* the display's own period rather than a guess, so an animation loop on a
|
|
117
|
+
* 165Hz output is not held to the 62.5fps a hardcoded 16 implies. `null`
|
|
118
|
+
* until the probe has answered; windows created before then start on the
|
|
119
|
+
* default and adopt this when it lands.
|
|
120
|
+
*
|
|
121
|
+
* A window that presents takes its rate from the display directly and needs
|
|
122
|
+
* none of this — see docs/window.md "What ends a frame". This is what the
|
|
123
|
+
* fence clock paces to, which is every window on a server without Present,
|
|
124
|
+
* and every window before its first frames have been shown.
|
|
125
|
+
*/
|
|
126
|
+
get frameInterval() {
|
|
127
|
+
const rate = this.refreshRate;
|
|
128
|
+
return rate ? 1000 / rate : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Ask RandR what the outputs are running at, once per connection, in the
|
|
133
|
+
* background.
|
|
134
|
+
*
|
|
135
|
+
* Not on the connect path: `createClient` is already four round trips deep
|
|
136
|
+
* and this is three more, for something no window needs before its first
|
|
137
|
+
* frame. Failures are silent by design — every one of them means "no rate
|
|
138
|
+
* to be had", which is exactly what the default covers.
|
|
139
|
+
*/
|
|
140
|
+
_probeRefreshRate() {
|
|
141
|
+
this._refreshProbe = null; // in progress; only ever started once
|
|
142
|
+
const X = this.X;
|
|
143
|
+
X.require('randr', (err, R) => {
|
|
144
|
+
if (err || !R) return;
|
|
145
|
+
const root = this.display.screen[0].root;
|
|
146
|
+
R.GetScreenResourcesCurrent(root, (resourcesError, resources) => {
|
|
147
|
+
if (resourcesError || !resources?.crtcs?.length) return;
|
|
148
|
+
const modes = new Map(resources.modeinfos.map((mode) => [mode.id, mode]));
|
|
149
|
+
let pending = resources.crtcs.length;
|
|
150
|
+
let best = 0;
|
|
151
|
+
for (const crtc of resources.crtcs) {
|
|
152
|
+
R.GetCrtcInfo(crtc, resources.config_timestamp, (crtcError, info) => {
|
|
153
|
+
// a crtc with no mode is one that is switched off
|
|
154
|
+
if (!crtcError && info) best = Math.max(best, modeRate(modes.get(info.mode)));
|
|
155
|
+
if (--pending) return;
|
|
156
|
+
if (best < MIN_REFRESH_RATE || best > MAX_REFRESH_RATE) return;
|
|
157
|
+
this._refreshRate = best;
|
|
158
|
+
this._adoptFrameInterval(1000 / best);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** hand the measured default to the windows that are still on the guess */
|
|
166
|
+
_adoptFrameInterval(ms) {
|
|
167
|
+
for (const wnd of Window._cacheFor(this).values()) wnd._adoptDefaultFrameInterval(ms);
|
|
168
|
+
}
|
|
169
|
+
|
|
62
170
|
/** the text API entry point: font matching/loading, shaping, layout */
|
|
63
171
|
get fonts() {
|
|
64
172
|
if (!this._fonts) this._fonts = new FontManager({ source: this.options.fontSource });
|
package/lib/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
defaultRasterizer,
|
|
37
37
|
setDefaultRasterizer
|
|
38
38
|
} from './rasterize.js';
|
|
39
|
+
import { DEFAULT_SHAPE_POLICY } from './shapeglyphs.js';
|
|
39
40
|
import { TextLayout } from './text/layout.js';
|
|
40
41
|
import HtmlView from './widgets/htmlview.js';
|
|
41
42
|
import SvgView from './widgets/svgview.js';
|
|
@@ -214,6 +215,7 @@ export {
|
|
|
214
215
|
defaultRasterizer,
|
|
215
216
|
setDefaultRasterizer,
|
|
216
217
|
DEFAULT_RASTER_POLICY,
|
|
218
|
+
DEFAULT_SHAPE_POLICY,
|
|
217
219
|
TextLayout,
|
|
218
220
|
HtmlView,
|
|
219
221
|
SvgView,
|
package/lib/path.js
CHANGED
|
@@ -45,6 +45,100 @@ export function matIsIdentity(m) {
|
|
|
45
45
|
return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// --------------------------------------------------------------------------
|
|
49
|
+
// arc tags
|
|
50
|
+
//
|
|
51
|
+
// A cubic emitted by ellipseCubics carries the arc it approximates, so the
|
|
52
|
+
// flattener can subdivide the arc itself instead of rediscovering its shape
|
|
53
|
+
// by bisecting the cubic (issue #213). The tag is the centre-plus-axis-
|
|
54
|
+
// vectors form:
|
|
55
|
+
//
|
|
56
|
+
// P(t) = (cx, cy) + u·cos t + v·sin t, t from t0 to t1
|
|
57
|
+
//
|
|
58
|
+
// u and v are the semi-axis *vectors*, which is what makes the tag survive
|
|
59
|
+
// transforms: an affine map takes this form to the same form — the centre
|
|
60
|
+
// moves, u and v go through the linear part — where radii plus a rotation
|
|
61
|
+
// angle would only survive a similarity, and a shear or a non-uniform scale
|
|
62
|
+
// would have to drop the tag and fall back.
|
|
63
|
+
//
|
|
64
|
+
// The tag also carries its own start point (sx, sy), which the lowering
|
|
65
|
+
// computes anyway. Flattening a cubic starts from the path's current point,
|
|
66
|
+
// so the arc route is only valid when that point is where the arc begins —
|
|
67
|
+
// true of every way a tagged cubic can be built here, and checked rather
|
|
68
|
+
// than assumed, since a path that broke the invariant would otherwise be
|
|
69
|
+
// drawn quietly wrong instead of falling back to bisection.
|
|
70
|
+
//
|
|
71
|
+
// Tags are treated as immutable: transformArc always builds a new one, so
|
|
72
|
+
// Path2D copies and addPath can share them by reference.
|
|
73
|
+
|
|
74
|
+
/** the flatness tolerance, in output pixels, everything here defaults to */
|
|
75
|
+
export const FLATTEN_TOLERANCE = 0.25;
|
|
76
|
+
|
|
77
|
+
// how many chords an arc may be split into, whatever the arithmetic says.
|
|
78
|
+
// Only reachable through a degenerate tolerance (0 or negative); a real
|
|
79
|
+
// drawing at tol 0.25 asks for ~800 chords for a full circle the size of
|
|
80
|
+
// the largest addressable surface.
|
|
81
|
+
const MAX_ARC_SEGMENTS = 4096;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The fewest equal chords that approximate an arc within `tol`.
|
|
85
|
+
*
|
|
86
|
+
* A chord spanning angle θ of a circle of radius R misses the arc by the
|
|
87
|
+
* sagitta R·(1 - cos(θ/2)) at its midpoint, so the largest angle a chord may
|
|
88
|
+
* span is 2·acos(1 - tol/R) and the count follows by division. Recursive
|
|
89
|
+
* bisection can only land on powers of two and so overshoots this by up to
|
|
90
|
+
* 2x — an arc needing 9 chords used to get 16.
|
|
91
|
+
*
|
|
92
|
+
* @param {number} sweep total angle covered, radians (unsigned)
|
|
93
|
+
* @param {number} radius the arc's radius — for an ellipse, the largest
|
|
94
|
+
* singular value of its axis matrix (see arcScale), which bounds the
|
|
95
|
+
* sagitta everywhere on the curve
|
|
96
|
+
* @param {number} tol allowed deviation, in the same units as radius
|
|
97
|
+
*/
|
|
98
|
+
export function arcSegmentCount(sweep, radius, tol = FLATTEN_TOLERANCE) {
|
|
99
|
+
if (!(sweep > 0) || !(radius > 0)) return 1;
|
|
100
|
+
// tol >= 2R: the whole circle is within tolerance of a single chord
|
|
101
|
+
const cos = 1 - tol / radius;
|
|
102
|
+
if (!(cos > -1)) return 1;
|
|
103
|
+
const theta = 2 * Math.acos(Math.min(1, cos));
|
|
104
|
+
if (!(theta > 0)) return MAX_ARC_SEGMENTS; // tol <= 0: as fine as allowed
|
|
105
|
+
return Math.max(1, Math.min(MAX_ARC_SEGMENTS, Math.ceil(sweep / theta)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The radius to measure an arc tag's sagitta against: the largest singular
|
|
110
|
+
* value of `[u | v]`, i.e. how far the map from the unit circle can stretch
|
|
111
|
+
* a distance. For a circle that is exactly its radius; for an ellipse it is
|
|
112
|
+
* the semi-major axis, which bounds the deviation everywhere on the curve
|
|
113
|
+
* (|A·d| <= σmax·|d|) and so is safe, if conservative on eccentric ones.
|
|
114
|
+
*
|
|
115
|
+
* σ1² + σ2² = ‖A‖_F² and σ1·σ2 = |det A| give it in closed form.
|
|
116
|
+
*/
|
|
117
|
+
function arcScale({ ux, uy, vx, vy }) {
|
|
118
|
+
const frob = ux * ux + uy * uy + vx * vx + vy * vy;
|
|
119
|
+
const det = ux * vy - uy * vx;
|
|
120
|
+
const disc = Math.sqrt(Math.max(0, frob * frob - 4 * det * det));
|
|
121
|
+
return Math.sqrt((frob + disc) / 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** an arc tag through an affine map — the centre and start point move, the
|
|
125
|
+
* axes go through the linear part; exact for every affine, shear and
|
|
126
|
+
* reflection included */
|
|
127
|
+
function transformArc(a, m) {
|
|
128
|
+
return {
|
|
129
|
+
cx: m[0] * a.cx + m[2] * a.cy + m[4],
|
|
130
|
+
cy: m[1] * a.cx + m[3] * a.cy + m[5],
|
|
131
|
+
ux: m[0] * a.ux + m[2] * a.uy,
|
|
132
|
+
uy: m[1] * a.ux + m[3] * a.uy,
|
|
133
|
+
vx: m[0] * a.vx + m[2] * a.vy,
|
|
134
|
+
vy: m[1] * a.vx + m[3] * a.vy,
|
|
135
|
+
sx: m[0] * a.sx + m[2] * a.sy + m[4],
|
|
136
|
+
sy: m[1] * a.sx + m[3] * a.sy + m[5],
|
|
137
|
+
t0: a.t0,
|
|
138
|
+
t1: a.t1
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
48
142
|
// --------------------------------------------------------------------------
|
|
49
143
|
// elliptical arcs -> cubics
|
|
50
144
|
|
|
@@ -53,6 +147,9 @@ export function matIsIdentity(m) {
|
|
|
53
147
|
* radii (rx, ry), rotated by phi, from angle a0 sweeping by da (signed,
|
|
54
148
|
* |da| <= 2π). Returns { start: {x, y}, cmds: [{type:'C', ...}, ...] };
|
|
55
149
|
* segments are split to <= 90° so the approximation error stays tiny.
|
|
150
|
+
*
|
|
151
|
+
* Each command carries an `arc` tag (see above) describing the piece of the
|
|
152
|
+
* true arc it stands for, which is what `flattenPath` subdivides.
|
|
56
153
|
*/
|
|
57
154
|
export function ellipseCubics(cx, cy, rx, ry, phi, a0, da) {
|
|
58
155
|
const cosPhi = Math.cos(phi);
|
|
@@ -68,6 +165,12 @@ export function ellipseCubics(cx, cy, rx, ry, phi, a0, da) {
|
|
|
68
165
|
return [cosPhi * x - sinPhi * y, sinPhi * x + cosPhi * y];
|
|
69
166
|
};
|
|
70
167
|
|
|
168
|
+
// the ellipse's semi-axis vectors, the form the arc tag keeps
|
|
169
|
+
const ux = rx * cosPhi;
|
|
170
|
+
const uy = rx * sinPhi;
|
|
171
|
+
const vx = -ry * sinPhi;
|
|
172
|
+
const vy = ry * cosPhi;
|
|
173
|
+
|
|
71
174
|
const [sx, sy] = point(a0);
|
|
72
175
|
const cmds = [];
|
|
73
176
|
const n = Math.max(1, Math.ceil(Math.abs(da) / (Math.PI / 2)));
|
|
@@ -87,7 +190,8 @@ export function ellipseCubics(cx, cy, rx, ry, phi, a0, da) {
|
|
|
87
190
|
x2: x3 - k * dx3,
|
|
88
191
|
y2: y3 - k * dy3,
|
|
89
192
|
x: x3,
|
|
90
|
-
y: y3
|
|
193
|
+
y: y3,
|
|
194
|
+
arc: { cx, cy, ux, uy, vx, vy, sx: x0, sy: y0, t0: a, t1: b }
|
|
91
195
|
});
|
|
92
196
|
a = b;
|
|
93
197
|
}
|
|
@@ -416,12 +520,22 @@ export class Path2D {
|
|
|
416
520
|
this._y = null;
|
|
417
521
|
this._sx = null; // subpath start
|
|
418
522
|
this._sy = null;
|
|
523
|
+
// When the whole path is exactly one roundRect(), this records what
|
|
524
|
+
// roundRect knew — { x, y, w, h, radii: [tl, tr, br, bl] } with the
|
|
525
|
+
// radii already normalized — so the 2d context can recognize the shape
|
|
526
|
+
// and route it to corner glyphs + FillRectangles instead of polygon
|
|
527
|
+
// rasterization (issue #211). Any other path verb clears it. The 2d
|
|
528
|
+
// context parks a bail-out reason in _roundRectMiss when it records a
|
|
529
|
+
// box the fast path can never take (e.g. under rotation).
|
|
530
|
+
this._roundRect = null;
|
|
531
|
+
this._roundRectMiss = null;
|
|
419
532
|
if (init instanceof Path2D) {
|
|
420
533
|
this._cmds = init._cmds.map((c) => ({ ...c }));
|
|
421
534
|
this._x = init._x;
|
|
422
535
|
this._y = init._y;
|
|
423
536
|
this._sx = init._sx;
|
|
424
537
|
this._sy = init._sy;
|
|
538
|
+
this._roundRect = init._roundRect;
|
|
425
539
|
} else if (typeof init === 'string') {
|
|
426
540
|
this._append(parseSvgPath(init));
|
|
427
541
|
}
|
|
@@ -443,6 +557,10 @@ export class Path2D {
|
|
|
443
557
|
}
|
|
444
558
|
|
|
445
559
|
_append(cmds) {
|
|
560
|
+
// every mutation funnels through here; a path that is no longer exactly
|
|
561
|
+
// one roundRect() loses the tag (roundRect itself re-sets it at the end)
|
|
562
|
+
this._roundRect = null;
|
|
563
|
+
this._roundRectMiss = null;
|
|
446
564
|
for (const c of cmds) {
|
|
447
565
|
this._cmds.push(c);
|
|
448
566
|
this._track(c);
|
|
@@ -532,7 +650,18 @@ export class Path2D {
|
|
|
532
650
|
}
|
|
533
651
|
|
|
534
652
|
roundRect(x, y, w, h, radii) {
|
|
535
|
-
|
|
653
|
+
// The tag is set only when this call is the whole path: recorded after
|
|
654
|
+
// the geometry lands (the builder calls below clear it via _append) and
|
|
655
|
+
// only if the path held nothing before.
|
|
656
|
+
const wasEmpty = this._cmds.length === 0;
|
|
657
|
+
if (typeof radii === 'number' && radii === 0) {
|
|
658
|
+
this.rect(x, y, w, h);
|
|
659
|
+
if (wasEmpty && w >= 0 && h >= 0) {
|
|
660
|
+
const zero = { x: 0, y: 0 };
|
|
661
|
+
this._roundRect = { x, y, w, h, radii: [zero, zero, zero, zero] };
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
536
665
|
const [tl, tr, br, bl] = normalizeRadii(Math.abs(w), Math.abs(h), radii);
|
|
537
666
|
if (w < 0 || h < 0) {
|
|
538
667
|
// degenerate: fall back to a plain rect on flipped geometry
|
|
@@ -549,6 +678,7 @@ export class Path2D {
|
|
|
549
678
|
if (tl.x || tl.y) this.ellipse(x + tl.x, y + tl.y, tl.x, tl.y, 0, Math.PI, Math.PI * 1.5);
|
|
550
679
|
this.closePath();
|
|
551
680
|
this.moveTo(x, y);
|
|
681
|
+
if (wasEmpty) this._roundRect = { x, y, w, h, radii: [tl, tr, br, bl] };
|
|
552
682
|
}
|
|
553
683
|
|
|
554
684
|
/** append another path, optionally transformed ([a,b,c,d,e,f] or {a..f}) */
|
|
@@ -578,7 +708,12 @@ export function transformCommands(cmds, m) {
|
|
|
578
708
|
const [x1, y1] = matApply(m, c.x1, c.y1);
|
|
579
709
|
const [x2, y2] = matApply(m, c.x2, c.y2);
|
|
580
710
|
const [x, y] = matApply(m, c.x, c.y);
|
|
581
|
-
|
|
711
|
+
const out_ = { type: 'C', x1, y1, x2, y2, x, y };
|
|
712
|
+
// an arc tag transforms exactly under any affine, so baking the CTM
|
|
713
|
+
// into the commands (which is what the context's default path does)
|
|
714
|
+
// keeps the arc route available downstream
|
|
715
|
+
if (c.arc) out_.arc = transformArc(c.arc, m);
|
|
716
|
+
out.push(out_);
|
|
582
717
|
break;
|
|
583
718
|
}
|
|
584
719
|
case 'Q': {
|
|
@@ -604,7 +739,6 @@ function addCubic(pts, x0, y0, x1, y1, x2, y2, x3, y3, tol2, depth) {
|
|
|
604
739
|
// cross products = (distance of control point from the chord) * |chord|
|
|
605
740
|
const d1 = (x1 - x0) * dy - (y1 - y0) * dx;
|
|
606
741
|
const d2 = (x2 - x0) * dy - (y2 - y0) * dx;
|
|
607
|
-
const err = (Math.abs(d1) + Math.abs(d2)) ** 2;
|
|
608
742
|
let flat;
|
|
609
743
|
if (chord2 < 1e-9) {
|
|
610
744
|
// degenerate chord: flat only if the control points are also close
|
|
@@ -612,7 +746,12 @@ function addCubic(pts, x0, y0, x1, y1, x2, y2, x3, y3, tol2, depth) {
|
|
|
612
746
|
const c2 = (x2 - x0) ** 2 + (y2 - y0) ** 2;
|
|
613
747
|
flat = Math.max(c1, c2) <= tol2;
|
|
614
748
|
} else {
|
|
615
|
-
//
|
|
749
|
+
// The curve's distance from its chord is bounded by (3/4)·max(dist1,
|
|
750
|
+
// dist2): B(t) - chord(t) = 3t(1-t)·((1-t)·D1 + t·D2), and 3t(1-t)
|
|
751
|
+
// peaks at 3/4. Testing dist1 + dist2 here (as this used to) is up to
|
|
752
|
+
// 8/3 stricter than that bound on symmetric cubics — every arc — which
|
|
753
|
+
// over-subdivided a quarter circle ~2x at any radius (issue #213).
|
|
754
|
+
const err = (0.75 * Math.max(Math.abs(d1), Math.abs(d2))) ** 2;
|
|
616
755
|
flat = err <= tol2 * chord2;
|
|
617
756
|
}
|
|
618
757
|
if (flat || depth >= 18) {
|
|
@@ -636,14 +775,64 @@ function addCubic(pts, x0, y0, x1, y1, x2, y2, x3, y3, tol2, depth) {
|
|
|
636
775
|
addCubic(pts, mx, my, bcx, bcy, cx, cy, x3, y3, tol2, depth + 1);
|
|
637
776
|
}
|
|
638
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Chords of the arc a tagged cubic stands for, appended to `pts`. Returns
|
|
780
|
+
* false — flatten the cubic instead — when the path's current point is not
|
|
781
|
+
* where the arc begins, so a caller that spliced commands together still
|
|
782
|
+
* gets a curve from where it actually is.
|
|
783
|
+
*
|
|
784
|
+
* The count comes from the sagitta formula rather than from bisecting the
|
|
785
|
+
* cubic, so an arc gets the fewest chords its tolerance allows instead of
|
|
786
|
+
* the next power of two (issue #213). The points are evaluated on the arc
|
|
787
|
+
* itself — closer to the geometry the caller asked for than the cubic
|
|
788
|
+
* proxy, whose own error is ~2.7e-4·R for the <=90° pieces used here.
|
|
789
|
+
*
|
|
790
|
+
* The last point is the command's own endpoint rather than a re-evaluated
|
|
791
|
+
* one, so consecutive segments still meet exactly, and an SVG arc whose
|
|
792
|
+
* final point was snapped to the requested endpoint keeps that snap.
|
|
793
|
+
*/
|
|
794
|
+
function addArcChords(pts, cmd, m, tol, x0, y0) {
|
|
795
|
+
const arc = m ? transformArc(cmd.arc, m) : cmd.arc;
|
|
796
|
+
const scale = arcScale(arc);
|
|
797
|
+
// The two agree bit-for-bit when the lowering produced both, so this only
|
|
798
|
+
// fires on spliced paths — and on the ~1e-13 drift where an SVG arc's
|
|
799
|
+
// endpoint was snapped and the next arc starts from the snapped value.
|
|
800
|
+
const slack = 1e-6 * (1 + scale);
|
|
801
|
+
if (Math.abs(x0 - arc.sx) > slack || Math.abs(y0 - arc.sy) > slack) {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
const sweep = arc.t1 - arc.t0;
|
|
805
|
+
const n = arcSegmentCount(Math.abs(sweep), scale, tol);
|
|
806
|
+
for (let i = 1; i < n; i++) {
|
|
807
|
+
const t = arc.t0 + (sweep * i) / n;
|
|
808
|
+
const cos = Math.cos(t);
|
|
809
|
+
const sin = Math.sin(t);
|
|
810
|
+
pts.push(
|
|
811
|
+
arc.cx + arc.ux * cos + arc.vx * sin,
|
|
812
|
+
arc.cy + arc.uy * cos + arc.vy * sin
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (m) {
|
|
816
|
+
const [x, y] = matApply(m, cmd.x, cmd.y);
|
|
817
|
+
pts.push(x, y);
|
|
818
|
+
} else {
|
|
819
|
+
pts.push(cmd.x, cmd.y);
|
|
820
|
+
}
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
|
|
639
824
|
/**
|
|
640
825
|
* Flatten normalized commands into polylines. `m` (optional affine) is
|
|
641
826
|
* applied to control points before subdivision, so the flatness tolerance
|
|
642
827
|
* `tol` (default 0.25) is measured in output/device pixels.
|
|
643
828
|
*
|
|
829
|
+
* Cubics that came from an arc carry a tag and are subdivided from their own
|
|
830
|
+
* geometry (see addArcChords); genuine beziers — hand-built paths, SVG curve
|
|
831
|
+
* commands, font outlines — take the adaptive bisection route.
|
|
832
|
+
*
|
|
644
833
|
* @returns {Array<{pts: number[], closed: boolean}>} flat [x0,y0,x1,y1,…]
|
|
645
834
|
*/
|
|
646
|
-
export function flattenPath(cmds, m = null, tol =
|
|
835
|
+
export function flattenPath(cmds, m = null, tol = FLATTEN_TOLERANCE) {
|
|
647
836
|
if (m && matIsIdentity(m)) m = null;
|
|
648
837
|
const tol2 = tol * tol;
|
|
649
838
|
const polys = [];
|
|
@@ -668,6 +857,7 @@ export function flattenPath(cmds, m = null, tol = 0.25) {
|
|
|
668
857
|
if (!pts) break;
|
|
669
858
|
const x0 = pts.pts[pts.pts.length - 2];
|
|
670
859
|
const y0 = pts.pts[pts.pts.length - 1];
|
|
860
|
+
if (c.arc && addArcChords(pts.pts, c, m, tol, x0, y0)) break;
|
|
671
861
|
const [x1, y1] = p(c.x1, c.y1);
|
|
672
862
|
const [x2, y2] = p(c.x2, c.y2);
|
|
673
863
|
const [x, y] = p(c.x, c.y);
|