@chrtco/sdk 0.10.43 → 0.10.44

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/CHANGELOG.md CHANGED
@@ -14,6 +14,18 @@ All notable changes to `@chrtco/sdk` will be documented in this file.
14
14
  source themselves, such as the tile-worker's TileJSON.
15
15
 
16
16
  ### Fixed
17
+ - **A palette switch held the map's tile answers behind its own SVG decodes.**
18
+ `rethemeSymbolImages` decoded every symbol as its own SVG `<img>`, and each
19
+ is a whole browser document whose many small main-thread tasks queued ahead
20
+ of MapLibre's worker messages. A tile reload started by the switch's
21
+ data-driven paint (two-shade soundings, light circles) landed only after
22
+ them, and until it did those kept their previous palette's colours. Symbols
23
+ are now decoded up to 32 to a document, and each is
24
+ drawn from its sheet shifted onto a canvas its own size. The pixels are
25
+ byte-identical to the per-symbol path for every family and palette. A
26
+ symbol that cannot safely share a document (a prolog, styles, a reference
27
+ to an id it does not define) still gets its own, as does every symbol of a
28
+ sheet that fails to decode. `loadSymbolImageSpecs` is unchanged.
17
29
  - **`offline.download()` left a second copy of every pack in a browser's HTTP
18
30
  cache.** In a browser, an Electron renderer or Electron `net.fetch`, the
19
31
  bundle GET was stored in the profile's disk cache as well as written through
@@ -1176,17 +1176,110 @@ async function decode(spec) {
1176
1176
  }
1177
1177
  }
1178
1178
  var scratch = null;
1179
- function rasterise(img, spec) {
1179
+ function scratchContext() {
1180
1180
  if (!scratch) {
1181
- const canvas2 = document.createElement("canvas");
1182
- scratch = canvas2.getContext("2d", { willReadFrequently: true });
1181
+ const canvas = document.createElement("canvas");
1182
+ scratch = canvas.getContext("2d", { willReadFrequently: true });
1183
1183
  if (!scratch) throw new Error("symbol-images: no 2d canvas context");
1184
1184
  }
1185
- const canvas = scratch.canvas;
1185
+ return scratch;
1186
+ }
1187
+ function rasterise(src, spec) {
1188
+ const ctx = scratchContext();
1189
+ const canvas = ctx.canvas;
1186
1190
  canvas.width = spec.width;
1187
1191
  canvas.height = spec.height;
1188
- scratch.drawImage(img, 0, 0, spec.width, spec.height);
1189
- return scratch.getImageData(0, 0, spec.width, spec.height);
1192
+ ctx.drawImage(src.img, 0, -src.y, src.width, src.height);
1193
+ return ctx.getImageData(0, 0, spec.width, spec.height);
1194
+ }
1195
+ var SHEET_MAX_SYMBOLS = 32;
1196
+ var SHEET_MAX_HEIGHT = 4096;
1197
+ var ROOT = /^\s*<svg\b([^>]*)>[\s\S]*<\/svg>\s*$/;
1198
+ var NOT_SHAREABLE = /<\?xml|<!DOCTYPE|<style\b|<script\b|<foreignObject\b|<svg\b[\s\S]*<svg\b/i;
1199
+ function rootSize(attrs) {
1200
+ const w = /\swidth="(\d+(?:\.\d+)?)"/.exec(attrs);
1201
+ const h = /\sheight="(\d+(?:\.\d+)?)"/.exec(attrs);
1202
+ return w && h ? { w: Number(w[1]), h: Number(h[1]) } : null;
1203
+ }
1204
+ function shareable(spec) {
1205
+ if (!(spec.width > 0 && spec.height > 0) || spec.height > SHEET_MAX_HEIGHT) return null;
1206
+ const root = ROOT.exec(spec.svg);
1207
+ const own = root ? rootSize(root[1]) : null;
1208
+ if (!own || NOT_SHAREABLE.test(spec.svg)) return null;
1209
+ const ids = /* @__PURE__ */ new Set();
1210
+ for (const m of spec.svg.matchAll(/\sid="([^"]*)"/g)) {
1211
+ if (ids.has(m[1])) return null;
1212
+ ids.add(m[1]);
1213
+ }
1214
+ for (const m of spec.svg.matchAll(/url\(\s*['"]?#([^'")\s]*)|href="#([^"]*)"/g)) {
1215
+ if (!ids.has(m[1] ?? m[2])) return null;
1216
+ }
1217
+ return { spec, own, ids };
1218
+ }
1219
+ function sheetOf(run) {
1220
+ let y = 0;
1221
+ let width = 0;
1222
+ const parts = [];
1223
+ const slots = [];
1224
+ for (const { spec, own } of run) {
1225
+ parts.push(
1226
+ `<svg x="0" y="${y}" width="${spec.width}" height="${spec.height}" viewBox="0 0 ${own.w} ${own.h}" preserveAspectRatio="none">${spec.svg}</svg>`
1227
+ );
1228
+ slots.push({ spec, y });
1229
+ y += spec.height;
1230
+ width = Math.max(width, spec.width);
1231
+ }
1232
+ return {
1233
+ svg: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${y}">${parts.join("")}</svg>`,
1234
+ width,
1235
+ height: y,
1236
+ slots
1237
+ };
1238
+ }
1239
+ function planRasterUnits(specs) {
1240
+ const units = [];
1241
+ let run = [];
1242
+ let runIds = /* @__PURE__ */ new Set();
1243
+ let runHeight = 0;
1244
+ const flush = () => {
1245
+ if (run.length === 1) units.push({ one: run[0].spec });
1246
+ else if (run.length > 1) units.push({ sheet: sheetOf(run) });
1247
+ run = [];
1248
+ runIds = /* @__PURE__ */ new Set();
1249
+ runHeight = 0;
1250
+ };
1251
+ for (const spec of specs) {
1252
+ const s = shareable(spec);
1253
+ if (!s) {
1254
+ flush();
1255
+ units.push({ one: spec });
1256
+ continue;
1257
+ }
1258
+ if (run.length >= SHEET_MAX_SYMBOLS || runHeight + spec.height > SHEET_MAX_HEIGHT || [...s.ids].some((id) => runIds.has(id))) {
1259
+ flush();
1260
+ }
1261
+ run.push(s);
1262
+ runHeight += spec.height;
1263
+ for (const id of s.ids) runIds.add(id);
1264
+ }
1265
+ flush();
1266
+ return units;
1267
+ }
1268
+ async function sourcesFor(unit, decoded) {
1269
+ const img = await decoded;
1270
+ if ("one" in unit) {
1271
+ return [img ? { img, y: 0, width: unit.one.width, height: unit.one.height } : null];
1272
+ }
1273
+ const { sheet } = unit;
1274
+ if (img) return sheet.slots.map(({ y }) => ({ img, y, width: sheet.width, height: sheet.height }));
1275
+ const singles = sheet.slots.map(({ spec }) => decode(spec));
1276
+ const out = [];
1277
+ for (let i = 0; i < singles.length; i++) {
1278
+ const one = await singles[i];
1279
+ const { spec } = sheet.slots[i];
1280
+ out.push(one ? { img: one, y: 0, width: spec.width, height: spec.height } : null);
1281
+ }
1282
+ return out;
1190
1283
  }
1191
1284
  function yieldToBrowser() {
1192
1285
  const scheduler = globalThis.scheduler;
@@ -1221,45 +1314,51 @@ async function rethemeSymbolImages(map, specs, opts = {}) {
1221
1314
  const total = specs.length;
1222
1315
  const result = { updated: 0, added: 0, failed: 0, aborted: false };
1223
1316
  let done = 0;
1224
- const decoded = specs.map((spec) => decode(spec));
1317
+ const units = planRasterUnits(specs);
1318
+ const decoded = units.map((unit) => decode("one" in unit ? unit.one : unit.sheet));
1225
1319
  let sliceStart = performance.now();
1226
1320
  let lastRepaint = sliceStart;
1227
- for (let i = 0; i < total; i++) {
1321
+ outer: for (let u = 0; u < units.length; u++) {
1322
+ const unit = units[u];
1323
+ const unitSpecs = "one" in unit ? [unit.one] : unit.sheet.slots.map((slot) => slot.spec);
1228
1324
  if (opts.signal?.aborted) {
1229
1325
  result.aborted = true;
1230
1326
  break;
1231
1327
  }
1232
- const spec = specs[i];
1233
- const img = await decoded[i];
1234
- if (opts.signal?.aborted) {
1235
- result.aborted = true;
1236
- break;
1237
- }
1238
- if (!img) {
1239
- result.failed++;
1240
- } else {
1241
- const pixels = rasterise(img, spec);
1242
- const have = existingSize(map, spec.id);
1243
- if (have && have.w === spec.width && have.h === spec.height) {
1244
- map.updateImage(spec.id, pixels);
1245
- result.updated++;
1328
+ const sources = await sourcesFor(unit, decoded[u]);
1329
+ for (let i = 0; i < unitSpecs.length; i++) {
1330
+ if (opts.signal?.aborted) {
1331
+ result.aborted = true;
1332
+ break outer;
1333
+ }
1334
+ const spec = unitSpecs[i];
1335
+ const src = sources[i];
1336
+ if (!src) {
1337
+ result.failed++;
1246
1338
  } else {
1247
- if (map.hasImage(spec.id)) map.removeImage(spec.id);
1248
- map.addImage(spec.id, pixels, { sdf: spec.sdf, pixelRatio: spec.pixelRatio });
1249
- result.added++;
1339
+ const px = rasterise(src, spec);
1340
+ const have = existingSize(map, spec.id);
1341
+ if (have && have.w === spec.width && have.h === spec.height) {
1342
+ map.updateImage(spec.id, px);
1343
+ result.updated++;
1344
+ } else {
1345
+ if (map.hasImage(spec.id)) map.removeImage(spec.id);
1346
+ map.addImage(spec.id, px, { sdf: spec.sdf, pixelRatio: spec.pixelRatio });
1347
+ result.added++;
1348
+ }
1349
+ opts.onApplied?.(spec.id);
1250
1350
  }
1251
- opts.onApplied?.(spec.id);
1252
- }
1253
- done++;
1254
- const now = performance.now();
1255
- if (now - sliceStart >= sliceMs) {
1256
- if (now - lastRepaint >= repaintEveryMs) {
1257
- map.triggerRepaint();
1258
- opts.onProgress?.({ done, total });
1259
- lastRepaint = now;
1351
+ done++;
1352
+ const now = performance.now();
1353
+ if (now - sliceStart >= sliceMs) {
1354
+ if (now - lastRepaint >= repaintEveryMs) {
1355
+ map.triggerRepaint();
1356
+ opts.onProgress?.({ done, total });
1357
+ lastRepaint = now;
1358
+ }
1359
+ await yieldToBrowser();
1360
+ sliceStart = performance.now();
1260
1361
  }
1261
- await yieldToBrowser();
1262
- sliceStart = performance.now();
1263
1362
  }
1264
1363
  }
1265
1364
  map.triggerRepaint();