diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index 68f27369157..0386a3feb88 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -276,12 +276,18 @@ function calculateBadness(lineWidth: number, return raggedness + Math.abs(penalty) * penalty; } -function calculatePenalty(codePoint: number, nextCodePoint: number) { +function calculatePenalty(codePoint: number, nextCodePoint: number, penalizableIdeographicBreak: boolean) { let penalty = 0; // Force break on newline if (codePoint === 0x0a) { penalty -= 10000; } + // Penalize breaks between characters that allow ideographic breaking because + // they are less preferable than breaks at spaces (or zero width spaces). + if (penalizableIdeographicBreak) { + penalty += 150; + } + // Penalize open parenthesis at end of line if (codePoint === 0x28 || codePoint === 0xff08) { penalty += 50; @@ -353,6 +359,8 @@ function determineLineBreaks(logicalInput: TaggedString, const potentialLineBreaks = []; const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap); + const hasServerSuggestedBreakpoints = logicalInput.text.indexOf("\u200b") >= 0; + let currentX = 0; for (let i = 0; i < logicalInput.length(); i++) { @@ -366,18 +374,19 @@ function determineLineBreaks(logicalInput: TaggedString, // Ideographic characters, spaces, and word-breaking punctuation that often appear without // surrounding spaces. - if ((i < logicalInput.length() - 1) && - (breakable[codePoint] || - charAllowsIdeographicBreaking(codePoint))) { - - potentialLineBreaks.push( - evaluateBreak( - i + 1, - currentX, - targetWidth, - potentialLineBreaks, - calculatePenalty(codePoint, logicalInput.getCharCode(i + 1)), - false)); + if ((i < logicalInput.length() - 1)) { + const ideographicBreak = charAllowsIdeographicBreaking(codePoint); + if (breakable[codePoint] || ideographicBreak) { + + potentialLineBreaks.push( + evaluateBreak( + i + 1, + currentX, + targetWidth, + potentialLineBreaks, + calculatePenalty(codePoint, logicalInput.getCharCode(i + 1), ideographicBreak && hasServerSuggestedBreakpoints), + false)); + } } } diff --git a/test/expected/text-shaping-zero-width-space.json b/test/expected/text-shaping-zero-width-space.json new file mode 100644 index 00000000000..4145103857e --- /dev/null +++ b/test/expected/text-shaping-zero-width-space.json @@ -0,0 +1,123 @@ +{ + "positionedGlyphs": [ + { + "glyph": 19977, + "x": -63, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": -42, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": -21, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": 0, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": 21, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": 42, + "y": -41, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": -63, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": -42, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": -21, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": 0, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": 21, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": 42, + "y": -17, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": -21, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test" + }, + { + "glyph": 19977, + "x": 0, + "y": 7, + "vertical": false, + "scale": 1, + "fontStack": "Test" + } + ], + "text": "三三​三三​三三​三三三三三三​三三", + "top": -36, + "bottom": 36, + "left": -63, + "right": 63, + "writingMode": 1, + "lineCount": 3 +} \ No newline at end of file diff --git a/test/fixtures/fontstack-glyphs.json b/test/fixtures/fontstack-glyphs.json index edfc6386a8b..7dbbcee7e49 100644 --- a/test/fixtures/fontstack-glyphs.json +++ b/test/fixtures/fontstack-glyphs.json @@ -1918,5 +1918,15 @@ "top": -5, "advance": 15 } + }, + "19977": { + "id": 19977, + "metrics": { + "width": 18, + "height": 18, + "left": 2, + "top": -8, + "advance": 21 + } } -} \ No newline at end of file +} diff --git a/test/unit/symbol/shaping.test.js b/test/unit/symbol/shaping.test.js index 8dc110dd77b..f27cd004731 100644 --- a/test/unit/symbol/shaping.test.js +++ b/test/unit/symbol/shaping.test.js @@ -55,6 +55,15 @@ test('shaping', (t) => { if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-newlines-in-middle.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedNewLinesInMiddle); + // Prefer zero width spaces when breaking lines. Zero width spaces are used by Mapbox data sources as a hint that + // a position is ideal for breaking. + const expectedZeroWidthSpaceBreak = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-zero-width-space.json'))); + + shaped = shaping.shapeText(Formatted.fromString('三三\u200b三三\u200b三三\u200b三三三三三三\u200b三三'), glyphs, fontStack, 5 * oneEm, oneEm, 'center', 'center', 0, [0, 0], WritingMode.horizontal); + if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-zero-width-space.json'), JSON.stringify(shaped, null, 2)); + t.deepEqual(shaped, expectedZeroWidthSpaceBreak); + + // Null shaping. shaped = shaping.shapeText(Formatted.fromString(''), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], WritingMode.horizontal); t.equal(false, shaped);