From 1f2c71ec8bc3b228db2d90751a5bae4e269e1dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Ka=CC=88fer?= Date: Mon, 17 Mar 2014 16:53:40 +0100 Subject: [PATCH] port of collision class --- common.gypi | 13 +- configure | 9 + include/llmr/text/collision.hpp | 78 ++++++++ include/llmr/text/rotation_range.hpp | 12 +- include/llmr/util/math.hpp | 20 ++ llmr.gyp | 1 - src/text/collision.cpp | 288 +++++++++++++++++++++++++++ src/text/rotation_range.cpp | 14 +- test/test.gyp | 2 +- 9 files changed, 415 insertions(+), 22 deletions(-) create mode 100644 include/llmr/text/collision.hpp create mode 100644 src/text/collision.cpp diff --git a/common.gypi b/common.gypi index a227e6d2c..049823758 100644 --- a/common.gypi +++ b/common.gypi @@ -9,16 +9,19 @@ 'cflags_cc': ['-std=c++11'], 'configurations': { 'Debug': { - 'cflags': [ '-g', '-O0' ], + 'cflags': [ '-g', '-O0', '-I<(boost_root)/include' ], 'defines': [ 'DEBUG' ], 'xcode_settings': { - 'OTHER_CPLUSPLUSFLAGS': [ '-g'] + 'OTHER_CPLUSPLUSFLAGS': [ '-g', '-I<(boost_root)/include' ] } }, 'Release': { - 'cflags': [ '-O3'], - 'defines': [ 'NDEBUG' ] + 'cflags': [ '-O3', '-I<(boost_root)/include' ], + 'defines': [ 'NDEBUG' ], + 'xcode_settings': { + 'OTHER_CPLUSPLUSFLAGS': [ '-I<(boost_root)/include' ] + } } } } -} \ No newline at end of file +} diff --git a/configure b/configure index e774317a2..b669e2295 100755 --- a/configure +++ b/configure @@ -19,6 +19,11 @@ parser.add_option("--debug", dest="debug", help="Also build debug build") +parser.add_option("--boost", + action="store", + dest="boost_root", + help="Path to boost (defaults to /usr/local)") + parser.add_option("--glfw3", action="store", dest="glfw3", @@ -55,6 +60,10 @@ def pkg_config(pkg): return (libs, cflags) def configure_llmr(o): + if options.boost_root: + o['variables']['boost_root'] = options.boost_root + else: + o['variables']['boost_root'] = '/usr/local' o['target_defaults']['default_configuration'] = 'Debug' if options.debug else 'Release' def configure_glfw3(o): diff --git a/include/llmr/text/collision.hpp b/include/llmr/text/collision.hpp new file mode 100644 index 000000000..d207a0866 --- /dev/null +++ b/include/llmr/text/collision.hpp @@ -0,0 +1,78 @@ +#ifndef LLMR_TEXT_COLLISION +#define LLMR_TEXT_COLLISION + +#include +#include +#include +#include + +#include + +namespace llmr { + +// These are the glyph boxes that we want to have placed. +struct GlyphBox { + GlyphBox() {} + GlyphBox(const CollisionRect &bbox, const CollisionRect &box, + float minScale) + : bbox(bbox), box(box), minScale(minScale) {} + + CollisionAnchor anchor; + CollisionRect bbox; + CollisionRect box; + bool rotate = false; + float minScale = 0.0f; + float maxScale = std::numeric_limits::infinity(); +}; + +typedef std::vector GlyphBoxes; + +// These are the placed boxes contained in the rtree. +struct PlacementBox { + CollisionAnchor anchor; + CollisionRect bbox; + CollisionRect box; + bool rotate = false; + PlacementRange placementRange = {{0.0f, 0.0f}}; + float placementScale = 0.0f; + float maxScale = std::numeric_limits::infinity(); + float padding = 0.0f; +}; + +struct Placement { + Placement() {} + Placement(float zoom, const PlacementRange &rotationRange) + : zoom(zoom), rotationRange(rotationRange) {} + + float zoom = -1.0f; + PlacementRange rotationRange = {{0.0f, 0.0f}}; +}; + +class Collision { + typedef boost::geometry::model::point Point; + typedef boost::geometry::model::box Box; + typedef std::pair PlacementValue; + typedef boost::geometry::index::rtree< + PlacementValue, boost::geometry::index::quadratic<16>> Tree; + + public: + Collision(); + + Placement place(const GlyphBoxes &boxes, const CollisionAnchor &anchor, + float minPlacementScale, float maxPlacementScale, float padding, + bool horizontal); + float getPlacementScale(const GlyphBoxes &glyphs, float minPlacementScale, + float maxPlacementScale, float pad); + PlacementRange getPlacementRange(const GlyphBoxes &glyphs, + float placementScale); + void insert(const GlyphBoxes &glyphs, const CollisionAnchor &anchor, + float placementScale, const PlacementRange &placementRange, + bool horizontal, float padding); + + private: + Tree tree; +}; +} + +#endif \ No newline at end of file diff --git a/include/llmr/text/rotation_range.hpp b/include/llmr/text/rotation_range.hpp index e3d3ab698..bf40c0c6a 100644 --- a/include/llmr/text/rotation_range.hpp +++ b/include/llmr/text/rotation_range.hpp @@ -31,13 +31,6 @@ struct CollisionRect { : tl(tl), br(br) {} }; -struct CollisionBox { - CollisionRect box; - CollisionPoint anchor; - bool rotate = false; - PlacementRange placementRange; -}; - /* * Combine an array of collision ranges to form a continuous * range that includes 0. Collisions within the ignoreRange are ignored @@ -76,8 +69,9 @@ CollisionList rotatingFixedCollisions(const CollisionRect &rotating, /* * Calculate the range a box conflicts with a second box */ -CollisionRange rotationRange(const CollisionBox &inserting, - const CollisionBox &blocker, double scale); +template +CollisionRange rotationRange(const InsertingBox &inserting, + const BlockingBox &blocker, float scale); } #endif diff --git a/include/llmr/util/math.hpp b/include/llmr/util/math.hpp index b5f0ca289..d356cf876 100644 --- a/include/llmr/util/math.hpp +++ b/include/llmr/util/math.hpp @@ -15,11 +15,31 @@ inline T max(T a, T b) { return b > a ? b : a; } +template +inline T max(T a, T b, T c) { + return max(max(a, b), c); +} + +template +inline T max(T a, T b, T c, T d) { + return max(max(a, b), max(c, d)); +} + template inline T min(T a, T b) { return b < a ? b : a; } +template +inline T min(T a, T b, T c) { + return min(min(a, b), c); +} + +template +inline T min(T a, T b, T c, T d) { + return min(min(a, b), min(c, d)); +} + // Find the angle of the two vectors, solving the formula for the cross product // a x b = |a||b|sin(θ) for θ. template diff --git a/llmr.gyp b/llmr.gyp index 412c5a3fe..55d4c774a 100644 --- a/llmr.gyp +++ b/llmr.gyp @@ -2,7 +2,6 @@ 'includes': [ './common.gypi', './config.gypi', - './deps/gtest/gtest.gyp', ], 'targets': [ { diff --git a/src/text/collision.cpp b/src/text/collision.cpp new file mode 100644 index 000000000..750495dc5 --- /dev/null +++ b/src/text/collision.cpp @@ -0,0 +1,288 @@ +#include +#include + +using namespace llmr; + +Collision::Collision() { + const float m = 4096; + + // Hack to prevent cross-tile labels + insert( + {{GlyphBox{ + CollisionRect{CollisionPoint{0, 0}, CollisionPoint{0, m * 8}}, + CollisionRect{CollisionPoint{0, 0}, CollisionPoint{0, m * 8}}, 0}, + GlyphBox{ + CollisionRect{CollisionPoint{0, 0}, CollisionPoint{m * 8, 0}}, + CollisionRect{CollisionPoint{0, 0}, CollisionPoint{m * 8, 0}}, + 0}}}, + CollisionAnchor(0, 0), 1, {{M_PI * 2, 0}}, false, 2); + + insert({{GlyphBox{ + CollisionRect{CollisionPoint{-m * 8, 0}, CollisionPoint{0, 0}}, + CollisionRect{CollisionPoint{-m * 8, 0}, CollisionPoint{0, 0}}, + 0}, + GlyphBox{ + CollisionRect{CollisionPoint{0, -m * 8}, CollisionPoint{0, 0}}, + CollisionRect{CollisionPoint{0, -m * 8}, CollisionPoint{0, 0}}, + 0}}}, + CollisionAnchor{m, m}, 1, {{M_PI * 2, 0}}, false, 2); +} + + +GlyphBox getMergedGlyphs(const GlyphBoxes &glyphs, bool horizontal, const CollisionAnchor& anchor) { + GlyphBox mergedGlyphs; + const float inf = std::numeric_limits::infinity(); + mergedGlyphs.box = {{inf,inf},{-inf,-inf}}; + mergedGlyphs.rotate = horizontal; + mergedGlyphs.anchor = anchor; + + CollisionRect& box = mergedGlyphs.box; + for (const GlyphBox& glyph : glyphs) { + const CollisionRect& gbox = glyph.box; + box.tl.x = util::min(box.tl.x, gbox.tl.x); + box.tl.y = util::min(box.tl.y, gbox.tl.y); + box.br.x = util::max(box.br.x, gbox.br.x); + box.br.y = util::max(box.br.y, gbox.br.y); + mergedGlyphs.minScale = util::max(mergedGlyphs.minScale, glyph.minScale); + } + + return mergedGlyphs; +} + +Placement Collision::place(const GlyphBoxes &boxes, + const CollisionAnchor &anchor, + float minPlacementScale, float maxPlacementScale, + float padding, bool horizontal) { + + float minScale = std::numeric_limits::infinity(); + for (const GlyphBox& box : boxes) { + minScale = util::min(minScale, box.minScale); + } + minPlacementScale = util::max(minPlacementScale, minScale); + + // Collision checks between rotating and fixed labels are + // relatively expensive, so we use one box per label, not per glyph + // for horizontal labels. + GlyphBoxes glyphs; + if (horizontal) { + glyphs.push_back(getMergedGlyphs(boxes, horizontal, anchor)); + } else { + glyphs = boxes; + } + + // Calculate bboxes for all the glyphs + for (GlyphBox &glyph : glyphs) { + if (horizontal) { + const CollisionRect &box = glyph.box; + float x12 = box.tl.x * box.tl.x, y12 = box.tl.y * box.tl.y, + x22 = box.br.x * box.br.x, y22 = box.br.y * box.br.y, + diag = sqrt( + util::max(x12 + y12, x12 + y22, x22 + y12, x22 + y22)); + + glyph.bbox = {{-diag, -diag}, {diag, diag}}; + } else { + glyph.bbox = glyph.box; + } + } + + // Calculate the minimum scale the entire label can be shown without collisions + float scale = getPlacementScale(boxes, minPlacementScale, maxPlacementScale, padding); + + // Return if the label can never be placed without collision + if (scale < 0) return {}; + + // Calculate the range it is safe to rotate all glyphs + PlacementRange rotationRange = getPlacementRange(glyphs, scale); + insert(glyphs, anchor, scale, rotationRange, horizontal, padding); + + float zoom = log(scale) / log(2); + + return {zoom, rotationRange}; +} + + +float Collision::getPlacementScale(const GlyphBoxes &glyphs, + float minPlacementScale, + float maxPlacementScale, float pad) { + + for (const GlyphBox &glyph : glyphs) { + const CollisionRect &bbox = glyph.bbox; + const CollisionRect &box = glyph.box; + const CollisionAnchor &anchor = glyph.anchor; + + if (anchor.x < 0 || anchor.x > 4096 || anchor.y < 0 || + anchor.y > 4096) { + return -1; + } + + float minScale = fmax(minPlacementScale, glyph.minScale); + float maxScale = glyph.maxScale; + + if (minScale >= maxScale) { + continue; + } + + // Compute the scaled bounding box of the unrotated glyph + float minPlacedX = anchor.x + bbox.tl.x / minScale; + float minPlacedY = anchor.y + bbox.tl.y / minScale; + float maxPlacedX = anchor.x + bbox.br.x / minScale; + float maxPlacedY = anchor.y + bbox.br.y / minScale; + + Box query_box{Point{minPlacedX, minPlacedY}, + Point{maxPlacedX, maxPlacedY}}; + + std::vector blocking; + tree.query(boost::geometry::index::intersects(query_box), + std::back_inserter(blocking)); + + if (blocking.size()) { + const CollisionAnchor &na = anchor; // new anchor + const CollisionRect &nb = box; // new box + + for (const PlacementValue &value : blocking) { + const PlacementBox &placement = value.second; + const CollisionAnchor &oa = placement.anchor; // old anchor + const CollisionRect &ob = placement.box; // old box + + // If anchors are identical, we're going to skip the label. + // NOTE: this isn't right because there can be glyphs with + // the same anchor but differing box offsets. + if (na == oa) { + return -1; + } + + // todo: unhardcode the 8 = tileExtent/tileSize + float padding = fmax(pad, placement.padding) * 8.0f; + + // Original algorithm: + float s1 = (ob.tl.x - nb.br.x - padding) / + (na.x - oa.x); // scale at which new box is to the + // left of old box + float s2 = (ob.br.x - nb.tl.x + padding) / + (na.x - oa.x); // scale at which new box is to the + // right of old box + float s3 = (ob.tl.y - nb.br.y - padding) / + (na.y - oa.y); // scale at which new box is to the + // top of old box + float s4 = (ob.br.y - nb.tl.y + padding) / + (na.y - oa.y); // scale at which new box is to the + // bottom of old box + + if (isnan(s1) || isnan(s2)) { + s1 = s2 = 1; + } + if (isnan(s3) || isnan(s4)) { + s3 = s4 = 1; + } + + float collisionFreeScale = fmin(fmax(s1, s2), fmax(s3, s4)); + + // Only update label's min scale if the glyph was + // restricted by a collision + if (collisionFreeScale > minPlacementScale && + collisionFreeScale > minScale && + collisionFreeScale < maxScale && + collisionFreeScale < placement.maxScale) { + minPlacementScale = collisionFreeScale; + } + + if (minPlacementScale > maxPlacementScale) { + return -1; + } + } + } + } + + return minPlacementScale; +} + +PlacementRange Collision::getPlacementRange(const GlyphBoxes &glyphs, + float placementScale) { + PlacementRange placementRange = {{2.0f * M_PI, 0}}; + + for (const GlyphBox &glyph : glyphs) { + const CollisionRect &bbox = glyph.bbox; + const CollisionAnchor &anchor = glyph.anchor; + + float minPlacedX = anchor.x + bbox.tl.x / placementScale; + float minPlacedY = anchor.y + bbox.tl.y / placementScale; + float maxPlacedX = anchor.x + bbox.br.x / placementScale; + float maxPlacedY = anchor.y + bbox.br.y / placementScale; + + Box query_box{Point{minPlacedX, minPlacedY}, + Point{maxPlacedX, maxPlacedY}}; + + std::vector blocking; + tree.query(boost::geometry::index::intersects(query_box), + std::back_inserter(blocking)); + + for (const PlacementValue &value : blocking) { + const Box &s = value.first; + const PlacementBox &b = value.second; + + float x1, x2, y1, y2, intersectX, intersectY; + + // Adjust and compare bboxes to see if the glyphs might intersect + if (placementScale > b.placementScale) { + x1 = b.anchor.x + b.bbox.tl.x / placementScale; + y1 = b.anchor.y + b.bbox.tl.y / placementScale; + x2 = b.anchor.x + b.bbox.br.x / placementScale; + y2 = b.anchor.y + b.bbox.br.y / placementScale; + intersectX = x1 < maxPlacedX && x2 > minPlacedX; + intersectY = y1 < maxPlacedY && y2 > minPlacedY; + } else { + x1 = anchor.x + bbox.tl.x / b.placementScale; + y1 = anchor.y + bbox.tl.y / b.placementScale; + x2 = anchor.x + bbox.br.x / b.placementScale; + y2 = anchor.y + bbox.br.y / b.placementScale; + + intersectX = x1 < s.max_corner().get<0>() && + x2 > s.min_corner().get<0>(); + intersectY = y1 < s.max_corner().get<1>() && + y2 > s.min_corner().get<1>(); + } + + // If they can't intersect, skip more expensive rotation calculation + if (!(intersectX && intersectY)) + continue; + + float scale = fmax(placementScale, b.placementScale); + // TODO? glyph.box or glyph.bbox? + CollisionRange range = rotationRange(glyph, b, scale); + + placementRange[0] = fmin(placementRange[0], range[0]); + placementRange[1] = fmax(placementRange[1], range[1]); + } + } + + return placementRange; +}; + +void Collision::insert(const GlyphBoxes &glyphs, const CollisionAnchor &anchor, + float placementScale, + const PlacementRange &placementRange, bool horizontal, + float padding) { + for (const GlyphBox &glyph : glyphs) { + const CollisionRect &bbox = glyph.bbox; + const CollisionRect &box = glyph.box; + + float minScale = fmax(placementScale, glyph.minScale); + + Box bounds{Point{anchor.x + bbox.tl.x / minScale, + anchor.y + bbox.tl.y / minScale}, + Point{anchor.x + bbox.br.x / minScale, + anchor.y + bbox.br.y / minScale}}; + + PlacementBox placement; + placement.anchor = anchor; + placement.box = box; + placement.bbox = bbox; + placement.rotate = horizontal; + placement.placementRange = placementRange; + placement.placementScale = minScale; + placement.maxScale = glyph.maxScale; + placement.padding = padding; + + tree.insert({bounds, placement}); + } +} diff --git a/src/text/rotation_range.cpp b/src/text/rotation_range.cpp index 496b6108f..d5840edf1 100644 --- a/src/text/rotation_range.cpp +++ b/src/text/rotation_range.cpp @@ -225,16 +225,18 @@ CollisionList rotatingFixedCollisions(const CollisionRect &rotating, /* * Calculate the range a box conflicts with a second box */ -CollisionRange rotationRange(const CollisionBox &inserting, - const CollisionBox &blocker, double scale) { +template +CollisionRange rotationRange(const InsertingBox &inserting, + const BlockingBox &blocker, float scale) { CollisionList collisions; - const CollisionBox &a = inserting; - const CollisionBox &b = blocker; + const InsertingBox &a = inserting; + const BlockingBox &b = blocker; // Instead of scaling the boxes, we move the anchors - CollisionAnchor relativeAnchor{static_cast((b.anchor.x - a.anchor.x) * scale), - static_cast((b.anchor.y - a.anchor.y) * scale)}; + CollisionAnchor relativeAnchor{ + static_cast((b.anchor.x - a.anchor.x) * scale), + static_cast((b.anchor.y - a.anchor.y) * scale)}; // Generate a list of collision interval if (a.rotate && b.rotate) { diff --git a/test/test.gyp b/test/test.gyp index 0cea1a285..b601af08c 100644 --- a/test/test.gyp +++ b/test/test.gyp @@ -13,7 +13,7 @@ "./rotation_range.cpp", ], "dependencies": [ - "../llmr.gyp:gtest", + "../deps/gtest/gtest.gyp:gtest", "../llmr.gyp:llmr-x86" ] },