From c278ec74bdf4aca5804a3548efe6aded0b5b31e2 Mon Sep 17 00:00:00 2001 From: Felecarp Date: Fri, 22 Jul 2022 12:39:38 +0200 Subject: [PATCH 1/4] feat(visible): polygon function to draw visible area feat(vector): orientation & intersection refactor(vector): use exponentiation operator refactor(vector): rename function orientation -> alignment feat(vector): polar_lt func to compare angle of vectors --- demos/visible/conf.lua | 19 +++++ demos/visible/main.lua | 124 +++++++++++++++++++++++++++++++ tests/perf_visible.lua | 32 ++++++++ tests/visible.lua | 165 +++++++++++++++++++++++++++++++++++++++++ vector.lua | 36 ++++++++- visible.lua | 156 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 530 insertions(+), 2 deletions(-) create mode 100755 demos/visible/conf.lua create mode 100644 demos/visible/main.lua create mode 100644 tests/perf_visible.lua create mode 100644 tests/visible.lua create mode 100644 visible.lua diff --git a/demos/visible/conf.lua b/demos/visible/conf.lua new file mode 100755 index 0000000..915f3b5 --- /dev/null +++ b/demos/visible/conf.lua @@ -0,0 +1,19 @@ +function love.conf(t) + t.releases = { + title = "Visibility Demo", + loveVersion = "11.0", + version = "1.0.0", + author = "Felecarp", + description = "Demonstration of visibility polygon function", + excludeFileList = { + "*.git", + "*.md", + "*.zip", + "*.love", + }, + releaseDirectory = "build", + } + t.window.title = t.releases.title + t.window.width = 512 + t.window.height = 512 +end diff --git a/demos/visible/main.lua b/demos/visible/main.lua new file mode 100644 index 0000000..6a7a59a --- /dev/null +++ b/demos/visible/main.lua @@ -0,0 +1,124 @@ +local vector = require("vector") +local visible = require("visible") + + +local BACKGROUND_COLOR = { .5, .5, .5 } +local SEGMENT_WIDTH = 3 +local SEGMENT_COLOR = { .3, .3, .3 } +local SEGMENT_POINT_SIZE = 12 +local VISIBLE_POINT_COLOR = { 0, 0, 0 } +local VISIBLE_POINT_SIZE = 8 +local VISIBLE_AREA_COLOR = { 1, 1, 1 } +local CAMERA_COLOR = { .8, .2, .2 } +local CAMERA_SIZE = 16 + +local segments +local visibles, triangles +local visibles_dirty +local camera +local camera_pressed +local create_segment + +local function print_info() + print("camera") + print("{", camera.x, ",", camera.y, "}") + print("segments") + print("{") + for i = 1, #segments, 4 do + print(segments[i], ",", segments[i + 1], ",", segments[i + 2], ",", segments[i + 3], ",") + end + print("}") +end + +local function init() + segments = { + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0 + } + visibles = {} + triangles = {} + visibles_dirty = true + camera = vector.new(256, 256) + camera_pressed = false +end + +function love.load() + init() +end + +function love.draw() + -- visible areas + love.graphics.setColor(VISIBLE_AREA_COLOR) + for _, triangle in ipairs(triangles) do + love.graphics.polygon("fill", triangle) + end + -- segments + love.graphics.setColor(SEGMENT_COLOR) + love.graphics.setLineWidth(SEGMENT_WIDTH) + love.graphics.setPointSize(SEGMENT_POINT_SIZE) + love.graphics.points(segments) + for i = 1, #segments, 4 do + love.graphics.line(segments[i], segments[i + 1], segments[i + 2], segments[i + 3]) + end + -- visible points + love.graphics.setColor(VISIBLE_POINT_COLOR) + love.graphics.setPointSize(VISIBLE_POINT_SIZE) + love.graphics.points(visibles) + -- camera + love.graphics.setColor(CAMERA_COLOR) + love.graphics.setPointSize(CAMERA_SIZE) + love.graphics.points(camera.x, camera.y) + + love.graphics.setBackgroundColor(BACKGROUND_COLOR) +end + +function love.update() + if visibles_dirty then + visibles = visible.polygon(segments, camera) + triangles = love.math.triangulate(visibles) + visibles_dirty = false + end +end + +function love.keypressed(key) + if key == "q" then + love.event.quit(0) + elseif key == "d" then + print("dirty") + visibles_dirty = true + elseif key == "s" then + print("setup") + init() + elseif key == "p" then + print_info() + end +end + +function love.mousepressed(x, y, button) + if button == 1 then + if camera:dist({ x = x, y = y }) <= CAMERA_SIZE then + camera_pressed = true + else + create_segment = { x, y } + end + end +end + +function love.mousereleased(x, y, button) + if button == 1 then + if camera_pressed then + camera.x = x + camera.y = y + camera_pressed = false + elseif create_segment ~= nil then + table.insert(segments, create_segment[1]) + table.insert(segments, create_segment[2]) + table.insert(segments, x) + table.insert(segments, y) + create_segment = nil + end + end + visibles_dirty = true +end diff --git a/tests/perf_visible.lua b/tests/perf_visible.lua new file mode 100644 index 0000000..bf12f3f --- /dev/null +++ b/tests/perf_visible.lua @@ -0,0 +1,32 @@ +local visible = require("visible") + +local segments = { + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 69, 426, 442, 436, + 386, 466, 383, 59, + 147, 458, 289, 382, + 303, 470, 218, 357, + 48, 73, 137, 285, + 130, 97, 50, 352, + 164, 142, 331, 146, + 224, 179, 281, 178, +} + +local center = { x = 256, y = 256 } + +local function use_visible_polygon() + visible.polygon(segments, center) +end + +local function timeit(t, f) + local s = os.clock() + for _ = 1, t do + f() + end + return os.clock() - s +end + +print("visible polygon", timeit(100000, use_visible_polygon)) diff --git a/tests/visible.lua b/tests/visible.lua new file mode 100644 index 0000000..dde82e7 --- /dev/null +++ b/tests/visible.lua @@ -0,0 +1,165 @@ +local lu = require("luaunit") +local vec = require("vector") +local vis = require("visible") + + +function testOrientation() + lu.assertTrue(vec.alignment(vec(0, 0), vec(1, 0), vec(0, -1)) > 0) + lu.assertTrue(vec.alignment(vec(0, 0), vec(1, 0), vec(0, 1)) < 0) + lu.assertTrue(vec.alignment(vec(0, 0), vec(1, 0), vec(2, 0)) == 0) + lu.assertTrue(vec.alignment(vec(0, 0), vec(0, -1), vec(0, 1)) == 0) + lu.assertTrue(vec.alignment(vec(0, 1), vec(1, 1), vec(2, 1)) == 0) +end + +function testIntersection() + lu.assertEquals( + vec.intersection(vec(2, 2), vec(2, 1), vec(0, 0), vec(1, 0)), + vec(2, 0) + ) +end + +TestVisibleSegments = {} + +function TestVisibleSegments:testHideOneSideOnStart() + lu.assertEquals( + vis.polygon({ + 1, 1, 1, 0, + 2, 2, 2, -2, + 3, 3, -3, 3, + -3, 3, -3, -3, + -3, -3, 3, -3, + 3, -3, 3, 3 + }), + { 1, 0, 1, 1, 3, 3, -3, 3, -3, -3, 3, -3, 2, -2, 2, 0 } + ) +end + +function TestVisibleSegments:testHideMiddle() + lu.assertEquals( + vis.polygon({ + 1, 1, 3, 1, + 0, 0, 4, 0, + 4, 0, 4, 4, + 4, 4, 0, 4, + 0, 4, 0, 0 + }, vec(2, 2)), + { 4, 2, 4, 4, 0, 4, 0, 0, 1, 1, 3, 1, 4, 0 } + ) +end + +function TestVisibleSegments:testStartOnMiddle() + lu.assertEquals( + vis.polygon({ + 1, 2, 2, 1, + 2, 2, 2, -1, + 2, 2, -1, 2, + -1, -1, -1, 2, + -1, -1, 2, -1 + }), + { 2, 0, 2, 1, 1, 2, -1, 2, -1, -1, 2, -1 } + ) +end + +function TestVisibleSegments:testStartOnEnd() + lu.assertEquals( + vis.polygon({ + 1, 0, 0, 1, + 0, 1, 1, 2, + 2, 0, 2, 2, + 0, 2, 2, 2, + 0, 2, 0, 0, + 2, 0, 0, 0 + }, vec(1, 1)), + { 2, 1, 2, 2, 1, 2, 0, 1, 1, 0, 2, 0 } + ) +end + +function TestVisibleSegments:testStartBehindAndCross() + lu.assertEquals( + vis.polygon({ + -2, -3, -2, 3, + -3, 1, -1, -1, + 2, -3, 2, 3, + -2, -3, 2, -3, + -2, 3, 2, 3 + }), + { 2, 0, 2, 3, -2, 3, -2, 0, -1, -1, -2, -2, -2, -3, 2, -3 } + ) +end + +function TestVisibleSegments:testFloatingValues() + local visibles = vis.polygon({ + 4, 2, 4, 8, + 2, 4, 8, 5, + -1, 10, 10, 10, + 10, -1, 10, 10, + -1, 10, -1, -1, + 10, -1, -1, -1 + }) + lu.assertEquals(#visibles, 18) +end + +function TestVisibleSegments:testTwoLinesOnStart() + lu.assertEquals( + vis.polygon({ + 1, 3, 3, -1, + 1, -1, 3, 3, + 3, 3, -1, 3, + -1, -1, 3, -1 + }), { 1.5, 0, 2, 1, 1, 3, -1, 3, -1, -1, 1, -1 } + ) +end + +function TestVisibleSegments:testCrossWithWallBehind() + lu.assertEquals( + vis.polygon({ + 0, 1, 3, 1, + 1, 0, 1, 3, + 3, 2, 2, 3, + 3, 3, -1, 3, + 3, 3, 3, -1, + -1, -1, -1, 3, + -1, -1, 3, -1 + }), + { 1, 0, 1, 1, 0, 1, 0, 3, -1, 3, -1, -1, 3, -1, 3, 0 } + ) +end + +function TestVisibleSegments:testRealBlank() + local visibles = vis.polygon({ + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 358, 97, 141, 73, + 322, 58, 196, 106, + }, vec(256, 256)) + lu.assertEquals(#visibles, 24) +end + +function TestVisibleSegments:testCrossStartLine() + local visibles = vis.polygon({ + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 420, 307, 347, 123, + 350, 292, 407, 147, + }, vec(256, 256)) + lu.assertEquals(#visibles, 18) +end + +function TestVisibleSegments:testTriangle() + local visibles = vis.polygon({ + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 153, 79, 361, 87, + 182, 54, 299, 178, + 318, 54, 227, 173 + }, vec(256, 256)) + lu.assertEquals(#visibles, 28) +end + +os.exit(lu.LuaUnit.run()) diff --git a/vector.lua b/vector.lua index 2d75d62..acb2160 100644 --- a/vector.lua +++ b/vector.lua @@ -51,6 +51,35 @@ local function randomDirection(len_min, len_max) math.random() * (len_max-len_min) + len_min) end +-- < 0 -> counterclockwise +-- = 0 -> colinear +-- > 0 -> clockwise +local function alignment(a, b, c) + return (c.x - b.x) * (b.y - a.y) - (c.y - b.y) * (b.x - a.x) +end + +-- find intersection between line a-b and line c-d +local function intersection(a, b, c, d) + local t = ((a.x-c.x) * (a.y-b.y) - (a.y-c.y) * (a.x-b.x)) + / ((d.x-c.x) * (a.y-b.y) - (d.y-c.y) * (a.x-b.x)) + return new( + (c.x + t * (d.x - c.x)), + (c.y + t * (d.y - c.y)) + ) +end + +-- true if a has inferior angle than b, dist if angle equals +local function polar_lt(a, b, center) + if (a.y - center.y) * (b.y - center.y)< 0 then return a.y > b.y end + if a.y == center.y and a.x > center.x then + return not (b.y == center.y and b.x > center.x and b.x < a.x) + end + if b.y == center.y and b.x > center.x then return false end + local align = alignment(center, a, b) + if align == 0 then return a:dist2(center) < b:dist2(center) end + return align < 0 +end + local function isvector(v) return type(v) == 'table' and type(v.x) == 'number' and type(v.y) == 'number' end @@ -119,11 +148,11 @@ function vector:toPolar() end function vector:len2() - return self.x * self.x + self.y * self.y + return self.x ^ 2 + self.y ^ 2 end function vector:len() - return sqrt(self.x * self.x + self.y * self.y) + return sqrt(self.x ^ 2 + self.y ^ 2) end function vector.dist(a, b) @@ -211,6 +240,9 @@ return setmetatable({ new = new, fromPolar = fromPolar, randomDirection = randomDirection, + alignment = alignment, + intersection = intersection, + polar_lt = polar_lt, isvector = isvector, zero = zero }, { diff --git a/visible.lua b/visible.lua new file mode 100644 index 0000000..1427ee9 --- /dev/null +++ b/visible.lua @@ -0,0 +1,156 @@ +local LIBRARY_PATH = (...):match("(.-)[^%.]+$") +local vector = require(LIBRARY_PATH .. "vector") +local visible = {} + + +local function comp(a, b, center) + if a.start == b.start then + if a.stop == nil then return false end + if b.stop == nil then return true end + return vector.alignment(a.start, a.stop, b.stop) > 0 + end + return vector.polar_lt(a.start, b.start, center) +end + +function visible.polygon(segments, center) + -- generic visibility function + -- return concave (frequently) polygon + if center == nil then center = vector.zero end + local endpoints = {} + do + local next_endpoints = {} + local missing_stops = {} + for i = 1, #segments, 4 do + local a = vector(segments[i], segments[i + 1]) + local b = vector(segments[i + 2], segments[i + 3]) + if a ~= b then + local align = vector.alignment(center, a, b) + local start, stop + if align > 0 or align == 0 and + b:dist2(center) < a:dist2(center) then + start, stop = b, a + else + start, stop = a, b + end + table.insert(next_endpoints, { start = start, stop = stop }) + missing_stops[stop] = true + end + end + local startpoint = center + vector.fromPolar(0, 1) + local nodes = {} + for i, epi in ipairs(next_endpoints) do + -- startline cut enpoints + nodes[i] = {} + if vector.alignment(center, startpoint, epi.start) > 0 and + vector.alignment(center, startpoint, epi.stop) < 0 and + vector.alignment(epi.start, epi.stop, center) < 0 then + local intersec = vector.intersection(epi.start, epi.stop, center, startpoint) + table.insert(nodes[i], intersec) + end + -- segments cut endpoints + for j, epj in ipairs(next_endpoints) do + if j == i then break end + if vector.alignment(epi.start, epi.stop, epj.start) + * vector.alignment(epi.start, epi.stop, epj.stop) <= 0 and + vector.alignment(epj.start, epj.stop, epi.start) + * vector.alignment(epj.start, epj.stop, epi.stop) <= 0 then + local intersec = + vector.intersection(epi.start, epi.stop, epj.start, epj.stop) + if intersec ~= epi.start and intersec ~= epi.stop then + table.insert(nodes[i], intersec) + end + if intersec ~= epj.start and intersec ~= epj.stop then + table.insert(nodes[j], intersec) + end + end + end + end + -- register endpoints inserting nodes + for i, endpoint in ipairs(next_endpoints) do + table.sort(nodes[i], function(a, b) + return a:dist2(endpoint.start) < b:dist2(endpoint.start) + end) + missing_stops[endpoint.start] = nil + local current_start = endpoint.start + for _, node in ipairs(nodes[i]) do + missing_stops[node] = nil + table.insert(endpoints, { start = current_start, stop = node }) + current_start = node + end + table.insert(endpoints, { start = current_start, stop = endpoint.stop }) + end + -- add missing stops + for stop, _ in pairs(missing_stops) do + table.insert(endpoints, { start = stop }) + end + end + table.sort(endpoints, function(a, b) return comp(a, b, center) end) + local polygon = {} + local current + local function cycle(epi) + -- if no current point, take the first + if current == nil then + if epi.stop ~= nil then + current = epi + table.insert(polygon, epi.start.x) + table.insert(polygon, epi.start.y) + end + -- if endpoint is the current target + elseif epi.start == current.stop then + table.insert(polygon, epi.start.x) + table.insert(polygon, epi.start.y) + current = nil + -- search an other endpoint behind + for _, epj in ipairs(endpoints) do + -- take the nearest point in radius not on previous segment + if epj.stop ~= nil and + vector.alignment(center, epi.start, epj.start) >= 0 and + vector.alignment(center, epi.start, epj.stop) < 0 and + (current == nil or + vector.alignment(epj.start, epj.stop, current.start) > 0) then + local intersec + if vector.alignment(center, epi.start, epj.start) == 0 then + -- epi.start == epj.start included here + -- epi.stop are already sorted so no need to compare + intersec = epj.start + else + intersec = vector.intersection( + epj.start, epj.stop, center, epi.start + ) + end + current = { start = intersec, stop = epj.stop } + end + end + if current ~= nil and current.start ~= epi.start then + table.insert(polygon, current.start.x) + table.insert(polygon, current.start.y) + end + -- if endpoint starts over the current + elseif epi.stop ~= nil and + vector.alignment(current.start, current.stop, epi.start) < 0 then + local intersec = vector.intersection( + current.start, current.stop, center, epi.start + ) + table.insert(polygon, intersec.x) + table.insert(polygon, intersec.y) + table.insert(polygon, epi.start.x) + table.insert(polygon, epi.start.y) + current = epi + end + end + + for _, epi in ipairs(endpoints) do + cycle(epi) + end + for _, epi in ipairs(endpoints) do + if epi.start.y ~= center.y or epi.start.x < center.x then break end + if epi.start.x ~= polygon[1] and epi.start == current.stop then + table.insert(polygon, epi.start.x) + table.insert(polygon, epi.start.y) + break + end + end + return polygon +end + +return visible From 9b9a7cec4855caf794ca612466a5bf97bb408d4d Mon Sep 17 00:00:00 2001 From: Felecarp Date: Sat, 23 Jul 2022 07:22:13 +0200 Subject: [PATCH 2/4] refactor(visible): camera is now a {x, y} vector --- demos/visible/main.lua | 14 +++++++------- tests/perf_visible.lua | 2 +- tests/visible.lua | 10 +++++----- visible.lua | 6 +++++- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/demos/visible/main.lua b/demos/visible/main.lua index 6a7a59a..8c440d4 100644 --- a/demos/visible/main.lua +++ b/demos/visible/main.lua @@ -1,4 +1,4 @@ -local vector = require("vector") +local vector = require("vector-light") local visible = require("visible") @@ -21,7 +21,7 @@ local create_segment local function print_info() print("camera") - print("{", camera.x, ",", camera.y, "}") + print("{", camera[1], ",", camera[2], "}") print("segments") print("{") for i = 1, #segments, 4 do @@ -40,7 +40,7 @@ local function init() visibles = {} triangles = {} visibles_dirty = true - camera = vector.new(256, 256) + camera = { 256, 256 } camera_pressed = false end @@ -69,7 +69,7 @@ function love.draw() -- camera love.graphics.setColor(CAMERA_COLOR) love.graphics.setPointSize(CAMERA_SIZE) - love.graphics.points(camera.x, camera.y) + love.graphics.points(camera[1], camera[2]) love.graphics.setBackgroundColor(BACKGROUND_COLOR) end @@ -98,7 +98,7 @@ end function love.mousepressed(x, y, button) if button == 1 then - if camera:dist({ x = x, y = y }) <= CAMERA_SIZE then + if vector.dist(camera[1], camera[2], x, y) <= CAMERA_SIZE then camera_pressed = true else create_segment = { x, y } @@ -109,8 +109,8 @@ end function love.mousereleased(x, y, button) if button == 1 then if camera_pressed then - camera.x = x - camera.y = y + camera[1] = x + camera[2] = y camera_pressed = false elseif create_segment ~= nil then table.insert(segments, create_segment[1]) diff --git a/tests/perf_visible.lua b/tests/perf_visible.lua index bf12f3f..0dde4f0 100644 --- a/tests/perf_visible.lua +++ b/tests/perf_visible.lua @@ -15,7 +15,7 @@ local segments = { 224, 179, 281, 178, } -local center = { x = 256, y = 256 } +local center = { 256, 256 } local function use_visible_polygon() visible.polygon(segments, center) diff --git a/tests/visible.lua b/tests/visible.lua index dde82e7..2bbc554 100644 --- a/tests/visible.lua +++ b/tests/visible.lua @@ -42,7 +42,7 @@ function TestVisibleSegments:testHideMiddle() 4, 0, 4, 4, 4, 4, 0, 4, 0, 4, 0, 0 - }, vec(2, 2)), + }, { 2, 2 }), { 4, 2, 4, 4, 0, 4, 0, 0, 1, 1, 3, 1, 4, 0 } ) end @@ -69,7 +69,7 @@ function TestVisibleSegments:testStartOnEnd() 0, 2, 2, 2, 0, 2, 0, 0, 2, 0, 0, 0 - }, vec(1, 1)), + }, { 1, 1 }), { 2, 1, 2, 2, 1, 2, 0, 1, 1, 0, 2, 0 } ) end @@ -133,7 +133,7 @@ function TestVisibleSegments:testRealBlank() 0, 512, 0, 0, 358, 97, 141, 73, 322, 58, 196, 106, - }, vec(256, 256)) + }, { 256, 256 }) lu.assertEquals(#visibles, 24) end @@ -145,7 +145,7 @@ function TestVisibleSegments:testCrossStartLine() 0, 512, 0, 0, 420, 307, 347, 123, 350, 292, 407, 147, - }, vec(256, 256)) + }, { 256, 256 }) lu.assertEquals(#visibles, 18) end @@ -158,7 +158,7 @@ function TestVisibleSegments:testTriangle() 153, 79, 361, 87, 182, 54, 299, 178, 318, 54, 227, 173 - }, vec(256, 256)) + }, { 256, 256 }) lu.assertEquals(#visibles, 28) end diff --git a/visible.lua b/visible.lua index 1427ee9..b19f5cb 100644 --- a/visible.lua +++ b/visible.lua @@ -15,7 +15,11 @@ end function visible.polygon(segments, center) -- generic visibility function -- return concave (frequently) polygon - if center == nil then center = vector.zero end + if center == nil then + center = { x = 0, y = 0 } + else + center = { x = center[1], y = center[2] } + end local endpoints = {} do local next_endpoints = {} From 2a67ab625aa3eeadca1c6b0a7b8e695049d794ca Mon Sep 17 00:00:00 2001 From: Felecarp Date: Sat, 23 Jul 2022 15:40:04 +0200 Subject: [PATCH 3/4] feat(vector-light): add alignment & intersection & polar_lt --- vector-light.lua | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/vector-light.lua b/vector-light.lua index 6fb20d2..d3f31a5 100644 --- a/vector-light.lua +++ b/vector-light.lua @@ -78,6 +78,32 @@ local function len(x,y) return sqrt(x*x + y*y) end +-- < 0 -> counterclockwise +-- = 0 -> colinear +-- > 0 -> clockwise +local function alignment(ax, ay, bx, by, cx, cy) + return (cx - bx) * (by - ay) - (cy - by) * (bx - ax) +end + +-- find intersection between line a-b and line c-d +local function intersection(ax, ay, bx, by, cx, cy, dx, dy) + local t = ((ax-cx) * (ay-by) - (ay-cy) * (ax-bx)) + / ((dx-cx) * (ay-by) - (dy-cy) * (ax-bx)) + return (cx + t * (dx - cx)), (cy + t * (dy - cy)) +end + +-- true if a has inferior angle than b, dist if angle equals +local function polar_lt(x1, y1, x2, y2) + if y1 * y2 < 0 then return y1 > y2 end + if y1 == 0 and x1 > 0 then + return not (y2 == 0 and x2 > 0 and x2 < x1) + end + if y2 == 0 and x2 > 0 then return false end + local align = alignment(0, 0, x1, y1, x2, y2) + if align == 0 then return len2(x1, y1) < len2(x2, y2) end + return align < 0 +end + local function fromPolar(angle, radius) radius = radius or 1 return cos(angle)*radius, sin(angle)*radius @@ -176,6 +202,9 @@ return { len = len, dist2 = dist2, dist = dist, + alignment = alignment, + intersection = intersection, + polar_lt = polar_lt, normalize = normalize, rotate = rotate, perpendicular = perpendicular, From 5a3cd23578abd404cdfb15f356f1b8b2d7e0583f Mon Sep 17 00:00:00 2001 From: Felecarp Date: Wed, 22 Feb 2023 11:00:26 +0100 Subject: [PATCH 4/4] dirty commit --- .luarc.json | 6 + debugger.lua | 678 ++++++++++++++++++++++++++++++++++++++++ demos/segments/main.lua | 26 ++ demos/visible/main.lua | 104 ++++-- pack-utils.lua | 126 ++++++++ segmentsfile.lua | 46 +++ segmentsinput.lua | 149 +++++++++ tests/comp.lua | 22 ++ tests/visible.lua | 299 +++++++++++++++++- vector-light.lua | 128 +++++--- visible.lua | 311 ++++++++++-------- 11 files changed, 1680 insertions(+), 215 deletions(-) create mode 100644 .luarc.json create mode 100644 debugger.lua create mode 100644 demos/segments/main.lua create mode 100644 pack-utils.lua create mode 100644 segmentsfile.lua create mode 100644 segmentsinput.lua create mode 100644 tests/comp.lua diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..d888b7a --- /dev/null +++ b/.luarc.json @@ -0,0 +1,6 @@ +{ + "runtime.version": "Lua 5.1", + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "Lua.workspace.checkThirdParty": false, + "Lua.diagnostics.globals": ["love"] +} \ No newline at end of file diff --git a/debugger.lua b/debugger.lua new file mode 100644 index 0000000..82bf4b7 --- /dev/null +++ b/debugger.lua @@ -0,0 +1,678 @@ +--[[ + Copyright (c) 2020 Scott Lembcke and Howling Moon Software + + 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. + + TODO: + * Print short function arguments as part of stack location. + * Properly handle being reentrant due to coroutines. +]] + +local dbg + +-- Use ANSI color codes in the prompt by default. +local COLOR_GRAY = "" +local COLOR_RED = "" +local COLOR_BLUE = "" +local COLOR_YELLOW = "" +local COLOR_RESET = "" +local GREEN_CARET = " => " + +local function pretty(obj, max_depth) + if max_depth == nil then max_depth = dbg.pretty_depth end + + -- Returns true if a table has a __tostring metamethod. + local function coerceable(tbl) + local meta = getmetatable(tbl) + return (meta and meta.__tostring) + end + + local function recurse(obj, depth) + if type(obj) == "string" then + -- Dump the string so that escape sequences are printed. + return string.format("%q", obj) + elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then + local str = "{" + + for k, v in pairs(obj) do + local pair = pretty(k, 0).." = "..recurse(v, depth + 1) + str = str..(str == "{" and pair or ", "..pair) + end + + return str.."}" + else + -- tostring() can fail if there is an error in a __tostring metamethod. + local success, value = pcall(function() return tostring(obj) end) + return (success and value or "") + end + end + + return recurse(obj, 0) +end + +-- The stack level that cmd_* functions use to access locals or info +-- The structure of the code very carefully ensures this. +local CMD_STACK_LEVEL = 6 + +-- Location of the top of the stack outside of the debugger. +-- Adjusted by some debugger entrypoints. +local stack_top = 0 + +-- The current stack frame index. +-- Changed using the up/down commands +local stack_inspect_offset = 0 + +-- LuaJIT has an off by one bug when setting local variables. +local LUA_JIT_SETLOCAL_WORKAROUND = 0 + +-- Default dbg.read function +local function dbg_read(prompt) + dbg.write(prompt) + io.flush() + return io.read() +end + +-- Default dbg.write function +local function dbg_write(str) + io.write(str) +end + +local function dbg_writeln(str, ...) + if select("#", ...) == 0 then + dbg.write((str or "").."\n") + else + dbg.write(string.format(str.."\n", ...)) + end +end + +local function format_loc(file, line) return COLOR_BLUE..file..COLOR_RESET..":"..COLOR_YELLOW..line..COLOR_RESET end +local function format_stack_frame_info(info) + local filename = info.source:match("@(.*)") + local source = filename and dbg.shorten_path(filename) or info.short_src + local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat) + local name = (info.name and "'"..COLOR_BLUE..info.name..COLOR_RESET.."'" or format_loc(source, info.linedefined)) + return format_loc(source, info.currentline).." in "..namewhat.." "..name +end + +local repl + +-- Return false for stack frames without source, +-- which includes C frames, Lua bytecode, and `loadstring` functions +local function frame_has_line(info) return info.currentline >= 0 end + +local function hook_factory(repl_threshold) + return function(offset, reason) + return function(event, _) + -- Skip events that don't have line information. + if not frame_has_line(debug.getinfo(2)) then return end + + -- Tail calls are specifically ignored since they also will have tail returns to balance out. + if event == "call" then + offset = offset + 1 + elseif event == "return" and offset > repl_threshold then + offset = offset - 1 + elseif event == "line" and offset <= repl_threshold then + repl(reason) + end + end + end +end + +local hook_step = hook_factory(1) +local hook_next = hook_factory(0) +local hook_finish = hook_factory(-1) + +-- Create a table of all the locally accessible variables. +-- Globals are not included when running the locals command, but are when running the print command. +local function local_bindings(offset, include_globals) + local level = offset + stack_inspect_offset + CMD_STACK_LEVEL + local func = debug.getinfo(level).func + local bindings = {} + + -- Retrieve the upvalues + do local i = 1; while true do + local name, value = debug.getupvalue(func, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the locals (overwriting any upvalues) + do local i = 1; while true do + local name, value = debug.getlocal(level, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the varargs (works in Lua 5.2 and LuaJIT) + local varargs = {} + do local i = 1; while true do + local name, value = debug.getlocal(level, -i) + if not name then break end + varargs[i] = value + i = i + 1 + end end + if #varargs > 0 then bindings["..."] = varargs end + + if include_globals then + -- In Lua 5.2, you have to get the environment table from the function's locals. + local env = (_VERSION <= "Lua 5.1" and getfenv(func) or bindings._ENV) + return setmetatable(bindings, {__index = env or _G}) + else + return bindings + end +end + +-- Used as a __newindex metamethod to modify variables in cmd_eval(). +local function mutate_bindings(_, name, value) + local FUNC_STACK_OFFSET = 3 -- Stack depth of this function. + local level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL + + -- Set a local. + do local i = 1; repeat + local var = debug.getlocal(level, i) + if name == var then + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set local variable "..COLOR_BLUE..name..COLOR_RESET) + return debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value) + end + i = i + 1 + until var == nil end + + -- Set an upvalue. + local func = debug.getinfo(level).func + do local i = 1; repeat + local var = debug.getupvalue(func, i) + if name == var then + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set upvalue "..COLOR_BLUE..name..COLOR_RESET) + return debug.setupvalue(func, i, value) + end + i = i + 1 + until var == nil end + + -- Set a global. + dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."Set global variable "..COLOR_BLUE..name..COLOR_RESET) + _G[name] = value +end + +-- Compile an expression with the given variable bindings. +local function compile_chunk(block, env) + local source = "debugger.lua REPL" + local chunk = nil + + if _VERSION <= "Lua 5.1" then + chunk = loadstring(block, source) + if chunk then setfenv(chunk, env) end + else + -- The Lua 5.2 way is a bit cleaner + chunk = load(block, source, "t", env) + end + + if not chunk then dbg_writeln(COLOR_RED.."Error: Could not compile block:\n"..COLOR_RESET..block) end + return chunk +end + +local SOURCE_CACHE = {} + +local function where(info, context_lines) + local source = SOURCE_CACHE[info.source] + if not source then + source = {} + local filename = info.source:match("@(.*)") + if filename then + pcall(function() for line in io.lines(filename) do table.insert(source, line) end end) + elseif info.source then + for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end + end + SOURCE_CACHE[info.source] = source + end + + if source and source[info.currentline] then + for i = info.currentline - context_lines, info.currentline + context_lines do + local tab_or_caret = (i == info.currentline and GREEN_CARET or " ") + local line = source[i] + if line then dbg_writeln(COLOR_GRAY.."% 4d"..tab_or_caret.."%s", i, line) end + end + else + dbg_writeln(COLOR_RED.."Error: Source not available for "..COLOR_BLUE..info.short_src); + end + + return false +end + +-- Wee version differences +local unpack = unpack or table.unpack +local pack = function(...) return {n = select("#", ...), ...} end + +local function cmd_step() + stack_inspect_offset = stack_top + return true, hook_step +end + +local function cmd_next() + stack_inspect_offset = stack_top + return true, hook_next +end + +local function cmd_finish() + local offset = stack_top - stack_inspect_offset + stack_inspect_offset = stack_top + return true, offset < 0 and hook_factory(offset - 1) or hook_finish +end + +local function cmd_print(expr) + local env = local_bindings(1, true) + local chunk = compile_chunk("return "..expr, env) + if chunk == nil then return false end + + -- Call the chunk and collect the results. + local results = pack(pcall(chunk, unpack(rawget(env, "...") or {}))) + + -- The first result is the pcall error. + if not results[1] then + dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." "..results[2]) + else + local output = "" + for i = 2, results.n do + output = output..(i ~= 2 and ", " or "")..pretty(results[i]) + end + + if output == "" then output = "" end + dbg_writeln(COLOR_BLUE..expr.. GREEN_CARET..output) + end + + return false +end + +local function cmd_eval(code) + local env = local_bindings(1, true) + local mutable_env = setmetatable({}, { + __index = env, + __newindex = mutate_bindings, + }) + + local chunk = compile_chunk(code, mutable_env) + if chunk == nil then return false end + + -- Call the chunk and collect the results. + local success, err = pcall(chunk, unpack(rawget(env, "...") or {})) + if not success then + dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." "..tostring(err)) + end + + return false +end + +local function cmd_down() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset + 1 + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until not info or frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: "..format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln("Already at the bottom of the stack.") + end + + return false +end + +local function cmd_up() + local offset = stack_inspect_offset + local info + + repeat -- Find the next frame with a file. + offset = offset - 1 + if offset < stack_top then info = nil; break end + info = debug.getinfo(offset + CMD_STACK_LEVEL) + until frame_has_line(info) + + if info then + stack_inspect_offset = offset + dbg_writeln("Inspecting frame: "..format_stack_frame_info(info)) + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + else + info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + dbg_writeln("Already at the top of the stack.") + end + + return false +end + +local function cmd_where(context_lines) + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL) + return (info and where(info, tonumber(context_lines) or 5)) +end + +local function cmd_trace() + dbg_writeln("Inspecting frame %d", stack_inspect_offset - stack_top) + local i = 0; while true do + local info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i) + if not info then break end + + local is_current_frame = (i + stack_top == stack_inspect_offset) + local tab_or_caret = (is_current_frame and GREEN_CARET or " ") + dbg_writeln(COLOR_GRAY.."% 4d"..COLOR_RESET..tab_or_caret.."%s", i, format_stack_frame_info(info)) + i = i + 1 + end + + return false +end + +local function cmd_locals() + local bindings = local_bindings(1, false) + + -- Get all the variable binding names and sort them + local keys = {} + for k, _ in pairs(bindings) do table.insert(keys, k) end + table.sort(keys) + + for _, k in ipairs(keys) do + local v = bindings[k] + + -- Skip the debugger object itself, "(*internal)" values, and Lua 5.2's _ENV object. + if not rawequal(v, dbg) and k ~= "_ENV" and not k:match("%(.*%)") then + dbg_writeln(" "..COLOR_BLUE..k.. GREEN_CARET..pretty(v)) + end + end + + return false +end + +local function cmd_help() + dbg.write("" + .. COLOR_BLUE.." "..GREEN_CARET.."re-run last command\n" + .. COLOR_BLUE.." c"..COLOR_YELLOW.."(ontinue)"..GREEN_CARET.."continue execution\n" + .. COLOR_BLUE.." s"..COLOR_YELLOW.."(tep)"..GREEN_CARET.."step forward by one line (into functions)\n" + .. COLOR_BLUE.." n"..COLOR_YELLOW.."(ext)"..GREEN_CARET.."step forward by one line (skipping over functions)\n" + .. COLOR_BLUE.." f"..COLOR_YELLOW.."(inish)"..GREEN_CARET.."step forward until exiting the current function\n" + .. COLOR_BLUE.." u"..COLOR_YELLOW.."(p)"..GREEN_CARET.."move up the stack by one frame\n" + .. COLOR_BLUE.." d"..COLOR_YELLOW.."(own)"..GREEN_CARET.."move down the stack by one frame\n" + .. COLOR_BLUE.." w"..COLOR_YELLOW.."(here) "..COLOR_BLUE.."[line count]"..GREEN_CARET.."print source code around the current line\n" + .. COLOR_BLUE.." e"..COLOR_YELLOW.."(val) "..COLOR_BLUE.."[statement]"..GREEN_CARET.."execute the statement\n" + .. COLOR_BLUE.." p"..COLOR_YELLOW.."(rint) "..COLOR_BLUE.."[expression]"..GREEN_CARET.."execute the expression and print the result\n" + .. COLOR_BLUE.." t"..COLOR_YELLOW.."(race)"..GREEN_CARET.."print the stack trace\n" + .. COLOR_BLUE.." l"..COLOR_YELLOW.."(ocals)"..GREEN_CARET.."print the function arguments, locals and upvalues.\n" + .. COLOR_BLUE.." h"..COLOR_YELLOW.."(elp)"..GREEN_CARET.."print this message\n" + .. COLOR_BLUE.." q"..COLOR_YELLOW.."(uit)"..GREEN_CARET.."halt execution\n" + ) + return false +end + +local last_cmd = false + +local commands = { + ["^c$"] = function() return true end, + ["^s$"] = cmd_step, + ["^n$"] = cmd_next, + ["^f$"] = cmd_finish, + ["^p%s+(.*)$"] = cmd_print, + ["^e%s+(.*)$"] = cmd_eval, + ["^u$"] = cmd_up, + ["^d$"] = cmd_down, + ["^w%s*(%d*)$"] = cmd_where, + ["^t$"] = cmd_trace, + ["^l$"] = cmd_locals, + ["^h$"] = cmd_help, + ["^q$"] = function() dbg.exit(0); return true end, +} + +local function match_command(line) + for pat, func in pairs(commands) do + -- Return the matching command and capture argument. + if line:find(pat) then return func, line:match(pat) end + end +end + +-- Run a command line +-- Returns true if the REPL should exit and the hook function factory +local function run_command(line) + -- GDB/LLDB exit on ctrl-d + if line == nil then dbg.exit(1); return true end + + -- Re-execute the last command if you press return. + if line == "" then line = last_cmd or "h" end + + local command, command_arg = match_command(line) + if command then + last_cmd = line + -- unpack({...}) prevents tail call elimination so the stack frame indices are predictable. + return unpack({command(command_arg)}) + elseif dbg.auto_eval then + return unpack({cmd_eval(line)}) + else + dbg_writeln(COLOR_RED.."Error:"..COLOR_RESET.." command '%s' not recognized.\nType 'h' and press return for a command list.", line) + return false + end +end + +repl = function(reason) + -- Skip frames without source info. + while not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do + stack_inspect_offset = stack_inspect_offset + 1 + end + + local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3) + reason = reason and (COLOR_YELLOW.."break via "..COLOR_RED..reason..GREEN_CARET) or "" + dbg_writeln(reason..format_stack_frame_info(info)) + + if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end + + repeat + local success, done, hook = pcall(run_command, dbg.read(COLOR_RED.."debugger.lua> "..COLOR_RESET)) + if success then + debug.sethook(hook and hook(0), "crl") + else + local message = COLOR_RED.."INTERNAL DEBUGGER.LUA ERROR. ABORTING\n:"..COLOR_RESET.." "..done + dbg_writeln(message) + error(message) + end + until done +end + +-- Make the debugger object callable like a function. +dbg = setmetatable({}, { + __call = function(_, condition, top_offset, source) + if condition then return end + + top_offset = (top_offset or 0) + stack_inspect_offset = top_offset + stack_top = top_offset + + debug.sethook(hook_next(1, source or "dbg()"), "crl") + return + end, +}) + +-- Expose the debugger's IO functions. +dbg.read = dbg_read +dbg.write = dbg_write +dbg.shorten_path = function (path) return path end +dbg.exit = function(err) os.exit(err) end + +dbg.writeln = dbg_writeln + +dbg.pretty_depth = 3 +dbg.pretty = pretty +dbg.pp = function(value, depth) dbg_writeln(pretty(value, depth)) end + +dbg.auto_where = false +dbg.auto_eval = false + +local lua_error, lua_assert = error, assert + +-- Works like error(), but invokes the debugger. +function dbg.error(err, level) + level = level or 1 + dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..pretty(err)) + dbg(false, level, "dbg.error()") + + lua_error(err, level) +end + +-- Works like assert(), but invokes the debugger on a failure. +function dbg.assert(condition, message) + if not condition then + dbg_writeln(COLOR_RED.."ERROR:"..COLOR_RESET..message) + dbg(false, 1, "dbg.assert()") + end + + return lua_assert(condition, message) +end + +-- Works like pcall(), but invokes the debugger on an error. +function dbg.call(f, ...) + return xpcall(f, function(err) + dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..pretty(err)) + dbg(false, 1, "dbg.call()") + + return err + end, ...) +end + +-- Error message handler that can be used with lua_pcall(). +function dbg.msgh(...) + if debug.getinfo(2) then + dbg_writeln(COLOR_RED.."ERROR: "..COLOR_RESET..pretty(...)) + dbg(false, 1, "dbg.msgh()") + else + dbg_writeln(COLOR_RED.."debugger.lua: "..COLOR_RESET.."Error did not occur in Lua code. Execution will continue after dbg_pcall().") + end + + return ... +end + +-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them. +local stdin_isatty = true +local stdout_isatty = true + +-- Conditionally enable the LuaJIT FFI. +local ffi = (jit and require("ffi")) +if ffi then + ffi.cdef[[ + int isatty(int); // Unix + int _isatty(int); // Windows + void free(void *ptr); + + char *readline(const char *); + int add_history(const char *); + ]] + + local function get_func_or_nil(sym) + local success, func = pcall(function() return ffi.C[sym] end) + return success and func or nil + end + + local isatty = get_func_or_nil("isatty") or get_func_or_nil("_isatty") or (ffi.load("ucrtbase"))["_isatty"] + stdin_isatty = isatty(0) + stdout_isatty = isatty(1) +end + +-- Conditionally enable color support. +local color_maybe_supported = (stdout_isatty and os.getenv("TERM") and os.getenv("TERM") ~= "dumb") +if color_maybe_supported and not os.getenv("DBG_NOCOLOR") then + COLOR_GRAY = string.char(27) .. "[90m" + COLOR_RED = string.char(27) .. "[91m" + COLOR_BLUE = string.char(27) .. "[94m" + COLOR_YELLOW = string.char(27) .. "[33m" + COLOR_RESET = string.char(27) .. "[0m" + GREEN_CARET = string.char(27) .. "[92m => "..COLOR_RESET +end + +if stdin_isatty and not os.getenv("DBG_NOREADLINE") then + pcall(function() + local linenoise = require 'linenoise' + + -- Load command history from ~/.lua_history + local hist_path = os.getenv('HOME') .. '/.lua_history' + linenoise.historyload(hist_path) + linenoise.historysetmaxlen(50) + + local function autocomplete(env, input, matches) + for name, _ in pairs(env) do + if name:match('^' .. input .. '.*') then + linenoise.addcompletion(matches, name) + end + end + end + + -- Auto-completion for locals and globals + linenoise.setcompletion(function(matches, input) + -- First, check the locals and upvalues. + local env = local_bindings(1, true) + autocomplete(env, input, matches) + + -- Then, check the implicit environment. + env = getmetatable(env).__index + autocomplete(env, input, matches) + end) + + dbg.read = function(prompt) + local str = linenoise.linenoise(prompt) + if str and not str:match "^%s*$" then + linenoise.historyadd(str) + linenoise.historysave(hist_path) + end + return str + end + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Linenoise support enabled.") + end) + + -- Conditionally enable LuaJIT readline support. + pcall(function() + if dbg.read == nil and ffi then + local readline = ffi.load("readline") + dbg.read = function(prompt) + local cstr = readline.readline(prompt) + if cstr ~= nil then + local str = ffi.string(cstr) + if string.match(str, "[^%s]+") then + readline.add_history(cstr) + end + + ffi.C.free(cstr) + return str + else + return nil + end + end + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Readline support enabled.") + end + end) +end + +-- Detect Lua version. +if jit then -- LuaJIT + LUA_JIT_SETLOCAL_WORKAROUND = -1 + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for "..jit.version) +elseif "Lua 5.1" <= _VERSION and _VERSION <= "Lua 5.4" then + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Loaded for ".._VERSION) +else + dbg_writeln(COLOR_YELLOW.."debugger.lua: "..COLOR_RESET.."Not tested against ".._VERSION) + dbg_writeln("Please send me feedback!") +end + +return dbg diff --git a/demos/segments/main.lua b/demos/segments/main.lua new file mode 100644 index 0000000..e181672 --- /dev/null +++ b/demos/segments/main.lua @@ -0,0 +1,26 @@ +local pack = require("pack-utils") + +local segments = { + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 128, 256, 512, 128, + 128, 256, 128, 256, + 256, 128, 256, 256, + 256, 256, 384, 128, + 256, 384, 128, 128, + 256, 128, 256, 384, + 256, 128, 256, 128, + 256, 256, 512, 384, + 128, 384, 256, 128, + 128, 384, 384, 128, + 256, 384, 256, 256, + 256, 128, 384, 512 +} + +function love.draw() + for _, x, y, xg, yg in pack.ipairs(segments, 4) do + love.graphics.line(x, y, xg, yg) + end +end diff --git a/demos/visible/main.lua b/demos/visible/main.lua index 8c440d4..7ba08c0 100644 --- a/demos/visible/main.lua +++ b/demos/visible/main.lua @@ -1,46 +1,45 @@ local vector = require("vector-light") local visible = require("visible") - +local pack = require("pack-utils") +local useSegmentsInput = require("segmentsinput") +local useSegmentsFile = require("segmentsfile") local BACKGROUND_COLOR = { .5, .5, .5 } -local SEGMENT_WIDTH = 3 local SEGMENT_COLOR = { .3, .3, .3 } -local SEGMENT_POINT_SIZE = 12 local VISIBLE_POINT_COLOR = { 0, 0, 0 } -local VISIBLE_POINT_SIZE = 8 local VISIBLE_AREA_COLOR = { 1, 1, 1 } local CAMERA_COLOR = { .8, .2, .2 } -local CAMERA_SIZE = 16 -local segments +local segments = {} local visibles, triangles local visibles_dirty -local camera +local function dirty() visibles_dirty = true end + +-- local segmentsinput = useSegmentsInput(segments, dirty) +local segmentsfile = useSegmentsFile(segments, "segments.txt", dirty) +local camera = { 512 / 2, 512 / 2 } local camera_pressed local create_segment +local step = 128 + +local SEGMENT_WIDTH = 3 +local SEGMENT_POINT_SIZE = 12 +local VISIBLE_POINT_SIZE = 8 +local CAMERA_SIZE = 16 -local function print_info() - print("camera") - print("{", camera[1], ",", camera[2], "}") - print("segments") - print("{") - for i = 1, #segments, 4 do - print(segments[i], ",", segments[i + 1], ",", segments[i + 2], ",", segments[i + 3], ",") - end - print("}") -end local function init() - segments = { + for k, _ in pairs(segments) do segments[k] = nil end + pack.insert(segments, { 0, 0, 512, 0, 512, 0, 512, 512, 512, 512, 0, 512, 0, 512, 0, 0 - } + }) + -- segmentsinput.init() visibles = {} triangles = {} visibles_dirty = true - camera = { 256, 256 } camera_pressed = false end @@ -54,18 +53,22 @@ function love.draw() for _, triangle in ipairs(triangles) do love.graphics.polygon("fill", triangle) end + -- segmentsinput.draw() -- segments love.graphics.setColor(SEGMENT_COLOR) love.graphics.setLineWidth(SEGMENT_WIDTH) love.graphics.setPointSize(SEGMENT_POINT_SIZE) love.graphics.points(segments) - for i = 1, #segments, 4 do - love.graphics.line(segments[i], segments[i + 1], segments[i + 2], segments[i + 3]) + for _, x, y, xg, yg in pack.ipairs(segments, 4) do + love.graphics.line(x, y, xg, yg) end -- visible points love.graphics.setColor(VISIBLE_POINT_COLOR) love.graphics.setPointSize(VISIBLE_POINT_SIZE) love.graphics.points(visibles) + for _, x, y in pack.ipairs(visibles, 2) do + love.graphics.print(vector.str(x, y), x, y) + end -- camera love.graphics.setColor(CAMERA_COLOR) love.graphics.setPointSize(CAMERA_SIZE) @@ -74,15 +77,22 @@ function love.draw() love.graphics.setBackgroundColor(BACKGROUND_COLOR) end -function love.update() +function love.update(dt) if visibles_dirty then visibles = visible.polygon(segments, camera) triangles = love.math.triangulate(visibles) visibles_dirty = false end + -- segmentsinput.update(dt) end +local function round(n) return math.floor(n / step + .5) * step end + function love.keypressed(key) + if --segmentsinput.keypressed(key) or-- + segmentsfile.keypressed(key) then + return + end if key == "q" then love.event.quit(0) elseif key == "d" then @@ -91,17 +101,42 @@ function love.keypressed(key) elseif key == "s" then print("setup") init() - elseif key == "p" then - print_info() + elseif key == "r" then + local x, y, xg, yg = round(math.random(512)), round(math.random(512)), + round(math.random(512)), round(math.random(512)) + table.insert(segments, x) + table.insert(segments, y) + table.insert(segments, xg) + table.insert(segments, yg) + print("random insert", vector.str(x, y), vector.str(xg, yg)) + visibles_dirty = true + -- segmentsinput.dirty() end end +function love.keyreleased(key) + -- segmentsinput.keyreleased(key) +end + function love.mousepressed(x, y, button) if button == 1 then + -- if segmentsinput.mousepressed(x, y, button) then return end if vector.dist(camera[1], camera[2], x, y) <= CAMERA_SIZE then camera_pressed = true else - create_segment = { x, y } + create_segment = { round(x), round(y) } + end + elseif button == 2 then + for i, xs, ys, xgs, ygs in pack.ipairs(segments, 4) do + if vector.segmentcontains(xs, ys, xgs, ygs, x, y, SEGMENT_WIDTH) then + table.remove(segments, i) + table.remove(segments, i) + table.remove(segments, i) + table.remove(segments, i) + visibles_dirty = true + -- segmentsinput.dirty() + break + end end end end @@ -109,16 +144,23 @@ end function love.mousereleased(x, y, button) if button == 1 then if camera_pressed then - camera[1] = x - camera[2] = y - camera_pressed = false + if round(x) ~= 0 and round(y) ~= 0 and round(x) ~= 512 and round(y) ~= 512 then + camera[1] = round(x) + camera[2] = round(y) + camera_pressed = false + end elseif create_segment ~= nil then table.insert(segments, create_segment[1]) table.insert(segments, create_segment[2]) - table.insert(segments, x) - table.insert(segments, y) + table.insert(segments, round(x)) + table.insert(segments, round(y)) create_segment = nil + -- segmentsinput.dirty() end end visibles_dirty = true end + +function love.textinput(text) + -- segmentsinput.textinput(text) +end diff --git a/pack-utils.lua b/pack-utils.lua new file mode 100644 index 0000000..a071bf2 --- /dev/null +++ b/pack-utils.lua @@ -0,0 +1,126 @@ +local function sort_pack(t, f, l) + for i = 1, #t, l do + for j = i - l, 1, -l do + local argsa = {} + for k = 0, l - 1 do table.insert(argsa, t[i + k]) end + local argsb = {} + for k = 0, l - 1 do table.insert(argsb, t[j + k]) end + if f(argsa, argsb) then + if j == 1 then + for k = 0, l - 1 do + table.insert(t, j + k, t[i + k]) + table.remove(t, i + k + 1) + end + break + end + elseif j ~= i - l then + for k = 0, l - 1 do + table.insert(t, j + k + l, t[i + k]) + table.remove(t, i + k + 1) + end + break + else break end + end + end +end + +local function insert_pack(t, v) + for _, value in ipairs(v) do table.insert(t, value) end +end + + +local function insert_sort_pack(t, v, f) + if #t > 0 then + for i = 1, #t, #v do + local values = {} + for k = 0, #v - 1 do table.insert(values, t[i + k]) end + -- print(v[1].." < "..values[1].." ?") + if f(v, values) then + -- print(v[1].." index "..i) + for k = 1, #v do table.insert(t, i + k - 1, v[k]) end + return + end + end + end + -- print(v[1].." at end") + for k = 1, #v do table.insert(t, v[k]) end +end + +-- do +-- local t = {} +-- local f = function(a, b) return a[1] < b[1] end +-- insert_pack(t, {3, 5, 1}, f) +-- insert_pack(t, {1, 3, 1}, f) +-- insert_pack(t, {4, 6, 1}, f) +-- insert_pack(t, {2, 4, 1}, f) +-- require("luaunit").assertEquals( +-- t, +-- {1, 3, 1, 2, 4, 1, 3, 5, 1, 4, 6, 1} +-- ) +-- print("success") +-- end + +local function ipairs_pack(t, l, max) + local index = 1 - l + local count = max or #t + return function() + index = index + l + if index <= count then + local values = {} + for k = 0, l - 1 do + table.insert(values, t[index + k]) + end + return index, unpack(values) + end + end +end + +local function print_pack(t, f, l) + for i = 1, #t - l + 1, l do + local values = {} + for k = 1, l do + table.insert(values, t[i + k - 1]) + end + (f or print)(unpack(values)) + end +end + +-- do +-- local t = { 1, 1, 0, 0, 2, 2, 2, 1 } +-- local f = function(a, b, c, d) return a + b < c + d end +-- local r = { 0, 0, 1, 1, 2, 1, 2, 2 } +-- sort_pack(t, f, 2) +-- for i, v in ipairs(t) do +-- assert(v == r[i], v .. " != " .. r[i] .. " (" .. i .. ")") +-- end +-- end + +-- do +-- local t = { +-- 1, 1, 0, +-- 2, 4, 2, +-- 2, 2, 5, +-- 2, 0, 5 +-- } +-- local f = function(a1, b1, c1, a2, b2, c2) +-- return a1 + b1 + c1 < a2 + b2 + c2 +-- end +-- local r = { +-- 1, 1, 0, +-- 2, 0, 5, +-- 2, 4, 2, +-- 2, 2, 5 +-- } +-- sort_pack(t, f, 3) +-- for i, v in ipairs(t) do +-- assert(v == r[i], v .. " != " .. r[i] .. " (" .. i .. ")") +-- end +-- end + +return { + sort = sort_pack, + ipairs = ipairs_pack, + print = print_pack, + insert = insert_pack, + insert_sort = insert_sort_pack +} diff --git a/segmentsfile.lua b/segmentsfile.lua new file mode 100644 index 0000000..bb4d0e8 --- /dev/null +++ b/segmentsfile.lua @@ -0,0 +1,46 @@ +local pack = require("pack-utils") + +return function(segments, filename, dirty) + local active = false + local function write() + local text = '' + for _, xs, ys, xgs, ygs in pack.ipairs(segments, 4) do + text = text .. string.format('%d %d %d %d\n', xs, ys, xgs, ygs) + end + local success, message = love.filesystem.write(filename, text) + if not success then error(message) end + end + + local function read() + do + local content, message = love.filesystem.read(filename) + if content == nil then error(message) end + local exp = {} + for value in string.gmatch(content, "%d+") do + table.insert(exp, tonumber(value)) + end + if #segments % 4 == 0 then + for k, _ in pairs(segments) do segments[k] = nil end + for _, value in ipairs(exp) do + table.insert(segments, value) + end + end + end + dirty() + write() + end + + local segmentsfile = {} + function segmentsfile.keypressed(key) + if active then + if key == "r" then read() + elseif key == "w" then write() end + active = false + return true + end + if key == "f" then active = true return true end + return false + end + + return segmentsfile +end diff --git a/segmentsinput.lua b/segmentsinput.lua new file mode 100644 index 0000000..0c8c47b --- /dev/null +++ b/segmentsinput.lua @@ -0,0 +1,149 @@ +local pack = require("pack-utils") +local utf8 = require("utf8") + +KEYPRESSED_DT = .1 + +local function rect_collides(rect) + return function(x, y) + return x >= rect.x and y >= rect.y and + x <= rect.x + rect.w and y <= rect.y + rect.h + end +end + +return function(segments, set_dirty) + local rect = { x = 16, y = 16, w = 128, h = 256 } + local colors = { + background = { .8, .8, .8, .5 }, + border = { .2, .2, .2, .5 }, + text = { .2, .2, .2 } + } + + local text, cursor + local active, visible, dirty + local keypressed, keypressed_timer + + local function import() + text = '' + for _, xs, ys, xgs, ygs in pack.ipairs(segments, 4) do + text = text .. string.format('%d %d %d %d\n', xs, ys, xgs, ygs) + end + cursor = utf8.offset(text, -1) + end + + local function open() + active = true + end + + local function close() + active = false + do + local exp = {} + for value in string.gmatch(text, "%d+") do + table.insert(exp, tonumber(value)) + end + if #segments % 4 == 0 then + for k, _ in pairs(segments) do + segments[k] = nil + end + for _, value in ipairs(exp) do + table.insert(segments, value) + end + end + end + import() + set_dirty() + end + + local function write(t) + text = string.sub(text, 0, cursor - 1) .. t .. string.sub(text, cursor) + cursor = cursor + #t + end + + local function movecursor(xmove, ymove) + cursor = cursor + xmove + ymove * 8 + end + + local segmentsinput = {} + function segmentsinput.init() + import() + active = false + visible = true + dirty = false + end + + function segmentsinput.draw() + if visible then + love.graphics.setColor(colors.background) + love.graphics.rectangle("fill", rect.x, rect.y, rect.w, rect.h) + if active then + love.graphics.setColor(colors.border) + love.graphics.rectangle("line", rect.x, rect.y, rect.w, rect.h) + end + love.graphics.setColor(colors.text) + local draw_text = string.sub(text, 0, cursor - 1) .. "|" .. string.sub(text, cursor) + love.graphics.printf(draw_text, rect.x, rect.y, rect.w, "left") + end + end + + function segmentsinput.update(dt) + if dirty then import() dirty = false end + if keypressed_timer ~= nil then + keypressed_timer = keypressed_timer + dt + if keypressed_timer >= KEYPRESSED_DT then + if keypressed == "left" then movecursor(-1, 0) + elseif keypressed == "right" then movecursor(1, 0) + elseif keypressed == "up" then movecursor(0, -1) + elseif keypressed == "down" then movecursor(0, 1) + elseif keypressed == "backspace" then + -- delete char + local byteoffset = utf8.offset(text, cursor) + if byteoffset then + text = string.sub(text, 1, byteoffset - 2) .. + string.sub(text, byteoffset) + cursor = cursor - 1 + end + elseif keypressed == "escape" then close() + elseif keypressed == "kpenter" then write("\n") + end + keypressed_timer = keypressed_timer - KEYPRESSED_DT + end + end + end + + function segmentsinput.keypressed(key) + if active then + if key == "v" and love.keyboard.isDown("lctrl") then + write(love.system.getClipboardText()) + else + if key == "kpenter" then write("\n") end + keypressed = key + keypressed_timer = 0 + end + return true + end + return false + end + + function segmentsinput.keyreleased(key) + if keypressed == key then keypressed = nil end + end + + function segmentsinput.mousepressed(x, y, button) + if button == 1 then + if rect_collides(rect)(x, y) then open() return true end + if active then + active = false + close() + end + return false + end + end + + function segmentsinput.textinput(t) + if active then write(t) end + end + + function segmentsinput.dirty() dirty = true end + + return segmentsinput +end diff --git a/tests/comp.lua b/tests/comp.lua new file mode 100644 index 0000000..c782ea5 --- /dev/null +++ b/tests/comp.lua @@ -0,0 +1,22 @@ + +do + assert(comp(0, 1, 1, 1, 0, 2, 2, 1, 0, 0)) +end + +do + local t = { + 0, 2, 2, 1, + 0, 1, 1, 1, + } + local f = function(x1, y1, xg1, yg1, x2, y2, xg2, yg2) + return comp(x1, y1, xg1, yg1, x2, y2, xg2, yg2, 0, 0) + end + local r = { + 0, 1, 1, 1, + 0, 2, 2, 1 + } + sort_pack(t, f, 4) + for i, v in ipairs(t) do + assert(v == r[i], v .. " != " .. r[i] .. " (" .. i .. ")") + end +end diff --git a/tests/visible.lua b/tests/visible.lua index 2bbc554..7cab14d 100644 --- a/tests/visible.lua +++ b/tests/visible.lua @@ -1,23 +1,97 @@ local lu = require("luaunit") -local vec = require("vector") +local vec = require("vector-light") local vis = require("visible") +local pack = require("pack-utils") function testOrientation() - lu.assertTrue(vec.alignment(vec(0, 0), vec(1, 0), vec(0, -1)) > 0) - lu.assertTrue(vec.alignment(vec(0, 0), vec(1, 0), vec(0, 1)) < 0) - lu.assertTrue(vec.alignment(vec(0, 0), vec(1, 0), vec(2, 0)) == 0) - lu.assertTrue(vec.alignment(vec(0, 0), vec(0, -1), vec(0, 1)) == 0) - lu.assertTrue(vec.alignment(vec(0, 1), vec(1, 1), vec(2, 1)) == 0) + lu.assertTrue(vec.alignment(0, 0, 1, 0, 0, -1) > 0) + lu.assertTrue(vec.alignment(0, 0, 1, 0, 0, 1) < 0) + lu.assertTrue(vec.alignment(0, 0, 1, 0, 2, 0) == 0) + lu.assertTrue(vec.alignment(0, 0, 0, -1, 0, 1) == 0) + lu.assertTrue(vec.alignment(0, 1, 1, 1, 2, 1) == 0) end function testIntersection() lu.assertEquals( - vec.intersection(vec(2, 2), vec(2, 1), vec(0, 0), vec(1, 0)), - vec(2, 0) + { vec.intersection(2, 2, 2, 1, 0, 0, 1, 0) }, { 2, 0 } ) end +function testPolarLt() + lu.assertFalse(vec.polar_lt(0, 1, 1, 3)) +end + +function testComp() + local t = { + 1, 0, 1, 1, + 3, 2, 2, 3, + 3, 2, 3, 3, + 1, 1, 0, 1, + 1, 3, -1, 3, + 0, 1, vis.nogoal, vis.nogoal, + -1, -1, 3, -1, + 3, -1, 3, 0, + } + local r = { + 1, 0, 1, 1, + 3, 2, 2, 3, + 3, 2, 3, 3, + 1, 1, 0, 1, + 1, 3, -1, 3, + 0, 1, vis.nogoal, vis.nogoal, + -1, -1, 3, -1, + 3, -1, 3, 0, + } + local f = function(a, b) + return vis.comp(a[1], a[2], a[3], a[4], b[1], b[2], b[3], b[4], 0, 0) + end + pack.sort(t, f, 4) + lu.assertEquals(t, r) +end + +function testComp2() + local t = { + 1, 1, 0, 1, + 1, 1, 1, 3, + 3, 2, 2, 3, + 1, 3, -1, 3, + 3, 2, 3, 3, + -1, 3, -1, -1, + -1, -1, 3, -1, + 3, 1, 1, 1, + 1, 0, 1, 1, + 3, 3, 2, 3, + 2, 3, 1, 3, + 3, -1, 3, 0, + 3, 0, 3, 1, + 3, 1, 3, 2, + 0, 1, vis.nogoal, vis.nogoal, + } + local r = { + 1, 0, 1, 1, + 3, 0, 3, 1, + 3, 1, 1, 1, + 3, 1, 3, 2, + 3, 2, 2, 3, + 3, 2, 3, 3, + 1, 1, 0, 1, + 1, 1, 1, 3, + 3, 3, 2, 3, + 2, 3, 1, 3, + 1, 3, -1, 3, + 0, 1, vis.nogoal, vis.nogoal, + -1, 3, -1, -1, + -1, -1, 3, -1, + 3, -1, 3, 0, + } + pack.sort(t, function(a, b) + return vis.comp(a[1], a[2], a[3], a[4], b[1], b[2], b[3], b[4], 0, 0) + end, 4) + lu.assertEquals(t, r) + lu.assertEquals(#t, #r) +end + TestVisibleSegments = {} function TestVisibleSegments:testHideOneSideOnStart() @@ -96,7 +170,7 @@ function TestVisibleSegments:testFloatingValues() -1, 10, -1, -1, 10, -1, -1, -1 }) - lu.assertEquals(#visibles, 18) + lu.assertEquals(visibles, {}) end function TestVisibleSegments:testTwoLinesOnStart() @@ -105,6 +179,7 @@ function TestVisibleSegments:testTwoLinesOnStart() 1, 3, 3, -1, 1, -1, 3, 3, 3, 3, -1, 3, + -1, 3, -1, -1, -1, -1, 3, -1 }), { 1.5, 0, 2, 1, 1, 3, -1, 3, -1, -1, 1, -1 } ) @@ -162,4 +237,210 @@ function TestVisibleSegments:testTriangle() lu.assertEquals(#visibles, 28) end +function TestVisibleSegments:testTwoCalls() + local segments = { + 0, 0, 8, 0, + 8, 0, 8, 8, + 8, 8, 0, 8, + 0, 8, 0, 0 + } + local r = { 8, 4, 8, 8, 0, 8, 0, 0, 8, 0 } + local poly1 = vis.polygon(segments, { 4, 4 }) + lu.assertEquals(poly1, r) + local poly2 = vis.polygon(segments, { 4, 4 }) + lu.assertEquals(poly2, r) +end + +function TestVisibleSegments:testSimpleReal() + local segments = { + 8, 0, 8, 8, + 8, 8, 0, 8, + 0, 8, 0, 0, + 0, 0, 8, 0, + 3, 2, 6, 3, + } + local r = { + 8, 4, 8, 8, + 0, 8, 0, 0, + 2, 0, 3, 2, + 6, 3, 8, 2 + } + local poly = vis.polygon(segments, { 4, 4 }) + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testRealBlankTwo() + local segments = { + 512, 128, 512, 256, + 384, 128, 384, 192, + 512, 256, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 0, 0, 512, 0, + 512, 0, 512, 128, + 384, 64, 384, 128, + } + local r = { 384, 128, 384, 192, 512, 256, 512, 512, 0, 512, 0, 0, 512, 0, 384, 64 } + local poly = vis.polygon(segments, { 256, 128 }) + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testStopOnCamera() + local segments = { + 0, 0, 2, 0, + 2, 0, 2, 2, + 2, 2, 0, 2, + 0, 2, 0, 0, + 0, 0, 1, 1 + } + local r = { 2, 1, 2, 2, 0, 2, 0, 0, 2, 0 } + local poly = vis.polygon(segments, { 1, 1 }) + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testRealBlank2() + local segments = { + 512, 128, 512, 512, + 512, 512, 0, 512, + 256, 256, 0, 0, + 0, 512, 0, 0, + 0, 0, 512, 0, + 512, 0, 512, 128, + 512, 384, 128, 512, + } + local poly = vis.polygon(segments, { 256, 128 }) + local r = { 512, 128, 512, 384, 256, 469 + 1 / 3, 256, 256, 0, 0, 512, 0 } + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testRealBlank3() + local segments = { + 298 + 2 / 3, 128, 384, 384, + 512, 128, 512, 512, + 512, 512, 0, 512, + 256, 384, 128, 0, + 0, 512, 0, 0, + 0, 0, 128, 0, + 128, 0, 256, 0, + 256, 0, 298 + 2 / 3, 128, + 256, 0, 512, 0, + 512, 0, 512, 128, + } + local poly = vis.polygon(segments, { 256, 128 }) + local r = { 298 + 2/3, 128, 384, 384, 448, 512, 256, 512, 256, 384, 128, 0, 256, 0 } + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testRealBlank4() + local segments = { + 341 + 1 / 3, 128, 256, 384, + 512, 128, 512, 512, + 512, 512, 0, 512, + 128, 384, 128, 0, + 0, 512, 0, 0, + 0, 0, 128, 0, + 128, 0, 384, 0, + 384, 0, 341 + 1 / 3, 128, + 384, 0, 512, 0, + 512, 0, 512, 128 + } + local poly = vis.polygon(segments, { 256, 128 }) + local r = { 341 + 1/3, 128, 256, 384, 256, 512, 64, 512, 128, 384, 128, 0, 384, 0 } + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testRealBlank5() + local segments = { + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 384, 384, 256, 384, + 128, 384, 0, 384, + 256, 256, 0, 0, + 512, 384, 128, 512, + 256, 384, 128, 128, + 256, 384, 256, 128, + 256, 128, 384, 128, + 384, 256, 0, 384, + 384, 384, 0, 384, + 128, 128, 512, 128, + 0, 128, 512, 0, + 256, 0, 384, 384, + 256, 384, 128, 0 + } + local poly = vis.polygon(segments, { 256, 256 }) + local r = { + 341 + 1 / 3, 256, + 345.6, 268.8, + 256, 298 + 2/3, + 230.4, 307.2, + 192, 192, + 170 + 2/3, 128, + 256, 128, + 298 + 2/3, 128 } + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testRealBlank6() + local camera = { 256, 256 } + local segments = { + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 512, 128, 128, 384, + 384, 512, 128, 0, + 384, 512, 128, 128, + 128, 256, 384, 128, + } + local poly = vis.polygon(segments, camera) + local r = { + 320, 256, + 272, 288, + 3200/13, 3968/13, + 192, 224, + 230.4, 204.8, + 384, 128, + 512, 0, + 512, 128, + } + lu.assertEquals(poly, r) +end + +function TestVisibleSegments:testRealBlank7() + local camera = { 256, 256 } + local segments = { + 0, 0, 512, 0, + 512, 0, 512, 512, + 512, 512, 0, 512, + 0, 512, 0, 0, + 128, 256, 512, 128, + 128, 256, 128, 256, + 256, 128, 256, 256, + 256, 256, 384, 128, + 256, 384, 128, 128, + 256, 128, 256, 384, + 256, 128, 256, 128, + 256, 256, 512, 384, + 128, 384, 256, 128, + 128, 384, 384, 128, + 256, 384, 256, 256, + 256, 128, 384, 512, + } + local poly = vis.polygon(segments, camera) + local r = { + 298 + 2/3, 256, + 307.2, 281.6, + 384, 512, + 256, 512, + 256, 384, + 213 + 1/3, 298 + 2/3, + 192, 256, + 204.8, 230.4, + 307, 282, + } + lu.assertEquals(poly, r) +end + os.exit(lu.LuaUnit.run()) diff --git a/vector-light.lua b/vector-light.lua index d3f31a5..ed23616 100644 --- a/vector-light.lua +++ b/vector-light.lua @@ -22,60 +22,74 @@ 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. -]]-- +]] -- local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2 -local function str(x,y) - return "("..tonumber(x)..","..tonumber(y)..")" +local function str(x, y) + return "(" .. tonumber(x) .. "," .. tonumber(y) .. ")" end -local function mul(s, x,y) - return s*x, s*y +local function mul(s, x, y) + return s * x, s * y end -local function div(s, x,y) - return x/s, y/s +local function div(s, x, y) + return x / s, y / s end -local function add(x1,y1, x2,y2) - return x1+x2, y1+y2 +local function add(x1, y1, x2, y2) + return x1 + x2, y1 + y2 end -local function sub(x1,y1, x2,y2) - return x1-x2, y1-y2 +local function sub(x1, y1, x2, y2) + return x1 - x2, y1 - y2 end -local function permul(x1,y1, x2,y2) - return x1*x2, y1*y2 +local function permul(x1, y1, x2, y2) + return x1 * x2, y1 * y2 end -local function dot(x1,y1, x2,y2) - return x1*x2 + y1*y2 +local function dot(x1, y1, x2, y2) + return x1 * x2 + y1 * y2 end -local function det(x1,y1, x2,y2) - return x1*y2 - y1*x2 +local function det(x1, y1, x2, y2) + return x1 * y2 - y1 * x2 end -local function eq(x1,y1, x2,y2) +local function eq(x1, y1, x2, y2) return x1 == x2 and y1 == y2 end -local function lt(x1,y1, x2,y2) +local function lt(x1, y1, x2, y2) return x1 < x2 or (x1 == x2 and y1 < y2) end -local function le(x1,y1, x2,y2) +local function le(x1, y1, x2, y2) return x1 <= x2 and y1 <= y2 end -local function len2(x,y) - return x*x + y*y +local function len2(x, y) + return x * x + y * y end -local function len(x,y) - return sqrt(x*x + y*y) +local function len(x, y) + return sqrt(x * x + y * y) +end + +-- equation of a line with a and b point +local function lineeq(xa, ya, xb, yb) + if xa == xb then return -1, 0, xa end + local c = ya - xa * (yb - ya) * (xb - xa) + return (ya * c - yb * c) / (xa * yb - ya * xb), + (xb * c - xa * c) / (xa * yb - ya * xb), + c +end + +-- if line (abc) is at dist r to point (x,y) +local function linecontains(a, b, c, x, y, r) + return (a * x + b * y + c) ^ 2 <= r ^ 2 * (a ^ 2 + b ^ 2) end -- < 0 -> counterclockwise @@ -87,9 +101,9 @@ end -- find intersection between line a-b and line c-d local function intersection(ax, ay, bx, by, cx, cy, dx, dy) - local t = ((ax-cx) * (ay-by) - (ay-cy) * (ax-bx)) - / ((dx-cx) * (ay-by) - (dy-cy) * (ax-bx)) - return (cx + t * (dx - cx)), (cy + t * (dy - cy)) + local n = (ax - cx) * (ay - by) - (ay - cy) * (ax - bx) + local d = (dx - cx) * (ay - by) - (dy - cy) * (ax - bx) + return (d * cx + n * (dx - cx)) / d, (d * cy + n * (dy - cy)) / d end -- true if a has inferior angle than b, dist if angle equals @@ -106,7 +120,7 @@ end local function fromPolar(angle, radius) radius = radius or 1 - return cos(angle)*radius, sin(angle)*radius + return cos(angle) * radius, sin(angle) * radius end local function randomDirection(len_min, len_max) @@ -116,47 +130,47 @@ local function randomDirection(len_min, len_max) assert(len_max > 0, "len_max must be greater than zero") assert(len_max >= len_min, "len_max must be greater than or equal to len_min") - return fromPolar(math.random()*2*math.pi, - math.random() * (len_max-len_min) + len_min) + return fromPolar(math.random() * 2 * math.pi, + math.random() * (len_max - len_min) + len_min) end local function toPolar(x, y) - return atan2(y,x), len(x,y) + return atan2(y, x), len(x, y) end -local function dist2(x1,y1, x2,y2) - return len2(x1-x2, y1-y2) +local function dist2(x1, y1, x2, y2) + return len2(x1 - x2, y1 - y2) end -local function dist(x1,y1, x2,y2) - return len(x1-x2, y1-y2) +local function dist(x1, y1, x2, y2) + return len(x1 - x2, y1 - y2) end -local function normalize(x,y) - local l = len(x,y) +local function normalize(x, y) + local l = len(x, y) if l > 0 then - return x/l, y/l + return x / l, y / l end - return x,y + return x, y end -local function rotate(phi, x,y) +local function rotate(phi, x, y) local c, s = cos(phi), sin(phi) - return c*x - s*y, s*x + c*y + return c * x - s * y, s * x + c * y end -local function perpendicular(x,y) +local function perpendicular(x, y) return -y, x end -local function project(x,y, u,v) - local s = (x*u + y*v) / (u*u + v*v) - return s*u, s*v +local function project(x, y, u, v) + local s = (x * u + y * v) / (u * u + v * v) + return s * u, s * v end -local function mirror(x,y, u,v) - local s = 2 * (x*u + y*v) / (u*u + v*v) - return s*u - x, s*v - y +local function mirror(x, y, u, v) + local s = 2 * (x * u + y * v) / (u * u + v * v) + return s * u - x, s * v - y end -- ref.: http://blog.signalsondisplay.com/?p=336 @@ -166,13 +180,28 @@ local function trim(maxLen, x, y) return x * s, y * s end -local function angleTo(x,y, u,v) +local function angleTo(x, y, u, v) if u and v then return atan2(y, x) - atan2(v, u) end return atan2(y, x) end +local function segmentcontains(xa, ya, xb, yb, xc, yc, r) + assert(xa ~= nil and ya ~= nil and xb ~= nil and yb ~= nil and xc ~= nil and yc ~= nil) + local r2 + if r == nil then r2 = 0 else r2 = r ^ 2 end + local d2ac = dist2(xa, ya, xc, yc) + local d2bc = dist2(xb, yb, xc, yc) + if d2ac <= r2 or d2bc <= r2 then return true end + local d2ab = dist2(xa, ya, xb, yb) + if d2ac <= d2ab and d2bc <= d2ab then + local a, b, c = lineeq(xa, ya, xb, yb) + return linecontains(a, b, c, xc, yc, r) + end + return false +end + -- the module return { str = str, @@ -212,4 +241,5 @@ return { mirror = mirror, trim = trim, angleTo = angleTo, + segmentcontains = segmentcontains } diff --git a/visible.lua b/visible.lua index b19f5cb..d92b968 100644 --- a/visible.lua +++ b/visible.lua @@ -1,158 +1,217 @@ local LIBRARY_PATH = (...):match("(.-)[^%.]+$") -local vector = require(LIBRARY_PATH .. "vector") +local vector = require(LIBRARY_PATH .. "vector-light") +local pack = require(LIBRARY_PATH .. "pack-utils") + local visible = {} +local function segment_str(x, y, xg, yg) + return vector.str(x, y) .. "->" .. vector.str(xg, yg) +end -local function comp(a, b, center) - if a.start == b.start then - if a.stop == nil then return false end - if b.stop == nil then return true end - return vector.alignment(a.start, a.stop, b.stop) > 0 +local function print_segments(segments) + pack.print(segments, + function(x, y, xg, yg) print(segment_str(x, y, xg, yg)) end, + 4) +end + +local function comp(x1, y1, xg1, yg1, x2, y2, xg2, yg2, xc, yc) + if x1 == x2 and y1 == y2 then + if x1 == xg1 and y1 == yg1 then return false end + if x2 == xg2 and y2 == yg2 then return true end + local align = vector.alignment(x1, y1, xg1, yg1, xg2, yg2) + if align == 0 then + return vector.dist2(x1, y1, xg1, yg1) < vector.dist2(x2, y2, xg2, yg2) + end + return vector.alignment(x1, y1, xg1, yg1, xg2, yg2) > 0 end - return vector.polar_lt(a.start, b.start, center) + return vector.polar_lt(x1 - xc, y1 - yc, x2 - xc, y2 - yc) end -function visible.polygon(segments, center) - -- generic visibility function - -- return concave (frequently) polygon - if center == nil then - center = { x = 0, y = 0 } - else - center = { x = center[1], y = center[2] } +local function endpoint_comp(xc, yc) + return function(a, b) + return comp(a.x, a.y, a.xg, a.yg, b.x, b.y, b.xg, b.yg, xc, yc) end - local endpoints = {} - do - local next_endpoints = {} - local missing_stops = {} - for i = 1, #segments, 4 do - local a = vector(segments[i], segments[i + 1]) - local b = vector(segments[i + 2], segments[i + 3]) - if a ~= b then - local align = vector.alignment(center, a, b) - local start, stop - if align > 0 or align == 0 and - b:dist2(center) < a:dist2(center) then - start, stop = b, a - else - start, stop = a, b - end - table.insert(next_endpoints, { start = start, stop = stop }) - missing_stops[stop] = true - end +end + +visible.comp = comp + +local function node_comp(xc, yc) + return function(a, b) + return vector.dist2(a[1], a[2], xc, yc) < vector.dist2(b[1], b[2], xc, yc) + end +end + +local function insert_endpoint(t, x, y, xg, yg, xo, yo, xog, yog) + for _, b in ipairs(t) do + if x == b.x and y == b.y and xg == b.xg and yg == b.yg then + return end - local startpoint = center + vector.fromPolar(0, 1) - local nodes = {} - for i, epi in ipairs(next_endpoints) do + end + table.insert(t, { + x = x, y = y, xg = xg, yg = yg, + xo = xo, yo = yo, xog = xog, yog = yog + }) +end + +local function parse_segments(segments, center) + local nodes = {} + local xc, yc = unpack(center) + for i, x, y, xg, yg in pack.ipairs(segments, 4) do + if nodes[i] == nil then nodes[i] = {} end + local xstop, ystop, dstop, jstop + local align = vector.alignment(x, y, xg, yg, xc, yc) + if x ~= xg or y ~= yg then + -- reverse if in bad order + -- centerpoint cut endpoints + if align > 0 then + segments[i], segments[i + 1] = xg, yg + segments[i + 2], segments[i + 3] = x, y + x, y, xg, yg = xg, yg, x, y + elseif align == 0 then + pack.insert(nodes[i], { xc, yc }) + end -- startline cut enpoints - nodes[i] = {} - if vector.alignment(center, startpoint, epi.start) > 0 and - vector.alignment(center, startpoint, epi.stop) < 0 and - vector.alignment(epi.start, epi.stop, center) < 0 then - local intersec = vector.intersection(epi.start, epi.stop, center, startpoint) - table.insert(nodes[i], intersec) + if y < yc and yg > yc then + local xinter, yinter = vector.intersection(x, y, xg, yg, xc, yc, xc + 1, yc) + assert(yinter == yc) + pack.insert(nodes[i], { xinter, yinter }) end -- segments cut endpoints - for j, epj in ipairs(next_endpoints) do - if j == i then break end - if vector.alignment(epi.start, epi.stop, epj.start) - * vector.alignment(epi.start, epi.stop, epj.stop) <= 0 and - vector.alignment(epj.start, epj.stop, epi.start) - * vector.alignment(epj.start, epj.stop, epi.stop) <= 0 then - local intersec = - vector.intersection(epi.start, epi.stop, epj.start, epj.stop) - if intersec ~= epi.start and intersec ~= epi.stop then - table.insert(nodes[i], intersec) + for j, x2, y2, xg2, yg2 in pack.ipairs(segments, 4) do + if j <= i - 4 then + -- segments intersection + if vector.alignment(x, y, xg, yg, x2, y2) + * vector.alignment(x, y, xg, yg, xg2, yg2) <= 0 and + vector.alignment(x2, y2, xg2, yg2, x, y) + * vector.alignment(x2, y2, xg2, yg2, xg, yg) <= 0 and + (y - yg) * (x2 - xg2) ~= (y2 - yg2) * (x - xg) then + local xinter, yinter = + vector.intersection(x, y, xg, yg, x2, y2, xg2, yg2) + if (xinter ~= x or yinter ~= y) and (xinter ~= xg or yinter ~= yg) then + pack.insert(nodes[i], { xinter, yinter }) + end + if (xinter ~= x2 or yinter ~= y2) and (xinter ~= xg2 or yinter ~= yg2) then + pack.insert(nodes[j], { xinter, yinter }) + end end - if intersec ~= epj.start and intersec ~= epj.stop then - table.insert(nodes[j], intersec) + end + -- segment end cut segments behind + if vector.alignment(xc, yc, x, y, x2, y2) < 0 and + vector.alignment(x2, y2, xg2, yg2, xg, yg) < 0 and + vector.alignment(xc, yc, xg, yg, x2, y2) <= 0 and + vector.alignment(xc, yc, xg, yg, xg2, yg2) >= 0 then + print(segment_str(x2, y2, xg2, yg2), "hides", segment_str(x, y, xg, yg)) + local xinter, yinter = vector.intersection(xc, yc, xg, yg, x2, y2, xg2, yg2) + local dinter = vector.dist2(xg, yg, xinter, yinter) + if xstop == nil or dstop > dinter or + xinter == xstop and yinter == ystop and + vector.alignment( + xinter, yinter, xg2, yg2, segments[jstop], segments[jstop + 1] + ) then + xstop, ystop, dstop, jstop = xinter, yinter, dinter, j end end end - end - -- register endpoints inserting nodes - for i, endpoint in ipairs(next_endpoints) do - table.sort(nodes[i], function(a, b) - return a:dist2(endpoint.start) < b:dist2(endpoint.start) - end) - missing_stops[endpoint.start] = nil - local current_start = endpoint.start - for _, node in ipairs(nodes[i]) do - missing_stops[node] = nil - table.insert(endpoints, { start = current_start, stop = node }) - current_start = node + -- end of segment + if xstop == nil then + pack.insert(nodes[i], { xg, yg }) + else + print(segment_str(x, y, xg, yg), "stop", vector.str(xstop, ystop)) + if nodes[jstop] == nil then nodes[jstop] = {} end + pack.insert(nodes[jstop], { xstop, ystop }) end - table.insert(endpoints, { start = current_start, stop = endpoint.stop }) end - -- add missing stops - for stop, _ in pairs(missing_stops) do - table.insert(endpoints, { start = stop }) + end + -- register endpoints inserting nodes + local ordereds = {} + for i, x, y, xg, yg in pack.ipairs(segments, 4) do + pack.sort(nodes[i], node_comp(x, y), 2) + local xstart, ystart = x, y + for _, xn, yn in pack.ipairs(nodes[i], 2) do + if xn ~= xstart or yn ~= ystart then + insert_endpoint(ordereds, xstart, ystart, xn, yn, x, y, xg, yg) + xstart, ystart = xn, yn + end end + insert_endpoint(ordereds, xstart, ystart, xg, yg, x, y, xg, yg) end - table.sort(endpoints, function(a, b) return comp(a, b, center) end) + table.sort(ordereds, endpoint_comp(xc, yc)) + return ordereds +end + +function visible.polygon(segments, center) + -- generic visibility function + -- return concave (frequently) polygon + if center == nil then center = { 0, 0 } end + local xc, yc = center[1], center[2] + local ordereds = parse_segments(segments, center) local polygon = {} local current - local function cycle(epi) - -- if no current point, take the first - if current == nil then - if epi.stop ~= nil then - current = epi - table.insert(polygon, epi.start.x) - table.insert(polygon, epi.start.y) - end - -- if endpoint is the current target - elseif epi.start == current.stop then - table.insert(polygon, epi.start.x) - table.insert(polygon, epi.start.y) - current = nil - -- search an other endpoint behind - for _, epj in ipairs(endpoints) do - -- take the nearest point in radius not on previous segment - if epj.stop ~= nil and - vector.alignment(center, epi.start, epj.start) >= 0 and - vector.alignment(center, epi.start, epj.stop) < 0 and - (current == nil or - vector.alignment(epj.start, epj.stop, current.start) > 0) then - local intersec - if vector.alignment(center, epi.start, epj.start) == 0 then - -- epi.start == epj.start included here - -- epi.stop are already sorted so no need to compare - intersec = epj.start + local function cycle(a) + if vector.alignment(xc, yc, a.xo, a.yo, a.xog, a.yog) ~= 0 then + print("iter " .. vector.str(a.x, a.y) .. " -> " .. vector.str(a.xg, a.yg)) + assert(a.y ~= yc or a.x ~= xc) + if current == nil then + table.insert(polygon, a.x) + table.insert(polygon, a.y) + if a.x ~= a.xg or a.y ~= a.yg then current = a end + elseif a.x == current.xg and a.y == current.yg then + table.insert(polygon, a.x) + table.insert(polygon, a.y) + if a.x == a.xg or a.y == a.yg then + current = nil + else current = a end + elseif vector.alignment(xc, yc, current.xg, current.yg, a.x, a.y) == 0 then + if a.y ~= yc or a.x < xc or a.x == current.xg then + table.insert(polygon, a.x) + table.insert(polygon, a.y) + if a.x == a.xg or a.y == a.yg then + for _, b in ipairs(ordereds) do + if vector.alignment(a.xo, a.yo, a.xog, a.yog, b.x, b.y) > 0 and + vector.alignment(xc, yc, a.x, a.y, b.x, b.y) <= 0 and + vector.alignment(xc, yc, a.x, a.y, b.xg, b.yg) > 0 then + local xinter, yinter = vector.intersection( + xc, yc, a.x, a.y, b.xo, b.yo, b.xog, b.yog + ) + current = { + x = xinter, y = yinter, xg = b.xg, yg = b.yg, + xo = b.xo, yo = b.yo, xog = b.xog, yog = b.yog + } + end + end else - intersec = vector.intersection( - epj.start, epj.stop, center, epi.start - ) + current = a end - current = { start = intersec, stop = epj.stop } end - end - if current ~= nil and current.start ~= epi.start then - table.insert(polygon, current.start.x) - table.insert(polygon, current.start.y) - end - -- if endpoint starts over the current - elseif epi.stop ~= nil and - vector.alignment(current.start, current.stop, epi.start) < 0 then - local intersec = vector.intersection( - current.start, current.stop, center, epi.start - ) - table.insert(polygon, intersec.x) - table.insert(polygon, intersec.y) - table.insert(polygon, epi.start.x) - table.insert(polygon, epi.start.y) - current = epi + elseif vector.alignment(xc, yc, current.xg, current.yg, a.x, a.y) > 0 then + assert( + vector.alignment(current.x, current.y, current.xg, current.yg, a.x, a.y) ~= 0 or + a.x == current.x and a.y == current.y, + vector.str(a.x, a.y) .. " on " .. + vector.str(current.x, current.y) .. "->" .. vector.str(current.xg, current.yg) + ) + -- endpoint start before + if vector.alignment(current.x, current.y, current.xg, current.yg, a.x, a.y) < 0 then + local xinter, yinter = vector.intersection( + current.xo, current.yo, current.xog, current.yog, xc, yc, a.x, a.y + ) + table.insert(polygon, xinter) + table.insert(polygon, yinter) + table.insert(polygon, a.x) + table.insert(polygon, a.y) + current = a + end + else assert(false) end + if current ~= nil then print("current", vector.str(current.x, current.y), vector.str(current.xg, current.yg)) end end end - for _, epi in ipairs(endpoints) do - cycle(epi) + for _, endpoint in ipairs(ordereds) do + cycle(endpoint) end - for _, epi in ipairs(endpoints) do - if epi.start.y ~= center.y or epi.start.x < center.x then break end - if epi.start.x ~= polygon[1] and epi.start == current.stop then - table.insert(polygon, epi.start.x) - table.insert(polygon, epi.start.y) - break - end + for _, endpoint in ipairs(ordereds) do + if endpoint.y ~= yc or endpoint.x < xc then break end + cycle(endpoint) end return polygon end