std::pair<bool,bool> CollisionIndex::placeFeature(CollisionFeature& feature, const mat4& posMatrix, const mat4& labelPlaneMatrix, const float textPixelRatio, PlacedSymbol& symbol, const float scale, const float fontSize, const bool allowOverlap, const bool pitchWithMap, const bool collisionDebug) { if (!feature.alongLine) { CollisionBox& box = feature.boxes.front(); const auto projectedPoint = projectAndGetPerspectiveRatio(posMatrix, box.anchor); const float tileToViewport = textPixelRatio * projectedPoint.second; box.px1 = box.x1 * tileToViewport + projectedPoint.first.x; box.py1 = box.y1 * tileToViewport + projectedPoint.first.y; box.px2 = box.x2 * tileToViewport + projectedPoint.first.x; box.py2 = box.y2 * tileToViewport + projectedPoint.first.y; if (!isInsideGrid(box) || (!allowOverlap && collisionGrid.hitTest({{ box.px1, box.py1 }, { box.px2, box.py2 }}))) { return { false, false }; } return {true, isOffscreen(box)}; } else { return placeLineFeature(feature, posMatrix, labelPlaneMatrix, textPixelRatio, symbol, scale, fontSize, allowOverlap, pitchWithMap, collisionDebug); } }
bool Crawler::update(float dt, TileMap *map) { if (inGravField) { animate(dt); return updateGravity(dt); } else if (frozen) updateFreeze(dt); else { animate(dt); updatePatrol(dt, map); if (onFire) return updateFlame(dt); if (isOffscreen()) { despawn(); return true; } } return false; }
std::pair<bool,bool> CollisionIndex::placeLineFeature(CollisionFeature& feature, const mat4& posMatrix, const mat4& labelPlaneMatrix, const float textPixelRatio, PlacedSymbol& symbol, const float scale, const float fontSize, const bool allowOverlap, const bool pitchWithMap, const bool collisionDebug) { const auto tileUnitAnchorPoint = symbol.anchorPoint; const auto projectedAnchor = projectAnchor(posMatrix, tileUnitAnchorPoint); const float fontScale = fontSize / 24; const float lineOffsetX = symbol.lineOffset[0] * fontSize; const float lineOffsetY = symbol.lineOffset[1] * fontSize; const auto labelPlaneAnchorPoint = project(tileUnitAnchorPoint, labelPlaneMatrix).first; const auto firstAndLastGlyph = placeFirstAndLastGlyph( fontScale, lineOffsetX, lineOffsetY, /*flip*/ false, labelPlaneAnchorPoint, tileUnitAnchorPoint, symbol, labelPlaneMatrix, /*return tile distance*/ true); bool collisionDetected = false; bool inGrid = false; bool entirelyOffscreen = true; const auto tileToViewport = projectedAnchor.first * textPixelRatio; // pixelsToTileUnits is used for translating line geometry to tile units // ... so we care about 'scale' but not 'perspectiveRatio' // equivalent to pixel_to_tile_units const auto pixelsToTileUnits = 1 / (textPixelRatio * scale); float firstTileDistance = 0, lastTileDistance = 0; if (firstAndLastGlyph) { firstTileDistance = approximateTileDistance(*(firstAndLastGlyph->first.tileDistance), firstAndLastGlyph->first.angle, pixelsToTileUnits, projectedAnchor.second, pitchWithMap); lastTileDistance = approximateTileDistance(*(firstAndLastGlyph->second.tileDistance), firstAndLastGlyph->second.angle, pixelsToTileUnits, projectedAnchor.second, pitchWithMap); } bool atLeastOneCirclePlaced = false; for (size_t i = 0; i < feature.boxes.size(); i++) { CollisionBox& circle = feature.boxes[i]; const float boxSignedDistanceFromAnchor = circle.signedDistanceFromAnchor; if (!firstAndLastGlyph || (boxSignedDistanceFromAnchor < -firstTileDistance) || (boxSignedDistanceFromAnchor > lastTileDistance)) { // The label either doesn't fit on its line or we // don't need to use this circle because the label // doesn't extend this far. Either way, mark the circle unused. circle.used = false; continue; } const auto projectedPoint = projectPoint(posMatrix, circle.anchor); const float tileUnitRadius = (circle.x2 - circle.x1) / 2; const float radius = tileUnitRadius * tileToViewport; if (atLeastOneCirclePlaced) { const CollisionBox& previousCircle = feature.boxes[i - 1]; const float dx = projectedPoint.x - previousCircle.px; const float dy = projectedPoint.y - previousCircle.py; // The circle edges touch when the distance between their centers is 2x the radius // When the distance is 1x the radius, they're doubled up, and we could remove // every other circle while keeping them all in touch. // We actually start removing circles when the distance is √2x the radius: // thinning the number of circles as much as possible is a major performance win, // and the small gaps introduced don't make a very noticeable difference. const bool placedTooDensely = radius * radius * 2 > dx * dx + dy * dy; if (placedTooDensely) { const bool atLeastOneMoreCircle = (i + 1) < feature.boxes.size(); if (atLeastOneMoreCircle) { const CollisionBox& nextCircle = feature.boxes[i + 1]; const float nextBoxDistanceFromAnchor = nextCircle.signedDistanceFromAnchor; if ((nextBoxDistanceFromAnchor > -firstTileDistance) && (nextBoxDistanceFromAnchor < lastTileDistance)) { // Hide significantly overlapping circles, unless this is the last one we can // use, in which case we want to keep it in place even if it's tightly packed // with the one before it. circle.used = false; continue; } } } } atLeastOneCirclePlaced = true; circle.px1 = projectedPoint.x - radius; circle.px2 = projectedPoint.x + radius; circle.py1 = projectedPoint.y - radius; circle.py2 = projectedPoint.y + radius; circle.used = true; circle.px = projectedPoint.x; circle.py = projectedPoint.y; circle.radius = radius; entirelyOffscreen &= isOffscreen(circle); inGrid |= isInsideGrid(circle); if (!allowOverlap) { if (collisionGrid.hitTest({{circle.px, circle.py}, circle.radius})) { if (!collisionDebug) { return {false, false}; } else { // Don't early exit if we're showing the debug circles because we still want to calculate // which circles are in use collisionDetected = true; } } } } return {!collisionDetected && firstAndLastGlyph && inGrid, entirelyOffscreen}; }