@mapwhit/tilerenderer 0.47.2 → 0.49.0

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.
Files changed (43) hide show
  1. package/build/min/package.json +1 -1
  2. package/package.json +3 -2
  3. package/src/data/array_types.js +169 -0
  4. package/src/data/bucket/symbol_attributes.js +18 -0
  5. package/src/data/bucket/symbol_bucket.js +116 -73
  6. package/src/data/program_configuration.js +18 -10
  7. package/src/geo/transform.js +17 -7
  8. package/src/index.js +1 -1
  9. package/src/render/glyph_atlas.js +3 -6
  10. package/src/render/image_atlas.js +3 -6
  11. package/src/render/image_manager.js +41 -41
  12. package/src/source/rtl_text_plugin.js +1 -0
  13. package/src/source/source_cache.js +1 -1
  14. package/src/source/tile.js +6 -5
  15. package/src/source/worker.js +1 -10
  16. package/src/style/style.js +4 -19
  17. package/src/style/style_layer/symbol_style_layer_properties.js +7 -1
  18. package/src/style-spec/expression/compound_expression.js +30 -16
  19. package/src/style-spec/expression/definitions/assertion.js +52 -5
  20. package/src/style-spec/expression/definitions/coercion.js +13 -0
  21. package/src/style-spec/expression/definitions/comparison.js +193 -0
  22. package/src/style-spec/expression/definitions/formatted.js +123 -0
  23. package/src/style-spec/expression/definitions/index.js +11 -62
  24. package/src/style-spec/expression/definitions/interpolate.js +17 -7
  25. package/src/style-spec/expression/definitions/literal.js +5 -0
  26. package/src/style-spec/expression/parsing_context.js +6 -7
  27. package/src/style-spec/expression/types.js +12 -1
  28. package/src/style-spec/feature_filter/convert.js +197 -0
  29. package/src/style-spec/feature_filter/index.js +5 -2
  30. package/src/style-spec/function/convert.js +78 -100
  31. package/src/style-spec/reference/v8.json +160 -52
  32. package/src/symbol/collision_index.js +0 -1
  33. package/src/symbol/cross_tile_symbol_index.js +12 -7
  34. package/src/symbol/get_anchors.js +11 -22
  35. package/src/symbol/mergelines.js +4 -1
  36. package/src/symbol/placement.js +58 -54
  37. package/src/symbol/quads.js +7 -6
  38. package/src/symbol/shaping.js +185 -40
  39. package/src/symbol/symbol_layout.js +40 -37
  40. package/src/symbol/transform_text.js +12 -1
  41. package/src/ui/map.js +8 -25
  42. package/src/style-spec/expression/definitions/array.js +0 -82
  43. package/src/style-spec/expression/definitions/equals.js +0 -93
@@ -17,7 +17,7 @@ function getAngleWindowSize(shapedText, glyphSize, boxScale) {
17
17
  return shapedText ? (3 / 5) * glyphSize * boxScale : 0;
18
18
  }
19
19
 
20
- function getLabelLength(shapedText, shapedIcon) {
20
+ function getShapedLabelLength(shapedText, shapedIcon) {
21
21
  return Math.max(
22
22
  shapedText ? shapedText.right - shapedText.left : 0,
23
23
  shapedIcon ? shapedIcon.right - shapedIcon.left : 0
@@ -26,7 +26,7 @@ function getLabelLength(shapedText, shapedIcon) {
26
26
 
27
27
  function getCenterAnchor(line, maxAngle, shapedText, shapedIcon, glyphSize, boxScale) {
28
28
  const angleWindowSize = getAngleWindowSize(shapedText, glyphSize, boxScale);
29
- const labelLength = getLabelLength(shapedText, shapedIcon);
29
+ const labelLength = getShapedLabelLength(shapedText, shapedIcon) * boxScale;
30
30
 
31
31
  let prevDistance = 0;
32
32
  const centerDistance = getLineLength(line) / 2;
@@ -45,11 +45,10 @@ function getCenterAnchor(line, maxAngle, shapedText, shapedIcon, glyphSize, boxS
45
45
 
46
46
  const anchor = new Anchor(x, y, b.angleTo(a), i);
47
47
  anchor._round();
48
- if (angleWindowSize && !checkMaxAngle(line, anchor, labelLength, angleWindowSize, maxAngle)) {
49
- return;
48
+ if (!angleWindowSize || checkMaxAngle(line, anchor, labelLength, angleWindowSize, maxAngle)) {
49
+ return anchor;
50
50
  }
51
-
52
- return anchor;
51
+ return;
53
52
  }
54
53
 
55
54
  prevDistance += segmentDistance;
@@ -62,16 +61,16 @@ function getAnchors(line, spacing, maxAngle, shapedText, shapedIcon, glyphSize,
62
61
  // on the line.
63
62
 
64
63
  const angleWindowSize = getAngleWindowSize(shapedText, glyphSize, boxScale);
65
-
66
- const labelLength = getLabelLength(shapedText, shapedIcon);
64
+ const shapedLabelLength = getShapedLabelLength(shapedText, shapedIcon);
65
+ const labelLength = shapedLabelLength * boxScale;
67
66
 
68
67
  // Is the line continued from outside the tile boundary?
69
68
  const isLineContinued = line[0].x === 0 || line[0].x === tileExtent || line[0].y === 0 || line[0].y === tileExtent;
70
69
 
71
70
  // Is the label long, relative to the spacing?
72
71
  // If so, adjust the spacing so there is always a minimum space of `spacing / 4` between label edges.
73
- if (spacing - labelLength * boxScale < spacing / 4) {
74
- spacing = labelLength * boxScale + spacing / 4;
72
+ if (spacing - labelLength < spacing / 4) {
73
+ spacing = labelLength + spacing / 4;
75
74
  }
76
75
 
77
76
  // Offset the first anchor by:
@@ -82,20 +81,10 @@ function getAnchors(line, spacing, maxAngle, shapedText, shapedIcon, glyphSize,
82
81
  const fixedExtraOffset = glyphSize * 2;
83
82
 
84
83
  const offset = !isLineContinued
85
- ? ((labelLength / 2 + fixedExtraOffset) * boxScale * overscaling) % spacing
84
+ ? ((shapedLabelLength / 2 + fixedExtraOffset) * boxScale * overscaling) % spacing
86
85
  : ((spacing / 2) * overscaling) % spacing;
87
86
 
88
- return resample(
89
- line,
90
- offset,
91
- spacing,
92
- angleWindowSize,
93
- maxAngle,
94
- labelLength * boxScale,
95
- isLineContinued,
96
- false,
97
- tileExtent
98
- );
87
+ return resample(line, offset, spacing, angleWindowSize, maxAngle, labelLength, isLineContinued, false, tileExtent);
99
88
  }
100
89
 
101
90
  function resample(
@@ -1,3 +1,5 @@
1
+ const { Formatted } = require('../style-spec/expression/definitions/formatted');
2
+
1
3
  module.exports = function (features) {
2
4
  const leftIndex = new Map();
3
5
  const rightIndex = new Map();
@@ -5,7 +7,8 @@ module.exports = function (features) {
5
7
  let mergedIndex = 0;
6
8
 
7
9
  for (let k = 0; k < features.length; k++) {
8
- const { geometry, text } = features[k];
10
+ const { geometry, text: featureText } = features[k];
11
+ const text = featureText instanceof Formatted ? featureText.toString() : featureText;
9
12
 
10
13
  if (!text) {
11
14
  add(k);
@@ -172,7 +172,12 @@ class Placement {
172
172
 
173
173
  const collisionGroup = this.collisionGroups.get(bucket.sourceID);
174
174
 
175
- for (const symbolInstance of bucket.symbolInstances) {
175
+ if (!bucket.collisionArrays && collisionBoxArray) {
176
+ bucket.deserializeCollisionBoxes(collisionBoxArray);
177
+ }
178
+
179
+ for (let i = 0; i < bucket.symbolInstances.length; i++) {
180
+ const symbolInstance = bucket.symbolInstances.get(i);
176
181
  if (!seenCrossTileIDs[symbolInstance.crossTileID]) {
177
182
  if (holdingForFade) {
178
183
  // Mark all symbols from this tile as "not placed", but don't add to seenCrossTileIDs, because we don't
@@ -192,22 +197,14 @@ class Placement {
192
197
  let textFeatureIndex = 0;
193
198
  let iconFeatureIndex = 0;
194
199
 
195
- if (!symbolInstance.collisionArrays) {
196
- symbolInstance.collisionArrays = bucket.deserializeCollisionBoxes(
197
- collisionBoxArray,
198
- symbolInstance.textBoxStartIndex,
199
- symbolInstance.textBoxEndIndex,
200
- symbolInstance.iconBoxStartIndex,
201
- symbolInstance.iconBoxEndIndex
202
- );
203
- }
200
+ const collisionArrays = bucket.collisionArrays[i];
204
201
 
205
- if (symbolInstance.collisionArrays.textFeatureIndex) {
206
- textFeatureIndex = symbolInstance.collisionArrays.textFeatureIndex;
202
+ if (collisionArrays.textFeatureIndex) {
203
+ textFeatureIndex = collisionArrays.textFeatureIndex;
207
204
  }
208
- if (symbolInstance.collisionArrays.textBox) {
205
+ if (collisionArrays.textBox) {
209
206
  placedGlyphBoxes = this.collisionIndex.placeCollisionBox(
210
- symbolInstance.collisionArrays.textBox,
207
+ collisionArrays.textBox,
211
208
  layout.get('text-allow-overlap'),
212
209
  textPixelRatio,
213
210
  posMatrix,
@@ -216,9 +213,9 @@ class Placement {
216
213
  placeText = placedGlyphBoxes.box.length > 0;
217
214
  offscreen = offscreen && placedGlyphBoxes.offscreen;
218
215
  }
219
- const textCircles = symbolInstance.collisionArrays.textCircles;
216
+ const textCircles = collisionArrays.textCircles;
220
217
  if (textCircles) {
221
- const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.placedTextSymbolIndices[0]);
218
+ const placedSymbol = bucket.text.placedSymbolArray.get(symbolInstance.horizontalPlacedTextSymbolIndex);
222
219
  const fontSize = symbolSize.evaluateSizeForFeature(
223
220
  bucket.textSizeData,
224
221
  partiallyEvaluatedTextSize,
@@ -229,7 +226,6 @@ class Placement {
229
226
  layout.get('text-allow-overlap'),
230
227
  scale,
231
228
  textPixelRatio,
232
- symbolInstance.key,
233
229
  placedSymbol,
234
230
  bucket.lineVertexArray,
235
231
  bucket.glyphOffsetArray,
@@ -248,12 +244,12 @@ class Placement {
248
244
  offscreen = offscreen && placedGlyphCircles.offscreen;
249
245
  }
250
246
 
251
- if (symbolInstance.collisionArrays.iconFeatureIndex) {
252
- iconFeatureIndex = symbolInstance.collisionArrays.iconFeatureIndex;
247
+ if (collisionArrays.iconFeatureIndex) {
248
+ iconFeatureIndex = collisionArrays.iconFeatureIndex;
253
249
  }
254
- if (symbolInstance.collisionArrays.iconBox) {
250
+ if (collisionArrays.iconBox) {
255
251
  placedIconBoxes = this.collisionIndex.placeCollisionBox(
256
- symbolInstance.collisionArrays.iconBox,
252
+ collisionArrays.iconBox,
257
253
  layout.get('icon-allow-overlap'),
258
254
  textPixelRatio,
259
255
  posMatrix,
@@ -398,16 +394,30 @@ class Placement {
398
394
 
399
395
  const layout = bucket.layers[0].layout;
400
396
  const duplicateOpacityState = new JointOpacityState(null, 0, false, false, true);
397
+ const textAllowOverlap = layout.get('text-allow-overlap');
398
+ const iconAllowOverlap = layout.get('icon-allow-overlap');
399
+ // If allow-overlap is true, we can show symbols before placement runs on them
400
+ // But we have to wait for placement if we potentially depend on a paired icon/text
401
+ // with allow-overlap: false.
402
+ // See https://github.com/mapbox/mapbox-gl-js/issues/7032
401
403
  const defaultOpacityState = new JointOpacityState(
402
404
  null,
403
405
  0,
404
- layout.get('text-allow-overlap'),
405
- layout.get('icon-allow-overlap'),
406
+ textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || layout.get('icon-optional')),
407
+ iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || layout.get('text-optional')),
406
408
  true
407
409
  );
408
410
 
411
+ if (
412
+ !bucket.collisionArrays &&
413
+ collisionBoxArray &&
414
+ (bucket.hasCollisionBoxData() || bucket.hasCollisionCircleData())
415
+ ) {
416
+ bucket.deserializeCollisionBoxes(collisionBoxArray);
417
+ }
418
+
409
419
  for (let s = 0; s < bucket.symbolInstances.length; s++) {
410
- const symbolInstance = bucket.symbolInstances[s];
420
+ const symbolInstance = bucket.symbolInstances.get(s);
411
421
  const isDuplicate = seenCrossTileIDs[symbolInstance.crossTileID];
412
422
 
413
423
  let opacityState = this.opacities[symbolInstance.crossTileID];
@@ -432,11 +442,14 @@ class Placement {
432
442
  for (let i = 0; i < opacityEntryCount; i++) {
433
443
  bucket.text.opacityVertexArray.emplaceBack(packedOpacity);
434
444
  }
435
- for (const placedTextSymbolIndex of symbolInstance.placedTextSymbolIndices) {
436
- const placedSymbol = bucket.text.placedSymbolArray.get(placedTextSymbolIndex);
437
- // If this label is completely faded, mark it so that we don't have to calculate
438
- // its position at render time
439
- placedSymbol.hidden = opacityState.text.isHidden();
445
+ // If this label is completely faded, mark it so that we don't have to calculate
446
+ // its position at render time
447
+ bucket.text.placedSymbolArray.get(symbolInstance.horizontalPlacedTextSymbolIndex).hidden =
448
+ opacityState.text.isHidden();
449
+
450
+ if (symbolInstance.verticalPlacedTextSymbolIndex >= 0) {
451
+ bucket.text.placedSymbolArray.get(symbolInstance.verticalPlacedTextSymbolIndex).hidden =
452
+ opacityState.text.isHidden();
440
453
  }
441
454
  }
442
455
 
@@ -445,35 +458,26 @@ class Placement {
445
458
  for (let i = 0; i < symbolInstance.numIconVertices / 4; i++) {
446
459
  bucket.icon.opacityVertexArray.emplaceBack(packedOpacity);
447
460
  }
448
- const placedSymbol = bucket.icon.placedSymbolArray.get(s);
449
- placedSymbol.hidden = opacityState.icon.isHidden();
461
+ bucket.icon.placedSymbolArray.get(s).hidden = opacityState.icon.isHidden();
450
462
  }
451
463
 
452
- if (!symbolInstance.collisionArrays) {
453
- symbolInstance.collisionArrays = bucket.deserializeCollisionBoxes(
454
- collisionBoxArray,
455
- symbolInstance.textBoxStartIndex,
456
- symbolInstance.textBoxEndIndex,
457
- symbolInstance.iconBoxStartIndex,
458
- symbolInstance.iconBoxEndIndex
459
- );
460
- }
461
-
462
- const collisionArrays = symbolInstance.collisionArrays;
463
- if (collisionArrays) {
464
- if (collisionArrays.textBox && bucket.hasCollisionBoxData()) {
465
- updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.text.placed, false);
466
- }
464
+ if (bucket.hasCollisionBoxData() || bucket.hasCollisionCircleData()) {
465
+ const collisionArrays = bucket.collisionArrays[s];
466
+ if (collisionArrays) {
467
+ if (collisionArrays.textBox) {
468
+ updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.text.placed, false);
469
+ }
467
470
 
468
- if (collisionArrays.iconBox && bucket.hasCollisionBoxData()) {
469
- updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.icon.placed, false);
470
- }
471
+ if (collisionArrays.iconBox) {
472
+ updateCollisionVertices(bucket.collisionBox.collisionVertexArray, opacityState.icon.placed, false);
473
+ }
471
474
 
472
- const textCircles = collisionArrays.textCircles;
473
- if (textCircles && bucket.hasCollisionCircleData()) {
474
- for (let k = 0; k < textCircles.length; k += 5) {
475
- const notUsed = isDuplicate || textCircles[k + 4] === 0;
476
- updateCollisionVertices(bucket.collisionCircle.collisionVertexArray, opacityState.text.placed, notUsed);
475
+ const textCircles = collisionArrays.textCircles;
476
+ if (textCircles && bucket.hasCollisionCircleData()) {
477
+ for (let k = 0; k < textCircles.length; k += 5) {
478
+ const notUsed = isDuplicate || textCircles[k + 4] === 0;
479
+ updateCollisionVertices(bucket.collisionCircle.collisionVertexArray, opacityState.text.placed, notUsed);
480
+ }
477
481
  }
478
482
  }
479
483
  }
@@ -105,7 +105,8 @@ function getGlyphQuads(anchor, shaping, layer, alongLine, feature, positions) {
105
105
 
106
106
  for (let k = 0; k < positionedGlyphs.length; k++) {
107
107
  const positionedGlyph = positionedGlyphs[k];
108
- const glyph = positions[positionedGlyph.glyph];
108
+ const glyphPositions = positions[positionedGlyph.fontStack];
109
+ const glyph = glyphPositions?.[positionedGlyph.glyph];
109
110
  if (!glyph) continue;
110
111
 
111
112
  const rect = glyph.rect;
@@ -115,7 +116,7 @@ function getGlyphQuads(anchor, shaping, layer, alongLine, feature, positions) {
115
116
  const glyphPadding = 1.0;
116
117
  const rectBuffer = GLYPH_PBF_BORDER + glyphPadding;
117
118
 
118
- const halfAdvance = glyph.metrics.advance / 2;
119
+ const halfAdvance = (glyph.metrics.advance * positionedGlyph.scale) / 2;
119
120
 
120
121
  const glyphOffset = alongLine ? [positionedGlyph.x + halfAdvance, positionedGlyph.y] : [0, 0];
121
122
 
@@ -123,10 +124,10 @@ function getGlyphQuads(anchor, shaping, layer, alongLine, feature, positions) {
123
124
  ? [0, 0]
124
125
  : [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1]];
125
126
 
126
- const x1 = glyph.metrics.left - rectBuffer - halfAdvance + builtInOffset[0];
127
- const y1 = -glyph.metrics.top - rectBuffer + builtInOffset[1];
128
- const x2 = x1 + rect.w;
129
- const y2 = y1 + rect.h;
127
+ const x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0];
128
+ const y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1];
129
+ const x2 = x1 + rect.w * positionedGlyph.scale;
130
+ const y2 = y1 + rect.h * positionedGlyph.scale;
130
131
 
131
132
  const tl = new Point(x1, y1);
132
133
  const tr = new Point(x2, y1);
@@ -1,6 +1,7 @@
1
1
  const { charHasUprightVerticalOrientation, charAllowsIdeographicBreaking } = require('../util/script_detection');
2
2
  const verticalizePunctuation = require('../util/verticalize_punctuation');
3
3
  const { plugin: rtlTextPlugin } = require('../source/rtl_text_plugin');
4
+ const { Formatted } = require('../style-spec/expression/definitions/formatted');
4
5
 
5
6
  const WritingMode = {
6
7
  horizontal: 1,
@@ -14,20 +15,94 @@ module.exports = {
14
15
  WritingMode
15
16
  };
16
17
 
17
- // The position of a glyph relative to the text's anchor point.
18
+ class TaggedString {
19
+ constructor() {
20
+ this.text = '';
21
+ this.sectionIndex = []; // maps each character in 'text' to its corresponding entry in 'sections'
22
+ this.sections = [];
23
+ }
18
24
 
19
- // A collection of positioned glyphs and some metadata
25
+ static fromFeature(text, defaultFontStack) {
26
+ const result = new TaggedString();
27
+ if (text instanceof Formatted) {
28
+ for (let i = 0; i < text.sections.length; i++) {
29
+ const section = text.sections[i];
30
+ result.sections.push({
31
+ scale: section.scale || 1,
32
+ fontStack: section.fontStack || defaultFontStack
33
+ });
34
+ result.text += section.text;
35
+ for (let j = 0; j < section.text.length; j++) {
36
+ result.sectionIndex.push(i);
37
+ }
38
+ }
39
+ } else {
40
+ result.text = text;
41
+ result.sections.push({ scale: 1, fontStack: defaultFontStack });
42
+ for (let i = 0; i < text.length; i++) {
43
+ result.sectionIndex.push(0);
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+
49
+ length() {
50
+ return this.text.length;
51
+ }
52
+
53
+ getSection(index) {
54
+ return this.sections[this.sectionIndex[index]];
55
+ }
56
+
57
+ getCharCode(index) {
58
+ return this.text.charCodeAt(index);
59
+ }
60
+
61
+ verticalizePunctuation() {
62
+ this.text = verticalizePunctuation(this.text);
63
+ }
64
+
65
+ trim() {
66
+ let beginningWhitespace = 0;
67
+ for (let i = 0; i < this.text.length && whitespace[this.text.charCodeAt(i)]; i++) {
68
+ beginningWhitespace++;
69
+ }
70
+ let trailingWhitespace = this.text.length;
71
+ for (let i = this.text.length - 1; i >= 0 && i >= beginningWhitespace && whitespace[this.text.charCodeAt(i)]; i--) {
72
+ trailingWhitespace--;
73
+ }
74
+ this.text = this.text.substring(beginningWhitespace, trailingWhitespace);
75
+ this.sectionIndex = this.sectionIndex.slice(beginningWhitespace, trailingWhitespace);
76
+ }
20
77
 
21
- function breakLines(text, lineBreakPoints) {
78
+ substring(start, end) {
79
+ const substring = new TaggedString();
80
+ substring.text = this.text.substring(start, end);
81
+ substring.sectionIndex = this.sectionIndex.slice(start, end);
82
+ substring.sections = this.sections;
83
+ return substring;
84
+ }
85
+
86
+ toString() {
87
+ return this.text;
88
+ }
89
+
90
+ getMaxScale() {
91
+ return this.sectionIndex.reduce((max, index) => Math.max(max, this.sections[index].scale), 0);
92
+ }
93
+ }
94
+
95
+ function breakLines(input, lineBreakPoints) {
22
96
  const lines = [];
97
+ const text = input.text;
23
98
  let start = 0;
24
99
  for (const lineBreak of lineBreakPoints) {
25
- lines.push(text.substring(start, lineBreak));
100
+ lines.push(input.substring(start, lineBreak));
26
101
  start = lineBreak;
27
102
  }
28
103
 
29
104
  if (start < text.length) {
30
- lines.push(text.substring(start, text.length));
105
+ lines.push(input.substring(start, text.length));
31
106
  }
32
107
  return lines;
33
108
  }
@@ -35,6 +110,7 @@ function breakLines(text, lineBreakPoints) {
35
110
  function shapeText(
36
111
  text,
37
112
  glyphs,
113
+ defaultFontStack,
38
114
  maxWidth,
39
115
  lineHeight,
40
116
  textAnchor,
@@ -44,9 +120,9 @@ function shapeText(
44
120
  verticalHeight,
45
121
  writingMode
46
122
  ) {
47
- let logicalInput = text.trim();
123
+ const logicalInput = TaggedString.fromFeature(text, defaultFontStack);
48
124
  if (writingMode === WritingMode.vertical) {
49
- logicalInput = verticalizePunctuation(logicalInput);
125
+ logicalInput.verticalizePunctuation();
50
126
  }
51
127
 
52
128
  const positionedGlyphs = [];
@@ -62,9 +138,39 @@ function shapeText(
62
138
 
63
139
  let lines;
64
140
 
65
- const { processBidirectionalText } = rtlTextPlugin;
66
- if (processBidirectionalText) {
67
- lines = processBidirectionalText(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphs));
141
+ const { processBidirectionalText, processStyledBidirectionalText } = rtlTextPlugin;
142
+ if (processBidirectionalText && logicalInput.sections.length === 1) {
143
+ // Bidi doesn't have to be style-aware
144
+ lines = [];
145
+ const untaggedLines = processBidirectionalText(
146
+ logicalInput.toString(),
147
+ determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)
148
+ );
149
+ for (const line of untaggedLines) {
150
+ const taggedLine = new TaggedString();
151
+ taggedLine.text = line;
152
+ taggedLine.sections = logicalInput.sections;
153
+ for (let i = 0; i < line.length; i++) {
154
+ taggedLine.sectionIndex.push(0);
155
+ }
156
+ lines.push(taggedLine);
157
+ }
158
+ } else if (processStyledBidirectionalText) {
159
+ // Need version of mapbox-gl-rtl-text with style support for combining RTL text
160
+ // with formatting
161
+ lines = [];
162
+ const processedLines = processStyledBidirectionalText(
163
+ logicalInput.text,
164
+ logicalInput.sectionIndex,
165
+ determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)
166
+ );
167
+ for (const line of processedLines) {
168
+ const taggedLine = new TaggedString();
169
+ taggedLine.text = line[0];
170
+ taggedLine.sectionIndex = line[1];
171
+ taggedLine.sections = logicalInput.sections;
172
+ lines.push(taggedLine);
173
+ }
68
174
  } else {
69
175
  lines = breakLines(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphs));
70
176
  }
@@ -73,6 +179,7 @@ function shapeText(
73
179
 
74
180
  if (!positionedGlyphs.length) return false;
75
181
 
182
+ shaping.text = shaping.text.toString();
76
183
  return shaping;
77
184
  }
78
185
 
@@ -105,13 +212,15 @@ const breakable = {
105
212
  // See https://github.com/mapbox/mapbox-gl-js/issues/3658
106
213
  };
107
214
 
108
- function determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphs) {
215
+ function determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap) {
109
216
  let totalWidth = 0;
110
217
 
111
- for (let index = 0; index < logicalInput.length; index++) {
112
- const glyph = glyphs[logicalInput.charCodeAt(index)];
218
+ for (let index = 0; index < logicalInput.length(); index++) {
219
+ const section = logicalInput.getSection(index);
220
+ const positions = glyphMap[section.fontStack];
221
+ const glyph = positions?.[logicalInput.getCharCode(index)];
113
222
  if (!glyph) continue;
114
- totalWidth += glyph.metrics.advance + spacing;
223
+ totalWidth += glyph.metrics.advance * section.scale + spacing;
115
224
  }
116
225
 
117
226
  const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth));
@@ -182,39 +291,41 @@ function leastBadBreaks(lastLineBreak) {
182
291
  return leastBadBreaks(lastLineBreak.priorBreak).concat(lastLineBreak.index);
183
292
  }
184
293
 
185
- function determineLineBreaks(logicalInput, spacing, maxWidth, glyphs) {
294
+ function determineLineBreaks(logicalInput, spacing, maxWidth, glyphMap) {
186
295
  if (!maxWidth) return [];
187
296
 
188
297
  if (!logicalInput) return [];
189
298
 
190
299
  const potentialLineBreaks = [];
191
- const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphs);
300
+ const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap);
192
301
 
193
302
  let currentX = 0;
194
303
 
195
- for (let i = 0; i < logicalInput.length; i++) {
196
- const codePoint = logicalInput.charCodeAt(i);
197
- const glyph = glyphs[codePoint];
304
+ for (let i = 0; i < logicalInput.length(); i++) {
305
+ const section = logicalInput.getSection(i);
306
+ const codePoint = logicalInput.getCharCode(i);
307
+ const positions = glyphMap[section.fontStack];
308
+ const glyph = positions?.[codePoint];
198
309
 
199
- if (glyph && !whitespace[codePoint]) currentX += glyph.metrics.advance + spacing;
310
+ if (glyph && !whitespace[codePoint]) currentX += glyph.metrics.advance * section.scale + spacing;
200
311
 
201
312
  // Ideographic characters, spaces, and word-breaking punctuation that often appear without
202
313
  // surrounding spaces.
203
- if (i < logicalInput.length - 1 && (breakable[codePoint] || charAllowsIdeographicBreaking(codePoint))) {
314
+ if (i < logicalInput.length() - 1 && (breakable[codePoint] || charAllowsIdeographicBreaking(codePoint))) {
204
315
  potentialLineBreaks.push(
205
316
  evaluateBreak(
206
317
  i + 1,
207
318
  currentX,
208
319
  targetWidth,
209
320
  potentialLineBreaks,
210
- calculatePenalty(codePoint, logicalInput.charCodeAt(i + 1)),
321
+ calculatePenalty(codePoint, logicalInput.getCharCode(i + 1)),
211
322
  false
212
323
  )
213
324
  );
214
325
  }
215
326
  }
216
327
 
217
- return leastBadBreaks(evaluateBreak(logicalInput.length, currentX, targetWidth, potentialLineBreaks, 0, true));
328
+ return leastBadBreaks(evaluateBreak(logicalInput.length(), currentX, targetWidth, potentialLineBreaks, 0, true));
218
329
  }
219
330
 
220
331
  function getAnchorAlignment(anchor) {
@@ -250,7 +361,17 @@ function getAnchorAlignment(anchor) {
250
361
  return { horizontalAlign, verticalAlign };
251
362
  }
252
363
 
253
- function shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify, writingMode, spacing, verticalHeight) {
364
+ function shapeLines(
365
+ shaping,
366
+ glyphMap,
367
+ lines,
368
+ lineHeight,
369
+ textAnchor,
370
+ textJustify,
371
+ writingMode,
372
+ spacing,
373
+ verticalHeight
374
+ ) {
254
375
  // the y offset *should* be part of the font metadata
255
376
  const yOffset = -17;
256
377
 
@@ -262,27 +383,49 @@ function shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify,
262
383
 
263
384
  const justify = textJustify === 'right' ? 1 : textJustify === 'left' ? 0 : 0.5;
264
385
 
265
- for (let line of lines) {
266
- line = line.trim();
386
+ for (const line of lines) {
387
+ line.trim();
388
+
389
+ const lineMaxScale = line.getMaxScale();
267
390
 
268
- if (!line.length) {
391
+ if (!line.length()) {
269
392
  y += lineHeight; // Still need a line feed after empty line
270
393
  continue;
271
394
  }
272
395
 
273
396
  const lineStartIndex = positionedGlyphs.length;
274
- for (let i = 0; i < line.length; i++) {
275
- const codePoint = line.charCodeAt(i);
276
- const glyph = glyphs[codePoint];
397
+ for (let i = 0; i < line.length(); i++) {
398
+ const section = line.getSection(i);
399
+ const codePoint = line.getCharCode(i);
400
+ // We don't know the baseline, but since we're laying out
401
+ // at 24 points, we can calculate how much it will move when
402
+ // we scale up or down.
403
+ const baselineOffset = (lineMaxScale - section.scale) * 24;
404
+ const positions = glyphMap[section.fontStack];
405
+ const glyph = positions?.[codePoint];
277
406
 
278
407
  if (!glyph) continue;
279
408
 
280
409
  if (!charHasUprightVerticalOrientation(codePoint) || writingMode === WritingMode.horizontal) {
281
- positionedGlyphs.push({ glyph: codePoint, x, y, vertical: false });
282
- x += glyph.metrics.advance + spacing;
410
+ positionedGlyphs.push({
411
+ glyph: codePoint,
412
+ x,
413
+ y: y + baselineOffset,
414
+ vertical: false,
415
+ scale: section.scale,
416
+ fontStack: section.fontStack
417
+ });
418
+ x += glyph.metrics.advance * section.scale + spacing;
283
419
  } else {
284
- positionedGlyphs.push({ glyph: codePoint, x, y: 0, vertical: true });
285
- x += verticalHeight + spacing;
420
+ positionedGlyphs.push({
421
+ glyph: codePoint,
422
+ x,
423
+ y: baselineOffset,
424
+ vertical: true,
425
+ scale: section.scale,
426
+ fontStack: section.fontStack
427
+ });
428
+ x += verticalHeight * section.scale + spacing;
286
429
  }
287
430
  }
288
431
 
@@ -291,18 +434,18 @@ function shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify,
291
434
  const lineLength = x - spacing;
292
435
  maxLineLength = Math.max(lineLength, maxLineLength);
293
436
 
294
- justifyLine(positionedGlyphs, glyphs, lineStartIndex, positionedGlyphs.length - 1, justify);
437
+ justifyLine(positionedGlyphs, glyphMap, lineStartIndex, positionedGlyphs.length - 1, justify);
295
438
  }
296
439
 
297
440
  x = 0;
298
- y += lineHeight;
441
+ y += lineHeight * lineMaxScale;
299
442
  }
300
443
 
301
444
  const { horizontalAlign, verticalAlign } = getAnchorAlignment(textAnchor);
302
445
  align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, lines.length);
303
446
 
304
447
  // Calculate the bounding box
305
- const height = lines.length * lineHeight;
448
+ const height = y - yOffset;
306
449
 
307
450
  shaping.top += -verticalAlign * height;
308
451
  shaping.bottom = shaping.top + height;
@@ -311,12 +454,14 @@ function shapeLines(shaping, glyphs, lines, lineHeight, textAnchor, textJustify,
311
454
  }
312
455
 
313
456
  // justify right = 1, left = 0, center = 0.5
314
- function justifyLine(positionedGlyphs, glyphs, start, end, justify) {
457
+ function justifyLine(positionedGlyphs, glyphMap, start, end, justify) {
315
458
  if (!justify) return;
316
459
 
317
- const glyph = glyphs[positionedGlyphs[end].glyph];
460
+ const lastPositionedGlyph = positionedGlyphs[end];
461
+ const positions = glyphMap[lastPositionedGlyph.fontStack];
462
+ const glyph = positions?.[lastPositionedGlyph.glyph];
318
463
  if (glyph) {
319
- const lastAdvance = glyph.metrics.advance;
464
+ const lastAdvance = glyph.metrics.advance * lastPositionedGlyph.scale;
320
465
  const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify;
321
466
 
322
467
  for (let j = start; j <= end; j++) {