28 inline float area()
const {
35 float l = std::max(box1.
left, box2.
left);
37 float t = std::max(box1.
top, box2.
top);
39 float w = std::max(0.0f, r - l);
40 float h = std::max(0.0f, b - t);
120 void resize(
int w,
int h,
int gridSize) {
123 int newCols = (w + gridSize - 1) / gridSize;
124 int newRows = (h + gridSize - 1) / gridSize;
125 cellW = (float)gridSize;
126 cellH = (float)gridSize;
130 if (newCols * newRows > (
int)
gridHead.size()) {
131 gridHead.resize(newCols * newRows, -1);
152 for (
int r = r1; r <= r2; ++r) {
153 int rowOffset = r *
cols;
154 for (
int c = c1; c <= c2; ++c) {
155 int idx = rowOffset + c;
165 template <
typename Visitor>
167 int cookie, Visitor &&visitor) {
173 for (
int r = r1; r <= r2; ++r) {
174 int rowOffset = r *
cols;
175 for (
int c = c1; c <= c2; ++c) {
176 int nodeIdx =
gridHead[rowOffset + c];
177 while (nodeIdx != -1) {
178 const auto &node =
nodes[nodeIdx];
179 if (visitedToken[node.id] != cookie) {
180 visitedToken[node.id] = cookie;
235 int selectedRelIndex;
238 float currentTotalCost;
242 int canvasWidth, canvasHeight;
243 std::function<
TextSize(
const std::string &,
int)> measureFunc;
245 std::vector<LayoutItem> items;
246 std::vector<Candidate> candidatePool;
247 std::vector<int> processOrder;
249 std::vector<int> visitedCookie;
250 int currentCookie = 0;
261 template <
typename Func>
264 : config(cfg), canvasWidth(w), canvasHeight(h),
265 measureFunc(std::forward<Func>(func)), rng(12345) {
267 candidatePool.reserve(4096);
268 visitedCookie.reserve(128);
283 candidatePool.clear();
284 processOrder.clear();
294 void add(
float l,
float t,
float r,
float b,
const std::string &text,
297 float cx = (l + r) * 0.5f;
302 float cy = (t + b) * 0.5f;
308 item.id = (int)items.size();
309 item.objectBox = {std::floor(l), std::floor(t), std::ceil(r), std::ceil(b)};
310 item.candStart = (uint32_t)candidatePool.size();
312 generateCandidatesInternal(item, text, baseFontSize);
313 item.candCount = (uint16_t)(candidatePool.size() - item.candStart);
315 if (item.candCount > 0) {
316 item.selectedRelIndex = 0;
317 const auto &c = candidatePool[item.candStart];
318 item.currentBox = c.box;
319 item.currentArea = c.area;
320 item.currentTotalCost = c.geometricCost;
323 dummy.
box = {0, 0, 0, 0};
328 dummy.
fontSize = (int16_t)baseFontSize;
330 candidatePool.push_back(dummy);
332 item.selectedRelIndex = 0;
333 item.currentBox = dummy.
box;
334 item.currentArea = 0.1f;
335 item.currentTotalCost = 1e9f;
337 items.push_back(std::move(item));
345 const size_t N = items.size();
347 if (visitedCookie.size() < N)
348 visitedCookie.resize(N, 0);
349 bool useGrid = (N >= (size_t)config.spatialIndexThreshold);
352 grid.resize(canvasWidth, canvasHeight, config.gridSize);
354 for (
const auto &item : items)
355 grid.insert(item.id, item.objectBox);
358 for (
auto &item : items) {
359 float minCost = std::numeric_limits<float>::max();
362 for (uint32_t i = 0; i < item.candCount; ++i) {
363 Candidate &cand = candidatePool[item.candStart + i];
364 float penalty = 0.0f;
366 auto checkStaticConflict = [&](
int otherId) {
367 const auto &other = items[otherId];
370 penalty += (inter * cand.
invArea) * config.costOccludeObj;
376 grid.query(cand.
box, visitedCookie, currentCookie,
377 checkStaticConflict);
379 for (
const auto &other : items)
380 checkStaticConflict(other.id);
385 if (total < minCost) {
390 item.selectedRelIndex = bestIdx;
391 const auto &bestCand = candidatePool[item.candStart + bestIdx];
392 item.currentBox = bestCand.box;
393 item.currentArea = bestCand.area;
394 item.currentTotalCost = minCost;
397 processOrder.resize(N);
398 for (
size_t i = 0; i < N; ++i)
399 processOrder[i] = (
int)i;
401 for (
int iter = 0; iter < config.maxIterations; ++iter) {
402 std::shuffle(processOrder.begin(), processOrder.end(), rng);
407 for (
const auto &item : items)
408 grid.insert(item.id, item.currentBox);
411 for (
int idx : processOrder) {
412 auto &item = items[idx];
414 auto calculateDynamicCost = [&](
const LayoutBox &box,
float invBoxArea,
415 int selfId) ->
float {
416 float overlapCost = 0.0f;
417 auto visitor = [&](
int otherId) {
418 if (selfId == otherId)
420 const auto &otherBox = items[otherId].currentBox;
423 overlapCost += (inter * invBoxArea) * config.costOverlapBase;
428 grid.query(box, visitedCookie, currentCookie, visitor);
430 for (
size_t j = 0; j < N; ++j)
436 const auto &curCand =
437 candidatePool[item.candStart + item.selectedRelIndex];
439 calculateDynamicCost(item.currentBox, curCand.invArea, item.id);
440 float currentRealTotal =
441 curCand.geometricCost + curCand.staticCost + curDyn;
443 if (currentRealTotal < 1.0f)
446 float bestIterCost = currentRealTotal;
449 for (
int i = 0; i < (int)item.candCount; ++i) {
450 if (i == item.selectedRelIndex)
452 const auto &cand = candidatePool[item.candStart + i];
454 float baseCost = cand.geometricCost + cand.staticCost;
455 if (baseCost >= bestIterCost)
459 calculateDynamicCost(cand.box, cand.invArea, item.id);
460 float newTotal = baseCost + newOverlap;
462 if (newTotal < bestIterCost) {
463 bestIterCost = newTotal;
468 if (bestRelIdx != -1) {
469 item.selectedRelIndex = bestRelIdx;
470 const auto &newCand = candidatePool[item.candStart + bestRelIdx];
471 item.currentBox = newCand.box;
472 item.currentArea = newCand.area;
476 if (changeCount == 0)
484 std::vector<LayoutResult> results;
485 results.reserve(items.size());
486 for (
const auto &item : items) {
487 const auto &cand = candidatePool[item.candStart + item.selectedRelIndex];
488 results.push_back({cand.box.left, cand.box.top, (int)cand.fontSize,
489 (
int)cand.box.width(), (int)cand.box.height(),
490 (int)cand.textAscent});
496 void generateCandidatesInternal(LayoutItem &item,
const std::string &text,
498 static const struct {
501 } levels[] = {{1.0f, 0}, {0.9f, 1}, {0.8f, 2}, {0.75f, 3}};
503 const auto &obj = item.objectBox;
504 for (
const auto &lvl : levels) {
505 int fontSize = (int)(baseFontSize * lvl.scale);
509 TextSize ts = measureFunc(text, fontSize);
510 float fW = std::ceil((
float)ts.width + config.paddingX * 2);
512 std::ceil((
float)(ts.height + ts.baseline + config.paddingY * 2));
513 float scalePenalty = lvl.tier * config.costScaleTier;
514 float area = fW * fH;
515 float invArea = 1.0f / (area > 0.1f ? area : 1.0f);
517 auto addCand = [&](
float x,
float y,
float posCost) {
518 if (x < 0 || y < 0 || x + fW > canvasWidth || y + fH > canvasHeight)
520 candidatePool.emplace_back();
521 auto &c = candidatePool.back();
522 c.box = {x, y, x + fW, y + fH};
523 c.geometricCost = posCost + scalePenalty;
527 c.fontSize = (int16_t)fontSize;
528 c.textAscent = (int16_t)ts.height;
532 addCand(obj.left, obj.top - fH, config.costPos1_Top);
534 addCand(obj.right, obj.top, config.costPos2_Right);
536 addCand(obj.left, obj.bottom, config.costPos3_Bottom);
538 addCand(obj.left - fW, obj.top, config.costPos4_Left);
540 const float baseSlidePenalty = config.costSlidingPenalty;
542 auto getDynamicSteps = [](
float rangeSize) {
543 return std::clamp((
int)(rangeSize / 40.0f), 3, 15);
546 float rangeX = std::max(0.0f, obj.right - fW - obj.left);
548 int stepsX = getDynamicSteps(rangeX);
549 float invStepsX = 1.0f / (float)stepsX;
550 for (
int i = 1; i < stepsX; ++i) {
551 float r = i * invStepsX;
552 float x = obj.left + rangeX * r;
553 float penalty = baseSlidePenalty + (r * 10.0f);
554 addCand(x, obj.top - fH, config.costPos1_Top + penalty);
555 addCand(x, obj.bottom, config.costPos3_Bottom + penalty);
559 float rangeY = std::max(0.0f, obj.bottom - fH - obj.top);
561 int stepsY = getDynamicSteps(rangeY);
562 float invStepsY = 1.0f / (float)stepsY;
563 for (
int i = 1; i < stepsY; ++i) {
564 float r = i * invStepsY;
565 float y = obj.top + rangeY * r;
566 float penalty = baseSlidePenalty + (r * 10.0f);
567 addCand(obj.right, y, config.costPos2_Right + penalty);
568 addCand(obj.left - fW, y, config.costPos4_Left + penalty);
LabelLayoutSolver(int w, int h, Func &&func, const LayoutConfig &cfg=LayoutConfig())
Construct a solver for a canvas of the given dimensions.
void solve()
Run the iterative placement optimisation.
std::vector< LayoutResult > getResults() const
Retrieve the placement result for each label in insertion order.
void add(float l, float t, float r, float b, const std::string &text, int baseFontSize)
Register a new label to be placed.
void setCanvasSize(int w, int h)
Change the canvas dimensions (e.g. after a resize).
void clear()
Remove all items from the solver (allows reuse across frames).
void setConfig(const LayoutConfig &cfg)
Update the layout configuration.
Internal representation of a single placement candidate for a label.
float area
Box area (cached for performance).
int16_t fontSize
Font size for this candidate.
float geometricCost
Cost based on anchor preference and font scale.
int16_t textAscent
Text ascent (for putText offset calculation).
float staticCost
Cost from overlap with object boxes (computed once).
float invArea
Reciprocal of area (cached for performance).
LayoutBox box
Candidate label box on the canvas.
Axis-aligned 2-D bounding box used internally by the label layout solver.
float top
Top edge y-coordinate.
float left
Left edge x-coordinate.
static bool intersects(const LayoutBox &box1, const LayoutBox &box2)
Returns true when two LayoutBoxes overlap.
float area() const
Returns the area of the box (clamped to 0 for inverted boxes).
float bottom
Bottom edge y-coordinate.
float right
Right edge x-coordinate.
static float intersectArea(const LayoutBox &box1, const LayoutBox &box2)
Computes the intersection area of two LayoutBoxes.
float height() const
Returns the height of the box.
float width() const
Returns the width of the box.
Tunable parameters controlling the label placement algorithm.
float costPos4_Left
Base geometric cost for anchor position 4 (left of object).
float costPos2_Right
Base geometric cost for anchor position 2 (right of object).
float costPos1_Top
Base geometric cost for anchor position 1 (top of object — preferred).
int spatialIndexThreshold
Minimum number of items before a grid index is used.
float costSlidingPenalty
Extra cost for a sliding (non-anchor) candidate. Must be > costPos4_Left.
float costOverlapBase
Base penalty per unit overlap with another label.
int gridSize
Spatial-index cell size in pixels.
int paddingY
Vertical padding around each label in pixels.
float costOccludeObj
Penalty per unit overlap with an object bounding box.
int maxIterations
Maximum refinement iterations (higher = better quality).
int paddingX
Horizontal padding around each label in pixels.
float costPos3_Bottom
Base geometric cost for anchor position 3 (bottom of object).
float costScaleTier
Cost multiplier per font-size scale tier (keep large to prefer position changes over size reduction).
Final placement result for a single label.
float x
Left edge of the placed label box.
int fontSize
Font size chosen for this label (may be scaled down).
int height
Height of the label box in pixels.
int textAscent
Ascent value to use when calling putText.
float y
Top edge of the placed label box.
int width
Width of the label box in pixels.
Measured bounding box of a rendered text string.
int baseline
Descent below the baseline in pixels.
int height
Rendered string height (ascent) in pixels.
int width
Rendered string width in pixels.