-- This is released into the public domain. -- No warranty is provided, implied or otherwise. -- app-nbox2018.lua : NODEBOX 2018 -- Authors: 20kdc -- program start local holos = neo.requestAccess("c.hologram") local icecap = neo.requireAccess("x.neo.pub.base", "filedialogs") local window = neo.requireAccess("x.neo.pub.window", "window")(40, 13) local fmttext = require("fmttext") -- [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 ", " a box can be selected by its ", " key. To print, press F8. " } 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 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) if p == 1 then -- plane 1 uses inverted Y y = 17 - y 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 render(line) 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 if line < 7 then window.span(1, line, "|" .. textA .. "|" .. textB .. "| ", 0, 0xFFFFFF) else if line == 7 then window.span(1, line, "|" .. textA .. "|" .. textB .. "|F6/F7", 0, 0xFFFFFF) elseif line == 8 then local rs = "R0" local bm = "B0" if redstone then rs = "R1" end if button then bm = "B1" end window.span(1, line, "|" .. textA .. "|" .. textB .. "|" .. rs .. " " .. bm, 0, 0xFFFFFF) end end for i = 1, 5 do local boxId = string.char(i + ((line - 1) * 5) + 64) if boxes[state][boxId] then if selectedBox == boxId then window.span(35 + i, line, boxId, 0xFFFFFF, 0) else window.span(35 + i, line, boxId, 0, 0xFFFFFF) end end end elseif line == 9 then local sts = "ON " if not state then sts = "OFF" end local actA = "---" local actB = "---" if not xyz then actA = "ACT" else actB = "ACT" end window.span(1, line, "+XY Ortho-" .. actA .. "----+XZ Ortho-" .. actB .. "--+-+S:" .. sts, 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], 32, true, true) .. menu[i] end window.span(1, line, text[line - 9] or "", 0, 0xFFFFFF) end end local function refresh() for i = 1, 14 do render(i) 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.minX, 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() end if event == "x.neo.pub.window" then if b == "line" then render(c) 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() elseif d == 200 then if not xyz then cy = math.min(16, cy + 1) else cz = math.max(1, cz - 1) end refresh() elseif d == 205 then cx = math.min(16, cx + 1) refresh() elseif d == 208 then if not xyz then cy = math.max(1, cy - 1) else cz = math.min(16, cz + 1) end refresh() else programStates[programState][2](c, d) refresh() end end end if b == "close" then return end end end