@versatiles/png-renderer 2.4.0 → 2.4.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/dist/index.d.ts CHANGED
@@ -2741,6 +2741,12 @@ declare class Feature implements Feature$1 {
2741
2741
  * clipped edges along the tile border are not part of the outline.
2742
2742
  */
2743
2743
  readonly outline?: Geometry;
2744
+ /**
2745
+ * The polygon as an opaque fill draws it — when it differs from the polygon's rings, i.e.
2746
+ * when the polygon was clipped to its tile: clipped a little beyond the tile instead, so
2747
+ * that it overlaps its continuation in the next tile, and no seam shows between them.
2748
+ */
2749
+ readonly fillGeometry?: Geometry;
2744
2750
  /**
2745
2751
  * For each line of the geometry, how far along the line in its tile it starts, in pixels
2746
2752
  * — where the line was clipped to its tile, which cut off its start. A dash pattern
@@ -2754,6 +2760,7 @@ declare class Feature implements Feature$1 {
2754
2760
  patterns?: Patterns;
2755
2761
  geometry: Geometry;
2756
2762
  outline?: Geometry;
2763
+ fillGeometry?: Geometry;
2757
2764
  lineStarts?: number[];
2758
2765
  });
2759
2766
  getBbox(): Bbox;
package/dist/index.js CHANGED
@@ -119,6 +119,12 @@ class Feature {
119
119
  * clipped edges along the tile border are not part of the outline.
120
120
  */
121
121
  outline;
122
+ /**
123
+ * The polygon as an opaque fill draws it — when it differs from the polygon's rings, i.e.
124
+ * when the polygon was clipped to its tile: clipped a little beyond the tile instead, so
125
+ * that it overlaps its continuation in the next tile, and no seam shows between them.
126
+ */
127
+ fillGeometry;
122
128
  /**
123
129
  * For each line of the geometry, how far along the line in its tile it starts, in pixels
124
130
  * — where the line was clipped to its tile, which cut off its start. A dash pattern
@@ -133,6 +139,7 @@ class Feature {
133
139
  this.patterns = opt.patterns;
134
140
  this.geometry = opt.geometry;
135
141
  this.outline = opt.outline;
142
+ this.fillGeometry = opt.fillGeometry;
136
143
  this.lineStarts = opt.lineStarts;
137
144
  }
138
145
  getBbox() {
@@ -2090,20 +2097,28 @@ function readUtf8$1(buf, pos, end) {
2090
2097
  */
2091
2098
  /** Whether any point lies outside of the square [min, max]². */
2092
2099
  function exceedsSquare(rings, min, max) {
2093
- return rings.some((ring) => ring.some(({ x, y }) => x < min || x > max || y < min || y > max));
2100
+ return exceedsBox(rings, [min, min, max, max]);
2101
+ }
2102
+ /** Whether any point lies outside of the box. */
2103
+ function exceedsBox(rings, [minX, minY, maxX, maxY]) {
2104
+ return rings.some((ring) => ring.some(({ x, y }) => x < minX || x > maxX || y < minY || y > maxY));
2094
2105
  }
2095
2106
  /**
2096
2107
  * Clips the rings of a polygon to the square [min, max]² (Sutherland–Hodgman). Rings that
2097
2108
  * vanish are dropped.
2098
2109
  */
2099
2110
  function clipPolygon(rings, min, max) {
2111
+ return clipPolygonToBox(rings, [min, min, max, max]);
2112
+ }
2113
+ /** Clips the rings of a polygon to the box, like `clipPolygon`. */
2114
+ function clipPolygonToBox(rings, [minX, minY, maxX, maxY]) {
2100
2115
  const result = [];
2101
2116
  for (const ring of rings) {
2102
2117
  let points = ring;
2103
- points = clipAgainst(points, (p) => p.x >= min, (a, b) => lerpAtX(a, b, min));
2104
- points = clipAgainst(points, (p) => p.x <= max, (a, b) => lerpAtX(a, b, max));
2105
- points = clipAgainst(points, (p) => p.y >= min, (a, b) => lerpAtY(a, b, min));
2106
- points = clipAgainst(points, (p) => p.y <= max, (a, b) => lerpAtY(a, b, max));
2118
+ points = clipAgainst(points, (p) => p.x >= minX, (a, b) => lerpAtX(a, b, minX));
2119
+ points = clipAgainst(points, (p) => p.x <= maxX, (a, b) => lerpAtX(a, b, maxX));
2120
+ points = clipAgainst(points, (p) => p.y >= minY, (a, b) => lerpAtY(a, b, minY));
2121
+ points = clipAgainst(points, (p) => p.y <= maxY, (a, b) => lerpAtY(a, b, maxY));
2107
2122
  if (points.length >= 3)
2108
2123
  result.push(points);
2109
2124
  }
@@ -7276,7 +7291,7 @@ function decodeVectorTile(buffer, encoding) {
7276
7291
  return new VectorTile(new PbfReader(buffer)).layers;
7277
7292
  }
7278
7293
  /** Adds the features of one vector tile's layers, projected to the screen. */
7279
- function addTileFeatures(layers, { project, clipToTile, pixelsPerUnit }, layerFeatures, width, height) {
7294
+ function addTileFeatures(layers, { project, clipToTile, pixelsPerUnit, fillBox }, layerFeatures, width, height) {
7280
7295
  for (const [name, layer] of Object.entries(layers)) {
7281
7296
  // Geometry is in units of the layer's extent, which MapLibre scales to its own.
7282
7297
  const scale = layer.extent > 0 ? TILE_EXTENT / layer.extent : 1;
@@ -7309,12 +7324,16 @@ function addTileFeatures(layers, { project, clipToTile, pixelsPerUnit }, layerFe
7309
7324
  if (scale !== 1)
7310
7325
  rings = rings.map((ring) => ring.map((p) => ({ x: p.x * scale, y: p.y * scale })));
7311
7326
  let outline;
7327
+ let fillGeometry;
7312
7328
  let lineStarts;
7313
7329
  // Clip polygons and lines to the tile, like MapLibre's stencil clipping: otherwise
7314
7330
  // the parts in the tile buffer are drawn twice, which shows when translucent.
7315
7331
  if (clipToTile && type !== 'Point' && exceedsSquare(rings, 0, TILE_EXTENT)) {
7316
7332
  if (type === 'Polygon') {
7317
7333
  outline = project('LineString', clipPolygonOutline(rings, 0, TILE_EXTENT));
7334
+ if (fillBox) {
7335
+ fillGeometry = project('Polygon', exceedsBox(rings, fillBox) ? clipPolygonToBox(rings, fillBox) : rings);
7336
+ }
7318
7337
  rings = clipPolygon(rings, 0, TILE_EXTENT);
7319
7338
  }
7320
7339
  else {
@@ -7346,6 +7365,7 @@ function addTileFeatures(layers, { project, clipToTile, pixelsPerUnit }, layerFe
7346
7365
  type,
7347
7366
  geometry,
7348
7367
  outline,
7368
+ fillGeometry,
7349
7369
  lineStarts,
7350
7370
  id: featureSrc.id,
7351
7371
  properties: featureSrc.properties,
@@ -7396,19 +7416,59 @@ function getTileProjections(source, job) {
7396
7416
  return tiles.map(({ x, y, offsetX, offsetY }) => {
7397
7417
  const offset = new Point2D(offsetX + (width - covered.width) / 2, offsetY + (height - covered.height) / 2);
7398
7418
  const scale = tileSize / TILE_EXTENT;
7419
+ const project = (_type, rings) => rings.map((ring) => ring.map((point) => {
7420
+ const p = new Point2D(point.x, point.y).scale(scale).translate(offset);
7421
+ return moved ? moved.fromNorthUp(p.x, p.y) : p;
7422
+ }));
7399
7423
  return {
7400
7424
  x,
7401
7425
  y,
7402
7426
  z: zoomLevel,
7403
7427
  clipToTile: true,
7404
7428
  pixelsPerUnit: scale,
7405
- project: (_type, rings) => rings.map((ring) => ring.map((point) => {
7406
- const p = new Point2D(point.x, point.y).scale(scale).translate(offset);
7407
- return moved ? moved.fromNorthUp(p.x, p.y) : p;
7408
- })),
7429
+ fillBox: pixelAlignedBox(project),
7430
+ project,
7409
7431
  };
7410
7432
  });
7411
7433
  }
7434
+ /**
7435
+ * The tile, widened to whole pixels on the screen, in tile units, or undefined where that is
7436
+ * the tile itself, or where the tile is turned on the screen.
7437
+ *
7438
+ * Opaque fills are clipped to this box instead of the tile. Where a polygon goes on in the
7439
+ * next tile, a fill clipped to the tile ends within a pixel: it covers part of the pixel,
7440
+ * and so does every other fill ending there, e.g. a land polygon over a sea polygon, both
7441
+ * covering the whole tile. Each is antialiased, so what is below shows through a little, and
7442
+ * the tile border shows as a faint line. MapLibre fills the inside of a polygon without
7443
+ * antialiasing, so it shows none. Ending on a pixel border, a fill covers each pixel entirely
7444
+ * or not at all; the next tile, drawn later, covers the pixels the two tiles share.
7445
+ */
7446
+ function pixelAlignedBox(project) {
7447
+ const [corners] = project('LineString', [
7448
+ [
7449
+ { x: 0, y: 0 },
7450
+ { x: TILE_EXTENT, y: 0 },
7451
+ { x: 0, y: TILE_EXTENT },
7452
+ ],
7453
+ ]);
7454
+ const [origin, right, down] = corners;
7455
+ // The tile's x axis must be the screen's, and its y axis the screen's.
7456
+ const epsilon = 1e-6;
7457
+ if (Math.abs(right.y - origin.y) > epsilon || Math.abs(down.x - origin.x) > epsilon) {
7458
+ return undefined;
7459
+ }
7460
+ const toTile = (screen, start, end) => ((screen - start) / (end - start)) * TILE_EXTENT;
7461
+ const widen = (start, end) => {
7462
+ const a = toTile(Math.floor(Math.min(start, end)), start, end);
7463
+ const b = toTile(Math.ceil(Math.max(start, end)), start, end);
7464
+ return [Math.min(a, b), Math.max(a, b)];
7465
+ };
7466
+ const [minX, maxX] = widen(origin.x, right.x);
7467
+ const [minY, maxY] = widen(origin.y, down.y);
7468
+ if (minX === 0 && minY === 0 && maxX === TILE_EXTENT && maxY === TILE_EXTENT)
7469
+ return undefined;
7470
+ return [minX, minY, maxX, maxY];
7471
+ }
7412
7472
 
7413
7473
  //#region src/reference/v8.json
7414
7474
  var v8_default = {
@@ -20274,7 +20334,13 @@ class SVGRenderer {
20274
20334
  currentFillKey = translucent ? undefined : key;
20275
20335
  }
20276
20336
  const group = fillGroups[fillGroups.length - 1];
20277
- for (const ring of rings)
20337
+ // An opaque color fills a polygon clipped to its tile a little beyond the tile, so
20338
+ // that it overlaps its continuation in the next tile without a seam. What is drawn
20339
+ // twice there must not show: not with a translucent fill, nor with a pattern.
20340
+ const fillRings = feature.fillGeometry && !translucent && !patternPaint
20341
+ ? feature.fillGeometry.map((ring) => ring.map((p) => roundXY(p.x, p.y)))
20342
+ : rings;
20343
+ for (const ring of fillRings)
20278
20344
  group.segments.push(ring);
20279
20345
  }
20280
20346
  // fill-antialias outline, in fill-outline-color or else the fill color. For an
@@ -21711,9 +21777,13 @@ class CanvasRenderer {
21711
21777
  });
21712
21778
  }
21713
21779
  else if (color.alpha > 0) {
21780
+ // An opaque color fills a polygon clipped to its tile a little beyond the tile, so
21781
+ // that it overlaps its continuation in the next tile without a seam. What is drawn
21782
+ // twice there must not show: not with a translucent fill, nor with a pattern.
21783
+ const geometry = (translucent ? undefined : feature.fillGeometry) ?? feature.geometry;
21714
21784
  this.#paint(this.ctx, style.translate, style.opacity, (ctx) => {
21715
21785
  ctx.fillStyle = color.hex;
21716
- this.#trace(ctx, toSegments(feature.geometry), true);
21786
+ this.#trace(ctx, toSegments(geometry), true);
21717
21787
  ctx.fill();
21718
21788
  });
21719
21789
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versatiles/png-renderer",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Renders MapLibre styles as PNG (and SVG) in Node.js.",
5
5
  "keywords": [
6
6
  "png",