From 9aa529576f4d0565bb81369f03a1fecdb8dd0886 Mon Sep 17 00:00:00 2001 From: Alexander Shalamov Date: Fri, 7 Jun 2019 15:38:08 +0300 Subject: [core] Implement support for "text-writing-mode" layout property --- src/mbgl/layout/symbol_feature.hpp | 1 + src/mbgl/layout/symbol_instance.cpp | 17 +- src/mbgl/layout/symbol_instance.hpp | 7 +- src/mbgl/layout/symbol_layout.cpp | 57 +++++- src/mbgl/layout/symbol_layout.hpp | 2 + src/mbgl/layout/symbol_projection.cpp | 6 +- src/mbgl/renderer/buckets/symbol_bucket.cpp | 8 +- src/mbgl/renderer/buckets/symbol_bucket.hpp | 11 +- src/mbgl/text/glyph.hpp | 2 + src/mbgl/text/placement.cpp | 277 +++++++++++++++++++++------- src/mbgl/text/placement.hpp | 5 +- src/mbgl/text/quads.cpp | 34 ++-- src/mbgl/text/quads.hpp | 3 +- src/mbgl/text/shaping.cpp | 10 +- src/mbgl/text/tagged_string.cpp | 8 + src/mbgl/text/tagged_string.hpp | 2 + 16 files changed, 343 insertions(+), 107 deletions(-) diff --git a/src/mbgl/layout/symbol_feature.hpp b/src/mbgl/layout/symbol_feature.hpp index ed9c0783d0..03d8e5018d 100644 --- a/src/mbgl/layout/symbol_feature.hpp +++ b/src/mbgl/layout/symbol_feature.hpp @@ -32,6 +32,7 @@ public: optional icon; float sortKey = 0.0f; std::size_t index; + bool allowsVerticalWritingMode = false; }; } // namespace mbgl diff --git a/src/mbgl/layout/symbol_instance.cpp b/src/mbgl/layout/symbol_instance.cpp index 133cdec698..8e8286bbd7 100644 --- a/src/mbgl/layout/symbol_instance.cpp +++ b/src/mbgl/layout/symbol_instance.cpp @@ -25,7 +25,8 @@ SymbolInstanceSharedData::SymbolInstanceSharedData(GeometryCoordinates line_, const float layoutTextSize, const style::SymbolPlacementType textPlacement, const std::array& textOffset, - const GlyphPositions& positions) : line(std::move(line_)) { + const GlyphPositions& positions, + bool allowVerticalPlacement) : line(std::move(line_)) { // Create the quads used for rendering the icon and glyphs. if (shapedIcon) { iconQuad = getIconQuad(*shapedIcon, layout, layoutTextSize, shapedTextOrientations.horizontal); @@ -34,11 +35,11 @@ SymbolInstanceSharedData::SymbolInstanceSharedData(GeometryCoordinates line_, bool singleLineInitialized = false; const auto initHorizontalGlyphQuads = [&] (SymbolQuads& quads, const Shaping& shaping) { if (!shapedTextOrientations.singleLine) { - quads = getGlyphQuads(shaping, textOffset, layout, textPlacement, positions); + quads = getGlyphQuads(shaping, textOffset, layout, textPlacement, positions, allowVerticalPlacement); return; } if (!singleLineInitialized) { - rightJustifiedGlyphQuads = getGlyphQuads(shaping, textOffset, layout, textPlacement, positions); + rightJustifiedGlyphQuads = getGlyphQuads(shaping, textOffset, layout, textPlacement, positions, allowVerticalPlacement); singleLineInitialized = true; } }; @@ -56,7 +57,7 @@ SymbolInstanceSharedData::SymbolInstanceSharedData(GeometryCoordinates line_, } if (shapedTextOrientations.vertical) { - verticalGlyphQuads = getGlyphQuads(shapedTextOrientations.vertical, textOffset, layout, textPlacement, positions); + verticalGlyphQuads = getGlyphQuads(shapedTextOrientations.vertical, textOffset, layout, textPlacement, positions, allowVerticalPlacement); } } @@ -81,7 +82,8 @@ SymbolInstance::SymbolInstance(Anchor& anchor_, std::u16string key_, const float overscaling, const float rotate, - float radialTextOffset_) : + float radialTextOffset_, + bool allowVerticalPlacement) : sharedData(std::move(sharedData_)), anchor(anchor_), // 'hasText' depends on finding at least one glyph in the shaping that's also in the GlyphPositionMap @@ -101,6 +103,11 @@ SymbolInstance::SymbolInstance(Anchor& anchor_, radialTextOffset(radialTextOffset_), singleLine(shapedTextOrientations.singleLine) { + if (allowVerticalPlacement && shapedTextOrientations.vertical) { + const float verticalPointLabelAngle = 90.0f; + verticalTextCollisionFeature = CollisionFeature(line(), anchor, shapedTextOrientations.vertical, textBoxScale_, textPadding, textPlacement, indexedFeature, overscaling, rotate + verticalPointLabelAngle); + } + rightJustifiedGlyphQuadsSize = sharedData->rightJustifiedGlyphQuads.size(); centerJustifiedGlyphQuadsSize = sharedData->centerJustifiedGlyphQuads.size(); leftJustifiedGlyphQuadsSize = sharedData->leftJustifiedGlyphQuads.size(); diff --git a/src/mbgl/layout/symbol_instance.hpp b/src/mbgl/layout/symbol_instance.hpp index 48bb2f0cbc..693d8917c7 100644 --- a/src/mbgl/layout/symbol_instance.hpp +++ b/src/mbgl/layout/symbol_instance.hpp @@ -29,7 +29,8 @@ struct SymbolInstanceSharedData { const float layoutTextSize, const style::SymbolPlacementType textPlacement, const std::array& textOffset, - const GlyphPositions& positions); + const GlyphPositions& positions, + bool allowVerticalPlacement); bool empty() const; GeometryCoordinates line; // Note: When singleLine == true, only `rightJustifiedGlyphQuads` is populated. @@ -59,7 +60,8 @@ public: std::u16string key, const float overscaling, const float rotate, - float radialTextOffset); + float radialTextOffset, + bool allowVerticalPlacement); optional getDefaultHorizontalPlacedTextIndex() const; const GeometryCoordinates& line() const; @@ -85,6 +87,7 @@ public: CollisionFeature textCollisionFeature; CollisionFeature iconCollisionFeature; + optional verticalTextCollisionFeature = nullopt; WritingModeType writingModes; std::size_t layoutFeatureIndex; // Index into the set of features included at layout time std::size_t dataFeatureIndex; // Index into the underlying tile data feature set diff --git a/src/mbgl/layout/symbol_layout.cpp b/src/mbgl/layout/symbol_layout.cpp index d269ca4144..be6444bb16 100644 --- a/src/mbgl/layout/symbol_layout.cpp +++ b/src/mbgl/layout/symbol_layout.cpp @@ -109,6 +109,19 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, const bool zOrderByViewportY = symbolZOrder == SymbolZOrderType::ViewportY || (symbolZOrder == SymbolZOrderType::Auto && !sortFeaturesByKey); sortFeaturesByY = zOrderByViewportY && (layout->get() || layout->get() || layout->get() || layout->get()); + if (layout->get() == SymbolPlacementType::Point) { + auto modes = layout->get(); + // Remove duplicates and preserve order. + std::set seen; + auto end = std::remove_if(modes.begin(), + modes.end(), + [&seen, this](const auto& placementMode) { + allowVerticalPlacement = allowVerticalPlacement || placementMode == style::TextWritingModeType::Vertical; + return !seen.insert(placementMode).second; + }); + modes.erase(end, modes.end()); + placementModes = std::move(modes); + } for (const auto& layer : layers) { layerPaintProperties.emplace(layer->baseImpl->id, layer); @@ -148,7 +161,7 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, const bool canVerticalizeText = layout->get() == AlignmentType::Map && layout->get() != SymbolPlacementType::Point - && util::i18n::allowsVerticalWritingMode(ft.formattedText->rawText()); + && ft.formattedText->allowsVerticalWritingMode(); // Loop through all characters of this text and collect unique codepoints. for (std::size_t j = 0; j < ft.formattedText->length(); j++) { @@ -156,7 +169,7 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, GlyphIDs& dependencies = glyphDependencies[sectionFontStack ? *sectionFontStack : baseFontStack]; char16_t codePoint = ft.formattedText->getCharCodeAt(j); dependencies.insert(codePoint); - if (canVerticalizeText) { + if (canVerticalizeText || (allowVerticalPlacement && ft.formattedText->allowsVerticalWritingMode())) { if (char16_t verticalChr = util::i18n::verticalizePunctuation(codePoint)) { dependencies.insert(verticalChr); } @@ -321,7 +334,18 @@ void SymbolLayout::prepareSymbols(const GlyphMap& glyphMap, const GlyphPositions } } TextJustifyType textJustify = textAlongLine ? TextJustifyType::Center : layout->evaluate(zoom, feature); - // If this layer uses text-variable-anchor, generate shapings for all justification possibilities. + + const auto addVerticalShapingForPointLabelIfNeeded = [&] { + if (allowVerticalPlacement && feature.formattedText->allowsVerticalWritingMode()) { + feature.formattedText->verticalizePunctuation(); + // Vertical POI label placement is meant to be used for scripts that support vertical + // writing mode, thus, default style::TextJustifyType::Left justification is used. If Latin + // scripts would need to be supported, this should take into account other justifications. + shapedTextOrientations.vertical = applyShaping(*feature.formattedText, WritingModeType::Vertical, textAnchor, style::TextJustifyType::Left); + } + }; + + // If this layer uses text-variable-anchor, generate shapings for all justification possibilities. if (!textAlongLine && !variableTextAnchor.empty()) { std::vector justifications; if (textJustify != TextJustifyType::Auto) { @@ -347,16 +371,25 @@ void SymbolLayout::prepareSymbols(const GlyphMap& glyphMap, const GlyphPositions } } } + + // Vertical point label shaping if allowVerticalPlacement is enabled. + addVerticalShapingForPointLabelIfNeeded(); } else { if (textJustify == TextJustifyType::Auto) { textJustify = getAnchorJustification(textAnchor); } + + // Horizontal point or line label. Shaping shaping = applyShaping(*feature.formattedText, WritingModeType::Horizontal, textAnchor, textJustify); if (shaping) { shapedTextOrientations.horizontal = std::move(shaping); } - if (util::i18n::allowsVerticalWritingMode(feature.formattedText->rawText()) && textAlongLine) { + // Vertical point label shaping if allowVerticalPlacement is enabled. + addVerticalShapingForPointLabelIfNeeded(); + + // Verticalized line label. + if (textAlongLine && feature.formattedText->allowsVerticalWritingMode()) { feature.formattedText->verticalizePunctuation(); shapedTextOrientations.vertical = applyShaping(*feature.formattedText, WritingModeType::Vertical, textAnchor, textJustify); } @@ -423,7 +456,8 @@ void SymbolLayout::addFeature(const std::size_t layoutFeatureIndex, const float textPadding = layout->get() * tilePixelRatio; const float iconPadding = layout->get() * tilePixelRatio; const float textMaxAngle = layout->get() * util::DEG2RAD; - const float rotation = layout->evaluate(zoom, feature); + const float iconRotation = layout->evaluate(zoom, feature); + const float textRotation = layout->evaluate(zoom, feature); const float radialTextOffset = layout->evaluate(zoom, feature) * util::ONE_EM; const SymbolPlacementType textPlacement = layout->get() != AlignmentType::Map ? SymbolPlacementType::Point @@ -448,14 +482,14 @@ void SymbolLayout::addFeature(const std::size_t layoutFeatureIndex, iconBoxScale, iconPadding, iconOffset, indexedFeature, layoutFeatureIndex, feature.index, feature.formattedText ? feature.formattedText->rawText() : std::u16string(), - overscaling, rotation, radialTextOffset); + overscaling, iconRotation, textRotation, radialTextOffset, allowVerticalPlacement); } }; const auto createSymbolInstanceSharedData = [&] (GeometryCoordinates line) { return std::make_shared(std::move(line), shapedTextOrientations, shapedIcon, evaluatedLayoutProperties, layoutTextSize, - textPlacement, textOffset, glyphPositions); + textPlacement, textOffset, glyphPositions, allowVerticalPlacement); }; const auto& type = feature.getType(); @@ -568,7 +602,10 @@ std::vector CalculateTileDistances(const GeometryCoordinates& line, const } void SymbolLayout::createBucket(const ImagePositions&, std::unique_ptr&, std::unordered_map& renderData, const bool firstLoad, const bool showCollisionBoxes) { - auto bucket = std::make_shared(layout, layerPaintProperties, textSize, iconSize, zoom, sdfIcons, iconsNeedLinear, sortFeaturesByY, bucketLeaderID, std::move(symbolInstances), tilePixelRatio); + auto bucket = std::make_shared(layout, layerPaintProperties, textSize, iconSize, zoom, sdfIcons, iconsNeedLinear, + sortFeaturesByY, bucketLeaderID, std::move(symbolInstances), tilePixelRatio, + allowVerticalPlacement, + std::move(placementModes)); for (SymbolInstance &symbolInstance : bucket->symbolInstances) { const bool hasText = symbolInstance.hasText; @@ -660,6 +697,7 @@ std::size_t SymbolLayout::addSymbolGlyphQuads(SymbolBucket& bucket, symbolInstance.textOffset, writingMode, symbolInstance.line(), CalculateTileDistances(symbolInstance.line(), symbolInstance.anchor)); placedIndex = bucket.text.placedSymbols.size() - 1; PlacedSymbol& placedSymbol = bucket.text.placedSymbols.back(); + placedSymbol.angle = (allowVerticalPlacement && writingMode == WritingModeType::Vertical) ? M_PI_2 : 0; bool firstSymbol = true; for (const auto& symbolQuad : glyphQuads) { @@ -795,6 +833,9 @@ void SymbolLayout::addToDebugBuffers(SymbolBucket& bucket) { } }; populateCollisionBox(symbolInstance.textCollisionFeature); + if (symbolInstance.verticalTextCollisionFeature) { + populateCollisionBox(*symbolInstance.verticalTextCollisionFeature); + } populateCollisionBox(symbolInstance.iconCollisionFeature); } } diff --git a/src/mbgl/layout/symbol_layout.hpp b/src/mbgl/layout/symbol_layout.hpp index 581c3ccb04..70a3482644 100644 --- a/src/mbgl/layout/symbol_layout.hpp +++ b/src/mbgl/layout/symbol_layout.hpp @@ -96,6 +96,8 @@ private: bool sdfIcons = false; bool iconsNeedLinear = false; bool sortFeaturesByY = false; + bool allowVerticalPlacement = false; + std::vector placementModes; style::TextSize::UnevaluatedType textSize; style::IconSize::UnevaluatedType iconSize; diff --git a/src/mbgl/layout/symbol_projection.cpp b/src/mbgl/layout/symbol_projection.cpp index aeea10ffa4..745c1c1d77 100644 --- a/src/mbgl/layout/symbol_projection.cpp +++ b/src/mbgl/layout/symbol_projection.cpp @@ -319,7 +319,11 @@ namespace mbgl { const float glyphOffsetX = symbol.glyphOffsets[glyphIndex]; // Since first and last glyph fit on the line, we're sure that the rest of the glyphs can be placed auto placedGlyph = placeGlyphAlongLine(glyphOffsetX * fontScale, lineOffsetX, lineOffsetY, flip, projectedAnchorPoint, symbol.anchorPoint, symbol.segment, symbol.line, symbol.tileDistances, labelPlaneMatrix, false); - placedGlyphs.push_back(*placedGlyph); + if (placedGlyph) { + placedGlyphs.push_back(*placedGlyph); + } else { + placedGlyphs.emplace_back(Point{-INFINITY, -INFINITY}, 0.0f, nullopt); + } } placedGlyphs.push_back(firstAndLastGlyph->second); } else if (symbol.glyphOffsets.size() == 1) { diff --git a/src/mbgl/renderer/buckets/symbol_bucket.cpp b/src/mbgl/renderer/buckets/symbol_bucket.cpp index 2a9f5df9c0..21a7870473 100644 --- a/src/mbgl/renderer/buckets/symbol_bucket.cpp +++ b/src/mbgl/renderer/buckets/symbol_bucket.cpp @@ -23,7 +23,9 @@ SymbolBucket::SymbolBucket(Immutable&& symbolInstances_, - float tilePixelRatio_) + float tilePixelRatio_, + bool allowVerticalPlacement_, + std::vector placementModes_) : layout(std::move(layout_)), bucketLeaderID(std::move(bucketName_)), sdfIcons(sdfIcons_), @@ -39,7 +41,9 @@ SymbolBucket::SymbolBucket(Immutable(pair.second); diff --git a/src/mbgl/renderer/buckets/symbol_bucket.hpp b/src/mbgl/renderer/buckets/symbol_bucket.hpp index a94073f7d0..f47ced8331 100644 --- a/src/mbgl/renderer/buckets/symbol_bucket.hpp +++ b/src/mbgl/renderer/buckets/symbol_bucket.hpp @@ -39,6 +39,10 @@ public: size_t vertexStartIndex; // The crossTileID is only filled/used on the foreground for variable text anchors uint32_t crossTileID = 0u; + // The placedOrientation is only used when symbol layer's property is set to support + // placement for orientation variants. + optional placedOrientation; + float angle = 0; }; class SymbolBucket final : public Bucket { @@ -53,7 +57,9 @@ public: bool sortFeaturesByY, const std::string bucketLeaderID, const std::vector&&, - const float tilePixelRatio); + const float tilePixelRatio, + bool allowVerticalPlacement, + std::vector placementModes); ~SymbolBucket() override; void upload(gfx::UploadPass&) override; @@ -149,7 +155,8 @@ public: const float tilePixelRatio; uint32_t bucketInstanceId; - + const bool allowVerticalPlacement; + const std::vector placementModes; mutable optional hasFormatSectionOverrides_; std::shared_ptr> featureSortOrder; diff --git a/src/mbgl/text/glyph.hpp b/src/mbgl/text/glyph.hpp index 5105528512..234f718975 100644 --- a/src/mbgl/text/glyph.hpp +++ b/src/mbgl/text/glyph.hpp @@ -89,6 +89,8 @@ class Shaping { WritingModeType writingMode; std::size_t lineCount = 0u; explicit operator bool() const { return !positionedGlyphs.empty(); } + // The y offset *should* be part of the font metadata. + static constexpr int32_t yOffset = -17; }; enum class WritingModeType : uint8_t { diff --git a/src/mbgl/text/placement.cpp b/src/mbgl/text/placement.cpp index f7d13dcb26..de282acf79 100644 --- a/src/mbgl/text/placement.cpp +++ b/src/mbgl/text/placement.cpp @@ -172,32 +172,80 @@ void Placement::placeBucket( placements.emplace(symbolInstance.crossTileID, JointPlacement(false, false, false)); return; } - textBoxes.clear(); iconBoxes.clear(); bool placeText = false; bool placeIcon = false; bool offscreen = true; + std::pair placed{ false, false }; + std::pair placedVertical{ false, false }; optional horizontalTextIndex = symbolInstance.getDefaultHorizontalPlacedTextIndex(); if (horizontalTextIndex) { - const CollisionFeature& textCollisionFeature = symbolInstance.textCollisionFeature; const PlacedSymbol& placedSymbol = bucket.text.placedSymbols.at(*horizontalTextIndex); const float fontSize = evaluateSizeForFeature(partiallyEvaluatedTextSize, placedSymbol); + const CollisionFeature& textCollisionFeature = symbolInstance.textCollisionFeature; + + const auto updatePreviousOrientationIfNotPlaced = [&](bool isPlaced) { + if (bucket.allowVerticalPlacement && !isPlaced && prevPlacement) { + auto prevOrientation = prevPlacement->placedOrientations.find(symbolInstance.crossTileID); + if (prevOrientation != prevPlacement->placedOrientations.end()) { + placedOrientations[symbolInstance.crossTileID] = prevOrientation->second; + } + } + }; + + const auto placeTextForPlacementModes = [&] (auto& placeHorizontalFn, auto& placeVerticalFn) { + if (bucket.allowVerticalPlacement && symbolInstance.writingModes & WritingModeType::Vertical) { + assert(!bucket.placementModes.empty()); + for (auto& placementMode : bucket.placementModes) { + if (placementMode == style::TextWritingModeType::Vertical) { + placedVertical = placed = placeVerticalFn(); + } else { + placed = placeHorizontalFn(); + } + + if (placed.first) { + break; + } + } + } else { + placed = placeHorizontalFn(); + } + }; + + // Line or point label placement if (variableTextAnchors.empty()) { - auto placed = collisionIndex.placeFeature(textCollisionFeature, {}, - posMatrix, textLabelPlaneMatrix, pixelRatio, - placedSymbol, scale, fontSize, - layout.get(), - pitchWithMap, - params.showCollisionBoxes, avoidEdges, collisionGroup.second, textBoxes); + const auto placeFeature = [&] (const CollisionFeature& collisionFeature, style::TextWritingModeType orientation) { + textBoxes.clear(); + auto placedFeature = collisionIndex.placeFeature(collisionFeature, {}, + posMatrix, textLabelPlaneMatrix, pixelRatio, + placedSymbol, scale, fontSize, + layout.get(), + pitchWithMap, + params.showCollisionBoxes, avoidEdges, collisionGroup.second, textBoxes); + if (placedFeature.first) { + placedOrientations.emplace(symbolInstance.crossTileID, orientation); + } + return placedFeature; + }; + + const auto placeHorizontal = [&] { + return placeFeature(symbolInstance.textCollisionFeature, style::TextWritingModeType::Horizontal); + }; + + const auto placeVertical = [&] { + if (bucket.allowVerticalPlacement && symbolInstance.verticalTextCollisionFeature) { + return placeFeature(*symbolInstance.verticalTextCollisionFeature, style::TextWritingModeType::Vertical); + } + return std::pair{false, false}; + }; + + placeTextForPlacementModes(placeHorizontal, placeVertical); + updatePreviousOrientationIfNotPlaced(placed.first); + placeText = placed.first; offscreen &= placed.second; } else if (!textCollisionFeature.alongLine && !textCollisionFeature.boxes.empty()) { - const CollisionBox& textBox = symbolInstance.textCollisionFeature.boxes[0]; - const float width = textBox.x2 - textBox.x1; - const float height = textBox.y2 - textBox.y1; - const float textBoxScale = symbolInstance.textBoxScale; - // If this symbol was in the last placement, shift the previously used // anchor to the front of the anchor list, only if the previous anchor // is still in the anchor list. @@ -220,55 +268,83 @@ void Placement::placeBucket( } } - for (auto anchor : variableTextAnchors) { - Point shift = calculateVariableLayoutOffset(anchor, width, height, symbolInstance.radialTextOffset, textBoxScale); - if (rotateWithMap) { - float angle = pitchWithMap ? state.getBearing() : -state.getBearing(); - shift = util::rotate(shift, angle); - } + const auto placeFeatureForVariableAnchors = [&] (const CollisionFeature& collisionFeature, style::TextWritingModeType orientation) { + const CollisionBox& textBox = collisionFeature.boxes[0]; + const float width = textBox.x2 - textBox.x1; + const float height = textBox.y2 - textBox.y1; + const float textBoxScale = symbolInstance.textBoxScale; + std::pair placedFeature = {false, false}; + for (auto anchor : variableTextAnchors) { + Point shift = calculateVariableLayoutOffset(anchor, width, height, symbolInstance.radialTextOffset, textBoxScale); + if (rotateWithMap) { + float angle = pitchWithMap ? state.getBearing() : -state.getBearing(); + shift = util::rotate(shift, angle); + } + + textBoxes.clear(); + placedFeature = collisionIndex.placeFeature(collisionFeature, shift, + posMatrix, mat4(), pixelRatio, + placedSymbol, scale, fontSize, + layout.get(), + pitchWithMap, + params.showCollisionBoxes, avoidEdges, collisionGroup.second, textBoxes); + if (placedFeature.first) { + assert(symbolInstance.crossTileID != 0u); + optional prevAnchor; + + // If this label was placed in the previous placement, record the anchor position + // to allow us to animate the transition + if (prevPlacement) { + auto prevOffset = prevPlacement->variableOffsets.find(symbolInstance.crossTileID); + auto prevPlacements = prevPlacement->placements.find(symbolInstance.crossTileID); + if (prevOffset != prevPlacement->variableOffsets.end() && + prevPlacements != prevPlacement->placements.end() && + prevPlacements->second.text) { + // TODO: The prevAnchor seems to be unused, needs to be fixed. + prevAnchor = prevOffset->second.anchor; + } + } - auto placed = collisionIndex.placeFeature(textCollisionFeature, shift, - posMatrix, mat4(), pixelRatio, - placedSymbol, scale, fontSize, - layout.get(), - pitchWithMap, - params.showCollisionBoxes, avoidEdges, collisionGroup.second, textBoxes); - - if (placed.first) { - assert(symbolInstance.crossTileID != 0u); - optional prevAnchor; - - // If this label was placed in the previous placement, record the anchor position - // to allow us to animate the transition - if (prevPlacement) { - auto prevOffset = prevPlacement->variableOffsets.find(symbolInstance.crossTileID); - auto prevPlacements = prevPlacement->placements.find(symbolInstance.crossTileID); - if (prevOffset != prevPlacement->variableOffsets.end() && - prevPlacements != prevPlacement->placements.end() && - prevPlacements->second.text) { - prevAnchor = prevOffset->second.anchor; + variableOffsets.insert(std::make_pair(symbolInstance.crossTileID, VariableOffset{ + symbolInstance.radialTextOffset, + width, + height, + anchor, + textBoxScale, + prevAnchor + })); + + if (bucket.allowVerticalPlacement) { + placedOrientations.emplace(symbolInstance.crossTileID, orientation); } + break; } + } + + return placedFeature; + }; - variableOffsets.insert(std::make_pair(symbolInstance.crossTileID, VariableOffset{ - symbolInstance.radialTextOffset, - width, - height, - anchor, - textBoxScale, - prevAnchor - })); - - placeText = placed.first; - offscreen &= placed.second; - break; + const auto placeHorizontal = [&] { + return placeFeatureForVariableAnchors(symbolInstance.textCollisionFeature, style::TextWritingModeType::Horizontal); + }; + + const auto placeVertical = [&] { + if (bucket.allowVerticalPlacement && !placed.first && symbolInstance.verticalTextCollisionFeature) { + return placeFeatureForVariableAnchors(*symbolInstance.verticalTextCollisionFeature, style::TextWritingModeType::Vertical); } - textBoxes.clear(); - } + return std::pair{false, false}; + }; + + placeTextForPlacementModes(placeHorizontal, placeVertical); + + placeText = placed.first; + offscreen &= placed.second; + + updatePreviousOrientationIfNotPlaced(placed.first); // If we didn't get placed, we still need to copy our position from the last placement for // fade animations - if (prevPlacement && variableOffsets.find(symbolInstance.crossTileID) == variableOffsets.end()) { + if (!placeText && prevPlacement) { auto prevOffset = prevPlacement->variableOffsets.find(symbolInstance.crossTileID); if (prevOffset != prevPlacement->variableOffsets.end()) { variableOffsets[symbolInstance.crossTileID] = prevOffset->second; @@ -281,14 +357,14 @@ void Placement::placeBucket( const PlacedSymbol& placedSymbol = bucket.icon.placedSymbols.at(*symbolInstance.placedIconIndex); const float fontSize = evaluateSizeForFeature(partiallyEvaluatedIconSize, placedSymbol); - auto placed = collisionIndex.placeFeature(symbolInstance.iconCollisionFeature, {}, + auto placedIcon = collisionIndex.placeFeature(symbolInstance.iconCollisionFeature, {}, posMatrix, iconLabelPlaneMatrix, pixelRatio, placedSymbol, scale, fontSize, layout.get(), pitchWithMap, params.showCollisionBoxes, avoidEdges, collisionGroup.second, iconBoxes); - placeIcon = placed.first; - offscreen &= placed.second; + placeIcon = placedIcon.first; + offscreen &= placedIcon.second; } const bool iconWithoutText = !symbolInstance.hasText || layout.get(); @@ -304,7 +380,11 @@ void Placement::placeBucket( } if (placeText) { - collisionIndex.insertFeature(symbolInstance.textCollisionFeature, textBoxes, layout.get(), bucket.bucketInstanceId, collisionGroup.first); + if (placedVertical.first && symbolInstance.verticalTextCollisionFeature) { + collisionIndex.insertFeature(*symbolInstance.verticalTextCollisionFeature, textBoxes, layout.get(), bucket.bucketInstanceId, collisionGroup.first); + } else { + collisionIndex.insertFeature(symbolInstance.textCollisionFeature, textBoxes, layout.get(), bucket.bucketInstanceId, collisionGroup.first); + } } if (placeIcon) { @@ -399,6 +479,15 @@ void Placement::commit(TimePoint now) { } } + for (auto& prevOrientation : prevPlacement->placedOrientations) { + const uint32_t crossTileID = prevOrientation.first; + auto foundOrientation = placedOrientations.find(crossTileID); + auto foundOpacity = opacities.find(crossTileID); + if (foundOrientation == placedOrientations.end() && foundOpacity != opacities.end() && !foundOpacity->second.isHidden()) { + placedOrientations[prevOrientation.first] = prevOrientation.second; + } + } + fadeStartTime = placementChanged ? commitTime : prevPlacement->fadeStartTime; } @@ -457,7 +546,8 @@ bool Placement::updateBucketDynamicVertices(SymbolBucket& bucket, const Transfor for (const PlacedSymbol& symbol : bucket.text.placedSymbols) { optional variableOffset; - if (!symbol.hidden && symbol.crossTileID != 0u) { + const bool skipOrientation = bucket.allowVerticalPlacement && !symbol.placedOrientation; + if (!symbol.hidden && symbol.crossTileID != 0u && !skipOrientation) { auto it = variableOffsets.find(symbol.crossTileID); if (it != variableOffsets.end()) { bucket.hasVariablePlacement = true; @@ -506,11 +596,23 @@ bool Placement::updateBucketDynamicVertices(SymbolBucket& bucket, const Transfor } for (std::size_t i = 0; i < symbol.glyphOffsets.size(); ++i) { - addDynamicAttributes(shiftedAnchor, 0, bucket.text.dynamicVertices); + addDynamicAttributes(shiftedAnchor, symbol.angle, bucket.text.dynamicVertices); } } } + result = true; + } else if (bucket.allowVerticalPlacement && bucket.hasTextData()) { + bucket.text.dynamicVertices.clear(); + for (const PlacedSymbol& symbol : bucket.text.placedSymbols) { + if (symbol.hidden || !symbol.placedOrientation) { + hideGlyphs(symbol.glyphOffsets.size(), bucket.text.dynamicVertices); + } else { + for (std::size_t i = 0; i < symbol.glyphOffsets.size(); ++i) { + addDynamicAttributes(symbol.anchorPoint, symbol.angle, bucket.text.dynamicVertices); + } + } + } result = true; } @@ -582,9 +684,18 @@ void Placement::updateBucketOpacities(SymbolBucket& bucket, const TransformState bucket.text.opacityVertices.extend(textOpacityVerticesSize, opacityVertex); - auto offset = variableOffsets.find(symbolInstance.crossTileID); - if (offset != variableOffsets.end()) { - markUsedJustification(bucket, offset->second.anchor, symbolInstance); + style::TextWritingModeType previousOrientation = style::TextWritingModeType::Horizontal; + if (bucket.allowVerticalPlacement) { + auto prevOrientation = placedOrientations.find(symbolInstance.crossTileID); + if (prevOrientation != placedOrientations.end()) { + previousOrientation = prevOrientation->second; + markUsedOrientation(bucket, prevOrientation->second, symbolInstance); + } + } + + auto prevOffset = variableOffsets.find(symbolInstance.crossTileID); + if (prevOffset != variableOffsets.end()) { + markUsedJustification(bucket, prevOffset->second.anchor, symbolInstance, previousOrientation); } } if (symbolInstance.hasIcon) { @@ -654,7 +765,11 @@ void Placement::updateBucketOpacities(SymbolBucket& bucket, const TransformState }; if (bucket.hasCollisionBoxData()) { + // TODO: update collision box opacity based on selected text variant (horizontal | vertical). updateCollisionTextBox(symbolInstance.textCollisionFeature, opacityState.text.placed); + if (bucket.allowVerticalPlacement && symbolInstance.verticalTextCollisionFeature) { + updateCollisionTextBox(*symbolInstance.verticalTextCollisionFeature, opacityState.text.placed); + } updateCollisionBox(symbolInstance.iconCollisionFeature, opacityState.icon.placed); } if (bucket.hasCollisionCircleData()) { @@ -671,7 +786,12 @@ void Placement::updateBucketOpacities(SymbolBucket& bucket, const TransformState } namespace { -optional justificationToIndex(style::TextJustifyType justify, const SymbolInstance& symbolInstance) { +optional justificationToIndex(style::TextJustifyType justify, const SymbolInstance& symbolInstance, style::TextWritingModeType orientation) { + // Vertical symbol has just one justification, style::TextJustifyType::Left. + if (orientation == style::TextWritingModeType::Vertical) { + return symbolInstance.placedVerticalTextIndex; + } + switch(justify) { case style::TextJustifyType::Right: return symbolInstance.placedRightTextIndex; case style::TextJustifyType::Center: return symbolInstance.placedCenterTextIndex; @@ -686,13 +806,13 @@ const style::TextJustifyType justifyTypes[] = {style::TextJustifyType::Right, st } // namespace -void Placement::markUsedJustification(SymbolBucket& bucket, style::TextVariableAnchorType placedAnchor, SymbolInstance& symbolInstance) { +void Placement::markUsedJustification(SymbolBucket& bucket, style::TextVariableAnchorType placedAnchor, SymbolInstance& symbolInstance, style::TextWritingModeType orientation) { style::TextJustifyType anchorJustify = getAnchorJustification(placedAnchor); assert(anchorJustify != style::TextJustifyType::Auto); - const optional& autoIndex = justificationToIndex(anchorJustify, symbolInstance); + const optional& autoIndex = justificationToIndex(anchorJustify, symbolInstance, orientation); for (auto& justify : justifyTypes) { - const optional index = justificationToIndex(justify, symbolInstance); + const optional index = justificationToIndex(justify, symbolInstance, orientation); if (index) { assert(bucket.text.placedSymbols.size() > *index); if (autoIndex && *index != *autoIndex) { @@ -706,6 +826,29 @@ void Placement::markUsedJustification(SymbolBucket& bucket, style::TextVariableA } } +void Placement::markUsedOrientation(SymbolBucket& bucket, style::TextWritingModeType orientation, SymbolInstance& symbolInstance) { + auto horizontal = orientation == style::TextWritingModeType::Horizontal ? + optional(orientation) : nullopt; + auto vertical = orientation == style::TextWritingModeType::Vertical ? + optional(orientation) : nullopt; + + if (symbolInstance.placedRightTextIndex) { + bucket.text.placedSymbols.at(*symbolInstance.placedRightTextIndex).placedOrientation = horizontal; + } + + if (symbolInstance.placedCenterTextIndex && !symbolInstance.singleLine) { + bucket.text.placedSymbols.at(*symbolInstance.placedCenterTextIndex).placedOrientation = horizontal; + } + + if (symbolInstance.placedLeftTextIndex && !symbolInstance.singleLine) { + bucket.text.placedSymbols.at(*symbolInstance.placedLeftTextIndex).placedOrientation = horizontal; + } + + if (symbolInstance.placedVerticalTextIndex) { + bucket.text.placedSymbols.at(*symbolInstance.placedVerticalTextIndex).placedOrientation = vertical; + } +} + float Placement::symbolFadeChange(TimePoint now) const { if (mapMode == MapMode::Continuous && transitionOptions.enablePlacementTransitions && transitionOptions.duration.value_or(util::DEFAULT_TRANSITION_DURATION) > Milliseconds(0)) { diff --git a/src/mbgl/text/placement.hpp b/src/mbgl/text/placement.hpp index 2a6a2e1d6e..33bfbd6527 100644 --- a/src/mbgl/text/placement.hpp +++ b/src/mbgl/text/placement.hpp @@ -12,6 +12,7 @@ namespace mbgl { class SymbolBucket; class SymbolInstance; +enum class PlacedSymbolOrientation : bool; class OpacityState { public: @@ -122,7 +123,8 @@ private: // Returns `true` if bucket vertices were updated; returns `false` otherwise. bool updateBucketDynamicVertices(SymbolBucket&, const TransformState&, const RenderTile& tile) const; void updateBucketOpacities(SymbolBucket&, const TransformState&, std::set&); - void markUsedJustification(SymbolBucket&, style::TextVariableAnchorType, SymbolInstance&); + void markUsedJustification(SymbolBucket&, style::TextVariableAnchorType, SymbolInstance&, style::TextWritingModeType orientation); + void markUsedOrientation(SymbolBucket&, style::TextWritingModeType, SymbolInstance&); CollisionIndex collisionIndex; @@ -135,6 +137,7 @@ private: std::unordered_map placements; std::unordered_map opacities; std::unordered_map variableOffsets; + std::unordered_map placedOrientations; bool stale = false; diff --git a/src/mbgl/text/quads.cpp b/src/mbgl/text/quads.cpp index 6be5d8c01e..9ff26ddd8d 100644 --- a/src/mbgl/text/quads.cpp +++ b/src/mbgl/text/quads.cpp @@ -95,8 +95,10 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, const std::array textOffset, const SymbolLayoutProperties::Evaluated& layout, const style::SymbolPlacementType placement, - const GlyphPositions& positions) { + const GlyphPositions& positions, + bool allowVerticalPlacement) { const float textRotate = layout.get() * util::DEG2RAD; + const bool alongLine = layout.get() == AlignmentType::Map && placement != SymbolPlacementType::Point; SymbolQuads quads; @@ -117,16 +119,23 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, const float rectBuffer = 3.0f + glyphPadding; const float halfAdvance = glyph.metrics.advance * positionedGlyph.scale / 2.0; - const bool alongLine = layout.get() == AlignmentType::Map && placement != SymbolPlacementType::Point; const Point glyphOffset = alongLine ? Point{ positionedGlyph.x + halfAdvance, positionedGlyph.y } : Point{ 0.0f, 0.0f }; - const Point builtInOffset = alongLine ? + Point builtInOffset = alongLine ? Point{ 0.0f, 0.0f } : Point{ positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] }; + Point verticalizedLabelOffset = { 0.0f, 0.0f }; + const bool rotateVerticalGlyph = (alongLine || allowVerticalPlacement) && positionedGlyph.vertical; + if (rotateVerticalGlyph) { + // Vertical POI labels, that are rotated 90deg CW and whose glyphs must preserve upright orientation + // need to be rotated 90deg CCW. After quad is rotated, it is translated to the original built-in offset. + verticalizedLabelOffset = builtInOffset; + builtInOffset = { 0.0f, 0.0f }; + } const float x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset.x; const float y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset.y; @@ -138,22 +147,25 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, Point bl{x1, y2}; Point br{x2, y2}; - if (alongLine && positionedGlyph.vertical) { + if (rotateVerticalGlyph) { // Vertical-supporting glyphs are laid out in 24x24 point boxes (1 square em) // In horizontal orientation, the y values for glyphs are below the midline // and we use a "yOffset" of -17 to pull them up to the middle. // By rotating counter-clockwise around the point at the center of the left // edge of a 24x24 layout box centered below the midline, we align the center // of the glyphs with the horizontal midline, so the yOffset is no longer - // necessary, but we also pull the glyph to the left along the x axis - const Point center{-halfAdvance, halfAdvance}; + // necessary, but we also pull the glyph to the left along the x axis. + // The y coordinate includes baseline yOffset, therefore, needs to be accounted + // for when glyph is rotated and translated. + + const Point center{ -halfAdvance, halfAdvance - Shaping::yOffset }; const float verticalRotation = -M_PI_2; - const Point xOffsetCorrection{5, 0}; + const Point xOffsetCorrection{ 5.0f - Shaping::yOffset, 0.0f }; - tl = util::rotate(tl - center, verticalRotation) + center + xOffsetCorrection; - tr = util::rotate(tr - center, verticalRotation) + center + xOffsetCorrection; - bl = util::rotate(bl - center, verticalRotation) + center + xOffsetCorrection; - br = util::rotate(br - center, verticalRotation) + center + xOffsetCorrection; + tl = util::rotate(tl - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; + tr = util::rotate(tr - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; + bl = util::rotate(bl - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; + br = util::rotate(br - center, verticalRotation) + center + xOffsetCorrection + verticalizedLabelOffset; } if (textRotate) { diff --git a/src/mbgl/text/quads.hpp b/src/mbgl/text/quads.hpp index 0bb892e4d1..1ec68189af 100644 --- a/src/mbgl/text/quads.hpp +++ b/src/mbgl/text/quads.hpp @@ -52,6 +52,7 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, const std::array textOffset, const style::SymbolLayoutProperties::Evaluated&, style::SymbolPlacementType placement, - const GlyphPositions& positions); + const GlyphPositions& positions, + bool allowVerticalPlacement); } // namespace mbgl diff --git a/src/mbgl/text/shaping.cpp b/src/mbgl/text/shaping.cpp index 1d95376b04..132f74661e 100644 --- a/src/mbgl/text/shaping.cpp +++ b/src/mbgl/text/shaping.cpp @@ -292,12 +292,8 @@ void shapeLines(Shaping& shaping, const style::TextJustifyType textJustify, const WritingModeType writingMode, const GlyphMap& glyphMap) { - - // the y offset *should* be part of the font metadata - const int32_t yOffset = -17; - float x = 0; - float y = yOffset; + float y = Shaping::yOffset; float maxLineLength = 0; @@ -342,7 +338,7 @@ void shapeLines(Shaping& shaping, shaping.positionedGlyphs.emplace_back(codePoint, x, y + baselineOffset, false, section.fontStackHash, section.scale, sectionIndex); x += glyph.metrics.advance * section.scale + spacing; } else { - shaping.positionedGlyphs.emplace_back(codePoint, x, baselineOffset, true, section.fontStackHash, section.scale, sectionIndex); + shaping.positionedGlyphs.emplace_back(codePoint, x, y + baselineOffset, true, section.fontStackHash, section.scale, sectionIndex); x += util::ONE_EM * section.scale + spacing; } } @@ -364,7 +360,7 @@ void shapeLines(Shaping& shaping, align(shaping, justify, anchorAlign.horizontalAlign, anchorAlign.verticalAlign, maxLineLength, lineHeight, lines.size()); - const float height = y - yOffset; + const float height = y - Shaping::yOffset; // Calculate the bounding box shaping.top += -anchorAlign.verticalAlign * height; diff --git a/src/mbgl/text/tagged_string.cpp b/src/mbgl/text/tagged_string.cpp index 8c4e3b02e8..e8a1c6f51f 100644 --- a/src/mbgl/text/tagged_string.cpp +++ b/src/mbgl/text/tagged_string.cpp @@ -8,6 +8,7 @@ void TaggedString::addSection(const std::u16string& sectionText, double scale, F styledText.first += sectionText; sections.emplace_back(scale, fontStack, std::move(textColor)); styledText.second.resize(styledText.first.size(), sections.size() - 1); + supportsVerticalWritingMode = nullopt; } void TaggedString::trim() { @@ -37,4 +38,11 @@ void TaggedString::verticalizePunctuation() { styledText.first = util::i18n::verticalizePunctuation(styledText.first); } +bool TaggedString::allowsVerticalWritingMode() { + if (!supportsVerticalWritingMode) { + supportsVerticalWritingMode = util::i18n::allowsVerticalWritingMode(rawText()); + } + return *supportsVerticalWritingMode; +} + } // namespace mbgl diff --git a/src/mbgl/text/tagged_string.hpp b/src/mbgl/text/tagged_string.hpp index 2607e10889..698e539a45 100644 --- a/src/mbgl/text/tagged_string.hpp +++ b/src/mbgl/text/tagged_string.hpp @@ -98,10 +98,12 @@ struct TaggedString { void trim(); void verticalizePunctuation(); + bool allowsVerticalWritingMode(); private: StyledText styledText; std::vector sections; + optional supportsVerticalWritingMode; }; } // namespace mbgl -- cgit v1.2.1