ntk 8.17.8 → 8.18.1

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/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: slab boundaries are the sorted unique vertex y
5
- // coordinates, so edges never cross inside a slab (self-intersecting input —
6
- // e.g. overlapping composite-glyph contours — degrades gracefully to a
7
- // near-correct fill instead of failing). Spans where the winding number is
8
- // non-zero become trapezoids; a span whose left/right edges continue through
9
- // consecutive slabs is merged into a single tall trapezoid, which is what
10
- // keeps the output at roughly one trapezoid per outline edge.
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
@@ -25,7 +37,9 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
25
37
  const evenodd = rule === 'evenodd';
26
38
  // collect non-horizontal edges, normalized to y0 < y1 with winding dir
27
39
  const edges = [];
28
- const ys = [];
40
+ let 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,24 +49,34 @@ 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, x1: bx, y1: by, dir: 1, xa: 0, xb: 0 }
41
- : { x0: bx, y0: by, x1: ax, y1: ay, dir: -1, xa: 0, xb: 0 }
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
- ys.sort((a, b) => a - b);
69
+ // a typed array sorts numerically without a comparator call per step
70
+ ys = Float64Array.from(ys).sort();
49
71
  edges.sort((a, b) => a.y0 - b.y0);
50
72
 
51
73
  const active = [];
52
74
  let nextEdge = 0;
53
- // spans open from the previous slab, for vertical merging:
54
- // { l: leftEdge, r: rightEdge, at: index of the trap in `out` }
75
+ // spans open from the previous slab, for vertical merging, and the ones
76
+ // this slab opens: flat [leftEdge, rightEdge, index of the trap in `out`]
77
+ // triples, two buffers that trade places every slab
55
78
  let prevSpans = [];
79
+ let spans = [];
56
80
 
57
81
  for (let yi = 0; yi < ys.length - 1; ++yi) {
58
82
  const ya = ys[yi];
@@ -60,64 +84,128 @@ export function trapezoidize(polys, dx = 0, dy = 0, out = [], rule = 'nonzero')
60
84
  if (yb === ya) continue;
61
85
 
62
86
  // update the active edge list for this slab
63
- for (let i = active.length - 1; i >= 0; --i) {
64
- if (active[i].y1 <= ya) active.splice(i, 1);
87
+ let kept = 0;
88
+ for (let i = 0; i < active.length; ++i) {
89
+ if (active[i].y1 > ya) active[kept++] = active[i];
65
90
  }
91
+ active.length = kept;
66
92
  while (nextEdge < edges.length && edges[nextEdge].y0 <= ya) {
67
93
  if (edges[nextEdge].y1 > ya) active.push(edges[nextEdge]);
68
94
  nextEdge++;
69
95
  }
70
96
  if (active.length === 0) {
71
- prevSpans = [];
97
+ prevSpans.length = 0;
72
98
  continue;
73
99
  }
74
100
 
75
- // x at slab top/bottom for each active edge; sort left-to-right
76
- for (const e of active) {
77
- const inv = (e.x1 - e.x0) / (e.y1 - e.y0);
78
- e.xa = e.x0 + (ya - e.y0) * inv;
79
- e.xb = e.x0 + (yb - e.y0) * inv;
101
+ // x at slab top/bottom for each active edge; sort left-to-right just
102
+ // below the top (edges leaving one point go by where they are headed)
103
+ for (let i = 0; i < active.length; ++i) {
104
+ const e = active[i];
105
+ e.xt = e.x0 + (ya - e.y0) * e.slope;
106
+ e.xb = e.x0 + (yb - e.y0) * e.slope;
107
+ }
108
+ // a slab holds a handful of edges, mostly in last slab's order: an
109
+ // insertion sort, stable as Array#sort is, so the order is the same
110
+ for (let i = 1; i < active.length; ++i) {
111
+ const e = active[i];
112
+ let j = i - 1;
113
+ while (j >= 0 && (active[j].xt - e.xt || active[j].xb - e.xb) > 0) {
114
+ active[j + 1] = active[j];
115
+ --j;
116
+ }
117
+ active[j + 1] = e;
80
118
  }
81
- active.sort((a, b) => a.xa + a.xb - (b.xa + b.xb));
82
119
 
83
- // emit a span per maximal interior interval; interiority is decided by
84
- // the fill rule (non-zero winding, or crossing parity for even-odd)
85
- const spans = [];
86
- let winding = 0;
87
- let crossings = 0;
88
- let left = null;
89
- for (const e of active) {
90
- const wasInside = evenodd ? (crossings & 1) === 1 : winding !== 0;
91
- winding += e.dir;
92
- crossings++;
93
- const isInside = evenodd ? (crossings & 1) === 1 : winding !== 0;
94
- if (!wasInside && isInside) {
95
- left = e;
96
- } else if (wasInside && !isInside && left) {
97
- // merge with a span from the previous slab bounded by the same edges
98
- let merged = false;
99
- for (const p of prevSpans) {
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
- }
120
+ // cut the slab at each crossing, top to bottom; `y` is where the part
121
+ // still to emit starts, and `xt` each edge's x there
122
+ let y = ya;
123
+ for (;;) {
124
+ let yc = yb;
125
+ let k = -1;
126
+ for (let i = 0; i + 1 < active.length; ++i) {
127
+ const a = active[i];
128
+ const b = active[i + 1];
129
+ // how far `a` ends up right of `b`, and how far left of it it starts
130
+ const over = a.xb - b.xb;
131
+ if (!(over > eps)) continue;
132
+ const gap = b.xt - a.xt;
133
+ const at = gap > 0 ? y + (yb - y) * (gap / (gap + over)) : y;
134
+ if (at < yc) {
135
+ yc = at;
136
+ k = i;
108
137
  }
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);
138
+ }
139
+ if (k < 0 || yc >= yb - eps) {
140
+ emitSpans(active, y, yb, true, evenodd, prevSpans, spans, out);
141
+ [prevSpans, spans] = [spans, prevSpans];
142
+ break;
143
+ }
144
+ if (yc > y + eps) {
145
+ for (let i = 0; i < active.length; ++i) {
146
+ const e = active[i];
147
+ e.xc = e.x0 + (yc - e.y0) * e.slope;
112
148
  }
113
- left = null;
149
+ emitSpans(active, y, yc, false, evenodd, prevSpans, spans, out);
150
+ [prevSpans, spans] = [spans, prevSpans];
151
+ for (let i = 0; i < active.length; ++i) active[i].xt = active[i].xc;
152
+ y = yc;
114
153
  }
154
+ const a = active[k];
155
+ active[k] = active[k + 1];
156
+ active[k + 1] = a;
115
157
  }
116
- prevSpans = spans;
117
158
  }
118
159
  return out;
119
160
  }
120
161
 
162
+ /**
163
+ * Emit a span per maximal interior interval of the slab `ya`..`yb`;
164
+ * interiority is decided by the fill rule (non-zero winding, or crossing
165
+ * parity for even-odd). Each edge is at `xt` at the top, and at the bottom
166
+ * at `xb` when this is the `last` part of its vertex slab, else at `xc`. A
167
+ * span bounded by the same two edges as one ending at `ya` extends that
168
+ * trapezoid instead. The spans go into `spans`, emptied first, for the next
169
+ * slab to merge with.
170
+ */
171
+ function emitSpans(active, ya, yb, last, evenodd, prevSpans, spans, out) {
172
+ spans.length = 0;
173
+ let winding = 0;
174
+ let crossings = 0;
175
+ let left = null;
176
+ for (let i = 0; i < active.length; ++i) {
177
+ const e = active[i];
178
+ const wasInside = evenodd ? (crossings & 1) === 1 : winding !== 0;
179
+ winding += e.dir;
180
+ crossings++;
181
+ const isInside = evenodd ? (crossings & 1) === 1 : winding !== 0;
182
+ if (!wasInside && isInside) {
183
+ left = e;
184
+ } else if (wasInside && !isInside && left) {
185
+ const lb = last ? left.xb : left.xc;
186
+ const rb = last ? e.xb : e.xc;
187
+ // merge with a span from the previous slab bounded by the same edges
188
+ let merged = false;
189
+ for (let p = 0; p < prevSpans.length; p += 3) {
190
+ const at = prevSpans[p + 2];
191
+ if (prevSpans[p] === left && prevSpans[p + 1] === e && out[at + 5] === ya) {
192
+ out[at + 3] = lb;
193
+ out[at + 4] = rb;
194
+ out[at + 5] = yb;
195
+ spans.push(left, e, at);
196
+ merged = true;
197
+ break;
198
+ }
199
+ }
200
+ if (!merged && (e.xt > left.xt || rb > lb)) {
201
+ spans.push(left, e, out.length);
202
+ out.push(left.xt, e.xt, ya, lb, rb, yb);
203
+ }
204
+ left = null;
205
+ }
206
+ }
207
+ }
208
+
121
209
  /** area covered by a trapezoid list (useful for tests/diagnostics) */
122
210
  export function trapArea(traps) {
123
211
  let area = 0;