1
0
mirror of https://github.com/20kdc/OC-KittenOS.git synced 2024-11-08 19:48:07 +11:00
OC-KittenOS/repository/apps/app-nbox2018.lua
20kdc 0c2fd021fe Fixup packaging stuff, add nbox2018 isometric 3D
Only a few tweaks left until R2!
2018-04-24 03:41:24 +01:00

685 lines
16 KiB
Lua

-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- app-nbox2018.lua : NODEBOX 2018
-- Authors: 20kdc
-- Current layout
-- 12345678901234567890123456789012345678901234567890
-- 1 | |
-- 2 | | 3d 32x32 panel
-- 3 | |
-- 4 | |
-- 5 | |
-- 6 | |
-- 7 | |
-- 8 | |
-- 9-XY Ortho-ACTIV-+-XZ Ortho-ACTIV-+-ST:OFF-+-FILE:-
--10This was the story of someone cal|ABCDEFGH|F1 New
--11led Stanley. Stanley got very cro|IJKLMNOP|F3 Load
--12ss because someone else used his |QRSTUVWX|F4 Save
--13name for a game. Stanley's silly.|YZ[\]^_`|F5 XYXZ
-- program start
local icecap = neo.requireAccess("x.neo.pub.base", "filedialogs")
local window = neo.requireAccess("x.neo.pub.window", "window")(50, 13)
local fmttext = require("fmttext")
local braille = require("braille")
-- [true] = {["A"] = {
-- tex = "",
-- -- numbers are 0 to 15:
-- minX = 0, minY = 0, minZ = 0,
-- maxX = 16, maxY = 16, maxZ = 16,
-- rgb = 0xFFFFFF
-- }}
local boxes = {
[true] = {},
[false] = {}
}
local redstone = false
local button = false
local fileLabel = "NB2018"
local fileTooltip = nil
-- program
local xyz = false
local state = false
local cx, cy, cz = 1, 1, 1
local cursorBlink = false
local selectedBox
local tintDigi = 0
-- minX/minY/minZ are +1 from the usual values
-- tex/rgb are defaults until edited
-- maxX/maxY/maxZ only present after 2nd point placed
-- final corrections performed on submission to boxes table
local workingOnBox = nil
local programState = "none"
-- ["state"] = {lines, keydown, clipboard}
local programStates = {
none = {
function (miText, mxText)
-- This state handles both box selected & box not selected,
-- because the box can get deselected out of program control
if selectedBox then
local targetBox = boxes[state][selectedBox]
return {
"'" .. selectedBox .. "' " .. targetBox.tex,
"Tint #" .. string.format("%06x", targetBox.rgb),
"Enter deselects, Delete deletes.",
"F9 and F10 change texture/tint."
}
end
local str = string.format("%02i, %02i, %02i", cx, cy, cz)
return {
"Nothing selected. " .. str,
"Enter starts a new box, while ",
" boxes are selected by letter. ",
"F8 prints, TAB toggles state. "
}
end,
function (ka, kc)
if ka == 13 then
if selectedBox then
selectedBox = nil
else
-- Beginning box!
workingOnBox = {
minX = cx,
minY = cy,
minZ = cz,
tex = "",
rgb = 0xFFFFFF
}
programState = "point2"
end
elseif kc == 67 then
-- Texture
if selectedBox then programState = "texture" end
elseif kc == 68 then
-- Tint
if selectedBox then tintDigi = 1 programState = "tint" end
elseif ka == 127 or ka == 8 then
-- Delete
if selectedBox then
boxes[state][selectedBox] = nil
selectedBox = nil
end
else
local cc = unicode.char(ka):upper()
if boxes[state][cc] then
selectedBox = cc
end
end
end,
function (text)
end
},
point2 = {
function (miText, mxText)
return {
"Placing Point 2:" .. miText .. "/" .. mxText,
"Enter confirms.",
"Arrows move 2nd point.",
"Delete/Backspace cancels."
}
end,
function (ka, kc)
if ka == 127 or ka == 8 then
workingOnBox = nil
programState = "none"
elseif ka == 13 then
workingOnBox.maxX = cx
workingOnBox.maxY = cy
workingOnBox.maxZ = cz
local ch = 65
while boxes[state][string.char(ch)] do
ch = ch + 1
end
local ax, ay, az = workingOnBox.minX, workingOnBox.minY, workingOnBox.minZ
local bx, by, bz = workingOnBox.maxX, workingOnBox.maxY, workingOnBox.maxZ
workingOnBox.minX = math.min(ax, bx) - 1
workingOnBox.minY = math.min(ay, by) - 1
workingOnBox.minZ = math.min(az, bz) - 1
workingOnBox.maxX = math.max(ax, bx)
workingOnBox.maxY = math.max(ay, by)
workingOnBox.maxZ = math.max(az, bz)
selectedBox = string.char(ch)
boxes[state][selectedBox] = workingOnBox
workingOnBox = nil
programState = "none"
end
end,
function (text)
end
},
texture = {
function (miText, mxText)
local targetBox = boxes[state][selectedBox]
local fieldContent = unicode.safeTextFormat(targetBox.tex)
fieldContent = fmttext.pad(fieldContent, 30, false, false)
fieldContent = unicode.sub(fieldContent, math.max(1, unicode.len(fieldContent) - 29))
return {
"Texturing Box:" .. miText .. "/" .. mxText,
"Type texture ID or use clipboard",
"[" .. fieldContent .. "]",
"Enter to confirm."
}
end,
function (ka, kc)
local targetBox = boxes[state][selectedBox]
if ka == 127 or ka == 8 then
targetBox.tex = unicode.sub(targetBox.tex, 1, unicode.len(targetBox.tex) - 1)
elseif ka == 13 then
programState = "none"
elseif ka >= 32 then
targetBox.tex = targetBox.tex .. unicode.char(ka)
end
end,
function (text)
boxes[state][selectedBox].tex = text
programState = "none"
end
},
tint = {
function (miText, mxText)
local targetBox = boxes[state][selectedBox]
local a = "#"
local b = " "
local rgb = targetBox.rgb
local div = 0x100000
for i = 1, 6 do
a = a .. string.format("%01x", math.floor(rgb / div) % 16)
if tintDigi == i then
b = b .. "^"
else
b = b .. " "
end
div = math.floor(div / 16)
end
return {
"Tinting Box:" .. miText .. "/" .. mxText,
a,
b,
"Enter hexadecimal digits."
}
end,
function (ka, kc)
local targetBox = boxes[state][selectedBox]
local shifts = {
20,
16,
12,
8,
4,
0
}
local hexChars = {
[48] = 0, [65] = 10, [97] = 10,
[49] = 1, [66] = 11, [98] = 11,
[50] = 2, [67] = 12, [99] = 12,
[51] = 3, [68] = 13, [100] = 13,
[52] = 4, [69] = 14, [101] = 14,
[53] = 5, [70] = 15, [102] = 15,
[54] = 6,
[55] = 7,
[56] = 8,
[57] = 9,
}
if hexChars[ka] then
local shift = math.floor(2^shifts[tintDigi])
local low = targetBox.rgb % shift
local high = math.floor(targetBox.rgb / (shift * 16)) * (shift * 16)
targetBox.rgb = low + high + (hexChars[ka] * shift)
tintDigi = 1 + (tintDigi or 1)
if tintDigi == 7 then
tintDigi = nil
programState = "none"
end
end
end,
function (text)
end
}
}
local function onRect(x, y, minX, minY, maxX, maxY)
-- Lines
if x == minX then
return y >= minY and y <= maxY
elseif x == maxX then
return y >= minY and y <= maxY
elseif y == minY then
return x >= minX and x <= maxX
elseif y == maxY then
return x >= minX and x <= maxX
end
return false
end
local function getPixel(x, y, p)
-- the reason is obvious for plane1, but less so for plane2
-- just consider that without this, the top of the screen would be facing you, but X would remain your left/right
y = 17 - y
if p == 1 then
if x == cx and y == cy then
return cursorBlink
end
else
if x == cx and y == cz then
return cursorBlink
end
end
if workingOnBox then
local minX, minY, minZ = workingOnBox.minX, workingOnBox.minY, workingOnBox.minZ
local maxX, maxY, maxZ = cx, cy, cz
if workingOnBox.maxX then
maxX, maxY, maxZ = workingOnBox.maxX, workingOnBox.maxY, workingOnBox.maxZ
end
minX, maxX = math.min(minX, maxX), math.max(minX, maxX)
minY, maxY = math.min(minY, maxY), math.max(minY, maxY)
minZ, maxZ = math.min(minZ, maxZ), math.max(minZ, maxZ)
if p == 1 then
if onRect(x, y, minX, minY, maxX, maxY) then
return cursorBlink
end
else
if onRect(x, y, minX, minZ, maxX, maxZ) then
return cursorBlink
end
end
end
for k, v in pairs(boxes[state]) do
if (not selectedBox) or (k == selectedBox) then
if p == 1 then
if onRect(x, y, v.minX + 1, v.minY + 1, v.maxX, v.maxY) then
return true
end
else
if onRect(x, y, v.minX + 1, v.minZ + 1, v.maxX, v.maxZ) then
return true
end
end
end
end
return false
end
local function get3DPixel(xo, yo)
local function inLine(xa, ya, xb, yb)
xa, ya, xb, yb = math.floor(xa), math.floor(ya), math.floor(xb), math.floor(yb)
local xd = math.abs(xa - xb)
local yd = math.abs(ya - yb)
if xd > yd then
local point = math.abs(xo - xa) / xd
local cast = math.floor((point * (0.99 + yb - ya)) + ya)
if cast ~= yo then
return false
end
elseif yd ~= 0 then
local point = math.abs(yo - ya) / yd
local cast = math.floor((point * (0.99 + xb - xa)) + xa)
if cast ~= xo then
return false
end
end
-- clipping
return
xo >= math.min(xa, xb) and
xo <= math.max(xa, xb) and
yo >= math.min(ya, yb) and
yo <= math.max(ya, yb)
end
local cacheX = {}
local cacheY = {}
local function point3(ax, ay, az)
local k = ax .. "_" .. ay .. "_" .. az
if cacheX[k] then return cacheX[k], cacheY[k] end
local ox = 16
local oy = 15.5
oy = oy - (ay / 2)
ox = ox + (ax / 2)
ox = ox - (az / 2)
oy = oy + (ax / 4)
oy = oy + (az / 4)
cacheX[k] = ox
cacheY[k] = oy
return ox, oy
end
local function in3Line(ax, ay, az, bx, by, bz)
local sc = 1.9
ax, ay = point3(ax * sc, ay * sc, az * sc)
bx, by = point3(bx * sc, by * sc, bz * sc)
return inLine(ax, ay, bx, by)
end
local function inShape(ax, ay, az, bx, by, bz)
return
in3Line(ax, ay, az, bx, ay, az) or
in3Line(ax, ay, az, ax, ay, bz) or
in3Line(bx, ay, az, bx, ay, bz) or
in3Line(ax, ay, bz, bx, ay, bz) or
in3Line(ax, ay, az, ax, by, az) or
in3Line(ax, ay, bz, ax, by, bz) or
in3Line(bx, ay, az, bx, by, az) or
in3Line(bx, ay, bz, bx, by, bz) or
in3Line(ax, by, az, bx, by, az) or
in3Line(ax, by, az, ax, by, bz) or
in3Line(bx, by, az, bx, by, bz) or
in3Line(ax, by, bz, bx, by, bz)
end
for k, v in pairs(boxes[state]) do
if (not selectedBox) or (k == selectedBox) then
if inShape(16 - v.minZ, v.minY, 16 - v.minX, 16 - v.maxZ, v.maxY, 16 - v.maxX) then
return true
end
end
end
return false
end
local function render(line, doBraille)
if line < 9 then
local textA, textB = "", ""
local bo = (line - 1) * 2
for i = 1, 16 do
for p = 1, 2 do
local pxH, pxL = getPixel(i, bo + 1, p), getPixel(i, bo + 2, p)
local tx
if pxH then
if pxL then
tx = ""
else
tx = ""
end
else
if pxL then
tx = ""
else
tx = " "
end
end
if p == 1 then
textA = textA .. tx
else
textB = textB .. tx
end
end
end
window.span(1, line, textA .. "|" .. textB .. "|", 0, 0xFFFFFF)
if doBraille then
braille.calcLine(35, line, 16, window.span, function (xo, yo)
if get3DPixel(xo, yo + ((line - 1) * 4)) then
return 255, 255, 255
else
return 0, 0, 0
end
end, nil)
end
elseif line == 9 then
local sts = "ON "
if not state then
sts = "OFF"
end
local actA = "--*F5"
local actB = "--*F5"
if not xyz then
actA = "ACT.-"
else
actB = "ACT.-"
end
window.span(1, line, "-XY Ortho-" .. actA .. "-+-XZ Ortho-" .. actB .. "-+-ST:" .. sts .. "-+-FILE:-", 0, 0xFFFFFF)
elseif line > 9 then
local mix, miy, miz = cx, cy, cz
local mxx, mxy, mxz = cx, cy, cz
if workingOnBox then
if workingOnBox.maxX then
local ax, ay, az = workingOnBox.minX, workingOnBox.minY, workingOnBox.minZ
local bx, by, bz = workingOnBox.maxX, workingOnBox.maxY, workingOnBox.maxZ
mix = math.min(ax, bx)
miy = math.min(ay, by)
miz = math.min(az, bz)
mxx = math.max(ax, bx)
mxy = math.max(ay, by)
mxz = math.max(az, bz)
else
local ax, ay, az = workingOnBox.minX, workingOnBox.minY, workingOnBox.minZ
mix = math.min(ax, cx)
miy = math.min(ay, cy)
miz = math.min(az, cz)
mxx = math.max(ax, cx)
mxy = math.max(ay, cy)
mxz = math.max(az, cz)
end
end
local miText = mix .. "," .. miy .. "," .. miz
local mxText = mxx .. "," .. mxy .. "," .. mxz
local text = programStates[programState][1](miText, mxText)
local menu = {
"| |F1 New ",
"| |F3 Load",
"| |F4 Save",
"| |F5 XYXZ"
}
for i = 1, 4 do
text[i] = fmttext.pad(text[i], 33, true, true) .. menu[i]
end
window.span(1, line, text[line - 9] or "", 0, 0xFFFFFF)
for i = 1, 8 do
local boxId = string.char(i + ((line - 10) * 8) + 64)
if boxes[state][boxId] then
if selectedBox == boxId then
window.span(34 + i, line, boxId, 0xFFFFFF, 0)
else
window.span(34 + i, line, boxId, 0, 0xFFFFFF)
end
end
end
end
end
local function refresh(n3d)
for i = 1, 14 do
render(i, not n3d)
end
end
local function reset()
boxes = {[true] = {}, [false] = {}}
state = false
selectedBox = nil
xyz = false
cx, cy, cz = 1, 1, 1
workingOnBox = nil
programState = "none"
end
local function loadObj(obj)
fileLabel = obj.label
fileTooltip = obj.tooltip
redstone = obj.emitRedstone
button = obj.buttonMode
local advances = {
[false] = 65,
[true] = 65
}
for k, v in ipairs(obj.shapes) do
local vs = v.state or false
boxes[vs][string.char(advances[vs])] = {
minX = v[1],
minY = v[2],
minZ = v[3],
maxX = v[4],
maxY = v[5],
maxZ = v[6],
tex = v.texture,
rgb = v.tint or 0xFFFFFF
}
advances[vs] = advances[vs] + 1
end
end
local function exportBoxes(shapes, st)
local order = {}
for k, v in pairs(boxes[st]) do
table.insert(order, k)
end
table.sort(order)
for _, kv in ipairs(order) do
local v = boxes[st][kv]
local tint = v.rgb
if tint == 0xFFFFFF then
tint = nil
end
table.insert(shapes, {
v.minX,
v.minY,
v.minZ,
v.maxX,
v.maxY,
v.maxZ,
texture = v.tex,
state = st,
tint = tint
})
end
end
local function makeObj()
local tbl = {
label = fileLabel,
tooltip = fileTooltip,
emitRedstone = redstone,
buttonMode = button,
shapes = {
}
}
exportBoxes(tbl.shapes, false)
exportBoxes(tbl.shapes, true)
return tbl
end
local lastFile = nil
local function waitForDialog(handle)
lastFile = nil
while true do
local event, b, c, d = coroutine.yield()
if event == "k.timer" then
neo.scheduleTimer(os.uptime() + 0.5)
end
if event == "x.neo.pub.window" then
if b == "close" then
return true
end
end
if event == "x.neo.pub.base" then
if b == "filedialog" then
if c == handle then
lastFile = d
return
end
end
end
end
end
neo.scheduleTimer(os.uptime())
while true do
local event, a, b, c, d, e = coroutine.yield()
if event == "k.timer" then
neo.scheduleTimer(os.uptime() + 0.5)
cursorBlink = not cursorBlink
refresh(true)
end
if event == "x.neo.pub.window" then
if b == "line" then
render(c, true)
end
if b == "clipboard" then
if workingOnBox and workingOnBox.maxX then
workingOnBox.tex = tostring(c)
b = "key"
c = 13
d = 0
e = true
end
end
if b == "key" then
if e then
--neo.emergency("key " .. tostring(c) .. " " .. tostring(d))
if d == 59 then
reset()
refresh()
elseif d == 61 then
-- Load
local handle = icecap.showFileDialogAsync(false)
if waitForDialog(handle) then return end
if lastFile then
reset()
local obj = require("serial").deserialize("return " .. lastFile.read("*a"))
loadObj(obj)
refresh()
lastFile.close()
end
elseif d == 62 then
-- Save
local handle = icecap.showFileDialogAsync(true)
if waitForDialog(handle) then return end
if lastFile then
lastFile.write(require("serial").serialize(makeObj()):sub(8))
lastFile.close()
end
elseif d == 63 then
xyz = not xyz
refresh()
elseif d == 64 then
redstone = not redstone
refresh()
elseif d == 65 then
button = not button
refresh()
elseif d == 66 then
-- Print
neo.executeAsync("app-nprt2018", makeObj())
elseif c == 9 then
state = not state
selectedBox = nil
-- we can safely switch between states
-- while working on a box
refresh()
elseif d == 203 then
cx = math.max(1, cx - 1)
refresh(true)
elseif d == 200 then
if not xyz then
cy = math.min(16, cy + 1)
else
cz = math.min(16, cz + 1)
end
refresh(true)
elseif d == 205 then
cx = math.min(16, cx + 1)
refresh(true)
elseif d == 208 then
if not xyz then
cy = math.max(1, cy - 1)
else
cz = math.max(1, cz - 1)
end
refresh(true)
else
programStates[programState][2](c, d)
refresh()
end
end
end
if b == "close" then
return
end
end
end