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/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..7ba08c0 --- /dev/null +++ b/demos/visible/main.lua @@ -0,0 +1,166 @@ +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_COLOR = { .3, .3, .3 } +local VISIBLE_POINT_COLOR = { 0, 0, 0 } +local VISIBLE_AREA_COLOR = { 1, 1, 1 } +local CAMERA_COLOR = { .8, .2, .2 } + +local segments = {} +local visibles, triangles +local visibles_dirty +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 init() + 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_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 + -- segmentsinput.draw() + -- segments + love.graphics.setColor(SEGMENT_COLOR) + love.graphics.setLineWidth(SEGMENT_WIDTH) + love.graphics.setPointSize(SEGMENT_POINT_SIZE) + love.graphics.points(segments) + 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) + love.graphics.points(camera[1], camera[2]) + + love.graphics.setBackgroundColor(BACKGROUND_COLOR) +end + +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 + print("dirty") + visibles_dirty = true + elseif key == "s" then + print("setup") + init() + 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 = { 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 + +function love.mousereleased(x, y, button) + if button == 1 then + if camera_pressed then + 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, 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/perf_visible.lua b/tests/perf_visible.lua new file mode 100644 index 0000000..0dde4f0 --- /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 = { 256, 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..7cab14d --- /dev/null +++ b/tests/visible.lua @@ -0,0 +1,446 @@ +local lu = require("luaunit") +local vec = require("vector-light") +local vis = require("visible") +local pack = require("pack-utils") + + +function testOrientation() + 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(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() + 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 + }, { 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 + }, { 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, {}) +end + +function TestVisibleSegments:testTwoLinesOnStart() + lu.assertEquals( + vis.polygon({ + 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 } + ) +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, + }, { 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, + }, { 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 + }, { 256, 256 }) + 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 6fb20d2..ed23616 100644 --- a/vector-light.lua +++ b/vector-light.lua @@ -22,65 +22,105 @@ 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 +-- = 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 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 +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 + return cos(angle) * radius, sin(angle) * radius end local function randomDirection(len_min, len_max) @@ -90,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 @@ -140,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, @@ -176,6 +231,9 @@ return { len = len, dist2 = dist2, dist = dist, + alignment = alignment, + intersection = intersection, + polar_lt = polar_lt, normalize = normalize, rotate = rotate, perpendicular = perpendicular, @@ -183,4 +241,5 @@ return { mirror = mirror, trim = trim, angleTo = angleTo, + segmentcontains = segmentcontains } 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..d92b968 --- /dev/null +++ b/visible.lua @@ -0,0 +1,219 @@ +local LIBRARY_PATH = (...):match("(.-)[^%.]+$") +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 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(x1 - xc, y1 - yc, x2 - xc, y2 - yc) +end + +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 +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 + 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 + 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, 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 + 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 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 + end + 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(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(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 + current = a + end + end + 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 _, endpoint in ipairs(ordereds) do + cycle(endpoint) + end + for _, endpoint in ipairs(ordereds) do + if endpoint.y ~= yc or endpoint.x < xc then break end + cycle(endpoint) + end + return polygon +end + +return visible