@massif-maps/style-tools 6.1.0-rc.3 → 6.1.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.
|
@@ -1844,6 +1844,16 @@
|
|
|
1844
1844
|
"live": false,
|
|
1845
1845
|
"baked": false
|
|
1846
1846
|
},
|
|
1847
|
+
{
|
|
1848
|
+
"cartocss": "shield-z-elevate",
|
|
1849
|
+
"mapnik": "z-elevate",
|
|
1850
|
+
"symbolizer": "shield",
|
|
1851
|
+
"kind": "bool",
|
|
1852
|
+
"type": "BoolProperty",
|
|
1853
|
+
"default": "false",
|
|
1854
|
+
"live": false,
|
|
1855
|
+
"baked": false
|
|
1856
|
+
},
|
|
1847
1857
|
{
|
|
1848
1858
|
"cartocss": "text-allow-overlap",
|
|
1849
1859
|
"mapnik": "allow-overlap",
|
|
@@ -2483,6 +2493,16 @@
|
|
|
2483
2493
|
"default": "0.0",
|
|
2484
2494
|
"live": false,
|
|
2485
2495
|
"baked": false
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
"cartocss": "text-z-elevate",
|
|
2499
|
+
"mapnik": "z-elevate",
|
|
2500
|
+
"symbolizer": "text",
|
|
2501
|
+
"kind": "bool",
|
|
2502
|
+
"type": "BoolProperty",
|
|
2503
|
+
"default": "false",
|
|
2504
|
+
"live": false,
|
|
2505
|
+
"baked": false
|
|
2486
2506
|
}
|
|
2487
2507
|
]
|
|
2488
2508
|
}
|
package/dist/mapbox2css/index.js
CHANGED
|
@@ -238,6 +238,8 @@ export function convert(style, table, options = {}) {
|
|
|
238
238
|
const lights = style.lights;
|
|
239
239
|
// Set while emitting, so the parameter is only declared when there is a building to gate.
|
|
240
240
|
let usesBuildings = false;
|
|
241
|
+
// The project's `constants`: zoom ranges a child project may move, see `massif:minzoom-const`.
|
|
242
|
+
const constants = new Map();
|
|
241
243
|
// The brightness of the style's OWN default preset, which the emissive fold measures against.
|
|
242
244
|
// sceneBrightness is an ambient-only proxy, so its absolute value is not a light level - but
|
|
243
245
|
// the RATIO between two presets of the same style is meaningful, and that is all this needs:
|
|
@@ -391,9 +393,13 @@ export function convert(style, table, options = {}) {
|
|
|
391
393
|
.flatMap((ordered) => splitLayer(ordered, coverage))
|
|
392
394
|
.flatMap((ordered) => splitDashByZoom(ordered, coverage))
|
|
393
395
|
.map(narrowLayer);
|
|
396
|
+
// `massif:attachment`: layers drawing into ONE attachment, so a child project's rule on it
|
|
397
|
+
// merges with whichever of them matches instead of drawing the feature a second time.
|
|
398
|
+
const sharedAttachment = layer.metadata?.['massif:attachment'];
|
|
394
399
|
variants.forEach((variant, branch) => {
|
|
395
400
|
const suffix = variants.length > 1 ? `_b${branch + 1}` : '';
|
|
396
|
-
|
|
401
|
+
const attachment = typeof sharedAttachment === 'string' ? attachmentName(sharedAttachment) : `${attachmentName(layer.id)}${suffix}`;
|
|
402
|
+
emitLayer(variant, attachment, target, symbolizer, index);
|
|
397
403
|
});
|
|
398
404
|
}
|
|
399
405
|
});
|
|
@@ -589,6 +595,12 @@ export function convert(style, table, options = {}) {
|
|
|
589
595
|
return declaration;
|
|
590
596
|
});
|
|
591
597
|
}
|
|
598
|
+
// On every attachment a sort key split the layer into, so a crossing between two of them
|
|
599
|
+
// is drawn once too - the renderer groups consecutive styles by this name.
|
|
600
|
+
const drawOnce = layer.metadata?.['massif:draw-once'];
|
|
601
|
+
if (declarations.length > 0 && drawOnce !== undefined && drawOnce !== '') {
|
|
602
|
+
declarations = [...declarations, `draw-once: ${translateExpression(drawOnce)};`];
|
|
603
|
+
}
|
|
592
604
|
if (declarations.length === 0)
|
|
593
605
|
return;
|
|
594
606
|
let selector;
|
|
@@ -610,9 +622,19 @@ export function convert(style, table, options = {}) {
|
|
|
610
622
|
const minDefault = typeof minParam === 'string' ? Number(parameters.get(minParam)?.default) : NaN;
|
|
611
623
|
const hasParam = typeof minParam === 'string' && zoomOffsetLevels() === 0;
|
|
612
624
|
const paramStart = hasParam && typeof layer.minzoom === 'number' && Math.floor(layer.minzoom) === minDefault;
|
|
625
|
+
// `massif:minzoom-const` / `massif:maxzoom-const`: the zoom range is a project constant, resolved
|
|
626
|
+
// when the style compiles - a child project overrides it in its `constants`, at no runtime cost.
|
|
627
|
+
const minConst = layer.metadata?.['massif:minzoom-const'];
|
|
628
|
+
const maxConst = layer.metadata?.['massif:maxzoom-const'];
|
|
629
|
+
if (typeof minConst === 'string' && typeof layer.minzoom === 'number')
|
|
630
|
+
constants.set(minConst, Math.floor(layer.minzoom) + zoomOffsetLevels());
|
|
631
|
+
if (typeof maxConst === 'string' && typeof layer.maxzoom === 'number')
|
|
632
|
+
constants.set(maxConst, Math.ceil(layer.maxzoom) + zoomOffsetLevels());
|
|
613
633
|
const predicates = [
|
|
614
634
|
...(hasParam ? [`[zoom >= 'param::${minParam}']`] : []),
|
|
615
|
-
...
|
|
635
|
+
...(typeof minConst === 'string' ? [`[zoom >= $${minConst}]`] : []),
|
|
636
|
+
...(typeof maxConst === 'string' ? [`[zoom < $${maxConst}]`] : []),
|
|
637
|
+
...zoomPredicates(paramStart || typeof minConst === 'string' ? undefined : layer.minzoom, typeof maxConst === 'string' ? undefined : layer.maxzoom),
|
|
616
638
|
...(buildings ? [buildings] : []),
|
|
617
639
|
...translateFilter(filter),
|
|
618
640
|
].map((p) => (p.startsWith('when(') ? ` ${p}` : p));
|
|
@@ -654,6 +676,8 @@ export function convert(style, table, options = {}) {
|
|
|
654
676
|
if ((options.liveLight || lights !== undefined) && !mapBlock.some((d) => d.startsWith('colors-prelit:'))) {
|
|
655
677
|
mapBlock.push('colors-prelit: 1;');
|
|
656
678
|
}
|
|
679
|
+
// The SDK shifts every zoom by log2(app / this), so the style holds whatever TileDrawSize the app picks.
|
|
680
|
+
mapBlock.push(`tile-draw-size: ${options.tileDrawSize ?? 256};`);
|
|
657
681
|
// The MEDIAN index, not the first. `road` has 82 layers spanning indices 3 to 130 in Mapbox
|
|
658
682
|
// Standard, and its FIRST is one early tunnel layer - ordering by that sank all 82 beneath
|
|
659
683
|
// landuse, so the landuse polygons painted over every road. The median puts a source-layer
|
|
@@ -739,11 +763,52 @@ export function convert(style, table, options = {}) {
|
|
|
739
763
|
if (style.name)
|
|
740
764
|
header.push(`/* Source style: ${style.name} */`);
|
|
741
765
|
const render = (block) => `${block.selector} {\n${block.declarations.map((d) => ` ${d}`).join('\n')}\n}`;
|
|
766
|
+
// `massif:template`: every property all layers of a template state goes into `%name`, at its most
|
|
767
|
+
// common value; a rule keeps what differs. A child project extends it to draw a rule of its own.
|
|
768
|
+
const templateOf = new Map();
|
|
769
|
+
for (const layer of style.layers ?? []) {
|
|
770
|
+
const name = layer.metadata?.['massif:template'];
|
|
771
|
+
if (typeof name === 'string')
|
|
772
|
+
templateOf.set(layer.id, attachmentName(name));
|
|
773
|
+
}
|
|
774
|
+
const templateBlocks = new Map();
|
|
775
|
+
for (const block of ruleBlocks) {
|
|
776
|
+
const name = templateOf.get(block.owner);
|
|
777
|
+
if (name)
|
|
778
|
+
(templateBlocks.get(name) ?? templateBlocks.set(name, []).get(name)).push(block);
|
|
779
|
+
}
|
|
780
|
+
const renderedRules = [];
|
|
781
|
+
const emittedTemplates = new Set();
|
|
782
|
+
for (const block of ruleBlocks) {
|
|
783
|
+
const name = templateOf.get(block.owner);
|
|
784
|
+
const group = name ? templateBlocks.get(name) : [];
|
|
785
|
+
if (!name || group.length < 2) {
|
|
786
|
+
renderedRules.push(render(block));
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const propertyOf = (d) => d.slice(0, d.indexOf(':'));
|
|
790
|
+
const common = group[0].declarations.map(propertyOf)
|
|
791
|
+
.filter((p) => group.every((other) => other.declarations.some((d) => propertyOf(d) === p)))
|
|
792
|
+
.map((p) => {
|
|
793
|
+
const counts = new Map();
|
|
794
|
+
for (const other of group) {
|
|
795
|
+
const d = other.declarations.find((x) => propertyOf(x) === p);
|
|
796
|
+
counts.set(d, (counts.get(d) ?? 0) + 1);
|
|
797
|
+
}
|
|
798
|
+
return [...counts].sort((a, b) => b[1] - a[1])[0][0];
|
|
799
|
+
});
|
|
800
|
+
if (!emittedTemplates.has(name)) {
|
|
801
|
+
emittedTemplates.add(name);
|
|
802
|
+
renderedRules.push(render({ selector: `%${name}`, declarations: common }));
|
|
803
|
+
}
|
|
804
|
+
renderedRules.push(render({ selector: block.selector,
|
|
805
|
+
declarations: [...block.declarations.filter((d) => !common.includes(d)), `@extend %${name};`] }));
|
|
806
|
+
}
|
|
742
807
|
const mss = [
|
|
743
808
|
...header,
|
|
744
809
|
'',
|
|
745
810
|
...(mapRule.declarations.length > 0 ? [render(mapRule), ''] : []),
|
|
746
|
-
...
|
|
811
|
+
...renderedRules,
|
|
747
812
|
'',
|
|
748
813
|
].join('\n');
|
|
749
814
|
const variables = palette.length > 0 ? [...paletteHeader(style.name), '', ...palette, ''].join('\n') : null;
|
|
@@ -762,6 +827,7 @@ export function convert(style, table, options = {}) {
|
|
|
762
827
|
...(options.styleParams.size > 0
|
|
763
828
|
? { styleparameters: Object.fromEntries([...options.styleParams].sort()) }
|
|
764
829
|
: {}),
|
|
830
|
+
...(constants.size > 0 ? { constants: Object.fromEntries([...constants].sort()) } : {}),
|
|
765
831
|
}, null, 2) + '\n';
|
|
766
832
|
// Only what DIFFERS from the shared table: a preset project extends project.json, and
|
|
767
833
|
// `extends` merges styleparameters, so an entry it does not restate is inherited.
|
|
@@ -1358,6 +1424,15 @@ function layerDeclarations(layer, symbolizer, allowed, coverage, options, layerI
|
|
|
1358
1424
|
coverage.emit('text-allow-overlap');
|
|
1359
1425
|
continue;
|
|
1360
1426
|
}
|
|
1427
|
+
// On the roof of the 3D building the anchor stands in; a shield takes it through the rename below.
|
|
1428
|
+
if (name === 'symbol-z-elevate') {
|
|
1429
|
+
const translated = tryTranslate(value, name, layer.id, coverage);
|
|
1430
|
+
if (translated === null)
|
|
1431
|
+
continue;
|
|
1432
|
+
out.push(`text-z-elevate: ${translated};`);
|
|
1433
|
+
coverage.emit('text-z-elevate');
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1361
1436
|
// MapBox places the LOWEST sort key first; CartoCSS's culler takes the highest priority.
|
|
1362
1437
|
if (name === 'symbol-sort-key')
|
|
1363
1438
|
continue; // folded into the layer's priority below
|
|
@@ -1446,14 +1521,15 @@ function layerDeclarations(layer, symbolizer, allowed, coverage, options, layerI
|
|
|
1446
1521
|
coverage.drop(name, 'no literal dash pattern to take', layer.id);
|
|
1447
1522
|
continue;
|
|
1448
1523
|
}
|
|
1449
|
-
const
|
|
1524
|
+
const zoom = dash.zoom;
|
|
1525
|
+
const pattern = (layer.dashZoom === undefined ? null : dashPatternAt(value, layer.dashZoom)) ?? dash.pattern;
|
|
1450
1526
|
// `[1, 0]` is MapBox's spelling of SOLID. Scaled by a line width it became a 430 px
|
|
1451
1527
|
// "dash", which is a 430 px bitmap rasterized to draw an unbroken line.
|
|
1452
1528
|
if (!pattern.some((v, i) => i % 2 === 1 && v > 0)) {
|
|
1453
1529
|
coverage.drop(name, 'the pattern has no gap, so the line is solid', layer.id);
|
|
1454
1530
|
continue;
|
|
1455
1531
|
}
|
|
1456
|
-
if (pattern !== value) {
|
|
1532
|
+
if (pattern !== value && layer.dashZoom === undefined) {
|
|
1457
1533
|
// MapTiler ramps its path dashes over zoom and CartoCSS takes ONE pattern. The
|
|
1458
1534
|
// base (the widest band, and every stop below the first) is what is on screen at
|
|
1459
1535
|
// nearly every zoom; taking nothing left every footway drawn solid.
|
|
@@ -1472,7 +1548,7 @@ function layerDeclarations(layer, symbolizer, allowed, coverage, options, layerI
|
|
|
1472
1548
|
coverage.approximate(`line-dasharray scaled by ${round(scale)}, a zoom-driven ` +
|
|
1473
1549
|
'line-width read at one zoom: CartoCSS takes one dash pattern, not a ramp');
|
|
1474
1550
|
}
|
|
1475
|
-
out.push(`line-dasharray: ${pattern.map((v) =>
|
|
1551
|
+
out.push(`line-dasharray: ${pixelDashes(pattern.map((v) => v * scale)).map(round).join(',')};`);
|
|
1476
1552
|
coverage.emit('line-dasharray');
|
|
1477
1553
|
continue;
|
|
1478
1554
|
}
|
|
@@ -2003,50 +2079,105 @@ function dashPattern(value) {
|
|
|
2003
2079
|
function splitDashByZoom(layer, coverage) {
|
|
2004
2080
|
const dash = layer.paint?.['line-dasharray'];
|
|
2005
2081
|
const width = dashWidth(layer.paint?.['line-width']);
|
|
2006
|
-
if (dash === undefined || layer.dashZoom !== undefined ||
|
|
2007
|
-
return [layer];
|
|
2008
|
-
const pattern = dashPattern(dash);
|
|
2009
|
-
// A dash the style RAMPS states the zoom its pattern begins at, and reading the width there is
|
|
2010
|
-
// already the targeted answer - Standard's treads depend on it. Banding is for the plain
|
|
2011
|
-
// literal dash, which has no zoom of its own to be read at.
|
|
2012
|
-
if (pattern === null || pattern.zoom !== null)
|
|
2082
|
+
if (dash === undefined || layer.dashZoom !== undefined || dashPattern(dash) === null)
|
|
2013
2083
|
return [layer];
|
|
2084
|
+
const zmin = layer.minzoom ?? 0;
|
|
2085
|
+
const zmax = Math.min(layer.maxzoom ?? 24, 24);
|
|
2086
|
+
// A dash the style STEPS over zoom is a rule per step: one pattern for all of them drew Standard's
|
|
2087
|
+
// sleepers 0.3 px every 3 px at z20, a grey band, and a ferry dashed below the zoom its dash starts.
|
|
2088
|
+
const cuts = new Set(dashSteps(dash).filter((z) => z > zmin && z < zmax));
|
|
2014
2089
|
// From the first stop that is actually DRAWN, not the first stop: a ramp starting at width 0 -
|
|
2015
2090
|
// Liberty's rail hatchings start (14.5, 0) - has nothing to keep the dash in proportion to
|
|
2016
2091
|
// below it. And no further than the last stop, above which the width is flat and a band would
|
|
2017
2092
|
// read the same number twice.
|
|
2018
|
-
const stops = rampStops(width);
|
|
2019
|
-
const lo = Math.floor(Math.max(
|
|
2020
|
-
const hi = Math.ceil(Math.min(
|
|
2093
|
+
const stops = Array.isArray(width) ? rampStops(width) : [];
|
|
2094
|
+
const lo = Math.floor(Math.max(zmin, stops.find(([, w]) => w > 0)?.[0] ?? zmin));
|
|
2095
|
+
const hi = Math.ceil(Math.min(zmax, stops[stops.length - 1]?.[0] ?? zmax));
|
|
2021
2096
|
const wLo = rampAt(width, lo);
|
|
2022
2097
|
const wHi = rampAt(width, hi);
|
|
2023
|
-
if (hi - lo
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2098
|
+
if (stops.length && hi - lo >= 2 && wLo && wHi && wLo > 0 && wHi > 0) {
|
|
2099
|
+
// Cut where the width DOUBLES, rounding up: a band spanning a 2x range is at worst sqrt(2) out
|
|
2100
|
+
// in the middle, which is the error this is willing to keep.
|
|
2101
|
+
const bandCount = Math.min(MAX_DASH_BANDS, hi - lo, Math.ceil(Math.log2(Math.max(wHi / wLo, wLo / wHi))));
|
|
2102
|
+
const step = Math.max(1, Math.round((hi - lo) / bandCount));
|
|
2103
|
+
for (let at = lo + step; bandCount >= 2 && at < hi; at += step)
|
|
2104
|
+
cuts.add(at);
|
|
2105
|
+
}
|
|
2106
|
+
if (!cuts.size)
|
|
2030
2107
|
return [layer];
|
|
2031
|
-
const
|
|
2108
|
+
const edges = [zmin, ...[...cuts].sort((a, b) => a - b), zmax];
|
|
2032
2109
|
const bands = [];
|
|
2033
|
-
for (let
|
|
2034
|
-
const
|
|
2110
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
2111
|
+
const from = edges[i], to = edges[i + 1];
|
|
2112
|
+
// The width is read in the middle of the band up to the ramp's last stop, past a stretch where
|
|
2113
|
+
// it is 0 if it starts with one; the pattern anywhere inside, a band never straddles a step.
|
|
2114
|
+
const b = Math.min(to, hi);
|
|
2115
|
+
let dashZoom = from < b ? (from + b) / 2 : from;
|
|
2116
|
+
if (stops.length && !((rampAt(width, dashZoom) ?? 0) > 0) && Math.max(from, lo) < b) {
|
|
2117
|
+
dashZoom = (Math.max(from, lo) + b) / 2;
|
|
2118
|
+
}
|
|
2035
2119
|
bands.push({
|
|
2036
2120
|
...layer,
|
|
2037
2121
|
// The ends keep whatever the layer stated, so banding never narrows what it draws.
|
|
2038
|
-
minzoom:
|
|
2039
|
-
maxzoom:
|
|
2040
|
-
dashZoom
|
|
2122
|
+
minzoom: i === 0 ? layer.minzoom : from,
|
|
2123
|
+
maxzoom: i + 2 === edges.length ? layer.maxzoom : to,
|
|
2124
|
+
dashZoom,
|
|
2041
2125
|
});
|
|
2042
2126
|
}
|
|
2127
|
+
// Two solid bands in a row are one: a width cut means nothing where no dash is drawn.
|
|
2128
|
+
const solid = (band) => !(dashPatternAt(dash, band.dashZoom) ?? [0, 1]).some((v, i) => i % 2 === 1 && v > 0);
|
|
2129
|
+
for (let i = bands.length - 1; i > 0; i--) {
|
|
2130
|
+
if (solid(bands[i]) && solid(bands[i - 1])) {
|
|
2131
|
+
bands[i - 1] = { ...bands[i - 1], maxzoom: bands[i].maxzoom };
|
|
2132
|
+
bands.splice(i, 1);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2043
2135
|
if (bands.length < 2)
|
|
2044
2136
|
return [layer];
|
|
2045
2137
|
coverage.approximate(`line-dasharray on "${layer.id}" split into ${bands.length} zoom bands: a `
|
|
2046
2138
|
+ 'dash is a multiple of the line width and CartoCSS takes one pattern per rule, so a '
|
|
2047
|
-
+ 'ramped width needs a rule per band
|
|
2139
|
+
+ 'ramped width or a stepped dash needs a rule per band');
|
|
2048
2140
|
return bands;
|
|
2049
2141
|
}
|
|
2142
|
+
/**
|
|
2143
|
+
* No dash under a pixel: drawn through a bitmap, a 0.5 px sleeper lights a pixel only where it happens to land,
|
|
2144
|
+
* and Standard's rail at z19 came out as blobs. Each dash is widened to 1 px, its gap paying for it, so the
|
|
2145
|
+
* period is kept.
|
|
2146
|
+
*/
|
|
2147
|
+
function pixelDashes(pattern) {
|
|
2148
|
+
const out = [...pattern];
|
|
2149
|
+
for (let i = 0; i + 1 < out.length; i += 2) {
|
|
2150
|
+
const grow = Math.max(0, 1 - out[i]);
|
|
2151
|
+
const take = Math.min(grow, Math.max(0, out[i + 1] - 1));
|
|
2152
|
+
out[i] += take;
|
|
2153
|
+
out[i + 1] -= take;
|
|
2154
|
+
}
|
|
2155
|
+
return out;
|
|
2156
|
+
}
|
|
2157
|
+
/** The zooms a `step` dash switches pattern at. */
|
|
2158
|
+
function dashSteps(value) {
|
|
2159
|
+
if (!Array.isArray(value) || value[0] !== 'step' || !Array.isArray(value[1]) || value[1][0] !== 'zoom')
|
|
2160
|
+
return [];
|
|
2161
|
+
const out = [];
|
|
2162
|
+
for (let i = 3; i + 1 < value.length; i += 2) {
|
|
2163
|
+
if (typeof value[i] === 'number')
|
|
2164
|
+
out.push(value[i]);
|
|
2165
|
+
}
|
|
2166
|
+
return out;
|
|
2167
|
+
}
|
|
2168
|
+
/** The pattern a `step` dash draws at `zoom`, or null when it is not one. */
|
|
2169
|
+
function dashPatternAt(value, zoom) {
|
|
2170
|
+
if (!dashSteps(value).length)
|
|
2171
|
+
return null;
|
|
2172
|
+
const list = (node) => (Array.isArray(node) && node[0] === 'literal' ? node[1] : node);
|
|
2173
|
+
let at = list(value[2]);
|
|
2174
|
+
for (let i = 3; i + 1 < value.length; i += 2) {
|
|
2175
|
+
if (value[i] > zoom)
|
|
2176
|
+
break;
|
|
2177
|
+
at = list(value[i + 1]);
|
|
2178
|
+
}
|
|
2179
|
+
return Array.isArray(at) && at.every((v) => typeof v === 'number') ? at : null;
|
|
2180
|
+
}
|
|
2050
2181
|
/**
|
|
2051
2182
|
* The width a dash is scaled by. A width chosen per config value (`match` on `["config", ...]`) is
|
|
2052
2183
|
* read at its fallback: its numbers averaged over every branch drew an e-ink track's 5 px dash 50 px.
|
|
@@ -2480,7 +2611,9 @@ function aliasSprites(names, file, prefix, options) {
|
|
|
2480
2611
|
}
|
|
2481
2612
|
function iconExpression(image, layer, coverage, options) {
|
|
2482
2613
|
const sprites = options.sprites;
|
|
2483
|
-
|
|
2614
|
+
// Layers sharing a template share its tables, so the icon lookup is a declaration they have in common.
|
|
2615
|
+
const template = layer.metadata?.['massif:template'];
|
|
2616
|
+
const slug = safeParamName(typeof template === 'string' ? template : layer.id);
|
|
2484
2617
|
let sample = null;
|
|
2485
2618
|
let tableIndex = 0;
|
|
2486
2619
|
// A recolourable icon is cut in two - the glyph as a field, the disc as the shield's plate - so
|