diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..cc99e07 --- /dev/null +++ b/conf.lua @@ -0,0 +1,3 @@ +function lovr.conf(t) + t.headset.overlay = true +end \ No newline at end of file diff --git a/config/action_button.txt b/config/action_button.txt new file mode 100644 index 0000000..2e65efe --- /dev/null +++ b/config/action_button.txt @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/config/check_density.txt b/config/check_density.txt new file mode 100644 index 0000000..30e2fb4 --- /dev/null +++ b/config/check_density.txt @@ -0,0 +1 @@ +0.05 \ No newline at end of file diff --git a/config/color_close_corners.json b/config/color_close_corners.json new file mode 100644 index 0000000..2c12a93 --- /dev/null +++ b/config/color_close_corners.json @@ -0,0 +1 @@ +[1.0,0.0,1.0,1.0] \ No newline at end of file diff --git a/config/color_close_grid.json b/config/color_close_grid.json new file mode 100644 index 0000000..2c12a93 --- /dev/null +++ b/config/color_close_grid.json @@ -0,0 +1 @@ +[1.0,0.0,1.0,1.0] \ No newline at end of file diff --git a/config/color_far_corners.json b/config/color_far_corners.json new file mode 100644 index 0000000..b0cf1ca --- /dev/null +++ b/config/color_far_corners.json @@ -0,0 +1 @@ +[1.0,0.0,1.0,0.1] \ No newline at end of file diff --git a/config/color_far_grid.json b/config/color_far_grid.json new file mode 100644 index 0000000..2c4a63a --- /dev/null +++ b/config/color_far_grid.json @@ -0,0 +1 @@ +[1.0,0.0,1.0,0.0] \ No newline at end of file diff --git a/config/fade_start.txt b/config/fade_start.txt new file mode 100644 index 0000000..389f774 --- /dev/null +++ b/config/fade_start.txt @@ -0,0 +1 @@ +4.0 \ No newline at end of file diff --git a/config/fade_stop.txt b/config/fade_stop.txt new file mode 100644 index 0000000..415b19f --- /dev/null +++ b/config/fade_stop.txt @@ -0,0 +1 @@ +2.0 \ No newline at end of file diff --git a/config/grid_bottom.txt b/config/grid_bottom.txt new file mode 100644 index 0000000..171538e --- /dev/null +++ b/config/grid_bottom.txt @@ -0,0 +1 @@ +0.0 \ No newline at end of file diff --git a/config/grid_density.txt b/config/grid_density.txt new file mode 100644 index 0000000..9f8e9b6 --- /dev/null +++ b/config/grid_density.txt @@ -0,0 +1 @@ +1.0 \ No newline at end of file diff --git a/config/grid_top.txt b/config/grid_top.txt new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/config/grid_top.txt @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/lib/json.lua b/lib/json.lua new file mode 100644 index 0000000..711ef78 --- /dev/null +++ b/lib/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- 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. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..07d41f5 --- /dev/null +++ b/main.lua @@ -0,0 +1,321 @@ +-- Bootstrap +appName = "lovr-playspace" +mainScriptPath = (debug.getinfo(1, "S").source:sub(2):match("(.*[/\\])") or "./"):sub(1,-2) +package.path = mainScriptPath.. "/lib/?.lua;" ..mainScriptPath.. "/lib/?/_main.lua;" ..package.path + +-- App +hands = {"hand/right","hand/left"} +limbs = { + "head", + "hand/left", + "hand/right", + "hand/left/point", + "hand/right/point", + "elbow/left", + "elbow/right", + "shoulder/left", + "shoulder/right", + "chest", + "waist", + "knee/left", + "knee/right", + "foot/left", + "foot/right" +} + +configDirs = {} +json = require("json") + +function platformConfig() + if os.getenv("HOME") ~= nil then + return os.getenv("HOME") .. "/.config" + end + + return os.getenv("APPDATA") +end + +function fileExists(fileName) + local file = io.open(fileName,"rb") + if file == nil then return false end + file:close() + return true +end + +function getConfigFile(fileName) + for _,path in ipairs(configDirs) do + if fileExists(path .. "/" .. fileName) then + return path .. "/" .. fileName + end + end +end + +function userConfig(fileName) + return configDirs[1] .. "/" ..fileName +end + +function fileWrite(fileName,content) + local file = io.open(fileName,"wb") + file:write(content) + file:close() +end + +function readFile(fileName) + local file = io.open(fileName,"rb") + content = file:read("*all") + file:close() + return content +end + +function getDistanceBetweenPoints3D(x1,y1,z1,x2,y2,z2) + return (((x2-x1)*(x2-x1)) + ((y2-y1)*(y2-y1)) + ((z2-z1)*(z2-z1))) / 2 +end + +-- This could be optimized by using a proper algorithm or determining which point of the line is closer to x,y,z first +function getLineDistance(x,y,z,point1,point2) + local lx1 = point1[1] + local ly1 = 0 + local lz1 = point1[2] + local lx2 = point2[1] + local ly2 = 0 + local lz2 = point2[2] + + local d = getDistanceBetweenPoints3D(lx1,ly1,lz1,lx2,ly2,lz2) + local dx = (lx2 - lx1) / d + local dy = (ly2 - ly1) / d + local dz = (lz2 - lz1) / d + local cx1 = lx1 + local cy1 = ly1 + local cz1 = lz1 + local lowestDist = 9999 + + while cx1 < lx1 do + local dist = getDistanceBetweenPoints3D(x,y,z,cx1,cy1,cz1) + if dist < lowestDist then + lowestDist = dist + else + return lowestDist + end + cx1 = cx1 + (dx * settings.check_density) + cy1 = cy1 + (dy * settings.check_density) + cz1 = cz1 + (dz * settings.check_density) + end + + return lowestDist +end + +function getButton(method,button,devices) + for _,device in ipairs(devices) do + if method(device,button) == true then return device end + end +end + +function isTracked(device) + local x,y,z = lovr.headset.getPosition(device) + if x == 0.0 and y == 0.0 and z == 0.0 then return false end + return true +end + +function drawSinglePointGrid(pass,point1,point2,cornerColor,miscColor) + local _,hy,_ = lovr.headset.getPosition("head") + local lx1 = point1[1] + local ly1 = hy + local lz1 = point1[2] + local lx2 = point2[1] + local ly2 = hy + local lz2 = point2[2] + + local d = getDistanceBetweenPoints3D(lx1,ly1,lz1,lx2,ly2,lz2) + local dx = (lx2 - lx1) / d + local dy = (ly2 - ly1) / d + local dz = (lz2 - lz1) / d + + pass:setColor(unpack(miscColor)) + local drawY = settings.grid_top + while drawY >= settings.grid_bottom do + pass:line({ + lx1,drawY,lz1, + lx2,drawY,lz2 + }) + drawY = drawY - settings.grid_density + end + + pass:setColor(unpack(cornerColor)) + pass:line({ + lx1,settings.grid_bottom,lz1, + lx1,settings.grid_top,lz1 + }) + + pass:line({ + lx1,settings.grid_bottom,lz1, + lx2,settings.grid_bottom,lz2 + }) + + pass:line({ + lx1,settings.grid_top,lz1, + lx2,settings.grid_top,lz2 + }) +end + +function drawPointGrid(pass,points,cornerColor,miscColor) + local index = 2 + local length = #points + if length < 1 then return end + while index <= length do + drawSinglePointGrid(pass,points[index - 1],points[index],cornerColor,miscColor) + index = index + 1 + end + drawSinglePointGrid(pass,points[length],points[1],cornerColor,miscColor) +end + +function lovr.load() + lovr.graphics.setBackgroundColor(0.0, 0.0, 0.0, 0.0) + --table.insert(configDirs,platformConfig() .. "/" .. appName) + table.insert(configDirs,mainScriptPath .. "/config") + + settings = {} + settings.action_button = readFile(getConfigFile("action_button.txt")) + settings.check_density = tonumber(readFile(getConfigFile("check_density.txt"))) + settings.fade_start = tonumber(readFile(getConfigFile("fade_start.txt"))) + settings.fade_stop = tonumber(readFile(getConfigFile("fade_stop.txt"))) + settings.grid_density = tonumber(readFile(getConfigFile("grid_density.txt"))) + settings.grid_bottom = tonumber(readFile(getConfigFile("grid_bottom.txt"))) + settings.grid_top = tonumber(readFile(getConfigFile("grid_top.txt"))) + settings.color_close_corners = json.decode(readFile(getConfigFile("color_close_corners.json"))) + settings.color_close_grid = json.decode(readFile(getConfigFile("color_close_grid.json"))) + settings.color_far_corners = json.decode(readFile(getConfigFile("color_far_corners.json"))) + settings.color_far_grid = json.decode(readFile(getConfigFile("color_far_grid.json"))) + settings.points = {} + + --[[if not lovr.filesystem.isDirectory(configDirs[1]) then + fileWrite(userConfig("action_button.txt"),readFile(getConfigFile("action_button.txt"))) + fileWrite(userConfig("check_density.txt"),readFile(getConfigFile("check_density.txt"))) + fileWrite(userConfig("fade_start.txt"),readFile(getConfigFile("fade_start.txt"))) + fileWrite(userConfig("fade_stop.txt"),readFile(getConfigFile("fade_stop.txt"))) + fileWrite(userConfig("grid_density.txt"),readFile(getConfigFile("grid_density.txt"))) + fileWrite(userConfig("grid_bottom.txt"),readFile(getConfigFile("grid_bottom.txt"))) + fileWrite(userConfig("grid_top.txt"),readFile(getConfigFile("grid_top.txt"))) + fileWrite(userConfig("color_close_corners.json"),readFile(getConfigFile("color_close_corners.json"))) + fileWrite(userConfig("color_close_grid.json"),readFile(getConfigFile("color_close_grid.json"))) + fileWrite(userConfig("color_far_corners.json"),readFile(getConfigFile("color_far_corners.json"))) + fileWrite(userConfig("color_far_grid.json"),readFile(getConfigFile("color_far_grid.json"))) + initConfigure() + return + end]]-- + + if getConfigFile("points.json") == nil then + initConfigure() + return + end + + for _,hand in ipairs(hands) do + if lovr.headset.isDown(hand,settings.action_button) then + initConfigure() + return + end + end + + settings.points = json.decode(readFile(getConfigFile("points.json"))) + mode = modeDraw +end + +function initConfigure() + saveProg = 1.0 + + lovr.update = function(dt) + deltaTime = dt + end + + mode = modeConfigure +end + +function deinitConfigure() + saveProg = nil + lovr.update = nil + deltaTime = nil + mode = modeDraw +end + +function modeConfigure(pass) + local _,hy,_ = lovr.headset.getPosition("head") + + for _,hand in ipairs(hands) do + if isTracked(hand .. "/point") then + local x,y,z = lovr.headset.getPosition(hand .. "/point") + pass:setColor(1,0,0,0.5 * saveProg) + pass:sphere(x,y,z,0.1) + pass:setColor(1,1,1,saveProg) + pass:text( + "- Press '" ..settings.action_button.. "' to add a point -\n" .. + "- Hold '" ..settings.action_button.. "' to save -\n\n" .. + string.format("%.2f",x) .. "," .. string.format("%.2f",y) .. "," .. string.format("%.2f",z) + ,x,y - 0.3,z,0.066) + end + end + + local inputDev = getButton(lovr.headset.wasReleased,settings.action_button,hands) + if inputDev ~= nil and isTracked(inputDev) then + local hx,_,hz = lovr.headset.getPosition(inputDev) + table.insert(settings.points,{hx,hz}) + end + + inputDev = getButton(lovr.headset.isDown,settings.action_button,hands) + if inputDev ~= nil then + saveProg = saveProg - (deltaTime / 3) + if saveProg <= 0 then + fileWrite(userConfig("points.json"),json.encode(settings.points)) + deinitConfigure() + modeDraw(pass) + return + end + else + saveProg = 1.0 + end + + pass:setColor(1,0,0,0.5) + for _,point in ipairs(settings.points) do + pass:sphere(point[1],1.5,point[2],0.1) + end + + modeDraw(pass) +end + +function modeDraw(pass) + local x,y,z = lovr.headset.getPosition("head") + local lowestDist = 9999 + local index = 2 + local length = #settings.points + if length < 1 then return end + while index <= length do + local dist = getLineDistance(x,y,z,settings.points[index - 1],settings.points[index]) + if dist < lowestDist then lowestDist = dist end + index = index + 1 + end + + lowestDist = (lowestDist - settings.fade_stop) / (settings.fade_start - settings.fade_stop) + if lowestDist < 0 then lowestDist = 0 end + if lowestDist > 1 then lowestDist = 1 end + + local cdr=settings.color_close_corners[1] - settings.color_far_corners[1] + cdr = settings.color_far_corners[1] + (cdr * lowestDist) + local cdg=settings.color_close_corners[2] - settings.color_far_corners[2] + cdg = settings.color_far_corners[2] + (cdg * lowestDist) + local cdb=settings.color_close_corners[3] - settings.color_far_corners[3] + cdb = settings.color_far_corners[3] + (cdb * lowestDist) + local cda=settings.color_close_corners[4] - settings.color_far_corners[4] + cda = settings.color_far_corners[4] + (cda * lowestDist) + + local gdr=settings.color_close_grid[1] - settings.color_far_grid[1] + gdr = settings.color_far_grid[1] + (gdr * lowestDist) + local gdg=settings.color_close_grid[2] - settings.color_far_grid[2] + gdg = settings.color_far_grid[2] + (gdg * lowestDist) + local gdb=settings.color_close_grid[3] - settings.color_far_grid[3] + gdb = settings.color_far_grid[3] + (gdb * lowestDist) + local gda=settings.color_close_grid[4] - settings.color_far_grid[4] + gda = settings.color_far_grid[4] + (gda * lowestDist) + + drawPointGrid(pass,settings.points,{cdr,cdg,cdb,cda},{gdr,gdg,gdb,gda}) +end + +function lovr.draw(pass) + mode(pass) +end \ No newline at end of file