diff --git a/sys/apis/region.lua b/sys/apis/region.lua new file mode 100644 index 0000000..f5abcea --- /dev/null +++ b/sys/apis/region.lua @@ -0,0 +1,358 @@ +------------------------------------------------------------------------------- +-- +-- tek.lib.region +-- Written by Timm S. Mueller +-- +-- Copyright 2008 - 2016 by the authors and contributors: +-- +-- * Timm S. Muller +-- * Franciska Schulze +-- * Tobias Schwinger +-- +-- 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. +-- +-- === Disclaimer === +-- +-- 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. +-- +-- OVERVIEW:: +-- This library implements the management of regions, which are +-- collections of non-overlapping rectangles. +-- +-- FUNCTIONS:: +-- - Region:andRect() - ''And''s a rectangle to a region +-- - Region:andRegion() - ''And''s a region to a region +-- - Region:checkIntersect() - Checks if a rectangle intersects a region +-- - Region:forEach() - Calls a function for each rectangle in a region +-- - Region:get() - Get region's min/max extents +-- - Region.intersect() - Returns the intersection of two rectangles +-- - Region:isEmpty() - Checks if a Region is empty +-- - Region.new() - Creates a new Region +-- - Region:orRect() - ''Or''s a rectangle to a region +-- - Region:orRegion() - ''Or''s a region to a region +-- - Region:setRect() - Resets a region to the given rectangle +-- - Region:shift() - Displaces a region +-- - Region:subRect() - Subtracts a rectangle from a region +-- - Region:subRegion() - Subtracts a region from a region +-- - Region:xorRect() - ''Exclusive Or''s a rectangle to a region +-- +------------------------------------------------------------------------------- + +local insert = table.insert +local ipairs = ipairs +local max = math.max +local min = math.min +local setmetatable = setmetatable +local unpack = unpack or table.unpack + +local Region = { } +Region._VERSION = "Region 11.3" + +Region.__index = Region + +------------------------------------------------------------------------------- +-- x0, y0, x1, y1 = Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4): +-- Returns the coordinates of a rectangle where a rectangle specified by +-- the coordinates s1, s2, s3, s4 overlaps with the rectangle specified +-- by the coordinates d1, d2, d3, d4. The return value is '''nil''' if +-- the rectangles do not overlap. +------------------------------------------------------------------------------- + +function Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4) + if s3 >= d1 and s1 <= d3 and s4 >= d2 and s2 <= d4 then + return max(s1, d1), max(s2, d2), min(s3, d3), min(s4, d4) + end +end + +------------------------------------------------------------------------------- +-- insertrect: insert rect to table, merging with an existing one if possible +------------------------------------------------------------------------------- + +local function insertrect(d, s1, s2, s3, s4) + for i = 1, min(4, #d) do + local a = d[i] + local a1, a2, a3, a4 = a[1], a[2], a[3], a[4] + if a2 == s2 and a4 == s4 then + if a3 + 1 == s1 then + a[3] = s3 + return + elseif a1 == s3 + 1 then + a[1] = s1 + return + end + elseif a1 == s1 and a3 == s3 then + if a4 + 1 == s2 then + a[4] = s4 + return + elseif a2 == s4 + 1 then + a[2] = s2 + return + end + end + end + insert(d, 1, { s1, s2, s3, s4 }) +end + +------------------------------------------------------------------------------- +-- cutrect: cut rect d into table of new rects, using rect s as a punch +------------------------------------------------------------------------------- + +local function cutrect(d1, d2, d3, d4, s1, s2, s3, s4) + if not Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4) then + return { { d1, d2, d3, d4 } } + end + local r = { } + if d1 < s1 then + insertrect(r, d1, d2, s1 - 1, d4) + d1 = s1 + end + if d2 < s2 then + insertrect(r, d1, d2, d3, s2 - 1) + d2 = s2 + end + if d3 > s3 then + insertrect(r, s3 + 1, d2, d3, d4) + d3 = s3 + end + if d4 > s4 then + insertrect(r, d1, s4 + 1, d3, d4) + end + return r +end + +------------------------------------------------------------------------------- +-- cutregion: cut region d, using s as a punch +------------------------------------------------------------------------------- + +local function cutregion(d, s1, s2, s3, s4) + local r = { } + for _, dr in ipairs(d) do + local d1, d2, d3, d4 = dr[1], dr[2], dr[3], dr[4] + for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do + insertrect(r, t[1], t[2], t[3], t[4]) + end + end + return r +end + +------------------------------------------------------------------------------- +-- region = Region.new(r1, r2, r3, r4): Creates a new region from the given +-- coordinates. +------------------------------------------------------------------------------- + +function Region.new(r1, r2, r3, r4) + if r1 then + return setmetatable({ region = { { r1, r2, r3, r4 } } }, Region) + end + return setmetatable({ region = { } }, Region) +end + +------------------------------------------------------------------------------- +-- self = region:setRect(r1, r2, r3, r4): Resets an existing region +-- to the specified rectangle. +------------------------------------------------------------------------------- + +function Region:setRect(r1, r2, r3, r4) + self.region = { { r1, r2, r3, r4 } } + return self +end + +------------------------------------------------------------------------------- +-- region:orRect(r1, r2, r3, r4): Logical ''or''s a rectangle to a region +------------------------------------------------------------------------------- + +function Region:orRect(s1, s2, s3, s4) + self.region = cutregion(self.region, s1, s2, s3, s4) + insertrect(self.region, s1, s2, s3, s4) +end + +------------------------------------------------------------------------------- +-- region:orRegion(region): Logical ''or''s another region to a region +------------------------------------------------------------------------------- + +function Region:orRegion(s) + for _, r in ipairs(s) do + self:orRect(r[1], r[2], r[3], r[4]) + end +end + +------------------------------------------------------------------------------- +-- region:andRect(r1, r2, r3, r4): Logical ''and''s a rectange to a region +------------------------------------------------------------------------------- + +function Region:andRect(s1, s2, s3, s4) + local r = { } + for _, d in ipairs(self.region) do + local t1, t2, t3, t4 = + Region.intersect(d[1], d[2], d[3], d[4], s1, s2, s3, s4) + if t1 then + insertrect(r, t1, t2, t3, t4) + end + end + self.region = r +end + +------------------------------------------------------------------------------- +-- region:xorRect(r1, r2, r3, r4): Logical ''xor''s a rectange to a region +------------------------------------------------------------------------------- + +function Region:xorRect(s1, s2, s3, s4) + local r1 = { } + local r2 = { { s1, s2, s3, s4 } } + for _, d in ipairs(self.region) do + local d1, d2, d3, d4 = d[1], d[2], d[3], d[4] + for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do + insertrect(r1, t[1], t[2], t[3], t[4]) + end + r2 = cutregion(r2, d1, d2, d3, d4) + end + self.region = r1 + self:orRegion(r2) +end + +------------------------------------------------------------------------------- +-- self = region:subRect(r1, r2, r3, r4): Subtracts a rectangle from a region +------------------------------------------------------------------------------- + +function Region:subRect(s1, s2, s3, s4) + local r1 = { } + for _, d in ipairs(self.region) do + local d1, d2, d3, d4 = d[1], d[2], d[3], d[4] + for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do + insertrect(r1, t[1], t[2], t[3], t[4]) + end + end + self.region = r1 + return self +end + +------------------------------------------------------------------------------- +-- region:getRect - gets an iterator on the rectangles in a region [internal] +------------------------------------------------------------------------------- + +function Region:getRects() + local index = 0 + return function(object) + index = index + 1 + if object[index] then + return unpack(object[index]) + end + end, self.region +end + +------------------------------------------------------------------------------- +-- success = region:checkIntersect(x0, y0, x1, y1): Returns a boolean +-- indicating whether a rectangle specified by its coordinates overlaps +-- with a region. +------------------------------------------------------------------------------- + +function Region:checkIntersect(s1, s2, s3, s4) + for _, d in ipairs(self.region) do + if Region.intersect(d[1], d[2], d[3], d[4], s1, s2, s3, s4) then + return true + end + end + return false +end + +------------------------------------------------------------------------------- +-- region:subRegion(region2): Subtracts {{region2}} from {{region}}. +------------------------------------------------------------------------------- + +function Region:subRegion(region) + if region then + for r1, r2, r3, r4 in region:getRects() do + self:subRect(r1, r2, r3, r4) + end + end +end + +------------------------------------------------------------------------------- +-- region:andRegion(r): Logically ''and''s a region to a region +------------------------------------------------------------------------------- + +function Region:andRegion(s) + local r = { } + for _, s in ipairs(s.region) do + for _, d in ipairs(self.region) do + local t1, t2, t3, t4 = + Region.intersect(d[1], d[2], d[3], d[4], + s[1], s[2], s[3], s[4]) + if t1 then + insertrect(r, t1, t2, t3, t4) + end + end + end + self.region = r +end + +------------------------------------------------------------------------------- +-- region:forEach(func, obj, ...): For each rectangle in a region, calls the +-- specified function according the following scheme: +-- func(obj, x0, y0, x1, y1, ...) +-- Extra arguments are passed through to the function. +------------------------------------------------------------------------------- + +function Region:forEach(func, obj, ...) + for x0, y0, x1, y1 in self:getRects() do + func(obj, x0, y0, x1, y1, ...) + end +end + +------------------------------------------------------------------------------- +-- region:shift(dx, dy): Shifts a region by delta x and y. +------------------------------------------------------------------------------- + +function Region:shift(dx, dy) + for _, r in ipairs(self.region) do + r[1] = r[1] + dx + r[2] = r[2] + dy + r[3] = r[3] + dx + r[4] = r[4] + dy + end +end + +------------------------------------------------------------------------------- +-- region:isEmpty(): Returns '''true''' if a region is empty. +------------------------------------------------------------------------------- + +function Region:isEmpty() + return #self.region == 0 +end + +------------------------------------------------------------------------------- +-- minx, miny, maxx, maxy = region:get(): Get region's min/max extents +------------------------------------------------------------------------------- + +function Region:get() + if #self.region > 0 then + local minx = 1000000 -- ui.HUGE + local miny = 1000000 + local maxx = 0 + local maxy = 0 + for _, r in ipairs(self.region) do + minx = min(minx, r[1]) + miny = min(miny, r[2]) + maxx = max(maxx, r[3]) + maxy = max(maxy, r[4]) + end + return minx, miny, maxx, maxy + end +end + +return Region