--- Dear ImGui -- This is a Lua port of original `imstb_rectpack.h` -- ALL TABLES IN THIS FILE ARE 1-BASED! -- Avoids table creation when possible -- [DEAR IMGUI] -- This is a slightly modified version of stb_rect_pack.h 1.01. -- Grep for [DEAR IMGUI] to find the changes. -- -- stb_rect_pack.h - v1.01 - public domain - rectangle packing -- Sean Barrett 2014 -- -- Useful for e.g. packing rectangular textures into an atlas. -- Does not do rotation. -- -- Not necessarily the awesomest packing method, but better than -- the totally naive one in stb_truetype (which is primarily what -- this is meant to replace). -- -- Has only had a few tests run, may have issues. -- -- More docs to come. -- -- Uses table.sort and assert (STBRP_SORT and STBRP_ASSERT below). -- -- This library currently uses the Skyline Bottom-Left algorithm. -- -- Please note: better rectangle packers are welcome! Please -- implement them to the same API, but with a different init -- function. -- -- Credits -- -- Library -- Sean Barrett -- Minor features -- Martins Mozeiko -- github:IntellectualKitty -- -- Bugfixes / warning fixes -- Jeremy Jaussaud -- Fabian Giesen -- -- Version history: -- -- 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section -- 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles -- 0.99 (2019-02-07) warning fixes -- 0.11 (2017-03-03) return packing success/fail result -- 0.10 (2016-10-25) remove cast-away-const to avoid warnings -- 0.09 (2016-08-27) fix compiler warnings -- 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0) -- 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0) -- 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort -- 0.05: added STBRP_ASSERT to allow replacing assert -- 0.04: fixed minor bug in STBRP_LARGE_RECTS support -- 0.01: initial release -- -- LICENSE -- -- See end of file for license information. --- The `prev_link` can be stbrp_context or stbrp_node --- @diagnostic disable local bit32 = bit32 local STBRP_SORT = table.sort local STBRP_ASSERT = assert local STBRP__NOTUSED = function(_) end ------------------------------------------------------------------------------- -- -- INCLUDE SECTION -- local STBRP__MAXVAL = 0x7fffffff -- Mostly for internal use, but this is the maximum supported coordinate value. --- @enum STBRP_HEURISTIC_Skyline local STBRP_HEURISTIC_Skyline = { default = 0, BL_sortHeight = 0, BF_sortHeight = 1 } local STBRP__INIT_skyline = 1 --- @enum STBRP__PREV_LINK_TYPE local STBRP__PREV_LINK_TYPE = { ACTIVE_HEAD = 0, NEXT = 1 } --- @alias stbrp_coord int --- @class stbrp_rect --- @field id int --- @field w stbrp_coord --- @field h stbrp_coord --- @field x stbrp_coord --- @field y stbrp_coord --- @field was_packed int --- @return stbrp_rect --- @nodiscard local function stbrp_rect() return { -- reserved for your use: id = 0, -- input: w = 0, h = 0, -- output: x = 0, y = 0, was_packed = 0 -- non-zero if valid packing } end --- @class stbrp_node --- @field x stbrp_coord --- @field y stbrp_coord --- @field next stbrp_node --- @return stbrp_node --- @nodiscard local function stbrp_node() return { x = 0, y = 0, next = nil } end --- @class stbrp_context --- @field width int --- @field height int --- @field align int --- @field init_mode int --- @field heuristic int --- @field num_nodes int --- @field active_head stbrp_node --- @field free_head stbrp_node --- @field extra stbrp_node[] --- @return stbrp_context --- @nodiscard local function stbrp_context() return { width = 0, height = 0, align = 0, init_mode = 0, heuristic = 0, num_nodes = 0, active_head = nil, free_head = nil, extra = {stbrp_node(), stbrp_node()} -- we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2' } end --- @class stbrp__findresult --- @field x int --- @field y int --- @field prev_link? table --- @field prev_link_type? int --- @return stbrp__findresult local function stbrp__findresult() return { x = 0, y = 0, prev_link = nil, prev_link_type = nil } end ------------------------------------------------------------------------------- -- -- IMPLEMENTATION SECTION -- -- Optionally select which packing heuristic the library should use. Different -- heuristics will produce better/worse results for different data sets. -- If you call init again, this will be reset to the default. --- @param context stbrp_context --- @param heuristic int local function stbrp_setup_heuristic(context, heuristic) if context.init_mode == STBRP__INIT_skyline then STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline.BL_sortHeight or heuristic == STBRP_HEURISTIC_Skyline.BF_sortHeight) context.heuristic = heuristic else STBRP_ASSERT(false) end end -- Optionally call this function after init but before doing any packing to -- change the handling of the out-of-temp-memory scenario, described above. -- If you call init again, this will be reset to the default (false). --- @param context stbrp_context --- @param allow_out_of_mem boolean local function stbrp_setup_allow_out_of_mem(context, allow_out_of_mem) if allow_out_of_mem then -- if it's ok to run out of memory, then don't bother aligning them; -- this gives better packing, but may fail due to OOM (even though -- the rectangles easily fit). @TODO a smarter approach would be to only -- quantize once we've hit OOM, then we could get rid of this parameter. context.align = 1 else -- if it's not ok to run out of memory, then quantize the widths -- so that num_nodes is always enough nodes. -- -- I.e. num_nodes * align >= width -- align >= width / num_nodes -- align = ceil(width/num_nodes) context.align = (context.width + context.num_nodes - 1) / context.num_nodes end end -- Initialize a rectangle packer to: -- pack a rectangle that is 'width' by 'height' in dimensions -- using temporary storage provided by the array 'nodes', which is 'num_nodes' long -- -- You must call this function every time you start packing into a new target. -- -- There is no "shutdown" function. The 'nodes' memory must stay valid for -- the following stbrp_pack_rects() call (or calls), but can be freed after -- the call (or calls) finish. -- -- Note: to guarantee best results, either: -- 1. make sure 'num_nodes' >= 'width' -- or 2. call stbrp_setup_allow_out_of_mem() defined above with 'allow_out_of_mem = true' -- -- If you don't do either of the above things, widths will be quantized to multiples -- of small integers to guarantee the algorithm doesn't run out of temporary storage. -- -- If you do #2, then the non-quantized algorithm will be used, but the algorithm -- may run out of temporary storage and be unable to pack some rectangles. --- @param context stbrp_context --- @param width int --- @param height int --- @param nodes stbrp_node[] --- @param num_nodes int function stbrp_init_target(context, width, height, nodes, num_nodes) for i = 1, num_nodes - 1 do nodes[i].next = nodes[i + 1] end nodes[num_nodes].next = nil context.init_mode = STBRP__INIT_skyline context.heuristic = STBRP_HEURISTIC_Skyline.default context.free_head = nodes[1] context.active_head = context.extra[1] context.width = width context.height = height context.num_nodes = num_nodes stbrp_setup_allow_out_of_mem(context, false) -- node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly) context.extra[1].x = 0 context.extra[1].y = 0 context.extra[1].next = context.extra[2] context.extra[2].x = width context.extra[2].y = bit32.lshift(1, 30) context.extra[2].next = nil end -- find minimum y position if it starts at x1 -- Returns (min_y, waste_area) --- @param c stbrp_context --- @param first stbrp_node --- @param x0 int --- @param width int --- @return int, int local function stbrp__skyline_find_min_y(c, first, x0, width) local node = first local x1 = x0 + width STBRP__NOTUSED(c) STBRP_ASSERT(first.x <= x0) STBRP_ASSERT(node.next.x > x0) -- we ended up handling this in the caller for efficiency STBRP_ASSERT(node.x <= x0) local min_y = 0 local waste_area = 0 local visited_width = 0 while node.x < x1 do if node.y > min_y then -- raise min_y higher. -- we've accounted for all waste up to min_y, -- but we'll now add more waste for everything we've visited waste_area = waste_area + visited_width * (node.y - min_y) min_y = node.y -- the first time through, visited_width might be reduced if node.x < x0 then visited_width = visited_width + node.next.x - x0 else visited_width = visited_width + node.next.x - node.x end else -- add waste area local under_width = node.next.x - node.x if under_width + visited_width > width then under_width = width - visited_width end waste_area = waste_area + under_width * (min_y - node.y) visited_width = visited_width + under_width end node = node.next end return min_y, waste_area end --- @param c stbrp_context --- @param width int --- @param height int --- @return stbrp__findresult local function stbrp__skyline_find_best_pos(c, width, height) local best_waste = bit32.lshift(1, 30) local best_x = 0 local best_y = bit32.lshift(1, 30) local fr = stbrp__findresult() local best_prev_link = nil local best_prev_link_type = nil -- align to multiple of c.align width = width + c.align - 1 width = width - width % c.align STBRP_ASSERT(width % c.align == 0) -- if it can't possibly fit, bail immediately if width > c.width or height > c.height then fr.prev_link = nil fr.prev_link_type = nil fr.x = 0 fr.y = 0 return fr end local prev_link = c local prev_link_type = STBRP__PREV_LINK_TYPE.ACTIVE_HEAD local node = c.active_head while (node.x + width <= c.width) do local y, waste = stbrp__skyline_find_min_y(c, node, node.x, width) if c.heuristic == STBRP_HEURISTIC_Skyline.BL_sortHeight then -- actually just want to test BL -- bottom left if y < best_y then best_y = y best_prev_link = prev_link best_prev_link_type = prev_link_type best_x = node.x end else -- best-fit if y + height <= c.height then -- can only use it if it first vertically if y < best_y or (y == best_y and waste < best_waste) then best_y = y best_waste = waste best_prev_link = prev_link best_prev_link_type = prev_link_type best_x = node.x end end end prev_link = node prev_link_type = STBRP__PREV_LINK_TYPE.NEXT node = node.next end -- if doing best-fit (BF), we also have to try aligning right edge to each node position -- -- e.g, if fitting -- -- ____________________ -- |____________________| -- -- into -- -- | | -- | ____________| -- |____________| -- -- then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned -- -- This makes BF take about 2x the time if c.heuristic == STBRP_HEURISTIC_Skyline.BF_sortHeight then local tail = c.active_head local node_scan = c.active_head local prev_link_scan = c local prev_link_scan_type = STBRP__PREV_LINK_TYPE.ACTIVE_HEAD -- find first node that's admissible while tail and tail.x < width do tail = tail.next end while tail do local xpos = tail.x - width STBRP_ASSERT(xpos >= 0) -- find the left position that matches this while node_scan.next and node_scan.next.x <= xpos do prev_link_scan = node_scan prev_link_scan_type = STBRP__PREV_LINK_TYPE.NEXT node_scan = node_scan.next end if node_scan then STBRP_ASSERT(node_scan.next.x > xpos and node_scan.x <= xpos) local y, waste = stbrp__skyline_find_min_y(c, node_scan, xpos, width) if y + height <= c.height then if y <= best_y then if y < best_y or waste < best_waste or (waste == best_waste and xpos < best_x) then best_x = xpos -- STBRP_ASSERT(y <= best_y); [DEAR IMGUI] best_y = y best_waste = waste best_prev_link = prev_link_scan best_prev_link_type = prev_link_scan_type end end end end tail = tail.next end end fr.prev_link = best_prev_link fr.prev_link_type = best_prev_link_type fr.x = best_x fr.y = best_y return fr end --- @param context stbrp_context --- @param width int --- @param height int --- @return stbrp__findresult local function stbrp__skyline_pack_rectangle(context, width, height) -- find best position according to heuristic local res = stbrp__skyline_find_best_pos(context, width, height) -- bail if: -- 1. it failed -- 2. the best node doesn't fit (we don't always check this) -- 3. we're out of memory if res.prev_link == nil or res.y + height > context.height or context.free_head == nil then res.prev_link = nil res.prev_link_type = nil return res end -- on success, create new node local node = context.free_head node.x = res.x node.y = res.y + height context.free_head = node.next -- insert the new node into the right starting point, and -- let 'cur' point to the remaining nodes needing to be -- stitched back in local cur if res.prev_link_type == STBRP__PREV_LINK_TYPE.ACTIVE_HEAD then cur = res.prev_link.active_head else cur = res.prev_link.next end if cur.x < res.x then -- preserve the existing one, so start testing with the next one local next = cur.next cur.next = node cur = next else if res.prev_link_type == STBRP__PREV_LINK_TYPE.ACTIVE_HEAD then res.prev_link.active_head = node else res.prev_link.next = node end end -- from here, traverse cur and free the nodes, until we get to one -- that shouldn't be freed while cur.next and cur.next.x <= res.x + width do local next = cur.next -- move the current node to the free list cur.next = context.free_head context.free_head = cur cur = next end -- stitch the list back in node.next = cur if cur.x < res.x + width then cur.x = res.x + width end return res end --- @param a stbrp_rect --- @param b stbrp_rect --- @return bool --- @package local function rect_height_compare(a, b) if a.h > b.h then return true elseif a.h < b.h then return false else return a.w > b.w end end --- @param a stbrp_rect --- @param b stbrp_rect --- @return bool --- @package local function rect_original_order(a, b) return a.was_packed < b.was_packed end -- Assign packed locations to rectangles. The rectangles are of type -- 'stbrp_rect' defined above, stored in the array 'rects', and there -- are 'num_rects' many of them. -- -- Rectangles which are successfully packed have the 'was_packed' flag -- set to a non-zero value and 'x' and 'y' store the minimum location -- on each axis (i.e. bottom-left in cartesian coordinates, top-left -- if you imagine y increasing downwards). Rectangles which do not fit -- have the 'was_packed' flag set to 0. -- -- You should not try to access the 'rects' array from another thread -- while this function is running, as the function temporarily reorders -- the array while it executes. -- -- To pack into another rectangle, you need to call stbrp_init_target -- again. To continue packing into the same rectangle, you can call -- this function again. Calling this multiple times with multiple rect -- arrays will probably produce worse packing results than calling it -- a single time with the full rectangle array, but the option is -- available. -- -- The function returns 1 if all of the rectangles were successfully -- packed and 0 otherwise. --- @param context stbrp_context --- @param rects stbrp_rect[] --- @param num_rects int --- @return int local function stbrp_pack_rects(context, rects, num_rects) local all_rects_packed = 1 -- we use the 'was_packed' field internally to allow sorting/unsorting for i = 1, num_rects do rects[i].was_packed = (i - 1) end -- sort according to heuristic STBRP_SORT(rects, rect_height_compare) for i = 1, num_rects do if rects[i].w == 0 or rects[i].h == 0 then rects[i].x = 0 -- empty rect needs no space rects[i].y = 0 else local fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h) if fr.prev_link then rects[i].x = fr.x rects[i].y = fr.y else rects[i].x = STBRP__MAXVAL rects[i].y = STBRP__MAXVAL end end end -- unsort STBRP_SORT(rects, rect_original_order) -- set was_packed flags and all_rects_packed status for i = 1, num_rects do rects[i].was_packed = (rects[i].x == STBRP__MAXVAL and rects[i].y == STBRP__MAXVAL) and 0 or 1 if rects[i].was_packed == 0 then all_rects_packed = 0 end end -- return the all_rects_packed status return all_rects_packed end return { context = stbrp_context, rect = stbrp_rect, node = stbrp_node, pack_rects = stbrp_pack_rects, init_target = stbrp_init_target, setup_allow_out_of_mem = stbrp_setup_allow_out_of_mem, setup_heuristic = stbrp_setup_heuristic } -- ------------------------------------------------------------------------------ -- This software is available under 2 licenses -- choose whichever you prefer. -- ------------------------------------------------------------------------------ -- ALTERNATIVE A - MIT License -- Copyright (c) 2017 Sean Barrett -- Permission is hereby granted, free of charge, to any person obtaining a copy of -- this software and associated documentation files (the "Software"), to deal in -- the Software without restriction, including without limitation the rights to -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -- of the Software, and to permit persons to whom the Software is furnished to do -- so, subject to the following conditions: -- The above copyright notice and this permission notice shall be included in all -- copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -- SOFTWARE. -- ------------------------------------------------------------------------------ -- ALTERNATIVE B - Public Domain (www.unlicense.org) -- This is free and unencumbered software released into the public domain. -- Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -- software, either in source code form or as a compiled binary, for any purpose, -- commercial or non-commercial, and by any means. -- In jurisdictions that recognize copyright laws, the author or authors of this -- software dedicate any and all copyright interest in the software to the public -- domain. We make this dedication for the benefit of the public at large and to -- the detriment of our heirs and successors. We intend this dedication to be an -- overt act of relinquishment in perpetuity of all present and future rights to -- this software under copyright law. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -- ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- ------------------------------------------------------------------------------