port of collision class
This commit is contained in:
13
common.gypi
13
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' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
configure
vendored
9
configure
vendored
@@ -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):
|
||||
|
||||
78
include/llmr/text/collision.hpp
Normal file
78
include/llmr/text/collision.hpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#ifndef LLMR_TEXT_COLLISION
|
||||
#define LLMR_TEXT_COLLISION
|
||||
|
||||
#include <boost/geometry.hpp>
|
||||
#include <boost/geometry/geometries/point.hpp>
|
||||
#include <boost/geometry/geometries/box.hpp>
|
||||
#include <boost/geometry/index/rtree.hpp>
|
||||
|
||||
#include <llmr/text/rotation_range.hpp>
|
||||
|
||||
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<float>::infinity();
|
||||
};
|
||||
|
||||
typedef std::vector<GlyphBox> 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<float>::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<float, 2,
|
||||
boost::geometry::cs::cartesian> Point;
|
||||
typedef boost::geometry::model::box<Point> Box;
|
||||
typedef std::pair<Box, PlacementBox> 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
|
||||
@@ -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 <typename InsertingBox, typename BlockingBox>
|
||||
CollisionRange rotationRange(const InsertingBox &inserting,
|
||||
const BlockingBox &blocker, float scale);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,11 +15,31 @@ inline T max(T a, T b) {
|
||||
return b > a ? b : a;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T max(T a, T b, T c) {
|
||||
return max(max(a, b), c);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T max(T a, T b, T c, T d) {
|
||||
return max(max(a, b), max(c, d));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T min(T a, T b) {
|
||||
return b < a ? b : a;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline T min(T a, T b, T c) {
|
||||
return min(min(a, b), c);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
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 <typename T = double, typename S>
|
||||
|
||||
1
llmr.gyp
1
llmr.gyp
@@ -2,7 +2,6 @@
|
||||
'includes': [
|
||||
'./common.gypi',
|
||||
'./config.gypi',
|
||||
'./deps/gtest/gtest.gyp',
|
||||
],
|
||||
'targets': [
|
||||
{
|
||||
|
||||
288
src/text/collision.cpp
Normal file
288
src/text/collision.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include <llmr/text/collision.hpp>
|
||||
#include <llmr/util/math.hpp>
|
||||
|
||||
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<float>::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<float>::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<PlacementValue> 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<PlacementValue> 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});
|
||||
}
|
||||
}
|
||||
@@ -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 <typename InsertingBox, typename BlockingBox>
|
||||
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<float>((b.anchor.x - a.anchor.x) * scale),
|
||||
static_cast<float>((b.anchor.y - a.anchor.y) * scale)};
|
||||
CollisionAnchor relativeAnchor{
|
||||
static_cast<float>((b.anchor.x - a.anchor.x) * scale),
|
||||
static_cast<float>((b.anchor.y - a.anchor.y) * scale)};
|
||||
|
||||
// Generate a list of collision interval
|
||||
if (a.rotate && b.rotate) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"./rotation_range.cpp",
|
||||
],
|
||||
"dependencies": [
|
||||
"../llmr.gyp:gtest",
|
||||
"../deps/gtest/gtest.gyp:gtest",
|
||||
"../llmr.gyp:llmr-x86"
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user