32 inline float area()
const {
39 float l = std::max(box1.
left, box2.
left);
41 float t = std::max(box1.
top, box2.
top);
43 float w = std::max(0.0f, r - l);
44 float h = std::max(0.0f, b - t);
124 void resize(
int w,
int h,
int gridSize) {
127 int newCols = (w + gridSize - 1) / gridSize;
128 int newRows = (h + gridSize - 1) / gridSize;
129 cellW = (float)gridSize;
130 cellH = (float)gridSize;
134 if (newCols * newRows > (
int)
gridHead.size()) {
135 gridHead.resize(newCols * newRows, -1);
156 for (
int r = r1; r <= r2; ++r) {
157 int rowOffset = r *
cols;
158 for (
int c = c1; c <= c2; ++c) {
159 int idx = rowOffset + c;
169 template <
typename Visitor>
171 int cookie, Visitor &&visitor) {
177 for (
int r = r1; r <= r2; ++r) {
178 int rowOffset = r *
cols;
179 for (
int c = c1; c <= c2; ++c) {
180 int nodeIdx =
gridHead[rowOffset + c];
181 while (nodeIdx != -1) {
182 const auto &node =
nodes[nodeIdx];
183 if (visitedToken[node.id] != cookie) {
184 visitedToken[node.id] = cookie;
239 int selectedRelIndex;
242 float currentTotalCost;
246 int canvasWidth, canvasHeight;
247 std::function<
TextSize(
const std::string &,
int)> measureFunc;
249 std::vector<LayoutItem> items;
250 std::vector<Candidate> candidatePool;
251 std::vector<int> processOrder;
253 std::vector<int> visitedCookie;
254 int currentCookie = 0;
265 template <
typename Func>
268 : config(cfg), canvasWidth(w), canvasHeight(h),
269 measureFunc(std::forward<Func>(func)), rng(12345) {
271 candidatePool.reserve(4096);
272 visitedCookie.reserve(128);
287 candidatePool.clear();
288 processOrder.clear();
298 void add(
float l,
float t,
float r,
float b,
const std::string &text,
301 float cx = (l + r) * 0.5f;
306 float cy = (t + b) * 0.5f;
312 item.id = (int)items.size();
313 item.objectBox = {std::floor(l), std::floor(t), std::ceil(r), std::ceil(b)};
314 item.candStart = (uint32_t)candidatePool.size();
316 generateCandidatesInternal(item, text, baseFontSize);
317 item.candCount = (uint16_t)(candidatePool.size() - item.candStart);
319 if (item.candCount > 0) {
320 item.selectedRelIndex = 0;
321 const auto &c = candidatePool[item.candStart];
322 item.currentBox = c.box;
323 item.currentArea = c.area;
324 item.currentTotalCost = c.geometricCost;
327 dummy.
box = {0, 0, 0, 0};
332 dummy.
fontSize = (int16_t)baseFontSize;
334 candidatePool.push_back(dummy);
336 item.selectedRelIndex = 0;
337 item.currentBox = dummy.
box;
338 item.currentArea = 0.1f;
339 item.currentTotalCost = 1e9f;
341 items.push_back(std::move(item));
349 const size_t N = items.size();
351 if (visitedCookie.size() < N)
352 visitedCookie.resize(N, 0);
353 bool useGrid = (N >= (size_t)config.spatialIndexThreshold);
356 grid.resize(canvasWidth, canvasHeight, config.gridSize);
358 for (
const auto &item : items)
359 grid.insert(item.id, item.objectBox);
362 for (
auto &item : items) {
363 float minCost = std::numeric_limits<float>::max();
366 for (uint32_t i = 0; i < item.candCount; ++i) {
367 Candidate &cand = candidatePool[item.candStart + i];
368 float penalty = 0.0f;
370 auto checkStaticConflict = [&](
int otherId) {
371 const auto &other = items[otherId];
374 penalty += (inter * cand.
invArea) * config.costOccludeObj;
380 grid.query(cand.
box, visitedCookie, currentCookie,
381 checkStaticConflict);
383 for (
const auto &other : items)
384 checkStaticConflict(other.id);
389 if (total < minCost) {
394 item.selectedRelIndex = bestIdx;
395 const auto &bestCand = candidatePool[item.candStart + bestIdx];
396 item.currentBox = bestCand.box;
397 item.currentArea = bestCand.area;
398 item.currentTotalCost = minCost;
401 processOrder.resize(N);
402 for (
size_t i = 0; i < N; ++i)
403 processOrder[i] = (
int)i;
405 for (
int iter = 0; iter < config.maxIterations; ++iter) {
406 std::shuffle(processOrder.begin(), processOrder.end(), rng);
411 for (
const auto &item : items)
412 grid.insert(item.id, item.currentBox);
415 for (
int idx : processOrder) {
416 auto &item = items[idx];
418 auto calculateDynamicCost = [&](
const LayoutBox &box,
float invBoxArea,
419 int selfId) ->
float {
420 float overlapCost = 0.0f;
421 auto visitor = [&](
int otherId) {
422 if (selfId == otherId)
424 const auto &otherBox = items[otherId].currentBox;
427 overlapCost += (inter * invBoxArea) * config.costOverlapBase;
432 grid.query(box, visitedCookie, currentCookie, visitor);
434 for (
size_t j = 0; j < N; ++j)
440 const auto &curCand =
441 candidatePool[item.candStart + item.selectedRelIndex];
443 calculateDynamicCost(item.currentBox, curCand.invArea, item.id);
444 float currentRealTotal =
445 curCand.geometricCost + curCand.staticCost + curDyn;
447 if (currentRealTotal < 1.0f)
450 float bestIterCost = currentRealTotal;
453 for (
int i = 0; i < (int)item.candCount; ++i) {
454 if (i == item.selectedRelIndex)
456 const auto &cand = candidatePool[item.candStart + i];
458 float baseCost = cand.geometricCost + cand.staticCost;
459 if (baseCost >= bestIterCost)
463 calculateDynamicCost(cand.box, cand.invArea, item.id);
464 float newTotal = baseCost + newOverlap;
466 if (newTotal < bestIterCost) {
467 bestIterCost = newTotal;
472 if (bestRelIdx != -1) {
473 item.selectedRelIndex = bestRelIdx;
474 const auto &newCand = candidatePool[item.candStart + bestRelIdx];
475 item.currentBox = newCand.box;
476 item.currentArea = newCand.area;
480 if (changeCount == 0)
488 std::vector<LayoutResult> results;
489 results.reserve(items.size());
490 for (
const auto &item : items) {
491 const auto &cand = candidatePool[item.candStart + item.selectedRelIndex];
492 results.push_back({cand.box.left, cand.box.top, (int)cand.fontSize,
493 (
int)cand.box.width(), (int)cand.box.height(),
494 (int)cand.textAscent});
500 void generateCandidatesInternal(LayoutItem &item,
const std::string &text,
502 static const struct {
505 } levels[] = {{1.0f, 0}, {0.9f, 1}, {0.8f, 2}, {0.75f, 3}};
507 const auto &obj = item.objectBox;
508 for (
const auto &lvl : levels) {
509 int fontSize = (int)(baseFontSize * lvl.scale);
513 TextSize ts = measureFunc(text, fontSize);
514 float fW = std::ceil((
float)ts.width + config.paddingX * 2);
516 std::ceil((
float)(ts.height + ts.baseline + config.paddingY * 2));
517 float scalePenalty = lvl.tier * config.costScaleTier;
518 float area = fW * fH;
519 float invArea = 1.0f / (area > 0.1f ? area : 1.0f);
521 auto addCand = [&](
float x,
float y,
float posCost) {
522 if (x < 0 || y < 0 || x + fW > canvasWidth || y + fH > canvasHeight)
524 candidatePool.emplace_back();
525 auto &c = candidatePool.back();
526 c.box = {x, y, x + fW, y + fH};
527 c.geometricCost = posCost + scalePenalty;
531 c.fontSize = (int16_t)fontSize;
532 c.textAscent = (int16_t)ts.height;
536 addCand(obj.left, obj.top - fH, config.costPos1_Top);
538 addCand(obj.right, obj.top, config.costPos2_Right);
540 addCand(obj.left, obj.bottom, config.costPos3_Bottom);
542 addCand(obj.left - fW, obj.top, config.costPos4_Left);
544 const float baseSlidePenalty = config.costSlidingPenalty;
546 auto getDynamicSteps = [](
float rangeSize) {
547 return std::clamp((
int)(rangeSize / 40.0f), 3, 15);
550 float rangeX = std::max(0.0f, obj.right - fW - obj.left);
552 int stepsX = getDynamicSteps(rangeX);
553 float invStepsX = 1.0f / (float)stepsX;
554 for (
int i = 1; i < stepsX; ++i) {
555 float r = i * invStepsX;
556 float x = obj.left + rangeX * r;
557 float penalty = baseSlidePenalty + (r * 10.0f);
558 addCand(x, obj.top - fH, config.costPos1_Top + penalty);
559 addCand(x, obj.bottom, config.costPos3_Bottom + penalty);
563 float rangeY = std::max(0.0f, obj.bottom - fH - obj.top);
565 int stepsY = getDynamicSteps(rangeY);
566 float invStepsY = 1.0f / (float)stepsY;
567 for (
int i = 1; i < stepsY; ++i) {
568 float r = i * invStepsY;
569 float y = obj.top + rangeY * r;
570 float penalty = baseSlidePenalty + (r * 10.0f);
571 addCand(obj.right, y, config.costPos2_Right + penalty);
572 addCand(obj.left - fW, y, config.costPos4_Left + penalty);
std::vector< LayoutResult > getResults() const
Retrieve the placement result for each label in insertion order.
void solve()
Run the iterative placement optimisation.
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 setConfig(const LayoutConfig &cfg)
Update the layout configuration.
void clear()
Remove all items from the solver (allows reuse across frames).
LabelLayoutSolver(int w, int h, Func &&func, const LayoutConfig &cfg=LayoutConfig())
Construct a solver for a canvas of the given dimensions.
Internal representation of a single placement candidate for a label.
float invArea
Reciprocal of area (cached for performance).
float geometricCost
Cost based on anchor preference and font scale.
LayoutBox box
Candidate label box on the canvas.
float area
Box area (cached for performance).
int16_t fontSize
Font size for this candidate.
int16_t textAscent
Text ascent (for putText offset calculation).
float staticCost
Cost from overlap with object boxes (computed once).
Axis-aligned 2-D bounding box used internally by the label layout solver.
float width() const
Returns the width of the box.
float height() const
Returns the height of the box.
static float intersectArea(const LayoutBox &box1, const LayoutBox &box2)
Computes the intersection area of two LayoutBoxes.
float area() const
Returns the area of the box (clamped to 0 for inverted boxes).
static bool intersects(const LayoutBox &box1, const LayoutBox &box2)
Returns true when two LayoutBoxes overlap.
float bottom
Bottom edge y-coordinate.
float top
Top edge y-coordinate.
float left
Left edge x-coordinate.
float right
Right edge x-coordinate.
Tunable parameters controlling the label placement algorithm.
float costScaleTier
Cost multiplier per font-size scale tier (keep large to prefer position changes over size reduction).
float costOverlapBase
Base penalty per unit overlap with another label.
int paddingY
Vertical padding around each label in pixels.
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.
int maxIterations
Maximum refinement iterations (higher = better quality).
int gridSize
Spatial-index cell size in pixels.
float costPos2_Right
Base geometric cost for anchor position 2 (right of object).
float costSlidingPenalty
Extra cost for a sliding (non-anchor) candidate. Must be > costPos4_Left.
float costPos4_Left
Base geometric cost for anchor position 4 (left of object).
float costOccludeObj
Penalty per unit overlap with an object bounding box.
float costPos3_Bottom
Base geometric cost for anchor position 3 (bottom of object).
int paddingX
Horizontal padding around each label in pixels.
Final placement result for a single label.
float y
Top edge of the placed label box.
int textAscent
Ascent value to use when calling putText.
int height
Height of the label box in pixels.
int width
Width of the label box in pixels.
float x
Left edge of the placed label box.
int fontSize
Font size chosen for this label (may be scaled down).
Measured bounding box of a rendered text string.
int width
Rendered string width in pixels.
int baseline
Descent below the baseline in pixels.
int height
Rendered string height (ascent) in pixels.