ntk 8.12.0 → 8.12.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.
@@ -94,6 +94,51 @@ function intersectBox(a, b) {
94
94
  return { x, y, w: right - x, h: bottom - y };
95
95
  }
96
96
 
97
+ /**
98
+ * Whether no two of a flat `[x, y, w, h, …]` list of whole-pixel rectangles
99
+ * share a pixel — touching edges do not. A sweep along x holding the
100
+ * rectangles it is inside as disjoint y spans in order, so a list that is
101
+ * disjoint, which is the list this is asked about, costs a binary search
102
+ * per rectangle: a chart's per-column spans, an occupancy grid's cells.
103
+ */
104
+ function disjointRects(rects) {
105
+ const n = rects.length / 4;
106
+ if (n < 2) return true;
107
+ // events: where a rectangle starts and where it ends, ends first at a tie
108
+ const events = [];
109
+ for (let i = 0; i < n; i++) {
110
+ events.push([rects[i * 4], 1, i], [rects[i * 4] + rects[i * 4 + 2], 0, i]);
111
+ }
112
+ events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
113
+ // the y spans the sweep is inside, sorted by their top, as rectangle ids
114
+ const active = [];
115
+ const top = (i) => rects[i * 4 + 1];
116
+ const bottom = (i) => rects[i * 4 + 1] + rects[i * 4 + 3];
117
+ const find = (y) => {
118
+ let lo = 0;
119
+ let hi = active.length;
120
+ while (lo < hi) {
121
+ const mid = (lo + hi) >> 1;
122
+ if (top(active[mid]) < y) lo = mid + 1;
123
+ else hi = mid;
124
+ }
125
+ return lo;
126
+ };
127
+ for (const [, starts, i] of events) {
128
+ if (!starts) {
129
+ let at = find(top(i));
130
+ while (active[at] !== i) at++;
131
+ active.splice(at, 1);
132
+ continue;
133
+ }
134
+ const at = find(top(i));
135
+ if (at > 0 && bottom(active[at - 1]) > top(i)) return false;
136
+ if (at < active.length && top(active[at]) < bottom(i)) return false;
137
+ active.splice(at, 0, i);
138
+ }
139
+ return true;
140
+ }
141
+
97
142
  /** The device-space ink bounds of a flattened path, unclamped. */
98
143
  function polysInk(polys) {
99
144
  let minX = Infinity;
@@ -1854,42 +1899,47 @@ class RenderingContext2d {
1854
1899
  * well as the AddTraps: two requests become one, and the one that is left
1855
1900
  * touches only the bounding box.
1856
1901
  */
1857
- _uploadCoverage(job, b, edges) {
1902
+ _uploadCoverage(job, b, out, edges) {
1858
1903
  const rasterizer = this.window.app.rasterizer;
1859
1904
  if (!rasterizer) return false;
1860
1905
  if (routeRaster(b.w, b.h, edges, this.window.app.rasterPolicy) !== "local")
1861
1906
  return false;
1862
1907
 
1908
+ // Rasterized over the whole box, as always, so a drawing that clips
1909
+ // split across passes — a pan's strips, a full repaint — gets the same
1910
+ // coverage in every one of them, byte for byte. Only `out`, the part the
1911
+ // clip lets through, goes on the wire (issue #372).
1863
1912
  const coverage = rasterizer.rasterize({ ...job, width: b.w, height: b.h });
1864
1913
  if (!coverage) return false;
1865
1914
 
1866
1915
  // X wants scanlines padded to 4 bytes; the rasterizer contract is
1867
- // unpadded rows. When the width is already a multiple of 4 — which every
1868
- // power-of-two icon box is — the coverage goes out as a view over the
1869
- // rasterizer's own bytes, with no copy at all.
1870
- const stride = (b.w + 3) & ~3;
1916
+ // unpadded rows. When the part sent is the whole box and its width is
1917
+ // already a multiple of 4 — which every power-of-two icon box is — the
1918
+ // coverage goes out as a view over the rasterizer's own bytes, with no
1919
+ // copy at all.
1920
+ const stride = (out.w + 3) & ~3;
1871
1921
  let data;
1872
- if (stride === b.w) {
1922
+ if (out.w === b.w && out.h === b.h && stride === b.w) {
1873
1923
  data = Buffer.isBuffer(coverage)
1874
1924
  ? coverage
1875
1925
  : Buffer.from(coverage.buffer, coverage.byteOffset, coverage.length);
1876
1926
  } else {
1877
- data = Buffer.alloc(stride * b.h);
1878
- for (let y = 0; y < b.h; ++y) {
1879
- Buffer.from(coverage.buffer, coverage.byteOffset + y * b.w, b.w).copy(
1880
- data,
1881
- y * stride,
1882
- );
1927
+ data = Buffer.alloc(stride * out.h);
1928
+ const left = out.x - b.x;
1929
+ const top = out.y - b.y;
1930
+ for (let y = 0; y < out.h; ++y) {
1931
+ const from = coverage.byteOffset + (top + y) * b.w + left;
1932
+ Buffer.from(coverage.buffer, from, out.w).copy(data, y * stride);
1883
1933
  }
1884
1934
  }
1885
1935
  this.X.PutImage(
1886
1936
  2,
1887
1937
  this.fillMaskDrawable.id,
1888
1938
  this._maskGC(),
1889
- b.w,
1890
- b.h,
1891
- b.x,
1892
- b.y,
1939
+ out.w,
1940
+ out.h,
1941
+ out.x,
1942
+ out.y,
1893
1943
  0,
1894
1944
  8,
1895
1945
  data,
@@ -1982,30 +2032,29 @@ class RenderingContext2d {
1982
2032
  */
1983
2033
  _paintThroughMask(b, job, { edges, src, op, alpha, server }) {
1984
2034
  const R = this.Render;
1985
- // A rectangular clip narrows where the coverage is *composited*; the
1986
- // mask content outside it is stale by the same argument as outside `b`,
1987
- // and a box the clip rejects outright needs no coverage at all. A stack
1988
- // whose mask was dropped by restore() may still hold a poly, and the mask
1989
- // comes back on demand. A region clip needs nothing here: the picture
1990
- // carries it, and it applies to the composite below like any other.
2035
+ // Everything is bounded to the part of the box the clip lets through:
2036
+ // the clip's rectangle, or the extents of a stack with a path in it —
2037
+ // the coverage rasterized and uploaded, the clip applied, the composite.
2038
+ // A stroke across a rounded pane used to upload its whole box through
2039
+ // the mask to paint a few rows of it (issue #372). The mask content
2040
+ // outside `out` is stale by the same argument as outside `b`, and a box
2041
+ // the clip rejects outright needs no coverage at all. A stack whose mask
2042
+ // was dropped by restore() may still hold a poly, and the mask comes
2043
+ // back on demand. A region clip needs nothing here: the picture carries
2044
+ // it, and it applies to the composite below like any other.
2045
+ const clip = this._hasPolyClip ? this._clipExtents() : this._clipRect();
1991
2046
  let out = b;
1992
- if (!this.clipMask) {
1993
- if (this._hasPolyClip) {
1994
- this._requireClipMask();
1995
- } else {
1996
- const cr = this._clipRect();
1997
- if (cr) {
1998
- out = intersectBox(b, cr);
1999
- if (!out) return false;
2000
- }
2001
- }
2047
+ if (clip) {
2048
+ out = intersectBox(b, clip);
2049
+ if (!out) return false;
2002
2050
  }
2003
- if (!this._uploadCoverage({ ...job, dx: -b.x, dy: -b.y }, b, edges)) {
2051
+ if (this._hasPolyClip) this._requireClipMask();
2052
+ if (!this._uploadCoverage({ ...job, dx: -b.x, dy: -b.y }, b, out, edges)) {
2004
2053
  R.FillRectangles(
2005
2054
  R.PictOp.Src,
2006
2055
  this.fillMask.id,
2007
2056
  [0, 0, 0, 0],
2008
- [b.x, b.y, b.w, b.h],
2057
+ [out.x, out.y, out.w, out.h],
2009
2058
  );
2010
2059
  server();
2011
2060
  }
@@ -3009,9 +3058,12 @@ class RenderingContext2d {
3009
3058
  * A solid-colour fillStyle under an identity transform and a rectangular
3010
3059
  * (or absent) clip compiles the whole list into a single
3011
3060
  * `Render.FillRectangles`, where N `fillRect` calls cost N composites.
3012
- * Gradient/Picture styles, transforms and non-rectangular clips fall
3013
- * back to that `fillRect` loop, so the answer is always right and only
3014
- * the request count varies.
3061
+ * Under a clip that is not a rectangle, rectangles that do not overlap
3062
+ * still cost three requests however many there are
3063
+ * (`_fillRectsThroughClip`). Gradient/Picture styles, transforms, and
3064
+ * overlapping rectangles or `copy` under a non-rectangular clip fall back
3065
+ * to that `fillRect` loop, so the answer is always right and only the
3066
+ * request count varies.
3015
3067
  */
3016
3068
  fillRects(rects) {
3017
3069
  if (!rects || !rects.length || this.globalAlpha <= 0) return;
@@ -3045,12 +3097,13 @@ class RenderingContext2d {
3045
3097
  if (
3046
3098
  !matIsIdentity(this._m) ||
3047
3099
  !isPlainColor(this._fillStyle) ||
3048
- this._hasPolyClip
3100
+ (this._hasPolyClip && !this._fillRectsThroughClip(flat))
3049
3101
  ) {
3050
3102
  for (let i = 0; i < flat.length; i += 4)
3051
3103
  this._fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
3052
3104
  return;
3053
3105
  }
3106
+ if (this._hasPolyClip) return;
3054
3107
  if (clip && (clip.w === 0 || clip.h === 0)) return; // clipped away
3055
3108
  const R = this.Render;
3056
3109
  if (clip) this._setPictureClip(clip);
@@ -3073,6 +3126,102 @@ class RenderingContext2d {
3073
3126
  this._markDirty();
3074
3127
  }
3075
3128
 
3129
+ /**
3130
+ * `fillRects` under a clip that is not a rectangle — a rounded card, a
3131
+ * path: every rectangle's coverage into the scratch a8 mask with one
3132
+ * `FillRectangles`, `globalAlpha` and the clip applied to it once each,
3133
+ * and the style composited through it once. One rectangle at a time is a
3134
+ * composite through the clip each, and a chart's marks inside a rounded
3135
+ * box a zoom had put off the pixel grid were sixty thousand of them a
3136
+ * frame (issue #374).
3137
+ *
3138
+ * Only where that is the loop's picture. The rectangles must not overlap:
3139
+ * the loop blends a pixel once per rectangle over it, at the clip's
3140
+ * coverage each time, so where two overlap on the clip's antialiased edge
3141
+ * it is darker than one blend through the mask — and where they do not,
3142
+ * every pixel is blended once either way, whatever the colour's alpha.
3143
+ * And the operator must leave the destination alone where the mask is
3144
+ * zero: `copy` clears every pixel of its box the mask misses, the gaps
3145
+ * between the rectangles here. The rectangles reach the mask as the wire
3146
+ * carries every box, in whole pixels truncated toward zero, which is what
3147
+ * `_fillRect`'s composites are cut to as well.
3148
+ *
3149
+ * @returns {boolean} whether the rectangles were drawn here
3150
+ */
3151
+ _fillRectsThroughClip(flat) {
3152
+ const R = this.Render;
3153
+ const op = this._op();
3154
+ if (!this._maskBounded(op)) return false;
3155
+ const rects = [];
3156
+ let x0 = Infinity;
3157
+ let y0 = Infinity;
3158
+ let x1 = -Infinity;
3159
+ let y1 = -Infinity;
3160
+ for (let i = 0; i < flat.length; i += 4) {
3161
+ const x = Math.trunc(flat[i]);
3162
+ const y = Math.trunc(flat[i + 1]);
3163
+ const w = Math.trunc(flat[i + 2]);
3164
+ const h = Math.trunc(flat[i + 3]);
3165
+ if (w <= 0 || h <= 0) continue;
3166
+ rects.push(x, y, w, h);
3167
+ if (x < x0) x0 = x;
3168
+ if (y < y0) y0 = y;
3169
+ if (x + w > x1) x1 = x + w;
3170
+ if (y + h > y1) y1 = y + h;
3171
+ }
3172
+ if (!disjointRects(rects)) return false;
3173
+ if (!prepareStyle(this._backgroundPicture, this._m)) return true;
3174
+ // the box they cover, on the surface: nothing outside it is sampled
3175
+ const bx = Math.max(0, x0);
3176
+ const by = Math.max(0, y0);
3177
+ const bw = Math.min(this.width, x1) - bx;
3178
+ const bh = Math.min(this.height, y1) - by;
3179
+ if (!(bw > 0 && bh > 0)) return true;
3180
+ this._ensureFillMask();
3181
+ const mask = this.fillMask.id;
3182
+ R.FillRectangles(R.PictOp.Src, mask, [0, 0, 0, 0], [bx, by, bw, bh]);
3183
+ // stay under the server's maximum request size
3184
+ const chunk = 10000 * 4;
3185
+ for (let i = 0; i < rects.length; i += chunk) {
3186
+ R.FillRectangles(
3187
+ R.PictOp.Src,
3188
+ mask,
3189
+ [0, 0, 0, this.globalAlpha < 1 ? this.globalAlpha : 1],
3190
+ i === 0 && rects.length <= chunk ? rects : rects.slice(i, i + chunk),
3191
+ );
3192
+ }
3193
+ R.Composite(
3194
+ R.PictOp.In,
3195
+ this._requireClipMask().id,
3196
+ 0,
3197
+ mask,
3198
+ bx,
3199
+ by,
3200
+ 0,
3201
+ 0,
3202
+ bx,
3203
+ by,
3204
+ bw,
3205
+ bh,
3206
+ );
3207
+ R.Composite(
3208
+ op,
3209
+ this._backgroundPicture.id,
3210
+ mask,
3211
+ this._dst(),
3212
+ bx,
3213
+ by,
3214
+ bx,
3215
+ by,
3216
+ bx,
3217
+ by,
3218
+ bw,
3219
+ bh,
3220
+ );
3221
+ this._markDirty();
3222
+ return true;
3223
+ }
3224
+
3076
3225
  strokeRect(x, y, w, h) {
3077
3226
  if (this._shadowed()) this._shadowOfRect(x, y, w, h, true);
3078
3227
  const m = this._m;
@@ -3531,6 +3680,40 @@ class RenderingContext2d {
3531
3680
  return { x, y, w, h };
3532
3681
  }
3533
3682
 
3683
+ /**
3684
+ * The box the clip stack lets anything through, in device pixels on the
3685
+ * surface: the intersection of its rectangles and of its paths' bounding
3686
+ * boxes. Null when the stack holds no shape — region entries are the
3687
+ * picture's, as in `_clipRect` — and an empty box when it holds a path
3688
+ * that lands nowhere. What a drawing through the a8 mask is bounded to
3689
+ * (issue #372). Cached against the stack, which is replaced on every
3690
+ * change rather than edited.
3691
+ */
3692
+ _clipExtents() {
3693
+ if (this._extentsOf === this._clips) return this._extents;
3694
+ let out = null;
3695
+ for (const entry of this._clips) {
3696
+ if (entry.region) continue;
3697
+ const box = entry.rect ?? (entry.bbox ??= this._polysBBox(entry.polys));
3698
+ const next = box && (out ? intersectBox(out, box) : box);
3699
+ if (!next) {
3700
+ out = { x: 0, y: 0, w: 0, h: 0 };
3701
+ break;
3702
+ }
3703
+ out = next;
3704
+ }
3705
+ if (out) {
3706
+ const x = Math.max(0, Math.min(out.x, this.width));
3707
+ const y = Math.max(0, Math.min(out.y, this.height));
3708
+ const w = Math.max(0, Math.min(out.x + out.w, this.width) - x);
3709
+ const h = Math.max(0, Math.min(out.y + out.h, this.height) - y);
3710
+ out = { x, y, w, h };
3711
+ }
3712
+ this._extentsOf = this._clips;
3713
+ this._extents = out;
3714
+ return out;
3715
+ }
3716
+
3534
3717
  /**
3535
3718
  * Replace the clip stack and re-derive what kinds of entry are in it.
3536
3719
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.12.0",
3
+ "version": "8.12.1",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",